@skitterbyte/skitterspec-jira 1.0.0 → 2.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 (42) hide show
  1. package/MIGRATION.md +178 -1
  2. package/README.md +3 -1
  3. package/assets/core/SETUP.md +1 -1
  4. package/assets/core/env.config.md +54 -2
  5. package/assets/core/jira.config.md +33 -6
  6. package/assets/hooks/main-guard.cjs +1 -1
  7. package/assets/hooks/review-gate.cjs +1 -1
  8. package/assets/rules/spec-planning.md +13 -3
  9. package/assets/rules/spec-reports.md +19 -4
  10. package/assets/skills/no-spec/SKILL.md +6 -3
  11. package/assets/skills/spec-bug/SKILL.md +3 -1
  12. package/assets/skills/spec-complete/SKILL.md +6 -3
  13. package/assets/skills/spec-hotfix/SKILL.md +3 -1
  14. package/assets/skills/spec-jira-setup/SKILL.md +2 -1
  15. package/assets/skills/spec-next/SKILL.md +3 -1
  16. package/assets/skills/spec-status/SKILL.md +27 -25
  17. package/assets/skills/spec-sync/SKILL.md +6 -4
  18. package/assets/skills/spec-to-main/SKILL.md +5 -3
  19. package/bin/skitterspec-jira.js +7 -0
  20. package/package.json +1 -1
  21. package/src/cli.js +82 -16
  22. package/src/env/config.js +111 -14
  23. package/src/env/glob.js +34 -0
  24. package/src/env/hotfix.js +33 -5
  25. package/src/env/integrate.js +52 -11
  26. package/src/env/live.js +1 -26
  27. package/src/env/proxy.js +33 -0
  28. package/src/env/review.js +47 -2
  29. package/src/init.js +3 -3
  30. package/src/vendor/jira/cli-sync.js +5 -9
  31. package/src/vendor/jira/config.js +5 -15
  32. package/src/vendor/provider-kit/src/apply.js +10 -0
  33. package/src/vendor/provider-kit/src/assignment.js +277 -0
  34. package/src/vendor/provider-kit/src/config.js +101 -48
  35. package/src/vendor/provider-kit/src/http.js +30 -11
  36. package/src/vendor/provider-kit/src/list-assignee.js +71 -0
  37. package/src/vendor/provider-kit/src/list-rows.js +35 -0
  38. package/src/vendor/provider-kit/src/released.js +80 -87
  39. package/src/vendor/provider-kit/src/specs.js +89 -7
  40. package/src/vendor/sync-core/index.js +5 -5
  41. package/src/vendor/sync-core/src/compare.js +38 -6
  42. package/src/vendor/sync-core/src/normalize.js +40 -58
package/MIGRATION.md CHANGED
@@ -1,5 +1,104 @@
1
1
  # Migration guide
2
2
 
