@skitterbyte/skitterspec-linear 11.0.0 → 13.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 (60) hide show
  1. package/MIGRATION.md +260 -10
  2. package/README.md +32 -2
  3. package/assets/claude-md-section.md +48 -2
  4. package/assets/commands/spec-connect.md +2 -2
  5. package/assets/commands/spec-live.md +2 -2
  6. package/assets/core/SETUP.md +21 -3
  7. package/assets/core/env.config.json.example +9 -3
  8. package/assets/core/env.config.md +102 -30
  9. package/assets/core/gating.config.json.example +4 -0
  10. package/assets/core/gating.config.md +81 -0
  11. package/assets/core/linear.config.md +67 -8
  12. package/assets/review/page.html +1501 -0
  13. package/assets/rules/spec-planning.md +224 -15
  14. package/assets/rules/spec-reports.md +269 -0
  15. package/assets/skills/spec/SKILL.md +64 -13
  16. package/assets/skills/spec-bug/SKILL.md +193 -27
  17. package/assets/skills/spec-cancel/SKILL.md +99 -8
  18. package/assets/skills/spec-claim/SKILL.md +114 -0
  19. package/assets/skills/spec-complete/SKILL.md +123 -22
  20. package/assets/skills/spec-diff/SKILL.md +564 -0
  21. package/assets/skills/spec-hotfix/SKILL.md +202 -22
  22. package/assets/skills/spec-init/SKILL.md +49 -9
  23. package/assets/skills/spec-linear-setup/SKILL.md +86 -7
  24. package/assets/skills/spec-list/SKILL.md +218 -0
  25. package/assets/skills/spec-next/SKILL.md +300 -7
  26. package/assets/skills/spec-push/SKILL.md +45 -22
  27. package/assets/skills/spec-review/SKILL.md +59 -11
  28. package/assets/skills/spec-reviewed/SKILL.md +241 -0
  29. package/assets/skills/spec-start/SKILL.md +426 -66
  30. package/assets/skills/spec-status/SKILL.md +24 -2
  31. package/assets/skills/spec-sync/SKILL.md +47 -11
  32. package/assets/skills/spec-to-main/SKILL.md +42 -20
  33. package/package.json +11 -7
  34. package/src/cli.js +1710 -80
  35. package/src/env/building.js +143 -0
  36. package/src/env/classify.js +91 -0
  37. package/src/env/config.js +57 -9
  38. package/src/env/provision.js +192 -19
  39. package/src/env/proxy.js +34 -1
  40. package/src/env/render.js +3 -12
  41. package/src/env/resolve.js +296 -9
  42. package/src/env/review.js +1329 -0
  43. package/src/env/serve.js +549 -0
  44. package/src/env/teardown.js +13 -6
  45. package/src/gating.js +155 -0
  46. package/src/init.js +124 -2
  47. package/src/prompts.js +10 -1
  48. package/src/vendor/linear/api.js +104 -1
  49. package/src/vendor/linear/cli-sync.js +874 -17
  50. package/src/vendor/linear/config.js +8 -0
  51. package/src/vendor/linear/credentials.js +94 -0
  52. package/src/vendor/linear/doctor.js +35 -0
  53. package/src/vendor/linear/identity.js +105 -0
  54. package/src/vendor/linear/mcp.js +26 -0
  55. package/src/vendor/sync-core/index.js +6 -2
  56. package/src/vendor/sync-core/src/compare.js +74 -5
  57. package/src/vendor/sync-core/src/normalize.js +30 -0
  58. package/src/vendor/sync-core/src/push.js +11 -1
  59. package/src/vendor/sync-core/src/write.js +38 -0
  60. package/LICENSE +0 -21
