@skitterbyte/skitterspec-azure-boards 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/MIGRATION.md +1126 -0
  2. package/README.md +55 -2
  3. package/assets/claude-md-section.md +85 -0
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-connect.md +13 -0
  6. package/assets/commands/spec-live.md +14 -0
  7. package/assets/commands/spec-remote-review.md +37 -0
  8. package/assets/commands/spec-skip.md +24 -0
  9. package/assets/core/SETUP.md +214 -0
  10. package/assets/core/azure-boards.config.json.example +35 -0
  11. package/assets/core/azure-boards.config.md +43 -0
  12. package/assets/core/env.config.json.example +56 -0
  13. package/assets/core/env.config.md +557 -0
  14. package/assets/core/gating.config.json.example +4 -0
  15. package/assets/core/gating.config.md +81 -0
  16. package/assets/hooks/main-guard.cjs +162 -0
  17. package/assets/hooks/review-gate.cjs +146 -0
  18. package/assets/review/page.html +3384 -0
  19. package/assets/rules/commit-trailers.md +97 -0
  20. package/assets/rules/negative-checks.md +69 -0
  21. package/assets/rules/offered-unblocks.md +100 -0
  22. package/assets/rules/spec-planning.md +692 -0
  23. package/assets/rules/spec-reports.md +557 -0
  24. package/assets/skills/no-spec/SKILL.md +246 -0
  25. package/assets/skills/spec/SKILL.md +719 -0
  26. package/assets/skills/spec-azure-setup/SKILL.md +227 -0
  27. package/assets/skills/spec-bug/SKILL.md +647 -0
  28. package/assets/skills/spec-cancel/SKILL.md +262 -0
  29. package/assets/skills/spec-claim/SKILL.md +122 -0
  30. package/assets/skills/spec-complete/SKILL.md +449 -0
  31. package/assets/skills/spec-diff/SKILL.md +870 -0
  32. package/assets/skills/spec-hotfix/SKILL.md +669 -0
  33. package/assets/skills/spec-init/SKILL.md +178 -0
  34. package/assets/skills/spec-list/SKILL.md +177 -0
  35. package/assets/skills/spec-next/SKILL.md +686 -0
  36. package/assets/skills/spec-push/SKILL.md +304 -0
  37. package/assets/skills/spec-review/SKILL.md +257 -0
  38. package/assets/skills/spec-reviewed/SKILL.md +284 -0
  39. package/assets/skills/spec-start/SKILL.md +554 -0
  40. package/assets/skills/spec-status/SKILL.md +112 -0
  41. package/assets/skills/spec-sync/SKILL.md +221 -0
  42. package/assets/skills/spec-to-main/SKILL.md +139 -0
  43. package/bin/skitterspec-azure-boards.js +62 -0
  44. package/package.json +41 -4
  45. package/src/cli.js +5907 -0
  46. package/src/deprecate.js +138 -0
  47. package/src/env/bootstrap.js +188 -0
  48. package/src/env/building.js +143 -0
  49. package/src/env/classify.js +176 -0
  50. package/src/env/commitcmd.js +108 -0
  51. package/src/env/config.js +753 -0
  52. package/src/env/dev.js +63 -0
  53. package/src/env/hooks.js +226 -0
  54. package/src/env/hotfix.js +411 -0
  55. package/src/env/hotfixland.js +302 -0
  56. package/src/env/integrate.js +126 -0
  57. package/src/env/live.js +504 -0
  58. package/src/env/mainguard.js +299 -0
  59. package/src/env/provision.js +466 -0
  60. package/src/env/proxy.js +225 -0
  61. package/src/env/prune.js +119 -0
  62. package/src/env/registry.js +159 -0
  63. package/src/env/render.js +17 -0
  64. package/src/env/resolve.js +716 -0
  65. package/src/env/review.js +2577 -0
  66. package/src/env/reviewers/coderabbit.js +228 -0
  67. package/src/env/reviewers.js +517 -0
  68. package/src/env/serve.js +1044 -0
  69. package/src/env/supervise.js +157 -0
  70. package/src/env/teardown.js +248 -0
  71. package/src/env/trust.js +87 -0
  72. package/src/gating.js +155 -0
  73. package/src/init.js +1150 -0
  74. package/src/lines-diff.js +114 -0
  75. package/src/prompts.js +129 -0
  76. package/src/vendor/azure-boards/api.js +561 -0
  77. package/src/vendor/azure-boards/cli-sync.js +1428 -0
  78. package/src/vendor/azure-boards/commands.js +44 -0
  79. package/src/vendor/azure-boards/config.js +365 -0
  80. package/src/vendor/azure-boards/credentials.js +109 -0
  81. package/src/vendor/azure-boards/dialect.js +100 -0
  82. package/src/vendor/azure-boards/identity.js +40 -0
  83. package/src/vendor/azure-boards/mcp.js +133 -0
  84. package/src/vendor/provider-kit/index.js +41 -0
  85. package/src/vendor/provider-kit/src/adapter.js +23 -0
  86. package/src/vendor/provider-kit/src/apply.js +207 -0
  87. package/src/vendor/provider-kit/src/args.js +68 -0
  88. package/src/vendor/provider-kit/src/assignment.js +277 -0
  89. package/src/vendor/provider-kit/src/config.js +198 -0
  90. package/src/vendor/provider-kit/src/credentials.js +404 -0
  91. package/src/vendor/provider-kit/src/doctor.js +35 -0
  92. package/src/vendor/provider-kit/src/http.js +63 -0
  93. package/src/vendor/provider-kit/src/identity.js +93 -0
  94. package/src/vendor/provider-kit/src/md-compare.js +39 -0
  95. package/src/vendor/provider-kit/src/md-tables.js +94 -0
  96. package/src/vendor/provider-kit/src/prompt.js +50 -0
  97. package/src/vendor/provider-kit/src/released.js +269 -0
  98. package/src/vendor/provider-kit/src/releases.js +380 -0
  99. package/src/vendor/provider-kit/src/specs.js +166 -0
  100. package/src/vendor/provider-kit/src/transport.js +22 -0
  101. package/src/vendor/sync-core/index.js +93 -0
  102. package/src/vendor/sync-core/src/base.js +81 -0
  103. package/src/vendor/sync-core/src/compare.js +338 -0
  104. package/src/vendor/sync-core/src/dialect.js +51 -0
  105. package/src/vendor/sync-core/src/fields.js +36 -0
  106. package/src/vendor/sync-core/src/legacy.js +100 -0
  107. package/src/vendor/sync-core/src/normalize.js +998 -0
  108. package/src/vendor/sync-core/src/push.js +85 -0
  109. package/src/vendor/sync-core/src/retarget.js +278 -0
  110. package/src/vendor/sync-core/src/task-block.js +308 -0
  111. package/src/vendor/sync-core/src/verify.js +67 -0
  112. package/src/vendor/sync-core/src/write.js +191 -0
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @skitterbyte/skitterspec-azure-boards
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Spec-driven development for Claude Code, with **one-way Azure Boards sync** — a
4
+ superset of [`@skitterbyte/skitterspec`](https://www.npmjs.com/package/@skitterbyte/skitterspec):
5
+ the base filesystem workflow plus `/spec-push` · `/spec-status` · `/spec-claim`
6
+ and the `spec-sync` CLI. The repo is canonical; Azure Boards is a generated
7
+ mirror.
8
+
9
+ Install exactly one distribution — this, the base, or one other superset —
10
+ never two:
11
+
12
+ ```
13
+ npx @skitterbyte/skitterspec-azure-boards init
14
+ ```
15
+
16
+ ## What the sync is
17
+
18
+ - A spec is a **work item**; each phase a **child work item** under a
19
+ parent/child link; tasks ride along as a markdown checklist in the body
20
+ (still a read-only mirror — the repo wins on every push).
21
+ - **Descriptions are markdown**, written verbatim with the format declared on
22
+ the field — checkboxes render natively, and a read-back is byte-comparable.
23
+ - **Work item types and states are the project's own.** The process template
24
+ (Basic, Agile, custom) decides them, so the config names the types and the
25
+ bucket→state map, validated against the process at run time.
26
+ - **One-way**: nothing is read back into the repo. A body edited in Boards is
27
+ reported and replaced on the next push, loudly.
28
+ - **Releases**: CI runs `spec-sync release create <tag>` on a tag push. It
29
+ mints one work item of `releases.workItemType` per release, holding exactly
30
+ the tickets that tag contains by their `Refs: AB#n` trailers;
31
+ `release deployed <tag> <env>` moves that item's state through your envs.
32
+ - **Assignment**: `/spec-start` assigns the work item to you; `/spec-claim`
33
+ hands it on. Identity derives from the PAT (`spec-sync whoami`).
34
+ - **Intake**: adopt a PM-filed work item as a spec's own, routed to `/spec`,
35
+ `/spec-bug` or `/spec-hotfix` by its tags, the original description preserved
36
+ as a comment before the spec replaces it.
37
+
38
+ ## Setup
39
+
40
+ You need a PAT with **Work Items (Read & Write)** — add **Identity (Read)** for
41
+ user search. `/spec-azure-setup` interviews you and writes
42
+ `specs/.core/azure-boards.config.json` (org URL, project, types, bucket→state
43
+ map). Keep the token out of the repo — export `AZURE_DEVOPS_EXT_PAT`, or:
44
+
45
+ ```
46
+ skitterspec-azure-boards spec-sync credentials set
47
+ ```
48
+
49
+ `skitterspec-azure-boards spec-sync doctor --check-remote` reports readiness
50
+ across every layer, the identity scope included.
51
+
52
+ Not yet shipped for Azure Boards: `retarget`, `reattach`, `stage`,
53
+ `spec-sanitise`.
54
+
55
+ This package's `bin/`, `src/` and `assets/` are composed at build time from the
56
+ skitterspec monorepo — see the repo for sources and development.
@@ -0,0 +1,85 @@
1
+ ## Spec workflow
2
+
3
+ Spec-driven development runs through the lifecycle **skills** — use them so
4
+ structure and lifecycle stay consistent. The everyday loop is
5
+ **`spec → start → next → commit → complete`**, with **`/spec-diff`** to read what
6
+ a phase changed and `/spec-connect` when you want to test the spec in a browser.
7
+
8
+ **Skills vs commands.** The lifecycle skills are read by Claude, which exercises
9
+ judgment. `/spec-connect` and `/spec-live` are **slash commands** instead — each
10
+ pre-executes one `spec-env` verb and relays it, so only you can run them; a
11
+ skill that wants one will tell you to type it.
12
+
13
+ **Seeing the work.** A phase is built in its own worktree, so `git diff` in your
14
+ terminal answers about the base branch. **`/spec-diff`** renders that worktree's
15
+ diff as a self-contained HTML page you open locally — or publish, and read on a
16
+ phone. The diff never passes through the model, so the page costs no context
17
+ tokens however large it is; the optional written review is the part that costs,
18
+ and it is offered rather than assumed. `/spec-next` writes the page at the end of
19
+ every phase.
20
+
21
+ **Handing the review back.** The page takes marks: tick `✓ accept` per file as
22
+ you read, note anything against a line or a whole file, answer the questions a
23
+ written review asked — then **end it in a decision**. At the end of a phase the
24
+ buttons are `✓ Commit`, `✓ Commit & Continue` (commit, then build the next
25
+ phase), `↺ Request changes` and `… Discuss first`. A spec that has just been
26
+ written gets `✓ Commit & Start` instead — the `commit-start` verdict, which is
27
+ the `commit && /spec-start` you would otherwise type. A **served** page hands
28
+ the pass to the engine, which **holds** it and shows a six-digit code. Committing
29
+ is unavailable while a note is open — you asked for something, so it cannot also
30
+ be fine — and it hands off to your own commit skill (`review.commitWith`,
31
+ `/commit` by default) rather than a copy living here. Fixes come back as
32
+ resolutions, so the next render shows each note struck through with what changed.
33
+ An accept remembers the file's content, so it lapses by itself when that file
34
+ changes again. The marks are information — nothing gates on them; the verdict is
35
+ the one thing you choose, once.
36
+
37
+ **A second opinion, when you want one** (`review.reviewers`, empty by default).
38
+ Point it at a code reviewer that runs from your terminal — CodeRabbit's CLI has
39
+ a free tier and `{ "use": "coderabbit" }` is the whole setup; anything printing
40
+ [rdjsonl](https://github.com/reviewdog/reviewdog) works too — and its findings
41
+ land on the page beside your own notes, each badged with the tool that found it
42
+ and linked to the line it is about. `/spec-next` runs them once per phase;
43
+ `/spec-diff --reviewers` is the mid-phase ask. **They never block a commit** —
44
+ reply to one and your reply does, because then a person has asked for something.
45
+ A reviewer that could not run says `did not run — <why>` rather than passing as
46
+ clean, because on the page you commit from those two must not look alike.
47
+ Adding one sends your diff to whatever the command talks to, which is why the
48
+ list starts empty and `init` never writes one.
49
+
50
+ **Above the verdicts sits one line that is not a verdict.** `▶ Put it live`
51
+ commits the phase, checks the branch out where your dev server can see it, and
52
+ hands you back the same page with the same options — so you can judge the change
53
+ by *using* it, not only by reading it. It clears no gate: you have looked at it
54
+ running and concluded nothing, so the phase still owes an answer.
55
+
56
+ **Every render lists where the page can be read**, labelled, in a fixed order —
57
+ `local`, `network`, `remote` — each either a URL or the one command that turns it
58
+ on, plus a `live:` line saying whether the change is also running. `local` and
59
+ `network` are two doors into one room: the same server and the same waiting
60
+ verdict, so one wait covers both. `remote` is a published page, a second store —
61
+ a verdict there needs `/spec-reviewed`, and it stays off until someone types
62
+ **`/spec-remote-review`**, which toggles it. Publishing is permitted by that
63
+ command, never performed by it: a published page is one this tooling cannot
64
+ remove.
65
+
66
+ **One ending.** Every spec skill finishes with the same block — a verdict
67
+ (`✅` · `⚠️` · `❌` · `⏸`), then a table of the fields that skill declares,
68
+ ending on the one thing to do next. A refusal emits it too, so "nothing
69
+ happened" is reported rather than absent, and `Follow-ups` is always there
70
+ because a recorded `none` is a decision. Skills stay quiet while they run. The
71
+ shape lives in **`.claude/rules/spec-reports.md`**.
72
+
73
+ The skill table, the spec type/folder conventions and the per-spec isolation
74
+ model all live in **`.claude/rules/spec-planning.md`**, the canonical reference
75
+ every spec skill points at. Tailor its per-phase test commands to this stack.
76
+
77
+ **Release gating** *(only when `specs/.core/gating.config.json` exists)* — each
78
+ spec records whether it ships behind a feature flag, as
79
+ `> **Gating:** <flag name>` or `> **Gating:** none: <one-line reason>`. `/spec`,
80
+ `/spec-bug` and `/spec-hotfix` ask; `/spec-review`, `/spec-start` and
81
+ `/spec-complete` report a spec that has no answer, and never block over it.
82
+ `skitterspec gating check` lists them and always exits 0. The point is that the
83
+ question is **on the record**: a missing line is an oversight, a reason is a
84
+ decision. Skitterspec never reads your flag system — it asks and cites the doc
85
+ you point it at. Without that config, none of this appears.
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: Allow writes on the base branch for this session — bare allows it with no reason, a reason records why, `off` clears it
3
+ argument-hint: "[<reason> | off]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env main allow:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env main allow $ARGUMENTS`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ **Only you can run this, and that is the whole guard.** The main-guard hook
12
+ refuses an `Edit` or a `Write` in the primary checkout while it sits on the base
13
+ branch, and it names two exits: move the work with `/no-spec` or `/spec-start`,
14
+ or have you lift the guard. Claude can take the first exit on its own and can
15
+ never take the second — a guard the model can lift is decoration, which is the
16
+ same line `/spec-reviewed` draws.
17
+
18
+ **Give it a reason.** `/allow-main fixing the lockfile by hand` records why; bare
19
+ records `none given`. The reason is the part a reviewer can argue with, exactly
20
+ as with a `Gating:` header — and it is what you will read when you wonder later
21
+ why the base branch has an uncommitted file on it.
22
+
23
+ **It lapses with the session where it can.** The allow is recorded against this
24
+ session, so it ends when the session does. Where the harness exposes no session
25
+ id it degrades to a repo-wide allow that outlives the session and is cleared only
26
+ by `/allow-main off`; the engine says which kind you got, so do not read a bare
27
+ "allowed" as temporary.
28
+
29
+ **It is machine-local and gitignored.** Unlike `/spec-remote-review`, this writes
30
+ nothing anyone else pulls: it is your decision about your checkout, not the
31
+ project's policy. The project-wide switch is `guards.mainIsLandingZone` in
32
+ `specs/.core/env.config.json`, and turning that off is a different, committed
33
+ decision.
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Expose one spec's dev servers on the canonical localhost ports — bare uses the spec you are on, `main` hands them back
3
+ argument-hint: "[<spec> | main]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env connect:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env connect $ARGUMENTS`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ Only if it reports a **canonical port is in use**: say that the main checkout's
12
+ dev server still holds that port, and that stopping it and re-running is the fix
13
+ (the proxy cannot share a port main is bound to).
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: Put one spec live on the already-running dev server — bare takes the spec you are on, `<spec>` takes that one, `main` hands the instance back
3
+ argument-hint: "[<spec> | main | status | release | abort]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env live:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env live $ARGUMENTS`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ The engine enforces every refusal itself and prints why — a hotfix, a stateful
12
+ spec (`Stack: worktree + docker`), a branch touching migrations, a dirty tree, a
13
+ rebase conflict, or another spec already holding the instance. Do not re-explain
14
+ or work around any of them.
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: Permit or forbid publishing this spec's review for a reader off your network — bare toggles it, `on`/`off` set it explicitly
3
+ argument-hint: "[on | off]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env review allow remote:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env review allow remote --set "$ARGUMENTS"`
8
+
9
+ Relay the engine output above verbatim. Add nothing to it.
10
+
11
+ **Then one thing, and only when the engine asked for it.** Where the output
12
+ carries a `review:` and a `publish:` line, typing this command **is** the ask
13
+ to publish that page: the banner named this command beside a review the reader
14
+ could not reach, so the link is what they are waiting for. Publish it exactly
15
+ as `/spec-diff` §6 describes (run the `publish:` command, publish the copy it
16
+ names with a store, write the URL file), then re-emit the review banner with
17
+ all three tiers, `remote` now carrying the URL. A wait already running keeps
18
+ running; do not restart it or the server. If the harness refuses the publish,
19
+ say so in one line and keep the banner as it was, because local and network
20
+ still work.
21
+
22
+ With no `publish:` line, run nothing else. Nothing has a page to publish, or
23
+ the tier was turned off.
24
+
25
+ **Bare toggles.** That is the common case: you are looking at a render whose
26
+ `remote:` line says which way it currently is, and what you want is the other
27
+ one. `on` and `off` are there for when you are not looking at it.
28
+
29
+ **It is per spec.** The engine resolves the spec from the worktree you are
30
+ standing in and records the permission in the gitignored review store, so
31
+ the base branch is untouched and no other spec may publish because of it.
32
+ `skitterspec spec-env review allow remote --project` is the committed,
33
+ every-spec form, for a team that wants it.
34
+
35
+ **A published page is one skitterspec cannot delete**, and a verdict pressed
36
+ on one needs `/spec-reviewed`, because nothing pushes from an artifact's store
37
+ into a conversation.
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Move on from a phase without a verdict, with the reason on the record — clears the review gate
3
+ argument-hint: "\"<reason>\""
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env review skip:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env review skip "$ARGUMENTS"`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ **It refuses with no reason, and that refusal is the feature.** A skip with no
12
+ reason is indistinguishable from nobody having looked — where
13
+ `none: additive, nothing to revert` is a decision a reviewer can argue with. If
14
+ the engine asked for a reason, relay that and stop; do not supply one on the
15
+ operator's behalf.
16
+
17
+ **It is one of exactly two exits**, and the other is a committing verdict on the
18
+ page. Nothing else discharges a phase's obligation — not a mid-run `Continue`,
19
+ not an action, not a render.
20
+
21
+ **Only you can run it.** Like `/allow-main`, this lifts a guard aimed at Claude,
22
+ so Claude must not be able to lift it. `/spec-connect` and `/spec-live` are
23
+ user-only for the milder reason that there is no judgement to apply; this one is
24
+ user-only because of who the guard is for.
@@ -0,0 +1,214 @@
1
+ # Setting up Azure Boards sync (one-way)
2
+
3
+ A start-to-finish guide to getting `/spec-status` and `/spec-push` working
4
+ against a real Azure DevOps organization. Sync is **one-way**: the repo is the
5
+ source of truth and the linked **work item** is a **generated mirror** —
6
+ content is pushed up, never read back or merged. A spec is a work item and each
7
+ phase a **child work item**, with the phase's tasks mirrored into that child's
8
+ description as a markdown checklist (read-only — the repo wins). Covers the
9
+ **Azure side** (credentials, finding your project) that the config reference
10
+ (`azure-boards.config.md`) assumes you already have.
11
+
12
+ > The whole feature is **opt-in**: until `specs/.core/azure-boards.config.json`
13
+ > exists, everything below is inert and the package behaves exactly like the
14
+ > base `@skitterbyte/skitterspec`.
15
+
16
+ Two things about Azure Boards shape everything below:
17
+
18
+ - **Descriptions are MARKDOWN**, written verbatim with the format declared
19
+ beside the field (`multilineFieldsFormat`) — so a read-back is normally the
20
+ pushed text byte for byte, and task checkboxes render natively. A field saved
21
+ as markdown cannot revert to HTML (Azure's rule; fine one-way). Items that
22
+ predate the link read back as HTML and are reduced to text for comparison.
23
+ - **States are written directly** — there is no transition API — and a process
24
+ rule refuses an unknown name only **at write time**, after the description
25
+ has landed. The engine therefore validates every configured state up front
26
+ and refuses loudly (naming what the types DO have) before anything writes.
27
+
28
+ ---
29
+
30
+ ## 1. Install the package
31
+
32
+ Install the Azure Boards superset (or, if you already run the base or another
33
+ superset, switch — every distribution contains the entire base, so install
34
+ exactly one):
35
+
36
+ ```sh
37
+ npx @skitterbyte/skitterspec-azure-boards init
38
+ ```
39
+
40
+ ## 2. Credentials — a Personal Access Token
41
+
42
+ Azure DevOps authenticates REST calls with basic auth: an empty username and a
43
+ PAT as the password. Create the token in Azure DevOps under User settings →
44
+ Personal access tokens, scoped **Work Items (Read & Write)** — add
45
+ **Identity (Read)** if you want user search later (work item sync is whole
46
+ without it).
47
+
48
+ Store it in the user-level credentials store (outside every repo, `0600`),
49
+ **in your own terminal — not through an assistant**:
50
+
51
+ ```sh
52
+ pnpm exec skitterspec-azure-boards spec-sync credentials set --org-url https://dev.azure.com/<org>
53
+ # Azure DevOps PAT for https://dev.azure.com/<org> (hidden): ●●●●●●●●
54
+ ```
55
+
56
+ (`--org-url` keys the store entry before the repo has a config; once
57
+ `azure-boards.config.json` exists it can be omitted.)
58
+
59
+ Or export `AZURE_DEVOPS_EXT_PAT` — the official CLI's variable, so a machine
60
+ set up for `az boards` needs nothing new. The environment wins over the store.
61
+
62
+ > **MCP alternative:** with no credentials at all, the sync skills fall back to
63
+ > Microsoft's official MCP server — remote at `https://mcp.dev.azure.com/{org}`,
64
+ > or local via `@azure-devops/mcp` — authenticated with your Microsoft account.
65
+ > The engine itself never calls MCP tools; the skills fetch and hand results to
66
+ > the engine as files (`--workspace-states`, `--stored`, `--text`).
67
+
68
+ ## 3. Configure — run `/spec-azure-setup`
69
+
70
+ With credentials in place, let the skill interview you:
71
+
72
+ ```
73
+ /spec-azure-setup
74
+ ```
75
+
76
+ It asks for the org URL, the project, the work item type names, and the four
77
+ bucket→state names — validating them against the types' real states
78
+ (`spec-sync states`) — then writes `specs/.core/azure-boards.config.json` via
79
+ `spec-sync init-config`. By hand instead:
80
+
81
+ ```sh
82
+ pnpm exec skitterspec-azure-boards spec-sync init-config \
83
+ --org-url https://dev.azure.com/<org> \
84
+ --project <project> \
85
+ --state complete=Done
86
+ ```
87
+
88
+ **Commit the file it writes.** Spec worktrees fork from commits, so an
89
+ uncommitted `azure-boards.config.json` is invisible inside every worktree —
90
+ which is where the sync skills actually run — and sync reports itself off
91
+ there.
92
+
93
+ `azure-boards.config.md` (installed beside this file) is the full field
94
+ reference. Two defaults worth knowing: the types are the **Basic** process's
95
+ (`Issue`/`Task`) — an Agile project usually wants `User Story`/`Task`; and
96
+ `cancelled` maps to `Done` because the Basic process carries no Cancelled
97
+ state (Agile's `Removed` is an honest override).
98
+
99
+ **Map buckets to work states, not deployment stages.** The buckets mirror the
100
+ repo's work lifecycle, so To Do / Doing / Done translate honestly — On Test /
101
+ On Live do not: a finished phase would show as deployed before anything was
102
+ released. Deployment position belongs to the release carrier
103
+ (`releases.series[].envs`, moving the release work item) —
104
+ `azure-boards.config.md` → **Choosing your state map** carries the reasoning.
105
+
106
+ ## 4. Check — `spec-sync doctor`
107
+
108
+ ```sh
109
+ pnpm exec skitterspec-azure-boards spec-sync doctor --check-remote
110
+ ```
111
+
112
+ Every row reports `ok`, or names the exact command that fixes it. `missing`
113
+ is an opt-in not taken (fine); `broken` is configured-but-wrong (exit 1).
114
+ `--check-remote` additionally proves the PAT reaches the org, that every
115
+ configured state exists on the types, and whether the PAT can search
116
+ identities (a warn, never broken — only user search degrades without it).
117
+
118
+ ## 5. Push a spec
119
+
120
+ `/spec-push <spec>` computes the plan, applies it, stamps the returned ids
121
+ into the spec's frontmatter (`azure_identifier` / `azure_url` /
122
+ `azure_item_id`), verifies the round-trip by words, and records the snapshot
123
+ later drift is measured against. From then on the lifecycle skills keep the
124
+ mirror fresh on their own.
125
+
126
+ ---
127
+
128
+ ## Release smoke checklist
129
+
130
+ Re-runnable, against a real Azure DevOps org — a throwaway org/project is
131
+ ideal (the free tier covers all of it). Run before cutting a release that
132
+ touches the adapter, the dialect or the apply path. `states.json` below is the
133
+ types' state names.
134
+
135
+ 1. **Doctor** — every row ok:
136
+
137
+ ```sh
138
+ pnpm exec skitterspec-azure-boards spec-sync doctor --check-remote
139
+ ```
140
+
141
+ 2. **States** — the types' state list:
142
+
143
+ ```sh
144
+ pnpm exec skitterspec-azure-boards spec-sync states --json > states.json
145
+ ```
146
+
147
+ 3. **Mint** — on a test spec with at least one phase and a task checklist:
148
+
149
+ ```sh
150
+ pnpm exec skitterspec-azure-boards spec-sync plan <spec> --workspace-states states.json --json > plan.json
151
+ pnpm exec skitterspec-azure-boards spec-sync apply <spec> --plan plan.json
152
+ ```
153
+
154
+ In Boards: the work item and its children exist, states match the buckets,
155
+ and the task checklist renders as markdown checkboxes.
156
+
157
+ 4. **Update** — edit the spec's prose and a phase's status, then plan + apply
158
+ again: the description is replaced and the child's state moves.
159
+
160
+ 5. **Adopt + preserve** — hand-create a work item with a description (in the
161
+ Boards UI, so it is HTML-era), stamp it onto a spec, and preserve before the
162
+ linking push:
163
+
164
+ ```sh
165
+ pnpm exec skitterspec-azure-boards spec-sync stamp <spec> --issue AB#123
166
+ pnpm exec skitterspec-azure-boards spec-sync preserve <spec>
167
+ ```
168
+
169
+ The original description appears as a comment headed **Original report**
170
+ (reduced to text from its HTML); a second `preserve` reports "already
171
+ preserved".
172
+
173
+ 6. **Verify** — save a read-back (`{ "issue": "<description text>",
174
+ "subIssues": { "<ref>": "<text>" } }`; a ref is the phase file's basename
175
+ without `.md`, e.g. `01-date-logic` — keyed with the extension it reports
176
+ every child as a stale ref) and:
177
+
178
+ ```sh
179
+ pnpm exec skitterspec-azure-boards spec-sync verify <spec> --stored readback.json
180
+ ```
181
+
182
+ "round-tripped intact", exit 0.
183
+
184
+ 7. **Released** — over a range with `Refs:` trailers:
185
+
186
+ ```sh
187
+ pnpm exec skitterspec-azure-boards spec-sync released <tag>..HEAD
188
+ ```
189
+
190
+ Tickets counted, unreferenced commits reported, an earlier tracker's
191
+ trailers counted as historical, bookkeeping ignored.
192
+
193
+ 8. **Release carrier** — with a `releases.series` configured and a tag pushed:
194
+
195
+ ```sh
196
+ pnpm exec skitterspec-azure-boards spec-sync release create <tag>
197
+ pnpm exec skitterspec-azure-boards spec-sync release create <tag> --apply
198
+ pnpm exec skitterspec-azure-boards spec-sync release deployed <tag> staged --apply
199
+ pnpm exec skitterspec-azure-boards spec-sync release complete <tag> --apply
200
+ ```
201
+
202
+ In Boards: one `releases.workItemType` item titled by the tag, the range's
203
+ tickets Related-linked with one comment each, the item's state moving
204
+ through the envs and closing on complete. A retry of `create --apply`
205
+ reports "unchanged", never a duplicate.
206
+
207
+ 9. **Drift** — edit the item's description **in Boards**, fetch the item JSON,
208
+ and:
209
+
210
+ ```sh
211
+ pnpm exec skitterspec-azure-boards spec-sync status <spec> --remote item.json
212
+ ```
213
+
214
+ The edit is reported; the next apply replaces it and says so loudly.
@@ -0,0 +1,35 @@
1
+ {
2
+ "azure": {
3
+ "orgUrl": "https://dev.azure.com/your-org",
4
+ "project": "your-project",
5
+ "workItemType": "Issue",
6
+ "childType": "Task",
7
+ "areaPath": ""
8
+ },
9
+ "states": {
10
+ "backlog": "To Do",
11
+ "in-progress": "Doing",
12
+ "complete": "Done",
13
+ "cancelled": "Done"
14
+ },
15
+ "intake": {
16
+ "tag": "",
17
+ "bugTags": ["bug"],
18
+ "hotfixTags": []
19
+ },
20
+ "auth": {
21
+ "tokenEnv": "AZURE_DEVOPS_EXT_PAT"
22
+ },
23
+ "refs": {
24
+ "foreignPatterns": []
25
+ },
26
+ "releases": {
27
+ "workItemType": "Release",
28
+ "series": [
29
+ {
30
+ "tagPrefix": "v",
31
+ "envs": { "staged": "Staged", "published": "Published" }
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,43 @@
1
+ # `azure-boards.config.json` — field reference
2
+
3
+ The committed, shareable half of Azure Boards sync. It holds ids, names and
4
+ env-var NAMES — never a credential. Credentials live in the user-level store
5
+ (`skitterspec-azure-boards spec-sync credentials set`) or the environment.
6
+
7
+ | Key | Meaning | Default |
8
+ |-----|---------|---------|
9
+ | `azure.orgUrl` | The organization (`https://dev.azure.com/<org>`). Also keys the credentials-store entry. | `""` |
10
+ | `azure.project` | The project spec work items land in. | `""` |
11
+ | `azure.workItemType` | The work item type a spec mints as. Types are process-defined (Basic: `Issue`; Agile: `User Story`; custom anything), so this is config, not a constant. | `"Issue"` |
12
+ | `azure.childType` | The type a phase mints as, linked parent/child under the spec's item. | `"Task"` |
13
+ | `azure.areaPath` | Optional area path for minted items; empty means the project default. | `""` |
14
+ | `mapping.specFolder` / `mapping.phases` / `mapping.tasks` | Spec → work item, phase → child item, tasks → markdown checklist. Same modes as the other providers (`subissue` \| `deferred` \| `inline`; `checklist` \| `none`). | `issue` / `subissue` / `checklist` |
15
+ | `states.<bucket>` | The Boards STATE name for each lifecycle bucket, per the project's own process. Moves are direct `System.State` writes; a state the process refuses is a named refusal listing the states the type has. `cancelled` defaults to `Done` because the Basic process carries no Cancelled state. | see example |
16
+ | `intake.tag` / `intake.bugTags` / `intake.hotfixTags` | Intake routes an adopted work item by its tags (Boards has tags, not labels): the inbox filter, and the routes to `/spec-bug` and `/spec-hotfix`. Empty lists route nothing. | `""` / `["bug"]` / `[]` |
17
+ | `auth.tokenEnv` | The env-var NAME for the PAT — the official CLI's by default, so a machine set up for `az boards` needs nothing new. Work Items (Read & Write) covers everything but identity search, which wants Identity (Read). | `AZURE_DEVOPS_EXT_PAT` |
18
+ | `refs.foreignPatterns` | Ref shapes that are KNOWN HISTORY rather than unreferenced commits — a repo migrating off another tracker lists its old key here (e.g. `"ERQ-\\d+"`). | `[]` |
19
+ | `releases.workItemType` / `releases.series` | The release seam's carrier: one work item of this type per release, found by title, its state moved through each series' `envs` map by `release deployed`. | `"Release"` / `[]` |
20
+ | `apply.transport` | `api` \| `mcp`, or empty for "api when credentials resolve, mcp when they don't". | `""` |
21
+ | `sync.baseDir` | Where last-pushed snapshots live. | `specs/.core/azure-base` |
22
+ | `sync.fieldOwnership` | Which projection fields push (`description`, `subIssues`, `workflowState`, `assignee`; `"none"` declines one). | all `push` |
23
+ | `release.stages` / `release.ignorePaths` | The deployment ladder and bookkeeping path prefixes, exactly as the other providers define them. | `[]` / `specs/`, changelogs |
24
+
25
+ Descriptions are stored as MARKDOWN: the adapter writes the projection's
26
+ markdown verbatim and declares the format beside the field
27
+ (`multilineFieldsFormat`), so a read-back is byte-comparable and task
28
+ checkboxes render natively. A field saved as markdown cannot revert to HTML —
29
+ Azure's rule, and fine one-way: this provider never writes HTML. Work items
30
+ that predate the link (every intake-adopted item) come back as HTML and are
31
+ reduced to text for comparison and preservation, never written back.
32
+
33
+ A box ticked in Boards is still overwritten on the next push — the mirror is
34
+ read-only, exactly as the other providers' are.
35
+
36
+ ## Choosing your state map
37
+
38
+ Map the four buckets to **work** states, never to deployment stages — the
39
+ same rule the Jira reference explains at length. The repo learns where work
40
+ **is** as it happens, but what **shipped** only when a release is cut, so a
41
+ pipeline state driven from the spec lifecycle is wrong the moment it moves.
42
+ Deployment position belongs to the release seam (`releases.series[].envs`,
43
+ moving the release work item), not to each ticket's state.
@@ -0,0 +1,56 @@
1
+ {
2
+ "mode": "worktree",
3
+ "worktree": {
4
+ "root": "../{repo}-wt",
5
+ "folderPattern": "{slug}"
6
+ },
7
+ "docker": {
8
+ "enabled": true,
9
+ "composeFile": "docker-compose.yml",
10
+ "projectNamePattern": "{repoSlug}_{slug}",
11
+ "portBase": 3000,
12
+ "portsPerSpec": 10,
13
+ "envFile": ".env",
14
+ "backupCommand": ""
15
+ },
16
+ "seedFiles": {
17
+ "mode": "symlink",
18
+ "files": [
19
+ ".env"
20
+ ]
21
+ },
22
+ "setup": [],
23
+ "dev": [],
24
+ "proxy": {
25
+ "enabled": true,
26
+ "host": "127.0.0.1"
27
+ },
28
+ "registry": ".spec-env/registry.json",
29
+ "branch": {
30
+ "pattern": "{type}/{slug}",
31
+ "identifierField": "azure_identifier"
32
+ },
33
+ "spec": {
34
+ "companionPaths": [
35
+ "specs/.core/azure-base/{identifier}.base.json"
36
+ ]
37
+ },
38
+ "baseBranch": "",
39
+ "guards": {
40
+ "refuseTeardownIfDirty": true,
41
+ "refuseTeardownIfUnpushed": true
42
+ },
43
+ "review": {
44
+ "reader": "detect",
45
+ "servePort": "auto",
46
+ "serve": "always",
47
+ "allowNetwork": true,
48
+ "allowRemote": false,
49
+ "commitWith": "/commit",
50
+ "required": true,
51
+ "reviewers": []
52
+ },
53
+ "live": {
54
+ "migrations": []
55
+ }
56
+ }