3
+ ## `@skitterbyte/skitterspec-github` v1 → v2 (tell, don't track)
4
+
5
+ Skitterspec now tells GitHub about **its own** transitions and nothing
6
+ else. It writes nothing about releases or deployments, and it never reads a
7
+ ticket's state back.
8
+
9
+ ### Breaking change 1 — The `release create|deployed|complete|reconcile` verbs are removed
10
+
11
+ They exit 1 with one line naming `spec-sync released <range> --json`. A pipeline
12
+ still calling them goes red rather than silently believing it mirrors
13
+ anything. **To migrate**, replace the step with your own script over the
14
+ release's ticket list:
15
+
16
+ ```sh
17
+ skitterspec-github spec-sync released "<previous-tag>..<tag>" --json
18
+ ```
19
+
20
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
21
+ linked), so the script needs no second lookup. Set whatever GitHub field,
22
+ state or release object your process uses from that list. The checkout needs
23
+ `fetch-depth: 0` so the previous tag is visible.
24
+
25
+ ### Breaking change 2 — The `releases` block (`deployedField`, `series`) and `release.stages` are retired
26
+
27
+ The keys are ignored, like any unknown key, so a config carrying them still
28
+ loads. `spec-sync doctor` names them as unused config (a `warn`, never a
29
+ failure). Delete them when you remove the CI step.
30
+
31
+ ### Breaking change 3 — no state drift
32
+
33
+ `spec-sync status --remote` and `/spec-status` no longer compare the tracker's
34
+ state with the spec's. A ticket someone moved on GitHub is not reported, and
35
+ push still resends state only when the spec itself moves. The edited-description
36
+ warning is unchanged.
37
+
38
+ ## `@skitterbyte/skitterspec-azure-boards` v1 → v2 (tell, don't track)
39
+
40
+ Skitterspec now tells Azure Boards about **its own** transitions and nothing
41
+ else. It writes nothing about releases or deployments, and it never reads a
42
+ ticket's state back.
43
+
44
+ ### Breaking change 1 — The `release create|deployed|complete` verbs are removed
45
+
46
+ They exit 1 with one line naming `spec-sync released <range> --json`. A pipeline
47
+ still calling them goes red rather than silently believing it mirrors
48
+ anything. **To migrate**, replace the step with your own script over the
49
+ release's ticket list:
50
+
51
+ ```sh
52
+ skitterspec-azure-boards spec-sync released "<previous-tag>..<tag>" --json
53
+ ```
54
+
55
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
56
+ linked), so the script needs no second lookup. Set whatever Azure Boards field,
57
+ state or release object your process uses from that list. The checkout needs
58
+ `fetch-depth: 0` so the previous tag is visible.
59
+
60
+ ### Breaking change 2 — The `releases` block (`workItemType`, `series`, `ticketTag`, `deployedField`) and `release.stages` are retired
61
+
62
+ The keys are ignored, like any unknown key, so a config carrying them still
63
+ loads. `spec-sync doctor` names them as unused config (a `warn`, never a
64
+ failure). Delete them when you remove the CI step.
65
+
66
+ ### Breaking change 3 — no state drift
67
+
68
+ `spec-sync status --remote` and `/spec-status` no longer compare the tracker's
69
+ state with the spec's. A ticket someone moved on Azure Boards is not reported, and
70
+ push still resends state only when the spec itself moves. The edited-description
71
+ warning is unchanged.
72
+
73
+ ## `@skitterbyte/skitterspec-jira` v1 → v2 (tell, don't track)
74
+
75
+ Skitterspec now tells Jira about **its own** transitions and nothing else. It
76
+ writes nothing about releases or deployments, and it never reads an issue's
77
+ status back.
78
+
79
+ ### Breaking change 1 — no status drift
80
+
81
+ `spec-sync status --remote` and `/spec-status` no longer compare Jira's status
82
+ with the spec's. An issue someone moved on Jira is not reported, and push still
83
+ transitions it only when the spec itself moves. The edited-description warning
84
+ is unchanged.
85
+
86
+ ### Breaking change 2 — `release.stages` retired
87
+
88
+ It was accepted and validated before, and never acted on. It is now ignored
89
+ like any unknown key, and `spec-sync doctor` names it as unused config (a
90
+ `warn`, never a failure). Delete it.
91
+
92
+ ### Also — `released --json` names the spec
93
+
94
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
95
+ linked), so CI can record a release, for example as a fix version, without a
96
+ second lookup:
97
+
98
+ ```sh
99
+ skitterspec-jira spec-sync released "<previous-tag>..<tag>" --json
100
+ ```
101
+
3
102
  ## `@skitterbyte/skitterspec` v23 → v24 (hotfix land runs the release itself)
4
103
 
5
104
  ### Breaking change 1 — `spec-env hotfix land` runs its steps, and holds before the carry
@@ -100,6 +199,43 @@ Nothing in `env.config.json` changes.
100
199
  **If you added a secrets-hook exception for the printed seed commands**, you
101
200
  can remove it now. Nothing shipped prints them any more.
102
201
 
