@skitterbyte/skitterspec 21.0.0 → 22.1.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 (43) hide show
  1. package/MIGRATION.md +218 -0
  2. package/README.md +127 -6
  3. package/assets/claude-md-section.md +42 -18
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-remote-review.md +22 -0
  6. package/assets/commands/spec-skip.md +24 -0
  7. package/assets/core/env.config.json.example +6 -3
  8. package/assets/core/env.config.md +183 -25
  9. package/assets/hooks/main-guard.cjs +145 -0
  10. package/assets/hooks/review-gate.cjs +8 -3
  11. package/assets/review/page.html +1174 -101
  12. package/assets/rules/offered-unblocks.md +100 -0
  13. package/assets/rules/spec-planning.md +195 -14
  14. package/assets/rules/spec-reports.md +181 -29
  15. package/assets/skills/no-spec/SKILL.md +219 -0
  16. package/assets/skills/spec/SKILL.md +248 -21
  17. package/assets/skills/spec-bug/SKILL.md +34 -24
  18. package/assets/skills/spec-cancel/SKILL.md +29 -0
  19. package/assets/skills/spec-complete/SKILL.md +25 -0
  20. package/assets/skills/spec-diff/SKILL.md +164 -37
  21. package/assets/skills/spec-hotfix/SKILL.md +22 -19
  22. package/assets/skills/spec-init/SKILL.md +4 -2
  23. package/assets/skills/spec-next/SKILL.md +166 -58
  24. package/assets/skills/spec-review/SKILL.md +87 -0
  25. package/assets/skills/spec-reviewed/SKILL.md +31 -5
  26. package/assets/skills/spec-start/SKILL.md +33 -0
  27. package/package.json +1 -1
  28. package/src/cli.js +1612 -172
  29. package/src/env/classify.js +87 -2
  30. package/src/env/config.js +333 -19
  31. package/src/env/hooks.js +102 -33
  32. package/src/env/live.js +124 -1
  33. package/src/env/mainguard.js +240 -0
  34. package/src/env/provision.js +106 -10
  35. package/src/env/registry.js +70 -6
  36. package/src/env/resolve.js +128 -5
  37. package/src/env/review.js +854 -29
  38. package/src/env/reviewers/coderabbit.js +228 -0
  39. package/src/env/reviewers.js +517 -0
  40. package/src/env/serve.js +474 -50
  41. package/src/env/supervise.js +8 -1
  42. package/src/env/teardown.js +12 -1
  43. package/src/init.js +185 -24