@@ -0,0 +1,218 @@
1
+ ---
2
+ name: spec-list
3
+ description: List every spec Linear holds — id, title, workflow state, who holds it, and the local spec folder name to paste into /spec-start. Read-only, and it starts nothing. Falls back to the repo's own listing when Linear is unreachable. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-list", "what specs are there", "what is in the backlog", "what is Jane working on", or "list specs from Linear".
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /spec-list — every spec Linear holds
8
+
9
+ > Stay silent while this runs — speak only to ask something you cannot answer
10
+ > yourself, or to report a failure at the moment it happens. Read
11
+ > `.claude/rules/spec-reports.md` before reporting; it defines the block this
12
+ > skill ends with.
13
+
14
+ Read-only. It answers a question and writes nothing — no branch, no folder move,
15
+ no Linear write.
16
+
17
+ Why ask Linear rather than `ls specs/`? Because `/spec-start` moves a spec to
18
+ `specs/in-progress/` **on that spec's own branch**. On the base branch an
19
+ in-flight spec still reads `backlog`, and a teammate's unlanded spec is not on
20
+ disk at all. Linear knows what is actually in progress and who holds it; the repo
21
+ knows what each issue is **called on disk**. The listing joins the two.
22
+
23
+ **Opt-in**: only runs when `specs/.core/linear.config.json` exists. If absent,
24
+ tell the user how to enable Linear sync (`/spec-linear-setup`) and stop.
25
+
26
+ ## 1. Turn the question into flags
27
+
28
+ The user asks in plain language; the engine takes flags. Map, then run:
29
+
30
+ | They ask | Flags |
31
+ |----------|-------|
32
+ | "what specs are there", "/spec-list" | *(none — the live default)* |
33
+ | "what's done", "everything" | `--all` |
34
+ | "what's cancelled", a named state | `--state "<name>"` (repeatable) |
35
+ | "what's next", "the next few in the backlog" | `--next N` |
36
+ | "what am I on", "assigned to me" | `--mine` |
37
+ | "what is Jane on", "Jane's specs" | `--by "Jane"` |
38
+ | "what's in flight", "what's being worked on" | `--in-progress` |
39
+ | "just the first few" | `--limit N` |
40
+ | "include the archived ones" | `--archived` |
41
+
42
+ The default scope is **live** — whatever `config.states` maps `backlog` and
43
+ `in-progress` to. It is deliberately not everything: in a workspace with any
44
+ history, Done dwarfs the rows anyone wanted.
45
+
46
+ `--mine` and `--by` filter by assignee and stack with any one scope flag —
47
+ `--next 5 --mine` and `--in-progress --by "Jane"` both read naturally. The scope
48
+ flags themselves (`--state`, `--all`, `--next`, `--in-progress`) are
49
+ **alternatives**, and the engine refuses any two of them rather than letting one
50
+ win silently.
51
+
52
+ `--next N` is **backlog-only and ordered**, so it does not combine with
53
+ `--state` or `--all`; the engine refuses that pair rather than picking a winner.
54
+ It reproduces Linear's own Backlog order — priority first, then the manual
55
+ drag-order — and when nothing is prioritised it says so, because that order is
56
+ then a person's arrangement rather than a ranking.
57
+
58
+ ## 2. Run the engine
59
+
60
+ ```
61
+ skitterspec spec-sync list [--state <name> …|--all] [--limit N] [--archived] [--json]
62
+ ```
63
+
64
+ - **`transport = api`** (a key is set) → it queried Linear and printed the
65
+ listing. **Relay its output verbatim** and stop. Do not re-format it and do not
66
+ make Linear calls of your own — the engine already joined the local spec
67
+ folders on.
68
+ - **`transport = mcp`** → the engine made no call. Go to step 3.
69
+ - Anything else it prints on one `spec-sync list:` line is a **degradation**, not
70
+ a crash — go to step 4.
71
+
72
+ **These two failures are not the same, and the engine says so.**
73
+ Relay the difference rather than flattening it. `--mine` when nobody can say
74
+ who you are (a shared or bot key, an offline machine) is an ordinary state: it
75
+ prints one line, lists nothing and exits 0. `--by "someone"` that matches no
76
+ user, or matches several, is a wrong argument: it lists the candidates or says
77
+ there are none, and exits non-zero. Neither ever falls back to the whole team —
78
+ a heading promising one person's work over everyone's is the failure both are
79
+ written to avoid.
80
+
81
+ ## 3. The MCP path
82
+
83
+ Only when the engine said `transport = mcp`. Discover the issue **list** tool at
84
+ runtime the way `/spec-push` describes (`list_issues`). If Linear isn't connected
85
+ or the tool is missing, go to step 4 — do not stop with nothing.
86
+
87
+ Then, per state you are listing:
88
+
89
+ - **One call per state.** The tool's `state` takes a single state name, not a
90
+ list, so the live default is **two** calls (`backlog` and `in-progress` from
91
+ `config.states`). Merge the results and de-duplicate by identifier.
92
+ - **Ask for the fields you print** — `fields: ["title", "status", "assignee",
93
+ "url", "parentId"]`. `parentId` is not optional: it is the discriminator in the
94
+ next bullet, and it is absent from the default response.
95
+ - **Filter out phase sub-issues yourself.** The tool can filter *to* a parent
96
+ (`parentId`) but has no "parentless" filter, so **you** must drop every issue
97
+ that came back carrying a `parentId`. A spec issue has none; a phase sub-issue
98
+ does.
99
+ - **Page, and never cap silently.** `limit` defaults to 50 and maxes at 250;
100
+ follow `cursor` until it runs out. If you stop early, say so on its own line
101
+ and say what you stopped at.
102
+ - Pass `includeArchived` only when the user asked for `--archived`.
103
+ - **Assignee needs no identity lookup here.** The tool takes `assignee` as a
104
+ user id, name, email **or the literal `"me"`** — so `--mine` is
105
+ `assignee: "me"` and `--by "Jane"` is `assignee: "Jane"`. Do **not** call
106
+ `spec-sync whoami` on this path; it exists for the API path, which needs an
107
+ id. If the name matches nobody, say so and list nothing.
108
+
109
+ **`--next` cannot be fully reproduced over MCP, and you must say so.** The tool
110
+ returns `priority` as a field, so order by that; but `sortOrder` — Linear's
111
+ manual drag-order — is **not among the fields it can return** at all, and its
112
+ `orderBy` offers only `createdAt` and `updatedAt`. So within one priority you
113
+ are printing Linear's default order, not the Backlog order. Say that on its own
114
+ line rather than letting the rows imply an order they do not have:
115
+
116
+ ```
117
+ sortOrder is unavailable over MCP — within a priority these are in Linear's
118
+ default order, not the Backlog drag-order. Set a Linear API key for the real one.
119
+ ```
120
+
121
+ **The current phase, on in-progress rows only.** For each row whose state is the
122
+ in-progress one, call the list tool again with `parentId` set to that issue and
123
+ `fields: ["title", "status"]`. Order the children by **identifier**, numerically
124
+ — `sortOrder` is unavailable here for the same reason it is under `--next`, and
125
+ sub-issues are minted in phase order, so the identifier carries it. Print the
126
+ live one as `2/5 — <title>`. Three shapes, none of them guessed at:
127
+
128
+ - **No child in progress** — print nothing extra. A spec sits between phases all
129
+ the time; that is not a missing phase.
130
+ - **More than one** — print the lowest-numbered, then append
131
+ `(+N more in progress)`. Two people on one spec is real, not an error.
132
+ - **`mapping.phases` is `inline` for this spec's bucket** — skip the call
133
+ entirely. Those phases live in the spec issue's own description, so there are
134
+ no children, and asking would report "no phase in progress" for a spec that is
135
+ mid-build.
136
+
137
+ Do **not** make this call for backlog, done or cancelled rows — one lookup per
138
+ in-progress row is the budget, and there are rarely many.
139
+
140
+ Then join locally — no second Linear call:
141
+
142
+ ```
143
+ skitterspec spec-sync linked --json
144
+ ```
145
+
146
+ That is `[{ spec, bucket, identifier }]`. Key it by `identifier` and attach each
147
+ issue's local `spec` folder name. Format the rows exactly as the API path does
148
+ (step 5), including the archived-exclusion line and the `showing N of M` count.
149
+
150
+ ## 4. Degrade — never leave them with nothing
151
+
152
+ Linear unreachable, no credential, no MCP tool, a Linear error: **do not stop**.
153
+ Print the local listing instead:
154
+
155
+ ```
156
+ skitterspec spec-sync linked --json
157
+ ```
158
+
159
+ Show `spec`, `bucket` and `identifier`, under a one-line banner naming what is
160
+ missing and why it may be wrong:
161
+
162
+ ```
163
+ Linear unreachable (<reason>) — this is the repo's own listing.
164
+ Buckets are this branch's; a spec someone else has started still reads "backlog" here.
165
+ ```
166
+
167
+ That caveat is the point of the banner. A local bucket is not evidence of a
168
+ spec's real state — it is evidence of what this branch knows, which is exactly
169
+ what the command exists to go beyond. A query command that fails closed is one
170
+ people stop typing.
171
+
172
+ ## 5. Report
173
+
174
+ Relay the listing. Whatever the path, three things must survive into what the
175
+ user reads:
176
+
177
+ - **The count, as `showing N of M`.** If they are not the same number, say what
178
+ was left out and how to see it.
179
+ - **The archived line.** Excluded by default, and said so — a blind spot named
180
+ rather than left to be discovered.
181
+ - **The phase, on in-progress rows** — `2/5 — <title>`, after the assignee.
182
+ - **A row with no local match stays in**, marked `— (not linked here)`. It is not
183
+ noise: a teammate's unlanded spec, or one authored inside another spec's
184
+ worktree, is precisely what the repo could not have told them.
185
+
186
+ Finish with the hand-off, because every row carries the folder name for it —
187
+ as a sentence, not a fenced block: say that `/spec-start <name>` starts any of
188
+ them, with the name copied from a row.
189
+
190
+ **Never start it yourself.** `/spec-start` provisions a branch, moves a folder
191
+ and commits, and it has dirty-tree refusals this skill does not reproduce.
192
+ Offering the name is the whole job.
193
+
194
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
195
+ the shape; this section carries only what is specific here.
196
+
197
+ **The listing goes above the block, unflattened.** It is rows of other people's
198
+ work; the block is the verdict on this run of the query. Do not fold the listing
199
+ into fields, and do not repeat it there.
200
+
201
+ **Verdicts**
202
+
203
+ - `✅` — the listing is Linear's, joined to the local folder names.
204
+ - `⚠️` — listed, with a caveat that changes how the rows read: a paging cap you
205
+ stopped at, or `sortOrder` unavailable over MCP so `--next` is not really
206
+ Backlog order.
207
+ - `⏸` — `--mine` with no identity available. It lists nothing and that is an
208
+ ordinary state, not a failure.
209
+ - `❌` — the query was wrong: `--by` matching nobody or several, or two scope
210
+ flags together.
211
+
212
+ **Fields:** `Follow-ups` · `Next`
213
+
214
+ **No `Tracker` row.** This skill names many specs and owns none, so an id row
215
+ would have to pick one. The counts — `showing N of M` — and the archived
216
+ exclusion belong to the listing above, where the engine already prints them.
217
+
218
+ `Next` is the hand-off: `/spec-start <name>`, with a name copied from a row.
@@ -1,10 +1,15 @@
1
1
  ---