202
+ ## `@skitterbyte/skitterspec-azure-boards` v0 → v1 (first release)
203
+
204
+ Nothing to migrate: this is the distribution's first release. It is a superset
205
+ of `@skitterbyte/skitterspec` — install exactly one of the base or one
206
+ superset. Coming from the base, run `npx @skitterbyte/skitterspec-azure-boards
207
+ update` and run `/spec-azure-setup`
208
+ (or `skitterspec-azure-boards spec-sync init-config --org-url … --project …`).
209
+
210
+ **Coming from `@skitterbyte/skitterspec-linear` or `-jira`** — a repo moving
211
+ trackers: swap the distribution, configure Azure as above, set
212
+ `refs.foreignPatterns` to the old team key (e.g. `"ERQ-\\d+"`), then push each
213
+ backlog and in-progress spec (`/spec-push`, or `spec-sync apply --all
214
+ in-progress` and `--all backlog`) to link it to a new work item. Finished specs
215
+ keep their old stamps (`linear_identifier`, …) as history; nothing rewrites
216
+ them. Old `Refs: ERQ-12` trailers stay in the log, and `spec-sync released`
217
+ counts them as `historical` rather than as missing a ref. New commits carry
218
+ `Refs: AB#n`. Delete the old `linear.config.json` / `jira.config.json` once the
219
+ open specs are linked.
220
+
221
+ ## `@skitterbyte/skitterspec-github` v0 → v1 (first release)
222
+
223
+ Nothing to migrate: this is the distribution's first release. It is a superset
224
+ of `@skitterbyte/skitterspec` — install exactly one of the base or one
225
+ superset. Coming from the base, run `npx @skitterbyte/skitterspec-github
226
+ update`, make a Projects v2 board for the repo, and run `/spec-github-setup`
227
+ (or `skitterspec-github spec-sync init-config --owner … --repo … --project N`).
228
+
229
+ **Coming from `@skitterbyte/skitterspec-linear` or `-jira`** — a repo moving
230
+ trackers: swap the distribution, configure GitHub as above, then push each
231
+ backlog and in-progress spec (`/spec-push`, or `spec-sync apply --all
232
+ in-progress` and `--all backlog`) to link it to a new issue. Finished specs keep
233
+ their old stamps (`linear_identifier`, …) as history; nothing rewrites them.
234
+ Old `Refs: SKS-12` trailers stay in the log, and `spec-sync released` counts
235
+ them as `historical` rather than as missing a ref. New commits carry
236
+ `Refs: #N`. Delete the old `linear.config.json` / `jira.config.json` once the
237
+ open specs are linked.
238
+
103
239
  ## `@skitterbyte/skitterspec-jira` v0 → v1 (first release)
104
240
 
105
241
  Nothing to migrate: this is the distribution's first release. It is a superset
@@ -238,7 +374,48 @@ can run it.
238
374
  committed file, so it changes for everyone who pulls and leaves that tree dirty.
239
375
  The engine says so when it does.
240
376
 
241
- ## `@skitterbyte/skitterspec-linear` v18 → v19 (hotfix land runs the release itself)
377
+ ## `@skitterbyte/skitterspec-linear` v18 → v19 (tell, don't track; hotfix land runs the release itself)
378
+
379
+ Skitterspec now tells Linear about **its own** transitions and nothing
380
+ else. It writes nothing about releases or deployments, and it never reads a
381
+ ticket's state back.
382
+
383
+ ### Breaking change 1 — The `spec-sync stage` verb is removed
384
+
385
+ It exits 1 with one line naming `spec-sync released <range> --json`. A pipeline
386
+ still calling it goes red rather than silently believing it mirrors
387
+ anything. **To migrate**, replace the step with your own script over the
388
+ release's ticket list:
389
+
390
+ ```sh
391
+ skitterspec-linear spec-sync released "<previous-tag>..<tag>" --json
392
+ ```
393
+
394
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
395
+ linked), so the script needs no second lookup. Set whatever Linear field,
396
+ state or release object your process uses from that list. The checkout needs
397
+ `fetch-depth: 0` so the previous tag is visible.
398
+
399
+ ### Breaking change 2 — `release.stages` (the deployment ladder) is retired
400
+
401
+ The keys are ignored, like any unknown key, so a config carrying them still
402
+ loads. `spec-sync doctor` names them as unused config (a `warn`, never a
403
+ failure). Delete them when you remove the CI step.
404
+
405
+ ### Breaking change 3 — no state drift
406
+
407
+ `spec-sync status --remote` and `/spec-status` no longer compare the tracker's
408
+ state with the spec's. A ticket someone moved on Linear is not reported, and
409
+ push still resends state only when the spec itself moves. The edited-description
410
+ warning is unchanged.
411
+
412
+ **A ladder rung no longer reads as `complete`.** Where a state was a
413
+ `release.stages` rung, the live `spec-sync list` now shows it as unmapped. To
414
+ keep it reading as finished, list it as an alias:
415
+ `"complete": ["Done", "On Test"]`. `init-config --stage` is gone too, and is
416
+ refused as an unknown flag.
417
+
418
+ ### Also — hotfix land runs the release itself
242
419
 
243
420
  The same change as `@skitterbyte/skitterspec` v23 → v24 above — this
244
421
  distribution composes the same engine. Read that entry; nothing here is
package/README.md CHANGED
@@ -37,7 +37,9 @@ skitterspec-jira spec-sync credentials set
37
37
  every layer.
38
38
 
39
39
  Not yet shipped for Jira (the Linear superset has them): assignment/`/spec-claim`,
40
- the release-stage ladder, `retarget`, `reattach`, `spec-sanitise`.
40
+ `retarget`, `reattach`, `spec-sanitise`. Releases and deploys are the host
41
+ project's to record on every provider: `spec-sync released <range> --json`
42
+ lists a range's tickets for its CI to act on.
41
43
 