@@ -0,0 +1,100 @@
1
+ # Offered Unblocks
2
+
3
+ A refusal says what is wrong. Some refusals can also say **what would fix it**,
4
+ and where the engine declares that, a skill may offer it rather than leaving the
5
+ operator at a dead end.
6
+
7
+ This file is the whole contract. A skill's own text says only *that* it acts on
8
+ an offer, never how.
9
+
10
+ ## The engine declares; the skill relays
11
+
12
+ An offer is a field on a refusal, never a judgement a caller makes from the
13
+ reason text:
14
+
15
+ ```json
16
+ {
17
+ "blocked": true,
18
+ "reason": "feat-x's worktree has uncommitted changes — commit or stash them…",
19
+ "offer": { "kind": "satisfy", "label": "Commit first, then go live", "command": "/commit" }
20
+ }
21
+ ```
22
+
23
+ **Absent stays absent.** Most refusals carry no `offer` at all, and that is the
24
+ correct, common answer. Where there is none, relay the refusal and stop.
25
+
26
+ **Never infer one.** If the engine did not declare an offer, there is not one —
27
+ however obviously the reason seems to name a fix. The boundary that matters most
28
+ lives in that decision, and it must not be re-litigated per skill:
29
+ **work that is not the operator's is never offered up.** A live workbench another
30
+ spec holds is that spec's operator's to free, so it declares nothing, and a
31
+ helpful picker is exactly how that would creep back in.
32
+
33
+ ## Two kinds
34
+
35
+ | `kind` | Means | Recorded |
36
+ |--------|-------|----------|
37
+ | `satisfy` | do what the guard asked for | no — nothing was weakened |
38
+ | `bypass` | step past a guard still unsatisfied | **yes** |
39
+
40
+ ## Relay first, then offer
41
+
42
+ In that order, and never instead. A picker that replaced the refusal would hide
43
+ what is wrong behind a choice about what to do next — and the refusal text is
44
+ where every exit is named, including the ones the picker does not offer.
45
+
46
+ Then, only where `offer` is present, put **two** options to the operator: the
47
+ offer's `label`, and Cancel.
48
+
49
+ - **`satisfy`** — run the command, then retry the original operation.
50
+ - **`bypass`** — run the command, then carry on.
51
+ - **Cancel** — stop. Record nothing: declining to bypass is not a decision
52
+ anyone needs to audit.
53
+
54
+ **The label is the engine's, shown verbatim.** Never reword a bypass into
55
+ something easier-sounding, never pre-select it, and never add a third option. A
56
+ bypass that arrives pre-selected is a bypass by default wearing a question mark.
57
+
58
+ ## A bypass is offered once per obligation
59
+
60
+ The engine enforces this; a skill does not count anything. Spend the offer by
61
+ recording it **when you raise the picker**:
62
+
63
+ ```
64
+ skitterspec spec-env review gate <spec> --offered
65
+ ```
66
+
67
+ After that the gate declares no offer for the same arming, so the next refused
68
+ commit is a plain refusal with its exits named in the text. A new phase ending
69
+ arms the gate again and is asked in its own right.
70
+
71
+ **Spent by being made, not by being taken.** An operator who declines still saw
72
+ it, and re-offering on every attempt is how a deliberate step becomes the
73
+ default.
74
+
75
+ **Do not spend it by reading.** `review gate --json` is asked on every
76
+ `/spec-next` run; only raising the picker spends the offer.
77
+
78
+ ## What this is not
79
+
80
+ **Not a `--force`.** One was asked for and rejected: a reasonless lift is
81
+ indistinguishable from nobody having looked, where
82
+ `none: additive, nothing to revert` is a decision a reviewer can argue with. The
83
+ gate's exits are unchanged — a committing verdict, or a recorded skip — and a
84
+ bypass is the second of those at an address a person will reach.
85
+
86
+ **Not a replacement for the refusal.** A picker exists only in a harness. The
87
+ engine and the `PreToolUse` hook keep refusing regardless, so this is a layer on
88
+ top of a guard and never the guard itself.
89
+
90
+ **Not Claude lifting a guard aimed at Claude.** `spec-planning.md` draws that
91
+ line for `/allow-main` and `/spec-skip`, and a bypass offer survives it because
92
+ three things hold at once, where a `--force` had none of them:
93
+
94
+ 1. **The operator answers.** Claude may raise the question and cannot answer it.
95
+ 2. **The choice is recorded** — a fixed reason, distinct from anything typed,
96
+ which shows on the next render as history.
97
+ 3. **It is offered once**, held by the sidecar rather than by prose, so it
98
+ survives a `/clear`, a compaction and a fresh session.
99
+
100
+ Remove any one of those and the objection returns.
@@ -1,15 +1,16 @@
1
1
  # Spec Planning
2
2
 