2
2
  name: spec-next
3
- description: Build the next unfinished phase of the spec currently in flight on this checkout — pre-flight, implement with tests, record progress and refresh the tracker. Refuses when no spec is in flight rather than guessing one. Use when the user says "/spec-next", "build the next phase", "continue the spec", or "carry on with this spec".
3
+ description: Build the next unfinished phase of the spec in flight for this session — pre-flight, implement with tests, record progress and refresh the tracker. Refuses when no spec is in flight rather than guessing one, and builds wherever that spec resolves rather than wherever the session happens to stand. Use when the user says "/spec-next", "build the next phase", "continue the spec", or "carry on with this spec".
4
4
  ---
5
5
 
6
6
  # /spec-next — build the next phase of the spec in flight
7
7
 
8
+ > Stay silent while this runs — speak only to ask something you cannot answer
9
+ > yourself, or to report a failure at the moment it happens. Read
10
+ > `.claude/rules/spec-reports.md` before reporting; it defines the block this
11
+ > skill ends with.
12
+
8
13
  It assumes the workbench is already set up: a spec is **in flight** on this
9
14
  checkout, and this skill implements its next unfinished phase. Putting a spec in
10
15
  flight — provisioning, moving it to `in-progress`, getting its branch here — is
@@ -13,7 +18,36 @@ flight — provisioning, moving it to `in-progress`, getting its branch here —
13
18
 