42
44
  This package's `bin/`, `src/` and `assets/` are composed at build time from the
43
45
  skitterspec monorepo — see the repo for sources and development.
@@ -114,7 +114,7 @@ every configured status exists in the project's workflows.
114
114
  `/spec-push <spec>` computes the plan, applies it, stamps the returned keys
115
115
  into the spec's frontmatter (`jira_identifier` / `jira_url` /
116
116
  `jira_issue_id`), verifies the round-trip by words, and records the snapshot
117
- later drift is measured against. From then on the lifecycle skills keep the
117
+ later pushes diff against. From then on the lifecycle skills keep the
118
118
  mirror fresh on their own.
119
119
 
120
120
  ---
@@ -143,6 +143,10 @@ through to a documented conservative default; see the field notes below.
143
143
  // exits non-zero; [] = none. Each string is a shell command; {slug}/{branch}/{worktreePath}/{projectName}/{portOffset}
144
144
  // expand (the cwd is already the worktree, so {worktreePath} is usually
145
145
  // redundant). Example: ["pnpm install --frozen-lockfile"].
146
+ // An entry may also be { "run": "<cmd>", "when": ["<glob>", ...] }. `when` is
147
+ // IGNORED here — a fresh worktree always runs every setup step — and exists
148
+ // so `afterLand` can re-run that step after a land that changes its inputs
149
+ // (`{ "setup": true }`, below).
146
150
  "setup": [],
147
151
 
148
152
  // Commands to run in the PRIMARY checkout after a land — the counterpart of
@@ -160,6 +164,51 @@ through to a documented conservative default; see the field notes below.
160
164
  // failure, report it as a caveat, and never undo the land or block
161
165
  // teardown. Literal strings — tokens are NOT expanded, because they run on
162
166
  // the base branch where a spec's {slug} or {portOffset} mean nothing.
167
+ //
168
+ // CONDITIONAL ENTRIES. A plain string always runs. An entry may instead be
169
+ // { "run": "<cmd>", "when": ["<glob>", ...] } (a single string `when` is
170
+ // fine), which runs only when the land changes a file one of its globs
171
+ // matches — so a generate or an install costs nothing on a land that does
172
+ // not need it, and is not forgotten on one that does:
173
+ //
174
+ // "afterLand": [
175
+ // { "run": "pnpm install --frozen-lockfile", "when": ["pnpm-lock.yaml", "**/package.json"] },
176
+ // { "run": "pnpm run db:generate", "when": ["prisma/schema.prisma"] },
177
+ // "pnpm db:data-migration"
178
+ // ]
179
+ //
180
+ // Globs are repo-relative: `*` stays within a directory, `**` crosses them
181
+ // (`**/package.json` matches the root one too), `?` is one character. An
182
+ // entry whose `when` holds no usable glob is DROPPED, never widened to
183
+ // always-run.
184
+ // The changed files are what the land brings into the base branch:
185
+ // `base...branch` for `integrate` (read before the rebase, so it is the
186
+ // branch's own changes), and the carried fix commits — minus the release's
187
+ // version commit — for `hotfix land`.
188
+ // When that cannot be known, the conditional entries RUN. That is the
189
+ // "already landed" plan (the land happened elsewhere, so its range is not
190
+ // here) and any diff git would not answer. A redundant generate costs
191
+ // seconds; a skipped one leaves the base red with nothing pointing at why.
192
+ // A land that changed no files is known, not unknown, and skips them.
193
+ // The plan says why for every entry: a matched one ends in
194
+ // `# <path> changed`, an unknown one in `# range unknown` with a line saying
195
+ // so, and the rest are listed under `skipped (nothing in the land matched):`
196
+ // with their globs. The reason is a shell comment, so a line run as printed
197
+ // stays one command. Skipped lines are information — run only the list
198
+ // under `then, … run`.
199
+ //
200
+ // REUSING A SETUP STEP. `{ "setup": true }` expands, in place, to every
201
+ // `setup` entry that declares `when`, so the trigger lives in one list:
202
+ //
203
+ // "setup": [
204
+ // { "run": "pnpm install --frozen-lockfile", "when": ["pnpm-lock.yaml", "**/package.json"] },
205
+ // { "run": "pnpm run db:generate", "when": ["prisma/schema.prisma"] }
206
+ // ],
207
+ // "afterLand": [{ "setup": true }, "pnpm db:data-migration"]
208
+ //
209
+ // Plain setup strings carry no trigger and are never pulled in. The command
210
+ // is copied literally — afterLand expands no tokens — so a setup step that
211
+ // relies on {slug}-style tokens is a poor one to reuse.
163
212
  "afterLand": [],
