feat(unic-ticket-specification): add portable ticket-specification workflow plugin#257
feat(unic-ticket-specification): add portable ticket-specification workflow plugin#257PetersSourceCode wants to merge 2 commits into
Conversation
…rkflow plugin Package a tracker-agnostic Archon workflow that takes a ticket from intake to "ready for implementation": detect input -> analyze across configured repos + linked docs -> classify Bug vs CR/Story -> rewrite to template -> non-blocking completeness check -> PERT estimate -> human approval gate -> apply (create or update) -> report. All tracker/tenant/repo/OS variability lives in a per-project config; nothing tracker-specific is in the workflow or command templates. - 11-node workflow DAG + 7 uts-* command templates + tracker MCP config + config template - /unic-ticket-specification:setup zero-config slash command (no JS lib) - four plugin ADRs under docs/adr/ - registered in the root marketplace; indexed in AGENTS.md + CONTEXT-MAP.md - Feature record at docs/issues/unic-ticket-specification/PRD.md - example wording in uts-rewrite-crstory genericised (no client-specific names) Refs #256 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Added a generic comment to the referenced issue: #256 (comment) |
orioltf
left a comment
There was a problem hiding this comment.
Bug: description written as raw Markdown into non-Markdown tracker fields
Found while running this workflow against a real Azure DevOps project (FZAG/dxp). The apply steps instruct the agent to write "the full contents of draft-description.md" (which is Markdown) verbatim into the tracker's description field. That only happens to be correct for GitHub (Markdown-native). For Azure DevOps and Jira the description field is not Markdown, so the raw #, **, |, - [ ] render as literal characters — an unreadable ticket for humans.
Evidence (two runs, same workflow)
- Run A — the apply step happened to convert MD→HTML before the write → readable ticket.
- Run B (resumed run) — followed the instruction literally, wrote raw Markdown into Azure DevOps
System.Description→ wall of literal##/**/|text.
The difference was pure model discretion: the spec never requires conversion, so determinism is left to chance.
Root cause
The description format is per-tracker, but the commands treat all three trackers as "paste Markdown":
| Tracker | Description field | Correct input |
|---|---|---|
| Azure DevOps | System.Description / Microsoft.VSTS.TCM.ReproSteps — HTML |
render MD → HTML |
| GitHub | issue body — Markdown-native | verbatim ✅ (current behaviour fine) |
| Jira | ADF / wiki markup | render, not raw MD |
Suggested fix
Keep draft-description.md as Markdown (it's what the human reviews and the local artifact stores), and convert at apply time, tracker-aware. Add a shared ## Description formatting section to both uts-apply-create.md and uts-apply-update.md:
## Description formatting — render to the tracker's native format
`draft-description.md` is Markdown. Render it to the tracker's format before writing —
never paste raw Markdown into a field that is not Markdown-native.
- GitHub — issue body is Markdown-native. Use draft-description.md verbatim.
- Azure DevOps — System.Description / ReproSteps are HTML fields. Convert MD → HTML
(headings → <h2>/<h3>, **bold** → <strong>, lists → <ul>/<li>,
tables → <table>, fenced code → <pre><code>, links → <a>,
task items - [ ] → checkboxes). Write to $ARTIFACTS_DIR/description.html
(inspectable + deterministic), then set the field from that file.
- Jira — description must be ADF / wiki markup, not raw Markdown. Convert before edit.
Then change each tracker's apply step to write the formatted output (HTML for ADO, ADF/wiki for Jira) instead of "full contents of draft-description.md". See the two inline comments for the exact lines.
Writing the HTML to description.html first makes the conversion deterministic-by-instruction rather than by luck, and keeps it inspectable in the run artifacts.
(Filed from a downstream install of this bundle; I've already verified the fix end-to-end — npx marked --gfm produces clean ADO-compatible HTML, and re-applying it to the affected work item flipped multilineFieldsFormat.System.Description to html and rendered correctly.)
| ### azure-devops | ||
|
|
||
| 1. Update the work item `key` in `tracker.azure_devops.org_url` / `project`, | ||
| setting the description/repro field to the full contents of |
There was a problem hiding this comment.
Bug: Azure DevOps System.Description (and Microsoft.VSTS.TCM.ReproSteps) are HTML fields. Writing the raw Markdown contents of draft-description.md here makes ##, **, | tables and - [ ] render as literal text — the ticket is unreadable to humans. This is exactly what happened on a real resumed run.
Suggest: render draft-description.md → HTML first (e.g. write to $ARTIFACTS_DIR/description.html), then set the field from that, per a shared ## Description formatting section. e.g.
1. Render `draft-description.md` to HTML per *Description formatting* and save to
`$ARTIFACTS_DIR/description.html`. Update the work item `key`, setting the
description/repro field to that HTML — never the raw Markdown.
GitHub (line 52) is fine as-is since the issue body is Markdown-native; Jira (line 36) should use ADF/wiki markup, also not raw Markdown.
| 1. Work-item type = `tracker.azure_devops.work_item_types.bug` when kind=BUG, | ||
| else `...work_item_types.cr_story`. | ||
| 2. Create the work item in `tracker.azure_devops.org_url` / `project` with the | ||
| title and the description = full contents of `draft-description.md` |
There was a problem hiding this comment.
Same bug on the create path. Azure DevOps System.Description is an HTML field; the raw Markdown from draft-description.md renders as literal #/**/| characters.
Suggest rendering MD → HTML before the write:
2. Render `draft-description.md` to HTML per *Description formatting* and save to
`$ARTIFACTS_DIR/description.html`. Create the work item with the title, then set
the description/repro field to that HTML — never the raw Markdown.
Mirror of the uts-apply-update.md comment. GitHub (line 67) is correct as Markdown; Jira (line 46) should be ADF/wiki markup.
There was a problem hiding this comment.
Suggestion: default the Azure DevOps MCP server to --authentication azcli
Found while running this workflow repeatedly against an Azure DevOps project (FZAG/dxp): @azure-devops/mcp defaults to interactive OAuth, so it pops a browser login on (re)spawn — which happens on every workflow run, making back-to-back ticket runs painful.
The server supports reusing an existing az login session via the --authentication azcli flag (per the Microsoft azure-devops-mcp docs). Switching to it means the user logs in once and every spawned MCP server piggybacks on the cached Azure CLI token — no per-run browser prompt.
Recommended generated config for the azure-devops branch of setup:
{
"azure-devops": {
"command": "npx",
"args": ["-y", "@azure-devops/mcp", "<org>", "--authentication", "azcli"]
}
}On --tenant (corrected): omit it by default. Do not auto-fill it from az account show --query tenantId — that returns the user's home/default tenant, which in consultancy/guest setups is frequently not the tenant that owns the ADO org, and hardcoding the wrong one breaks token resolution. AzureCliCredential resolves the tenant from the session; only pass --tenant <id> for a genuine cross-tenant case, and then it must be the ADO org's tenant, sourced deliberately.
Caveat worth documenting (not a code change): azcli reduces prompts but cannot eliminate them where org Conditional Access enforces a sign-in frequency — the user still re-runs az login when the CA window lapses (we hit AADSTS70043 for exactly this). With azcli that's the only time a prompt appears, vs. every run today.
The server is Entra-only for interactive/CLI auth (azcli or interactive); it also has a non-interactive pat mode reading PERSONAL_ACCESS_TOKEN from env (base64 email:pat) for environments that prefer a PAT. See the inline note on setup.md. Verified end-to-end on a downstream install.
| server that matches `tracker.type`: | ||
| - jira → the Atlassian MCP (`npx -y mcp-remote https://mcp.atlassian.com/v1/mcp/authv2`), exactly | ||
| as shipped in `${CLAUDE_PLUGIN_ROOT}/.archon/mcp/ticket-spec-tracker.json`. | ||
| - azure-devops → the Azure DevOps MCP server the team uses. |
There was a problem hiding this comment.
When setup writes the Azure DevOps MCP server here, suggest emitting the --authentication azcli flag so it reuses the user's az login session instead of the default interactive OAuth (which prompts a browser login on every workflow run):
"args": ["-y", "@azure-devops/mcp", "<org>", "--authentication", "azcli"]With azcli the user logs in once (az login) and back-to-back ticket runs reuse the cached token — no per-run prompt (until org Conditional Access forces a fresh az login).
Correction / caveat on --tenant: do not auto-fill --tenant from az account show --query tenantId — that returns the user's home/default tenant, which in consultancy/guest setups is often not the tenant that owns the ADO org (e.g. a vendor dev signed into their own tenant working on a client's org). Hardcoding the wrong tenant breaks token resolution. Omit --tenant by default and let AzureCliCredential resolve it from the session; only add it for a genuine cross-tenant case, and then it must be the ADO org's tenant id, sourced deliberately — not the user's default.
orioltf
left a comment
There was a problem hiding this comment.
Doc suggestion: note the concurrency model (worktree off → serial runs)
With worktree.enabled: false, the workflow runs on the current branch in a single shared working tree + git index, so Archon serialises runs — a second run while one is in progress fails (must use the current branch). This surprised me when trying to groom several tickets at once; worth a short note in the README so users know it's by design and how to parallelise. Suggested text inline on the Notes section.
| - This bundle lives in the shared `unic-agents-plugins` repo and ships only the | ||
| config **template** (`ticket-spec.config.example.yaml`). Each project copies it to | ||
| `ticket-spec.config.yaml` and fills in its own tracker/repo detail; the active | ||
| config is never committed to the shared bundle. |
There was a problem hiding this comment.
Consider adding a short Concurrency & parallel runs note here. Because worktree.enabled: false, the workflow runs directly on the current branch (one shared working tree + git index), so Archon serialises runs — starting a second run while one is in progress fails (it reports the run must use the current branch). It's worth stating this is by design (Archon isolates per worktree, not per file, so it can't assume two runs won't clash — and any node that writes the persisted .md or commits shares the one git index), plus the escape hatch:
## Concurrency & parallel runs
`worktree.enabled: false` runs on the current branch (one shared working tree +
git index), so Archon serialises runs — a second run while one is in progress
fails. To run tickets in parallel, isolate each run with the CLI `--branch` flag
(creates a dedicated worktree + branch per run):
archon workflow run unic-ticket-specification --input <id> --branch spec/<id>
Avoid `worktree.enabled: true` globally unless any write/commit step is reworked
to target a stable branch, or commits scatter across throwaway run branches.

What
Adds
unic-ticket-specification— a Claude Code plugin packaging a portable Archon workflow that takes a tracker ticket from intake to "ready for implementation". Tracker-agnostic (Jira / Azure DevOps / GitHub), multi-repo, and OS-independent (Windows / macOS).Tracking issue: #256 · Feature PRD:
docs/issues/unic-ticket-specification/PRD.mdWorkflow
11-node DAG:
detect-input → analyze → classify → rewrite (Bug | CR-Story) → assess-completeness (non-blocking) → estimate (PERT) → persist-local → present-draft → approval-gate ✓ → apply (create | update) → report. Nothing is written to the tracker before the human approval gate.Contents
.archon/): workflow DAG, sevenuts-*command templates, tracker MCP config, documentedticket-spec.config.example.yaml, bundle README/unic-ticket-specification:setup: zero-config conversational install/config command (ships no JS).claude-plugin/manifests,package.json,CONTEXT.md,AGENTS.md,CLAUDE.mdsymlink,README.md,CHANGELOG.mddocs/adr/): tool-agnostic config-driven, MCP-first/CLI-fallback, Markdown-only descriptions, setup-as-conversational-commandAGENTS.md+CONTEXT-MAP.md(app:unic-ticket-specification)Design notes
ticket-spec.config.yamlis created per project.uts-rewrite-crstory.mdwas genericised (no client-specific names).LICENSEfile added (maintainer manages those by hand per monorepo convention).Validation
pnpm --filter unic-ticket-specification verify:changelogpassestest/typecheckscriptNotes for reviewer
test-git-push-permissionscommit (just the rootAGENTS.mdworkspace-tree line) — squashing on merge will fold it away.pnpm-lock.yamlchange is the new workspace member only.resolved; close it when this merges (Gitflow default branch ismain, so it won't auto-close on the develop merge).🤖 Generated with Claude Code