14
19
  ## 1. Identify the spec in flight
15
20
 
16
- Resolve **in this order**, and stop at the first that answers:
21
+ **`--worktree <path>` answers before anything else.** When the invocation names a
22
+ worktree, that is the spec to build and that is where it is built — cwd is not
23
+ consulted. It is how `/spec-start` carries on into phase 1 without moving your
24
+ session, and you can type it yourself.
25
+
26
+ **This is not a loosening of the refusal below.** That refusal exists against
27
+ *guessing* which spec to build, and a path someone typed is not a guess. A bare
28
+ `/spec-next` still refuses exactly as it does today.
29
+
30
+ **Validate the path before writing a line into it.** Run the resolver *from* the
31
+ path, so the answer comes from where you are about to write:
32
+
33
+ ```
34
+ cd "<path>" && skitterspec spec-env resolve
35
+ ```
36
+
37
+ Check the `worktree:` line it prints is that same path. If it is not — or the
38
+ command reports that isolation is not enabled — refuse and stop, naming what you
39
+ were given. A path that is not a provisioned worktree must never become a place
40
+ to write code.
41
+ **Read the output, not the exit status** — it exits 0 even when it
42
+ cannot resolve anything.
43
+
44
+ **Not `--dir <path>`.** That flag sets the **repo root**, not the worktree to
45
+ resolve from, so it answers a different question: on a repo with two or more
46
+ worktrees it refuses with *"no spec given, and N specs have worktrees"* and
47
+ validates nothing at all. The `cd` form is what makes the path itself the
48
+ evidence.
49
+
50
+ Otherwise resolve **in this order**, and stop at the first that answers:
17
51
 