164
213
 
165
214
  // Host dev servers `spec-env dev up <spec>` starts on the spec's port block
@@ -292,8 +341,11 @@ through to a documented conservative default; see the field notes below.
292
341
  // one. Empty (the default) = the built-in tagger.
293
342
  //
294
343
  // `bump` is the built-in tagger's strategy: "patch" (v33.16.4 -> v33.16.5) or
295
- // "hotfix-suffix" (v33.16.4 -> v33.16.4-hotfix.1, then -hotfix.2, ...; a base
296
- // that is already a hotfix tag continues its line). Any other value is refused
344
+ // "hotfix-suffix" (v33.16.4 -> v33.16.4-hotfix.1, then -hotfix.2, ...). Under
345
+ // either, a base that is already a hotfix tag continues its line
346
+ // (v36.2.3-hotfix.4 -> v36.2.3-hotfix.5), so "patch" never takes a number the
347
+ // mainline owns. When the patch tag already exists, the refusal names the
348
+ // free hotfix tag that "hotfix-suffix" would cut. Any other value is refused
297
349
  // when `hotfix land` runs. Tags order X.Y.Z-rc.N < X.Y.Z < X.Y.Z-hotfix.N <
298
350
  // X.Y.(Z+1), the same as skittership.
299
351
  //
@@ -16,7 +16,7 @@ NAMES — never a credential. Credentials live in the user-level store
16
16
  | `apply.transport` | `api` \| `mcp`, or empty for "api when credentials resolve, mcp when they don't". | `""` |
17
17
  | `sync.baseDir` | Where last-pushed snapshots live. | `specs/.core/jira-base` |
18
18
  | `sync.fieldOwnership` | Which projection fields push (`description`, `subIssues`, `workflowState`, `assignee`; `"none"` declines one). | all `push` |
19
- | `release.stages` / `release.ignorePaths` | The deployment ladder and bookkeeping path prefixes, exactly as the Linear provider defines them. | `[]` / `specs/`, changelogs |
19
+ | `release.ignorePaths` | Bookkeeping path prefixes `released` ignores, exactly as the Linear provider defines them. | `specs/`, changelogs |
20
20
 
21
21
  Descriptions are stored as ADF (Atlassian Document Format): the adapter
22
22
  renders markdown to ADF on push and reduces ADF back to text on read-back, so
@@ -24,6 +24,31 @@ verification compares words, not bytes. Phase checklists render as live ADF
24
24
  task lists — still a read-only mirror: a box ticked in Jira is overwritten on
25
25
  the next push.
26
26
 
27
+ ## More than one name per bucket — read-only aliases
28
+
29
+ A board usually has more states than the four buckets: "Blocked", "Next
30
+ Sprint", "Released". A bucket's value may be an **array** instead of a string:
31
+
32
+ ```jsonc
33
+ "states": {
34
+ "in-progress": ["Active", "Blocked"],
35
+ "complete": ["Closed", "Released"]
36
+ }
37
+ ```
38
+
39
+ - **The first name is the only one push writes**, and only when the spec
40
+ itself moves. The rest are never written.
41
+ - **The rest are read as that bucket** wherever a listing interprets a status.
42
+ The array is parsed and validated like every other provider's, so one config
43
+ shape works across all of them. `spec-sync status` never reads a ticket's
44
+ status at all: skitterspec tells Jira about its own transitions and nothing
45
+ else.
46
+ - **Nothing is pulled.** An alias changes what the report *says* about a
47
+ ticket, never the spec.
48
+ - A plain string works exactly as before. An alias that also names another
49
+ bucket is refused at load. An alias the tracker lacks is reported by
50
+ `spec-sync states` but never blocks a push, because it is never written.
51
+
27
52
  ## Choosing your status map
28
53
 
29
54
  Map the four buckets to **work** statuses, never to deployment stages. The
@@ -44,8 +69,9 @@ the map honest:
44
69
  type keeps the pipeline — subtasks genuinely have no delivery lifecycle, and
45
70
  Jira lets you say so structurally.
46
71
  - **Track "which release contains this" with fix versions** — the Releases