3
3
  Spec-driven development is driven by ten lifecycle skills (plus the
4
- `/spec-connect` and `/spec-live` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
4
+ `/spec-connect`, `/spec-live` and `/spec-remote-review` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
5
5
  and lifecycle stay consistent. Each sets a status on the spec header
6
6
  (`> **Status:** …`):
7
7
 
8
8
  | Skill | Purpose | Status | Folder |
9
9
  |-------|---------|--------|--------|
10
- | `/spec` | (Feature) Grill to a clear shared understanding, then write a groomed spec | `Ready` (or `Draft`) | `specs/backlog/` |
10
+ | `/spec` | (Feature) Grill to a clear shared understanding, write a groomed spec in its own worktree, then land it on the verdict | `Ready` (or `Draft`) | `specs/backlog/` (on the base branch, by a fast-forward) |
11
11
  | `/spec-bug` | (Bug) Reproduce with a failing test, capture spec, drive red→green | `In Progress` | `specs/in-progress/` |
12
12
  | `/spec-hotfix` | (Hotfix) Fork a worktree from a release tag, red→green, land by tag + cherry-pick | `In Progress` | `specs/in-progress/` |
13
+ | `/no-spec` | Work that genuinely has no spec — its own branch, worktree, page and land. Writes no spec and moves nothing through the lifecycle | `—` | (none) |
13
14
  | `/spec-review` | Re-validate a spec against the codebase; refresh stale parts | `—` | (unchanged) |
14
15
  | `/spec-start` | Put a spec in flight — provision its branch, then build phase 1 | `In Progress` | `specs/in-progress/` |
15
16
  | `/spec-next` | Build the next phase of the spec this session is in (re-run per phase) | `In Progress` (unchanged) | (unchanged) |
@@ -20,13 +21,24 @@ and lifecycle stay consistent. Each sets a status on the spec header
20
21
  | `/spec-init` | Bootstrap/repair this workflow in a project (idempotent) | — | — |
21
22
 
22
23
  **Skills vs commands.** The table above lists **skills** — Claude reads them and
23
- exercises judgment. `/spec-connect` and `/spec-live` are instead **slash commands**
24
+ exercises judgment. `/spec-connect`, `/spec-live` and `/spec-remote-review` are
25
+ instead **slash commands**
24
26
  (`.claude/commands/`): each pre-executes one `skitterspec spec-env` verb and
25
27
  relays its output, so there is no judgment to apply and no model turn spent
26
28
  finding one. They are marked `disable-model-invocation`, meaning
27
29
  **only you can run them** — a skill that wants one will tell you to type it
28
30
  rather than invoking it.
29
31
 
32
+ **`/spec-remote-review`** is the newest and the smallest: it toggles
33
+ `review.allowRemote`, and takes `on`/`off` when you would rather say which.
34
+ It exists because a render's `remote:` line has to name something a person can
35
+ act on, and `skitterspec spec-env review allow remote` is not what anyone
36
+ reconstructs from a page they are reading on a phone. Making the line
37
+ *clickable* was considered and rejected — the only clickable thing markdown has
38
+ is a URL, and a URL that acts when it is **fetched** is one a link previewer or
39
+ a prefetcher fires with nobody involved. It **permits** publishing; it publishes
40
+ nothing.
41
+
30
42
  `/spec-reviewed` is a **skill** and **user-only**, and there the marking is not
31
43
  convenience — it is the enforcement of `/spec-diff` step 0's rule that a waiting
32
44
  review pass is never claimed unasked. Because the model cannot invoke this
@@ -41,6 +53,81 @@ anything — scoped to the window, refusing when two arrive. `/spec-reviewed` is
41
53
  what answers everything outside it: a pass sent when nobody was waiting, two
42
54
  passes to choose between, and every harness with no file-watch to wait with.
43
55
 
56
+ **`/no-spec` is the one row that writes no spec**, and it is in the table
57
+ because it is the same lane: a branch, a worktree, a review page, a verdict, a
58
+ land. What it leaves out is the document, because mechanical work — a version
59
+ bump, a lockfile refresh, a rename — genuinely has none, and a one-phase spec
60
+ written for it is a document nobody reads. What it does **not** leave out is the
61
+ branch. Doing that work in the primary checkout leaves the base branch dirty for
62
+ every in-flight spec to replay over, and renders no page, so it is the one kind
63
+ of change that reaches the base branch unreviewed. It is model-invocable, which
64
+ matters: it is where Claude is sent when it would otherwise write on the base
65
+ branch. Beneath it, `skitterspec spec-env nospec <name>` records the branch —
66
+ and that record is what makes a name with nothing under `specs/**` resolvable by
67
+ `review`, `integrate`, `down` and a bare `resolve`.
68
+
69
+ **`/allow-main` is the fourth command.** It is user-only for a different reason
70
+ from the other three: `/spec-connect`, `/spec-live` and `/spec-remote-review`
71
+ are marked that way because there is no judgment to apply. This one is marked that
72
+ way because **Claude must not be able to lift a guard aimed at Claude.**
73
+
74
+ The guard is `.claude/hooks/main-guard.cjs`, a `PreToolUse` hook on the write
75
+ tools, and it refuses an `Edit` or a `Write` in the **primary checkout** while
76
+ it is on the **base branch**. `main` is where work lands, not where it happens:
77
+ work done there leaves the base branch dirty for every in-flight spec to replay
78
+ over, and renders no review page at all, so it is the one kind of change that
79
+ arrives unreviewed. It refuses the FIRST write rather than the commit, because
80
+ nothing has been written yet and the fix is one command — where a commit-time
81
+ gate fires once the work already exists in a checkout that cannot `switch -c`
82
+ out from under the other worktrees.
83
+
84
+ It fires only on a **positive signal** — isolation configured, the guard not
85
+ switched off, this directory IS the primary checkout, HEAD IS the base branch —
86
+ and every cannot-tell allows the write in silence: no repo, no config, a
87
+ worktree, an unresolvable base, a missing or crashed engine
88
+ (`.claude/rules/negative-checks.md`). `skitterspec spec-env main
89
+ <check|allow|status>` is the engine, and `guards.mainIsLandingZone` in
90
+ `env.config.json` turns it off for a project. It is
91
+ **on by default wherever isolation is configured**, matching the review gate.
92
+
93
+ There is **no allowlist**, and it is empty by construction rather than by
94
+ omission: `/spec` authors into its own worktree and `/no-spec` catches ad-hoc
95
+ work, so no legitimate writer to the base branch is left. `/spec-init` looks
96
+ like the exception and is not one — it bootstraps a repo with no
97
+ `env.config.json` yet, so the guard never fires there.
98
+
99
+ **Two exits, and they are not equal.** `/no-spec <name>` and `/spec-start
100
+ <name>` move the work, and Claude takes those on its own; `/allow-main` lifts
101
+ the guard, and only a person can type it. That asymmetry is the same line
102
+ `/spec-reviewed` draws, and `spec-planning.md` already records that prose alone
103
+ did not hold it once.
104
+
105
+ **`/spec-skip` is the fifth command**, and it is user-only for `/allow-main`'s
106
+ reason rather than the mild one: it lifts the review gate, which is a guard
107
+ aimed at Claude. It pre-executes `skitterspec spec-env review skip "<reason>"`
108
+ and relays it — the same exit that always existed, at an address a person will
109
+ actually type. The long form stays exactly as valid and is what the engine
110
+ prints; what changed is that a refusal now names something reachable, because a
111
+ gate whose exit nobody can find gets switched off wholesale instead of answered.
112
+
113
+ **It is not a `--force`, and one was asked for.** A reasonless lift reopens
114
+ precisely what the reason requirement closed: `none: additive, nothing to
115
+ revert` is a decision a reviewer can argue with, and silence is an oversight.
116
+ So a bare `/spec-skip` refuses and asks for a reason, and the two things that
117
+ discharge a phase's obligation are still a committing verdict and a recorded
118
+ skip — never a flag. Putting the flag on `/commit` was the other suggestion and
119
+ is worse: `/commit` belongs to skittership, which is why the gate is enforced
120
+ by a hook rather than by editing that skill.
121
+
122
+ **A refusal may OFFER its own exit, and that is a different thing again.**
123
+ Where the engine declares one, a skill relays the refusal and **then** offers it
124
+ — once per obligation, with the choice recorded, and answered by the operator
125
+ rather than by Claude. `.claude/rules/offered-unblocks.md` is the whole
126
+ contract. The three properties named there are what let a Claude-raised bypass
127
+ survive the rule above, where a reasonless `--force` did not: the operator
128
+ answers, the choice is on the record, and it is offered once. Remove any one and
129
+ the objection returns.
130
+
44
131
  `/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
45
132
  real judgment (green tests before a land; an MCP fetch and a team-key check; ten
46
133
  subcommands) — but they are marked user-only too, since nobody reaches them
@@ -87,7 +174,7 @@ your canonical `localhost` ports so you can test it at the normal URL
87
174
  move, header edits, the code) happens on the spec's branch in the worktree; `main`
88
175
  changes only when it merges. Teardown is folded into `/spec-complete` ·
89
176
  `/spec-cancel`. Beneath the skills, `skitterspec spec-env
90
- <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
177
+ <up|nospec|main|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
91
178
  engine. **Omit the spec name anywhere and it uses the worktree you are standing in**,
92
179
  else the sole provisioned spec — with no exceptions left: a bare
93
180
  `/spec-connect` connects, and a bare `/spec-live` takes, where both once meant
@@ -118,11 +205,42 @@ stateful ones (`Stack: worktree + docker`, or a branch touching migrations) —
118
205
  `/spec-connect` + a Docker stack for those, and for genuinely parallel testing.
119
206
  Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engine.
120
207
 
121
- **Staging one spec and not another (`spec-env stage`).** Several specs are
122
- authored at once — that is what the worktree mode is for — and they all land in
123
- the same `specs/` folder until they are committed. A `git add specs/` there
124
- stages a *directory*, sweeping whatever another session happens to have written
125
- into this spec's commit, under this spec's ticket trailer.
208
+ **Authoring a spec is work, so it happens off the base branch too (`/spec`).**
209
+ `main` is where work **lands**, not where it happens, and a spec document is no
210
+ exception. With isolation configured, `/spec` grills first — which puts nothing
211
+ on disk — then provisions the new spec's own worktree with
212
+ `spec-env up <name> --docs`, moves the session into it, and writes the spec
213
+ there. `--docs` skips the `setup` commands and Docker, so a tree to write
214
+ markdown in costs one `git worktree add`; `/spec-start` re-runs `up` without the
215
+ flag and the install happens then, at the first moment the tree holds code.
216
+
217
+ **The committing verdict is what puts it on the base branch.** Phase C2's page
218
+ offers `Commit & Start` and `Commit`, and **both** commit on the branch and then
219
+ `spec-env integrate` — rebase, fast-forward — so the base gains the spec as one
220
+ commit. That is not optional tidiness: `ls specs/backlog/` is how specs are
221
+ found, and a folder bucket is only the truth on the branch you are standing on,
222
+ so a spec left unlanded is invisible to everyone. They differ only in the
223
+ worktree: `Commit & Start` keeps it and carries into `/spec-start`, plain
224
+ `commit` tears it down, because a spec parked in the backlog should hold no
225
+ checkout.
226
+
227
+ **What this bought is the reason it changed.** The page waits for a verdict
228
+ without a timeout — deliberately, because a reader walking away from a diff is
229
+ the normal case — and while the spec was authored in the primary checkout that
230
+ wait held the base branch dirty for as long as it lasted, so a release could not
231
+ be cut through it. Now an unanswered spec sits on its own branch in its own
232
+ worktree, in nothing's way. Phase C2 still **arms nothing**: the gate asserts
233
+ that a phase which ended owes an answer, and a backlog spec owes no phase — and
234
+ walking away from it now costs nobody anything rather than leaving a mess
235
+ someone else has to work around.
236
+
237
+ With isolation **absent** none of this applies: there is no worktree to make and
238
+ no land to do, so `/spec` writes the folder where the session is standing,
239
+ exactly as it always did.
240
+
241
+ **Staging one spec and not another (`spec-env stage`).** A `git add specs/`
242
+ stages a *directory*, sweeping whatever else happens to be sitting in it into
243
+ this spec's commit, under this spec's ticket trailer.
126
244
  `skitterspec spec-env stage [<spec>] [--json]` is how a skill asks instead of
127
245
  guessing: it splits the uncommitted tree into the paths that are this spec's —
128
246
  its folder in **any** bucket, so a tree mid-`git mv` is handled, plus each
@@ -133,6 +251,14 @@ the primary checkout outside, and prints which. The `owned` half is the spec's
133
251
  the commits this bounds are the lifecycle ones. Nothing here refuses, writes, or
134
252
  exits non-zero — `foreign` is a list of paths to leave alone, not an accusation.
135
253
 
254
+ **And it stays load-bearing now that `/spec` authors in its own worktree**, even
255
+ though that tree holds one spec. It was once justified by several specs sharing
256
+ one `specs/` folder, and that justification is gone; what replaces it is smaller
257
+ and just as real. A project's `spec.companionPaths`, a hand-edited
258
+ `specs/.core/`, and a tracker snapshot all land in the same tree as the spec, and
259
+ only the spec's own documents belong under its authoring verdict. Where isolation
260
+ is absent the original reason applies unchanged.
261
+
136
262
  **Naming the paths is necessary and not sufficient**, which is why every spec
137
263
  commit is also pathspec-limited: `git add -- <paths>` **and**
138
264
  `git commit -m "…" -- <paths>`. A checkout has one `.git/index` and every session
@@ -226,6 +352,34 @@ silently. All of it lives beside the page in gitignored `.spec-env/`, and
226
352
  **the marks are information, never a gate**: nothing counts them and nothing
227
353
  refuses on them.
228
354
 
355
+ **A second opinion, from something that did not write the code (`review.reviewers`).**
356
+ The page can also carry findings from code reviewers
357
+ that run from a CLI — CodeRabbit's has a free tier, PR-Agent and Kodus are
358
+ self-hosted — and they arrive as **checks**, the same shape a written review
359
+ uses. `/spec-next` runs them once per phase before the render, so the page is
360
+ populated when it is opened; `/spec-diff --reviewers` is the opt-in mid-phase
361
+ run, and a bare `/spec-diff` never spends one. Results are cached against a hash
362
+ of the diff, so re-reading unchanged work costs nothing.
363
+
364
+ **They inform; they never gate.** A check is not a comment, so a page carrying
365
+ twenty findings still offers `Commit` — and a reader who replies to one creates
366
+ a comment, which does gate, because then a person has asked for something. There
367
+ is deliberately no severity threshold that blocks: that is the counting gate
368
+ this whole design exists against, wearing a vendor's name.
369
+
370
+ **And a reviewer that could not run says so**, which is the one place
371
+ `.claude/rules/negative-checks.md` inverts. Silence is normally the safe branch;
372
+ here a rate-limited or unauthenticated reviewer would render identically to one
373
+ that read the diff and approved of it, on the page a commit decision is made
374
+ from. So each gets a line — `12 findings` · `clean` · `did not run — <why>` —
375
+ and none of it ever refuses a render, a verdict or a commit.
376
+
377
+ The list is **empty by default** and `init` never writes one: configuring a
378
+ hosted reviewer sends the worktree's diff to a third party, which is a real
379
+ change for a tool that otherwise touches nothing outside `.spec-env/`. Every
380
+ finding on the page is badged with the reviewer that produced it. `env.config.md`
381
+ carries the field reference.
382
+
229
383
  **The verdict is chosen, never derived.** A verdict is one person's conclusion,
230
384
  sent once per pass; a counting gate is a refusal computed from how many boxes
231
385
  are ticked, and it stays forbidden. The one refusal here is not a count of
@@ -300,8 +454,8 @@ its verdicts and its fields.
300
454
  | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-orders` · phase 2 moved |
301
455
  | **Branch** | `spec/feat-orders` · 3 commits, clean |
302
456
  | **Built** | POST /orders handler, orders schema |
303
- | **Tests** | 128 passed · npm test |
304
- | **Review** | 7 files, +212 −18 · [open the page](file:///…) |
457
+ | **Tests** | 128 passed · dotnet test |
458
+ | **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 |
305
459
  | **Follow-ups** | none |
306
460
  | **Next** | `/spec-next` → phase 3 (Auth) |
307
461
 
@@ -333,6 +487,27 @@ against a committed last-pushed snapshot and applies it) and `/spec-status`
333
487
  workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
334
488
  the tracker is never read back or merged.
335
489
 
490
+ **One-way means the repo wins, and it now wins *loudly*.** Those are different
491
+ claims and only the first was ever in doubt. A ticket's description is generated
492
+ from the spec, so a person editing it there is writing something the next push
493
+ replaces — which is correct, and which used to happen in silence. The engine now
494
+ compares a hash of what the tracker holds against a hash of what the repo last
495
+ sent, and says *that* it changed and where to read it.
496
+ It **reports and never refuses**: only a person can tell a typo fix from a
497
+ paragraph worth keeping.
498
+
499
+ **This is still not a pull.** Nothing read is merged, written to a spec, or fed
500
+ into the projection, and the text never enters the conversation — the comparison
501
+ is hashes, and the output is one line for a human. It is the same thing
502
+ `compareStored` has always done for a different question.
503
+
504
+ **And adoption no longer costs the reporter their words.** When a provider
505
+ adopts an existing ticket — one a PM or colleague filed — the linking push
506
+ replaces its description with the spec. Before that happens the original is
507
+ posted back onto the ticket as a **comment**: a surface one-way sync never
508
+ touches, so no later push can clobber it. Where a provider supports it, this is
509
+ automatic and has an opt-out. See the provider's own docs.
510
+
336
511
  A provider may also ship a **read-only listing** of what the tracker holds. That
337
512
  is not a pull: nothing is merged back and no spec file is written. It exists
338
513
  because the folder buckets are only the truth *on the branch you are standing
@@ -381,9 +556,15 @@ The spec skills tell you to run "your project's typecheck and test commands" and
381
556
  to "honour project conventions". Make those concrete here so specs stay
382
557
  consistent with the codebase:
383
558
 
384
- - **Typecheck command:** `<e.g. npm run typecheck>`
385
- - **Test command:** `<e.g. npm test>` (single file/dir: `<e.g. npx vitest run path>`)
386
- - **Lint/format:** `<e.g. npm run lint>`
559
+ Skitterspec never runs these itself and knows nothing about what this project
560
+ is written in — it shells out to `git`, and to whatever you write below. The
561
+ examples span several stacks on purpose: pick the shape that fits, or write one
562
+ none of them describes.
563
+
564
+ - **Typecheck command:** `<e.g. npm run typecheck · dotnet build · mypy . · go vet ./...>`
565
+ - **Test command:** `<e.g. npm test · dotnet test · pytest · go test ./...>`
566
+ (single file/dir: `<e.g. npx vitest run path · dotnet test --filter · pytest path>`)
567
+ - **Lint/format:** `<e.g. npm run lint · dotnet format · ruff check · golangci-lint run>`
387
568
  - **Other rules specs must honour:** link the relevant `.claude/rules/*.md`
388
569
  (architecture, code style, testing, database, etc.) rather than restating them.
389
570