18
52
  1. **The live spec of this checkout** — run
19
53
  `skitterspec spec-env live status` and read its `live:` line. `live: yes`
@@ -24,15 +58,60 @@ Resolve **in this order**, and stop at the first that answers:
24
58
  opened in one is its own workbench.
25
59
  3. **The current branch, in `checkout` mode** — no worktrees exist, so the
26
60
  branch the checkout is on names the spec.
61
+ 4. **The only spec provisioned in this repo** — ask the engine, with no spec
62
+ named, from wherever you happen to be standing:
63
+
64
+ ```
65
+ skitterspec spec-env resolve
66
+ ```
67
+
68
+ Take its `spec:` line **only when it names exactly one spec**.
69
+
70
+ This is the **durable** rung, and that is the whole reason it exists. Rungs 1
71
+ to 3 all read *session* state, and session state does not survive a `/clear`,
72
+ a new terminal tab, or coming back tomorrow — while the provisioned worktree
73
+ they are each a proxy for is **on disk** and survives all three. `/spec-start`
74
+ does leave the session standing in the worktree and that `cd` is real; what it
75
+ is not is durable. Without this rung, a repo holding exactly one answer sends
76
+ the operator away to re-supply something it already knew.
77
+
78
+ It is also the resolution every other bare command in this workflow already
79
+ uses — the worktree you are standing in, else the sole provisioned spec — so
80
+ this rung is what stops `/spec-next` being a silent exception to a rule
81
+ `.claude/rules/spec-planning.md` states has none left.
82
+
83
+ **Say which spec you resolved and how**, before writing a line of it:
84
+ *"not standing in a worktree — `<spec>` is the only spec provisioned"*. Rungs
85
+ 1 to 3 are self-evident to whoever typed the command; this one is not, and
86
+ the operator cannot see from where they sit what you picked.
87
+
88
+ **Several worktrees stay a refusal.** The engine names them and resolves
89
+ nothing — exactly the ambiguity the refusal below exists for. Relay its list
90
+ unchanged and stop; never pick from it.
91
+
92
+ WHAT WOULD FOOL THIS: a worktree left behind by a declined teardown is still
93
+ a worktree, so a finished spec can go on counting as provisioned. That widens
94
+ the candidate set, so the failure it produces is an extra candidate — an
95
+ ambiguity the engine refuses on — and never a wrong spec built. It cannot
96
+ manufacture an *absence*, which is why the absence below is still worth
97
+ refusing on.
27
98
 
28
99
  **If none answers, refuse and stop:**
29
- `no spec in flight — run /spec-start <name> to put one on this checkout`.
100
+ `no spec in flight — run /spec-start <name> to start one`.
101
+
102
+ That now answers a real absence — no worktree anywhere, the engine included —
103
+ rather than a session that merely lost track of where it was standing.
30
104
 
31
105
  **Never fall back to the spec "in context".** A spec discussed in conversation
32
106
  is not a spec in flight, and this skill writes real code: building the wrong
33
107
  spec's phase produces commits on a branch nobody asked for. The refusal is
34
108
  cheap; the mistake is not.
35
109
 
110
+ **Rung 4 is not that fallback wearing a hat.** A provisioned worktree is a
111
+ record that someone ran `/spec-start`: it is on disk, the engine reads it, and it
112
+ either names one spec or refuses. A spec named in conversation is a guess about
113
+ intent with nothing underneath it, and no number of them ever resolves to one.
114
+
36
115
  A **name argument** is accepted, but it must *match* the spec in flight — it