47
- hub, the analogue of what `spec-sync released` computes from `Refs:`
48
- trailers. (Available on every plan; in the current "Spaces" UI it is not a
72
+ hub. Your CI can fill it from `spec-sync released "<prev>..<tag>" --json`,
73
+ which lists the tickets a range contains from their `Refs:` trailers, each
74
+ with its spec and url. (Available on every plan; in the current "Spaces" UI it is not a
49
75
  Features toggle but a **tab** — add it with `+` in the space's tab bar.)
50
76
  - **Let CI/CD integrations stamp environment position** (the deployments
51
77
  panel), so it never needs to be a status at all.
@@ -56,6 +82,7 @@ status's board category, the closest match when one is missing, and the
56
82
  one-line remap. Run it whenever the wiring feels uncertain; it changes
57
83
  nothing.
58
84
 
59
- Walking the parent through pipeline stages at *release* time is the
60
- `release.stages` ladder — deferred in this provider's v1 — not a job for the
61
- lifecycle skills.
85
+ Walking the parent through pipeline stages at *release* time is the host
86
+ project's job, not skitterspec's: it writes nothing about releases or
87
+ deployments on any provider. A leftover `release.stages` is flagged by
88
+ `spec-sync doctor` as unused config.
@@ -85,7 +85,7 @@ function findEngine(projectDir) {
85
85
  if (process.env.SKITTERSPEC_BIN) return process.env.SKITTERSPEC_BIN
86
86
  let at = projectDir
87
87
  for (let i = 0; i < 6 && at; i++) {
88
- for (const name of ['skitterspec', 'skitterspec-linear', 'skitterspec-jira']) {
88
+ for (const name of ['skitterspec', 'skitterspec-linear', 'skitterspec-jira', 'skitterspec-github', 'skitterspec-azure-boards']) {
89
89
  const candidate = path.join(at, 'node_modules', '.bin', name)
90
90
  if (fs.existsSync(candidate)) return candidate
91
91
  }
@@ -82,7 +82,7 @@ function findEngine(projectDir) {
82
82
  if (process.env.SKITTERSPEC_BIN) return process.env.SKITTERSPEC_BIN
83
83
  let at = projectDir
84
84
  for (let i = 0; i < 6 && at; i++) {
85
- for (const name of ['skitterspec', 'skitterspec-linear', 'skitterspec-jira']) {
85
+ for (const name of ['skitterspec', 'skitterspec-linear', 'skitterspec-jira', 'skitterspec-github', 'skitterspec-azure-boards']) {
86
86
  const candidate = path.join(at, 'node_modules', '.bin', name)
87
87
  if (fs.existsSync(candidate)) return candidate
88
88
  }
@@ -492,9 +492,19 @@ missing alias.
492
492
  Sync is **one-way**: the repo is the source of truth and the tracker is a
493
493
  **generated mirror**. It ships `/spec-push` (repo→tracker; computes a create/update plan
494
494
  against a committed last-pushed snapshot and applies it) and `/spec-status`
495
- (read-only drift report — what would push, and whether the tracker's
496
- workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
497
- the tracker is never read back or merged.
495
+ (read-only — what would push, and whether someone edited a description there),
496
+ backed by a `spec-sync` CLI. There is no content pull — the tracker is never
497
+ read back or merged.
498
+
499
+ **It tells, it doesn't track.** Skitterspec sends the tracker its own
500
+ transitions — a spec starting, a phase finishing, a spec completing or being
501
+ cancelled — and nothing else. A status someone changes on the tracker is
502
+ theirs to have changed: it is never read back, never reported, and only
503
+ overwritten when the spec itself next moves. Releases and deployments are the
504
+ host project's to record. The one release question skitterspec answers is which
505
+ tickets a commit range contains (`spec-sync released <range> --json`, each row
506
+ with its spec and url), because only the `Refs:` trailers know. What the
507
+ project's CI does with that list is its own business.
498
508
 
499
509
  **One-way means the repo wins, and it now wins *loudly*.** Those are different
500
510
  claims and only the first was ever in doubt. A ticket's description is generated
@@ -36,7 +36,7 @@ A verdict sentence, a blank line, then a two-column table:
36
36
  | **Branch** | `spec/feat-foo` · 3 commits, clean |
37
37
  | **Built** | POST /orders handler, orders schema |
38
38
  | **Tests** | 128 passed · dotnet test |
39
- | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) · **remote** off |
39
+ | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) also [http://100.126.111.34:7760/…](http://100.126.111.34:7760/…) (tailnet) · **remote** off |
40
40
  | **Follow-ups** | none |
41
41
  | **Next** | `/spec-next` → phase 3 (Auth) |
42
42
 
@@ -89,6 +89,7 @@ the options may be offered after the table.
89
89
 
90
90
  - **local** — [http://127.0.0.1:7760/7e9e123e7540/feat-orders](http://127.0.0.1:7760/7e9e123e7540/feat-orders)
91
91
  - **network** — [http://192.168.0.136:7760/7e9e123e7540/feat-orders](http://192.168.0.136:7760/7e9e123e7540/feat-orders)
92
+ - also — [http://100.126.111.34:7760/7e9e123e7540/feat-orders](http://100.126.111.34:7760/7e9e123e7540/feat-orders) (tailnet)
92
93
  - **remote** — off · `/spec-remote-review` turns it on
93
94
 
94
95
  live: off — /spec-live to put it live
@@ -156,6 +157,17 @@ on. Dropping the line is what left a reader who had walked out of the house with
156
157
  nothing on screen to tell them a remote page was even possible — they could not
157
158
  ask for what they could not see.
158
159
 
160
+ **The `network` tier may carry alternates, and they are part of it — not a fourth tier.**
161
+ The engine offers the other addresses this machine has (a
162
+ tailnet one, for a reader off the wifi) as `also:` lines under `network`, each
163
+ labelled; `--json` carries them as that tier's `alternates`, labelled the same
164
+ way. Copy them as indented sub-bullets under the network bullet, verbatim,
165
+ labels included. This is not an amendment to the tier rule: the stack is the
166
+ engine's, copied rather than composed, and the engine was already printing
167
+ these — a contract that made the skill edit them out was the composing this
168
+ section forbids. A machine with no alternates renders the banner exactly as
169
+ above, minus the sub-bullet.
170
+
159
171
  **Only promise a wait the transport can deliver.** The wait itself is real in
160
172
  every case; what varies is what carries it back, not whether the run is
161
173
  stopped. Three transports, three true sentences:
@@ -298,8 +310,11 @@ behind, and they cannot.
298
310
  `fetch(location.pathname, …)`, so a page opened at `127.0.0.1:7760/…` and one
299
311
  opened at `192.168.0.136:7760/…` reach the same server process and the same
300
312
  pending store. One wait covers both, and a verdict pressed on either wakes the
301
- same run. Between those two the objection does not apply — which is why both are
302
- offered, and why offering them gives nothing up.
313
+ same run. A `network` alternate — the `also:` tailnet address — is one more
314
+ door into the same room: the same server answers on it, so the wait-coverage
315
+ line stays true with nothing added to it. Between all of those the objection
316
+ does not apply — which is why they are offered, and why offering them gives
317
+ nothing up.
303
318
 
304
319
  **So two things are required, and the second is what the old rule lacked.**
305
320
  Every link is **labelled** with its tier, and one line says
@@ -410,7 +425,7 @@ behind it — say nothing rather than reporting that there was nothing to report
410
425
  | `Landed` | A fast-forward, a tag, a cherry-pick. |
411
426
  | `Worktree` | A worktree provisioned, entered, or torn down. |
412
427
  | `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
413
- | `Review` | The rendered diff page: files, `+`/`−`, and the stack — the same tiers the banner lists, labelled, run together with `·` because a row is one cell, plus `live` where the engine reported one. **No question** — a row cannot be waited on, so a question in one is unanswerable by construction (see *asking implies waiting*). **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
428
+ | `Review` | The rendered diff page: files, `+`/`−`, and the stack — the same tiers the banner lists, labelled, run together with `·` because a row is one cell, with any `network` alternates inline after the network URL (`also <url> (tailnet)`), plus `live` where the engine reported one. **No question** — a row cannot be waited on, so a question in one is unanswerable by construction (see *asking implies waiting*). **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
414
429
  | `Follow-ups` | **Always.** `none`, or one line each. |
415
430
  | `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
416
431
 
@@ -174,7 +174,10 @@ three, in that order — never the bare `page:` path.
174
174
  **Between the two, run the after-land commands.** Where the `integrate` plan
175
175
  ends with a `then, in the primary checkout (<path>), run:` section — the
176
176
  project's `afterLand` — run those commands there, in order, once the
177
- fast-forward has succeeded, and on an already-landed no-op too. A lockfile
177
+ fast-forward has succeeded, and on an already-landed no-op too. Run only the
178
+ lines under that heading; a `skipped (nothing in the land matched):` block
179
+ lists conditional entries this land did not need, and is information, never
180
+ commands. A lockfile
178
181
  refresh is exactly the land that needs them.
179
182
  **Stop at the first non-zero exit**: the run becomes `⚠️`, naming the command that failed and the ones that
180
183
  did not run, and it **never undoes the land and never stops the `down`**.
@@ -205,8 +208,8 @@ the shape; this section carries only what is specific here.
205
208
 
206
209
  - `✅` — the work is done, the tests are green, and the verdict was acted on.
207
210
  - `⚠️` — done and green, with something worth knowing — an after-land command
208
- that failed, for one. `Notes` carries what the after-land commands ran, or
209
- where they stopped; nothing about them when none are configured.
211
+ that failed, for one. `Notes` carries what the after-land commands ran and
212
+ skipped, or where they stopped; nothing about them when none are configured.
210
213
  - `❌` — the tests are red, or the land failed part-way. Quote the failure and
211
214
  say what is standing.
212
215
  - `⏸` — refused before acting: no name given, the name is already a spec, the
@@ -527,7 +527,9 @@ underneath a reader when detection flipped mid-session.
527
527
 
528
528
  So: **relay all three tier lines, every time.** Any `also:` lines sit under
529
529
  `network` — the other addresses this machine has, offered because the
530
- best-guess one can be wrong — so pass them on rather than editing them out.
530
+ best-guess one can be wrong — so pass them on rather than editing them out: in
531
+ the banner they are indented sub-bullets under **network**, labels
532
+ (`(tailnet)`) included, exactly as `.claude/rules/spec-reports.md` shows.
531
533
 
532
534
  **Never read an environment variable to decide anything about the offer** — not
533
535
  `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
@@ -169,8 +169,8 @@ the shape; this section carries only what is specific here.
169
169
  **Fields:** `Tracker` · `Spec` · `Tests` · `Notes` · `Landed` · `Worktree` ·
170
170
  `Follow-ups` · `Next`
171
171
 
172
- `Notes` carries the after-land commands step 6 ran, and where — or, on a
173
- failure, the command that failed and the ones that did not run. A project with
172
+ `Notes` carries the after-land commands step 6 ran, and where, and any it
173
+ skipped because the land matched no `when` — or, on a failure, the command that failed and the ones that did not run. A project with
174
174
  no `afterLand` says nothing about it.
175
175
 
176
176
  **Report this spec and no other.** A completion is the moment it is most
@@ -287,7 +287,10 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
287
287
  3. **Run the after-land commands.** Where the plan ends with a
288
288
  `then, in the primary checkout (<path>), run:` section — the project's
289
289
  `afterLand`, such as a data migration — run those commands in that
290
- directory, in order, after the fast-forward succeeds. **Also on the no-op**:
290
+ directory, in order, after the fast-forward succeeds. Run only the lines under that
291
+ heading; a `skipped (nothing in the land matched):` block lists conditional
292
+ entries this land did not need, and is information, never commands.
293
+ **Also on the no-op**:
291
294
  the engine prints them there deliberately, because a land made on another
292
295
  machine leaves them pending here. **Stop at the first non-zero exit** — a
293
296
  later command may depend on it — and carry on with the rest of the skill:
@@ -538,7 +538,9 @@ underneath a reader when detection flipped mid-session.
538
538
 
539
539
  So: **relay all three tier lines, every time.** Any `also:` lines sit under
540
540
  `network` — the other addresses this machine has, offered because the
541
- best-guess one can be wrong — so pass them on rather than editing them out.
541
+ best-guess one can be wrong — so pass them on rather than editing them out: in
542
+ the banner they are indented sub-bullets under **network**, labels
543
+ (`(tailnet)`) included, exactly as `.claude/rules/spec-reports.md` shows.
542
544
 
543
545
  **Never read an environment variable to decide anything about the offer** — not
544
546
  `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
@@ -189,7 +189,8 @@ Then name the next step:
189
189
 
190
190
  - `/spec` — write a spec; with Jira configured it creates the linked issue in
191
191
  the configured project and stamps the key.
192
- - `/spec-status` — read-only drift report, the safe way to prove the link works.
192
+ - `/spec-status` — read-only report of what would push, the safe way to prove
193
+ the link works.
193
194
  - `/spec-push` — send a spec up.
194
195
 
195
196
  Mention what setup did **not** configure, so the defaults aren't mistaken for
@@ -574,7 +574,9 @@ underneath a reader when detection flipped mid-session.
574
574
 
575
575
  So: **relay all three tier lines, every time.** Any `also:` lines sit under
576
576
  `network` — the other addresses this machine has, offered because the
577
- best-guess one can be wrong — so pass them on rather than editing them out.
577
+ best-guess one can be wrong — so pass them on rather than editing them out: in
578
+ the banner they are indented sub-bullets under **network**, labels
579
+ (`(tailnet)`) included, exactly as `.claude/rules/spec-reports.md` shows.
578
580
 
579
581
  **Never read an environment variable to decide anything about the offer** — not
580
582
  `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left