37
116
  narrows a re-run, it does not select a different spec. A mismatch refuses,
38
117
  naming both.
@@ -79,6 +158,17 @@ once it is over. Without a provider this is a no-op and nothing below changes.
79
158
  them. Skip it and a started spec stays mirrored as a phase-less issue.
80
159
  - **Never mint the spec issue.** An unlinked spec is skipped, not created —
81
160
  `/spec-push` is how someone opts in.
161
+ - **Backfill a missing assignee silently; never ask.** Only when
162
+ `sync.fieldOwnership` includes `assignee`. If the spec records nobody and
163
+ `skitterspec spec-sync whoami --json` answers, stamp it with
164
+ `skitterspec spec-sync assign <spec> --to <id> --name "<name>"` and let the
165
+ push above carry it. If identity is unknown, skip it in silence.
166
+ **Do not prompt**: an assignment question in the middle of a build is an
167
+ interruption with no deadline, and `/spec-claim` answers it whenever the
168
+ operator likes.
169
+ - **A spec assigned to someone else is left alone.** Say so once
170
+ (`assigned to <name> — /spec-claim to take it`) and change nothing. Picking up
171
+ a colleague's spec is a decision, not a side effect of running the next phase.
82
172
  - **Never fatal.** If the push fails — offline, no key, a Linear error — say so
83
173
  and **carry on with the build**. The repo is correct regardless; the mirror is
84
174
  disposable and the next push repairs it.
@@ -89,6 +179,33 @@ once it is over. Without a provider this is a no-op and nothing below changes.
89
179
  branch/PR you pushed in step 2; that's expected and the repo still wins on the
90
180
  next `/spec-push`.
91
181
 
182
+ **Before building, compare the worktree against where you are standing.** Take
183
+ the `worktree:` line from `skitterspec spec-env resolve <spec>` and compare it
184
+ with this session's cwd, resolving both paths first so a symlinked or
185
+ trailing-slash spelling of one tree does not read as two.
186
+
187
+ Same tree — the ordinary case, since `/spec-start` leaves the session standing
188
+ in it — and the rest of this step is inert.
189
+ **Different trees, and the discipline below applies however the spec was resolved.**
190
+ `--worktree <path>` is one way to get here and §1's rung 4 is another: a bare
191
+ `/spec-next` typed from the primary checkout resolves the sole provisioned spec
192
+ and builds it somewhere this session is not. What makes the discipline necessary
193
+ is the two trees, so that is what it is conditioned on — not the shape of the
194
+ invocation, which cannot see rung 4 at all.
195
+
196
+ Record the baseline before you write anything:
197
+
198
+ ```
199
+ skitterspec spec-env resolve <spec> --record-primary
200
+ ```
201
+
202
+ Then build as below, with one discipline on top.
203
+ **The session is not standing in the worktree**, so every write takes an
204
+ absolute path under it and every command
205
+ is prefixed `cd "<worktreePath>" &&` — typecheck and tests included. A single
206
+ relative path lands the work in the primary checkout, on the base branch, and
207
+ nothing about it looks wrong at the time. Step 4b is what catches it.
208
+
92
209
  Then build it, following the project rules in `.claude/rules/*.md` and `CLAUDE.md`:
93
210
 
94
211
  - Work task by task through the phase file. Make focused edits that match
@@ -134,8 +251,184 @@ sub-issue jump from Backlog straight to Done, with nothing visible in between.
134
251
  - **Say what happened** in the skill's report: mirror updated, skipped as
135
252
  unlinked, or failed with the reason.
136
253
 
137
- ## 5. Report
254
+ ## 4b. Prove nothing leaked into the primary checkout
255
+
256
+ **Only when the resolved worktree is not this session's cwd** — the same
257
+ comparison step 3 made, and it holds however the spec was resolved. Standing in
258
+ the worktree there is no second tree to have written into, so this step does not
259
+ apply and there is nothing to check.
260
+
261
+ WHAT WOULD FOOL THIS CHECK: it watches the **primary checkout** and nothing
262
+ else, so a build run from inside *another* spec's worktree would leak there
263
+ unseen. That is left unhandled deliberately rather than overlooked — reaching it
264
+ takes an explicit `--worktree` typed from a second worktree — and the cost of the
265
+ gap is a missed leak, never a false accusation.
266
+
267
+ The phase is built and its progress recorded — all of it written into a tree this
268
+ session is not standing in. Before reporting any of it as done:
269
+
270
+ ```
271
+ skitterspec spec-env resolve <spec> --assert-primary-clean
272
+ ```
273
+
274
+ - **Exit 0, "primary checkout clean"** — carry on.
275
+ - **Non-zero** — stop and relay the engine's message unchanged. It names the
276
+ paths and both readings: this build wrote them and they belong in the worktree,
277
+ or something else did and the baseline wants re-recording.
278
+ **Do not guess which, and do not delete anything.**
279
+ A path that appeared is not proof of who put it there.
280
+ - **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
281
+ claims nothing; say so in one line and carry on. An absence is not evidence.
282
+
283
+ ## 5. Render the page — then offer the review, never write it
284
+
285
+ **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
286
+ present). Without it there is no worktree to read and this step does not exist.
287
+
288
+ The phase is built, its tests are green, and nothing is committed yet. That is
289
+ the moment the page is about, so render it now — **after** the tests pass and
290
+ **before** the commit:
291
+
292
+ ```
293
+ skitterspec spec-env review <spec>
294
+ ```
295
+
296
+ **This is free.** It is the engine reading git and splicing text into a template;
297
+ the diff never passes through you, so a 266KB patch costs nothing. Report the
298
+ path it prints and move on.
299
+
300
+ **Then offer `/spec-diff`. Do not run it.** The written review is the part that
301
+ costs — roughly **700 output tokens**, because writing it means reading the diff
302
+ — and that spend is the operator's call, not a default.
303
+
304
+ **The offer is the `Review` row of step 6's block** — the counts, the page link
305
+ and a question, in one row:
306
+
307
+ | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
308
+
309
+ **It ends in a question, addressed to someone.** It was once a fenced block of
310
+ engine output, and it fired on every phase and was never once taken: two quoted
311
+ lines under the test counts, addressed to nobody, with the report then closing
312
+ on *"commit this first"* — the last instruction the reader got was to move on,
313
+ so they did. A row in a labelled table is findable; a question in it is
314
+ answerable. Both halves are load-bearing.
315
+
316
+ **Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
317
+ the page and the question stay in the same row: two adjacent rows about one page
318
+ make the reader resolve a distinction before acting on either. A later edit that
319
+ moves it out of the block, or separates the link from the question, undoes this
320
+ and should be read as a regression rather than tidying.
321
+
322
+ **Non-blocking, deliberately.** Do not end your turn waiting on the answer.
323
+ Phases get chained — `/commit && /spec-next` typed as one line — and a question
324
+ that stops the run taxes every phase to fix a problem the row already fixes.
325
+
326
+ Relay the **`open:`** line the engine prints, not the bare path: a path is not
327
+ clickable in any terminal, and a page nobody can open is a page nobody reads.
328
+
329
+ - **Never write the review unasked**, and **never publish**. Publishing leaves
330
+ something behind that this tooling cannot remove, so it is always something
331
+ someone asks for. A `file://` link is no use on a phone, and saying so **is**
332
+ the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
333
+ **Follow the `reader:` line the engine printed — do not sniff for it.** It
334
+ answers where the person reading this is sitting, and the offer changes with it:
335
+
336
+ - **absent** (`unknown`) — the `file://` URL, exactly as always. **Do not warn:**
337
+ unknown is the ordinary state of a local machine, and a warning there is an
338
+ accusation against a healthy session.
339
+ - **`local`** — the `file://` URL.
340
+ - **`remote`** — the engine has already stood its local server up and put a URL
341
+ the reader can open on `open:`. So there is **nothing special to say**: relay
342
+ that line like any other. Any `also:` lines under it are the other addresses
343
+ this machine has, offered because the best-guess one can be wrong — pass them
344
+ on rather than editing them out.
345
+
346
+ **Never read an environment variable to decide this** — not `SSH_CONNECTION`,
347
+ not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
348
+ and in `--json`, and a second implementation here could not be tested and would
349
+ drift.
350
+
351
+ **Serving is the engine's to do; publishing is never.** A `remote` reader
352
+ authorises a local server — one process, ended by one flag, leaving nothing
353
+ behind — and authorises nothing else. Publishing leaves a page this tooling
354
+ cannot remove, so it stays an ask in every case, always. If the engine could not
355
+ serve (a busy port, a machine with no network address) it falls back to the
356
+ `file://` URL with its marker, and that is when publishing is worth naming.
357
+
358
+ - **Never fatal.** A failed render — no worktree, a git error — is one line and
359
+ the phase is still done. The page is a convenience; the repo is the record.
360
+ - If the project has no isolation config, skip the whole step in silence rather
361
+ than explaining an absence.
362
+
363
+ ## 6. Report
364
+
365
+ Do **not** `git commit` unless the user asks — finish, verify, and wait.
366
+
367
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
368
+ the shape; this section carries only what is specific here.
369
+
370
+ **Verdicts**
371
+
372
+ - `✅` — the phase is built and its tests are green.
373
+ - `⚠️` — built and green, with something worth knowing (a mirror that did not
374
+ refresh, a deviation from the plan).
375
+ - `❌` — the phase's tests are red, or it stopped part-way. Quote the failure.
376
+ - `⏸` — no spec in flight, or the name given does not match the one that is.
377
+
378
+ **Fields:** `Tracker` · `Branch` · `Built` · `Tests` · `Snags` · `Review` ·
379
+ `Follow-ups` · `Next`
380
+
381
+ ## 6a. End in a picker
382
+
383
+ The block says what happened; this is what to do about it. Offer the same four
384
+ endings the review page carries, so a review finishes the same way wherever the
385
+ reader is standing — the page, a pasted code, or here.
386
+
387
+ | Option | Does |
388
+ |--------|------|
389
+ | `Reviewed` | Claims the waiting pass and routes on its verdict |
390
+ | `Commit` | Runs the project's commit skill, and stops |
391
+ | `Commit & Continue` | Commits, then `/spec-next` — and **stops there** |
392
+ | `Discuss` | Asks what is up; changes nothing |
393
+
394
+ **`Reviewed` only when a pass is actually waiting.** The render's `pending:`
395
+ block already says. Offering a pickup with nothing to pick up is the empty
396
+ gesture this exists against — the other three stand on their own.
397
+
398
+ **Do not restate the routing.** `/spec-diff` §2, §2a and §4 own it, including
399
+ the commit hand-off through `review.commitWith` and what `commit-continue` does
400
+ after. Two copies of a routing rule is how the two come to disagree.
401
+
402
+ **It does not break a chained run.** `/commit && /spec-next` is typed as one
403
+ line and the picker appears at the **end**, by which point the chain has already
404
+ finished. The cost that was feared here — a question stopping a run mid-way — is
405
+ not a cost this placement has.
406
+
407
+ **Nothing may claim a pass without a pick.** `/spec-reviewed` is user-only by
408
+ *harness enforcement*, because prose alone once failed to stop an agent claiming
409
+ a pass nobody asked it to. A pick keeps the **property** that makes that safe —
410
+ a person in the conversation chose, and a device that reaches the page cannot —
411
+ while routing around the **mechanism**, since the claim runs downstream of the
412
+ pick. That trade is deliberate and it has exactly one condition: a run that
413
+ shows no picker claims nothing, and a picker nobody answered claims nothing.
414
+
415
+ WHAT WOULD FOOL THIS: a picker shown reflexively at the end of every run trains
416
+ the reader to dismiss it, and a dismissed picker is indistinguishable from a
417
+ considered decline. So offer it where there is a real choice, and let the `Next`
418
+ row carry the rest.
419
+
420
+ `Next` names the commit and then the phase, as
421
+ `/commit, then /spec-next → phase 3 (Auth)`, so the block says what to do and
422
+ which phase is next without a line of prose for either.
423
+
424
+ **The commit is not optional politeness.** Step 6 above deliberately leaves the
425
+ phase uncommitted, and §2 of this very skill refuses to build the next phase on
426
+ top of an uncommitted one — so a `Next` that names only `/spec-next` sends the
427
+ reader straight into that refusal. The two halves are four hundred lines apart,
428
+ which is exactly how they drifted.
138
429
 
139
- Summarise what was implemented, the test result (quote failures if any), and
140
- which phase is next. Do **not** `git commit` unless the user asks finish,
141
- verify, and wait.
430
+ **Step 5's offer is the `Review` row.** It is not a paragraph after the block,
431
+ because nothing is after the block: the counts, the page link and the question
432
+ go in one row, above `Follow-ups` and `Next`. Step 5 renders before the commit
433
+ and this step is where its offer lands, so the two must not disagree about where
434
+ it goes.