@skitterbyte/skitterspec-jira 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/MIGRATION.md +1089 -0
  2. package/README.md +42 -2
  3. package/assets/claude-md-section.md +85 -0
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-connect.md +13 -0
  6. package/assets/commands/spec-live.md +14 -0
  7. package/assets/commands/spec-remote-review.md +37 -0
  8. package/assets/commands/spec-skip.md +24 -0
  9. package/assets/core/SETUP.md +192 -0
  10. package/assets/core/env.config.json.example +56 -0
  11. package/assets/core/env.config.md +554 -0
  12. package/assets/core/gating.config.json.example +4 -0
  13. package/assets/core/gating.config.md +81 -0
  14. package/assets/core/jira.config.json.example +18 -0
  15. package/assets/core/jira.config.md +61 -0
  16. package/assets/hooks/main-guard.cjs +162 -0
  17. package/assets/hooks/review-gate.cjs +146 -0
  18. package/assets/review/page.html +3384 -0
  19. package/assets/rules/commit-trailers.md +96 -0
  20. package/assets/rules/negative-checks.md +69 -0
  21. package/assets/rules/offered-unblocks.md +100 -0
  22. package/assets/rules/spec-planning.md +692 -0
  23. package/assets/rules/spec-reports.md +557 -0
  24. package/assets/skills/no-spec/SKILL.md +246 -0
  25. package/assets/skills/spec/SKILL.md +714 -0
  26. package/assets/skills/spec-bug/SKILL.md +583 -0
  27. package/assets/skills/spec-cancel/SKILL.md +261 -0
  28. package/assets/skills/spec-complete/SKILL.md +448 -0
  29. package/assets/skills/spec-diff/SKILL.md +870 -0
  30. package/assets/skills/spec-hotfix/SKILL.md +605 -0
  31. package/assets/skills/spec-init/SKILL.md +178 -0
  32. package/assets/skills/spec-jira-setup/SKILL.md +220 -0
  33. package/assets/skills/spec-list/SKILL.md +173 -0
  34. package/assets/skills/spec-next/SKILL.md +676 -0
  35. package/assets/skills/spec-push/SKILL.md +306 -0
  36. package/assets/skills/spec-review/SKILL.md +256 -0
  37. package/assets/skills/spec-reviewed/SKILL.md +284 -0
  38. package/assets/skills/spec-start/SKILL.md +496 -0
  39. package/assets/skills/spec-status/SKILL.md +112 -0
  40. package/assets/skills/spec-sync/SKILL.md +218 -0
  41. package/assets/skills/spec-to-main/SKILL.md +139 -0
  42. package/bin/skitterspec-jira.js +61 -0
  43. package/package.json +40 -4
  44. package/src/cli.js +5901 -0
  45. package/src/deprecate.js +138 -0
  46. package/src/env/bootstrap.js +188 -0
  47. package/src/env/building.js +143 -0
  48. package/src/env/classify.js +176 -0
  49. package/src/env/commitcmd.js +108 -0
  50. package/src/env/config.js +753 -0
  51. package/src/env/dev.js +63 -0
  52. package/src/env/hooks.js +226 -0
  53. package/src/env/hotfix.js +385 -0
  54. package/src/env/hotfixland.js +302 -0
  55. package/src/env/integrate.js +126 -0
  56. package/src/env/live.js +504 -0
  57. package/src/env/mainguard.js +299 -0
  58. package/src/env/provision.js +466 -0
  59. package/src/env/proxy.js +192 -0
  60. package/src/env/prune.js +119 -0
  61. package/src/env/registry.js +159 -0
  62. package/src/env/render.js +17 -0
  63. package/src/env/resolve.js +716 -0
  64. package/src/env/review.js +2577 -0
  65. package/src/env/reviewers/coderabbit.js +228 -0
  66. package/src/env/reviewers.js +517 -0
  67. package/src/env/serve.js +1044 -0
  68. package/src/env/supervise.js +157 -0
  69. package/src/env/teardown.js +248 -0
  70. package/src/env/trust.js +87 -0
  71. package/src/gating.js +155 -0
  72. package/src/init.js +1150 -0
  73. package/src/lines-diff.js +114 -0
  74. package/src/prompts.js +129 -0
  75. package/src/vendor/jira/adf.js +414 -0
  76. package/src/vendor/jira/api.js +295 -0
  77. package/src/vendor/jira/cli-sync.js +1256 -0
  78. package/src/vendor/jira/commands.js +44 -0
  79. package/src/vendor/jira/config.js +296 -0
  80. package/src/vendor/jira/credentials.js +108 -0
  81. package/src/vendor/jira/dialect.js +45 -0
  82. package/src/vendor/jira/mcp.js +129 -0
  83. package/src/vendor/provider-kit/index.js +41 -0
  84. package/src/vendor/provider-kit/src/adapter.js +23 -0
  85. package/src/vendor/provider-kit/src/apply.js +197 -0
  86. package/src/vendor/provider-kit/src/args.js +68 -0
  87. package/src/vendor/provider-kit/src/config.js +198 -0
  88. package/src/vendor/provider-kit/src/credentials.js +404 -0
  89. package/src/vendor/provider-kit/src/doctor.js +35 -0
  90. package/src/vendor/provider-kit/src/http.js +44 -0
  91. package/src/vendor/provider-kit/src/identity.js +93 -0
  92. package/src/vendor/provider-kit/src/md-compare.js +39 -0
  93. package/src/vendor/provider-kit/src/md-tables.js +94 -0
  94. package/src/vendor/provider-kit/src/prompt.js +50 -0
  95. package/src/vendor/provider-kit/src/released.js +226 -0
  96. package/src/vendor/provider-kit/src/specs.js +166 -0
  97. package/src/vendor/provider-kit/src/transport.js +22 -0
  98. package/src/vendor/sync-core/index.js +92 -0
  99. package/src/vendor/sync-core/src/base.js +81 -0
  100. package/src/vendor/sync-core/src/compare.js +306 -0
  101. package/src/vendor/sync-core/src/dialect.js +51 -0
  102. package/src/vendor/sync-core/src/fields.js +36 -0
  103. package/src/vendor/sync-core/src/legacy.js +100 -0
  104. package/src/vendor/sync-core/src/normalize.js +989 -0
  105. package/src/vendor/sync-core/src/push.js +85 -0
  106. package/src/vendor/sync-core/src/retarget.js +278 -0
  107. package/src/vendor/sync-core/src/task-block.js +308 -0
  108. package/src/vendor/sync-core/src/verify.js +67 -0
  109. package/src/vendor/sync-core/src/write.js +191 -0
@@ -0,0 +1,554 @@
1
+ # `env.config.json` — per-spec isolation config
2
+
3
+ Opt-in config for per-spec isolation (git worktree + optional namespaced Docker
4
+ stack + host dev servers + a front-door proxy per in-progress spec).
5
+ Provisioning is folded into `/spec-start`, teardown into `/spec-complete` ·
6
+ `/spec-cancel`, traffic diversion is `/spec-connect`, and reading a spec's diff
7
+ is `/spec-diff`; the `skitterspec spec-env
8
+ <up|bootstrap|nospec|main|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` CLI is
9
+ the engine beneath them.
10
+
11
+ **Once this file is present, isolation is the default policy:** `/spec-start` gives
12
+ **every** in-progress spec its own git worktree automatically. Docker is a
13
+ **per- spec escalation** — a spec brings up a stack only when its
14
+ `> **Stack:**` header is `worktree + docker` (set at `/spec` when it touches
15
+ the DB / stateful services). A `worktree`-only spec takes no registry slot, no
16
+ port block, and no `.env`.
17
+
18
+ **Adopt it** with `skitterspec init --isolation` (or copy
19
+ `env.config.json.example` → `env.config.json` here) and edit the values. While
20
+ `env.config.json` is absent the feature is simply unused — every skill behaves
21
+ exactly as it does today.
22
+
23
+ The loader (`src/env/config.js` → `loadEnvConfig`) merges your file over the
24
+ frozen defaults below and returns `{ config, present, unknown }`;
25
+ `present:false` means no live `env.config.json` was found.
26
+
27
+ **A key not listed below is ignored — and says so.** Every `spec-env` command
28
+ prints one advisory line per unrecognised key, top-level or nested:
29
+
30
+ ```
31
+ spec-env: env.config.json — unknown key "docker.portbase" is ignored.
32
+ ```
33
+
34
+ It is advisory in the strongest sense: the key is dropped exactly as it always
35
+ was, nothing refuses, and the exit status is unchanged. It exists because the
36
+ two things an unknown key can be — a deliberate forward-compat entry and a typo
37
+ — are indistinguishable from here, and only one of them is a mistake you would
38
+ want to hear about. A mis-typed `review.required` leaves the commit gate on and
39
+ a mis-typed `teardown.deleteRemoteBranch` reverts to `prompt`; before this, the
40
+ only signal either gave was that nothing happened.
41
+
42
+ A **known** key whose value is rejected — `mode: "Checkout"`,
43
+ `teardown.deleteRemoteBranch: "yes"` — is not reported here. Each of those falls
44
+ through to a documented conservative default; see the field notes below.
45
+
46
+ ## Fields
47
+
48
+ ```jsonc
49
+ {
50
+ // Where sibling worktrees are created and how their dirs are named.
51
+ // Where a spec's branch gets built.
52
+ //
53
+ // "worktree" (default) — every spec gets its own git worktree. Several
54
+ // specs run side by side and `main` stays free. `/spec-start`
55
+ // moves your session into the spec's worktree with a `cd`, so
56
+ // the terminal you are already in follows the work.
57
+ // "checkout" — the branch is built in the primary checkout instead. One
58
+ // spec at a time, and nowhere else to stand: the work comes to
59
+ // your terminal rather than your terminal going to it.
60
+ //
61
+ // Pick it for how you work, not for what this repo contains — a project with
62
+ // no dev servers may still want several specs in flight. An unrecognised
63
+ // value falls back to "worktree" rather than erroring.
64
+ //
65
+ // Not to be confused with `seedFiles.mode`, which is "symlink" | "copy".
66
+ "mode": "worktree",
67
+
68
+ "worktree": {
69
+ "root": "../{repo}-wt", // dir that holds all spec worktrees; sibling of
70
+ // the primary checkout, never nested inside it.
71
+ "folderPattern": "{slug}" // per-spec worktree dir name.
72
+ },
73
+
74
+ // Per-spec Docker stack. COMPOSE_PROJECT_NAME namespaces containers,
75
+ // networks, and named volumes; PORT_OFFSET shifts the spec's port block.
76
+ "docker": {
77
+ // Master switch: "is Docker escalation available on this project?" — NOT
78
+ // "always run Docker". true = specs MAY escalate (a spec still needs
79
+ // `Stack: worktree + docker` to actually get a stack); the default stack is
80
+ // worktree-only. false = every spec is worktree-only and the escalation is
81
+ // hidden. (Was "always provision Docker" in the pre-Stack engine.)
82
+ "enabled": true,
83
+ // Passed as `-f` to `docker compose` on up and down, but ONLY when the file
84
+ // is actually there — a project whose file has a name docker finds by
85
+ // itself keeps working untouched. It is also the signal that lets a spec
86
+ // with no `Stack:` header inherit `enabled` above: the switch is a
87
+ // project-wide default, the file is evidence there is a stack to bring up.
88
+ // An explicit header is checked first and never consults it.
89
+ "composeFile": "docker-compose.yml",
90
+ "projectNamePattern": "{repoSlug}_{slug}", // → COMPOSE_PROJECT_NAME
91
+ "portBase": 3000, // first port of slot 0's block
92
+ "portsPerSpec": 10, // block width; slot n → portBase + n*portsPerSpec
93
+ // Written into the worktree by `spec-env bootstrap`, and passed to compose
94
+ // as `--env-file`. A Docker spec's worktree steps run in this order:
95
+ // seedFiles, this env file
96
+ // (COMPOSE_PROJECT_NAME and PORT_OFFSET appended only where missing, so a
97
+ // value `setup` or you put there is kept), `setup`, then
98
+ // `docker compose … up -d` — all from inside the worktree, so the stack
99
+ // reads this spec's values and not the primary checkout's.
100
+ // It must be a file of its OWN: if it is also in `seedFiles` as a symlink,
101
+ // the write is refused rather than send this spec's port block into the
102
+ // primary checkout's file. Name a different file here (".worktree.env")
103
+ // and have your compose file read the values from it.
104
+ "envFile": ".env",
105
+ "backupCommand": "" // optional pre-teardown backup (e.g. pg_dump);
106
+ // empty = no backup, volumes dropped directly.
107
+ },
108
+
109
+ // Gitignored files seeded from the primary checkout into a fresh worktree by
110
+ // `spec-env bootstrap`, after `git worktree add` and BEFORE `setup` runs — so a
111
+ // fresh linked worktree (which starts with none of the repo's gitignored files)
112
+ // has the .env / local secret overrides / local config that setup steps and
113
+ // git hooks depend on. Without this any setup step that reads .env — a schema
114
+ // or client generator, a codegen pass — hard-fails in the new worktree
115
+ // because the file it expects isn't there.
116
+ // mode "symlink" (default) points the worktree file at the main file, so it
117
+ // stays in sync; "copy" makes an independent copy.
118
+ // files repo-relative paths to seed. A source absent in main is a printed
119
+ // no-op (not an error); a target that already exists is left untouched
120
+ // (idempotent — safe to re-run `spec-env bootstrap` over an existing
121
+ // worktree). Seeding is done by the engine itself, never printed as
122
+ // shell, so no command line ever names one of these files.
123
+ // Shorthand: `"seedFiles": [".env", …]` == `{ "mode": "symlink", "files": […] }`.
124
+ // `.env` is an ILLUSTRATION, not an expectation — skitterspec never reads any
125
+ // of these files, it only links or copies them. Name whatever your stack keeps
126
+ // out of git: `appsettings.Development.json` or a user-secrets file, `.envrc`,
127
+ // `local.settings.json`, `config/local.yml`.
128
+ // Seeded files are gitignored, so they never make the worktree "dirty" and
129
+ // never block teardown; they vanish with the worktree at `spec-env down`.
130
+ // On a Docker project, do not seed `docker.envFile` as a symlink — the env
131
+ // file step refuses to write through one (see `envFile` above).
132
+ // [] (or absent) = seed nothing (current behaviour).
133
+ "seedFiles": {
134
+ "mode": "symlink",
135
+ "files": [".env"]
136
+ },
137
+
138
+ // Bootstrap commands `spec-env bootstrap <spec>` runs IN the worktree, after
139
+ // the seeds and the env file and before the stack, on every provision including
140
+ // re-attach — so a fresh worktree's dependencies exist and git hooks,
141
+ // typechecks, builds and tests work immediately instead of failing on a
142
+ // missing node_modules. An array, run in order, stopping at the first that
143
+ // exits non-zero; [] = none. Each string is a shell command; {slug}/{branch}/{worktreePath}/{projectName}/{portOffset}
144
+ // expand (the cwd is already the worktree, so {worktreePath} is usually
145
+ // redundant). Example: ["pnpm install --frozen-lockfile"].
146
+ "setup": [],
147
+
148
+ // Commands to run in the PRIMARY checkout after a land — the counterpart of
149
+ // `setup`, for whatever landed code needs before the base branch is usable
150
+ // locally (e.g. ["pnpm db:data-migration"] for imperative data migrations
151
+ // your deploy runs and nothing runs on your dev DB). An array, run in order;
152
+ // [] (or absent) = none, and every plan prints exactly what it always did.
153
+ // `spec-env integrate` prints them after the fast-forward (worktree and
154
+ // checkout mode), and also after "already landed" — a land on another
155
+ // machine leaves them pending here. `spec-env hotfix land` prints them
156
+ // after a carry that moved the base branch, and not at all when it moved
157
+ // nothing (held, already carried, or `hotfix.cherryPickMain` false).
158
+ // The engine only plans: /spec-complete, /spec-to-main and /no-spec run
159
+ // them (a /spec documents-only land does not). They stop at the first
160
+ // failure, report it as a caveat, and never undo the land or block
161
+ // teardown. Literal strings — tokens are NOT expanded, because they run on
162
+ // the base branch where a spec's {slug} or {portOffset} mean nothing.
163
+ "afterLand": [],
164
+
165
+ // Host dev servers `spec-env dev up <spec>` starts on the spec's port block
166
+ // (for apps that run via `pnpm dev` on the host, not inside the Docker stack).
167
+ // An array so UI + API (or more) are supervised independently; [] = none.
168
+ // Each entry:
169
+ // name label used in logs/pid files (.spec-env/{logs,pids}/<spec>-<name>).
170
+ // command the launch command, run detached in the worktree. {portVar} and
171
+ // {port} expand to the process's resolved port.
172
+ // portVar env var the command reads for its port; injected into its env.
173
+ // Process i in the block gets portBase + slot*portsPerSpec + i.
174
+ // health optional URL polled until it answers before reporting ready;
175
+ // {portVar}/{port} expand here too. Omit for no health gate.
176
+ // frontPort optional canonical origin this process fronts (e.g. 3000 for the
177
+ // UI, 8080 for the API) — used by the proxy to route to it.
178
+ "dev": [],
179
+
180
+ // Front-door proxy for `spec-env connect <spec>` — a small bundled Node
181
+ // reverse proxy (no external install) that exposes ONE connected spec's
182
+ // frontPort dev servers on the canonical ports. Exclusive: one target at a
183
+ // time. `connect main` stops it so the primary checkout owns the ports again.
184
+ "proxy": {
185
+ "enabled": true, // false = the connect command is unavailable
186
+ "host": "127.0.0.1" // bind host for the canonical ports
187
+ },
188
+
189
+ // Machine-local slot registry (spec → slot index). Resolved against the
190
+ // primary checkout root, shared by all worktrees, gitignored.
191
+ "registry": ".spec-env/registry.json",
192
+
193
+ // Git branch naming, provider-neutral. `pattern` expands {type} and {slug}
194
+ // (e.g. "feat/add-widget"). When a ticketing provider is linked and you want
195
+ // tracker ids in branch names, use {identifier} in the pattern and point
196
+ // `identifierField` at the 00-overview.md frontmatter field the provider
197
+ // writes the id into — pushing that branch can then fire the tracker's
198
+ // automation. Empty `identifierField` (or a spec missing that field) makes a
199
+ // pattern with {identifier} fall back to {type}/{slug}.
200
+ "branch": {
201
+ "pattern": "{type}/{slug}",
202
+ "identifierField": ""
203
+ },
204
+
205
+ // Paths that belong to a spec ALONGSIDE its own `specs/<bucket>/<name>/`
206
+ // folder. `/spec-start` uses this to tell "the spec you just wrote, not yet
207
+ // committed" apart from someone else's uncommitted work: if every dirty path
208
+ // belongs to the spec being started it is committed for you, and if a single
209
+ // path does not, the start is refused as before.
210
+ //
211
+ // Provider-neutral by design — the base engine must not know that any
212
+ // particular tracker exists — so you declare the shape here. `{slug}` and
213
+ // `{identifier}` expand exactly as in `branch.pattern` above, `{identifier}`
214
+ // via `branch.identifierField`.
215
+ //
216
+ // A pattern using {identifier} matches NOTHING when no identifier resolves
217
+ // (no `identifierField` set, or a spec never pushed to a tracker). That is
218
+ // deliberate: the file it names then belongs to some other spec, and the safe
219
+ // failure is a refusal you clear with /commit, not a stranger's file swept
220
+ // into your commit. Default: none — a spec owns only its own folder.
221
+ //
222
+ // THIS DISTRIBUTION SEEDS BOTH KEYS. `skitterspec init --isolation` writes an
223
+ // `env.config.json` already carrying
224
+ // `"companionPaths": ["specs/.core/jira-base/{identifier}.base.json"]` and
225
+ // `"identifierField": "jira_identifier"`, because the Jira snapshot is
226
+ // written by this package, and only this package knows where. What follows is
227
+ // the base engine's default — what you get if you clear them.
228
+ //
229
+ // Both or neither: `{identifier}` resolves through `branch.identifierField`,
230
+ // so `companionPaths` on its own expands to nothing and owns nothing.
231
+ "spec": {
232
+ "companionPaths": []
233
+ },
234
+
235
+ // Integration base branch — the branch specs fork from and land back onto
236
+ // (used by the teardown "merged?" guard and, later, the integrate step).
237
+ // Empty = auto-detect: origin/HEAD → main → master. Set it when your default
238
+ // branch isn't discoverable (e.g. no remote) or differs (trunk, develop).
239
+ "baseBranch": "",
240
+
241
+ // Teardown safety. --force overrides both. refuseTeardownIfUnpushed only
242
+ // blocks when the commits are ALSO unmerged into the base branch — a branch
243
+ // already landed on base tears down (and its branch is deleted) without
244
+ // --force, even with no remote.
245
+ "guards": {
246
+ "refuseTeardownIfDirty": true,
247
+ "refuseTeardownIfUnpushed": true
248
+ },
249
+
250
+ // What teardown cleans up beyond this machine. Nothing publishes a spec
251
+ // branch for you, so a remote copy exists only because you pushed it by hand —
252
+ // and without this a completed spec leaves that merged branch on the remote
253
+ // forever. `deleteRemoteBranch`:
254
+ // "prompt" (default) — plan `git push <remote> --delete <branch>` in its own
255
+ // "confirm with the user first" section; /spec-complete and
256
+ // /spec-cancel ask before running it.
257
+ // "never" — omit it; clean the remote up yourself.
258
+ // "always" — fold it into `run these:` and never ask.
259
+ // Only ever planned for a branch that has LANDED (merged into base, or captured
260
+ // by a hotfix's deploy tag) and whose remote-tracking ref this clone can
261
+ // actually see. Until a branch lands, the remote copy is its only backup, so
262
+ // --force does NOT enable this. A branch pushed from another machine has no
263
+ // local ref here and is simply missed — teardown under-cleans rather than
264
+ // deleting something it cannot see. An unrecognised value means "prompt".
265
+ "teardown": {
266
+ "deleteRemoteBranch": "prompt"
267
+ },
268
+
269
+ // Live overlay (`spec-env live` / `/spec-live`): test a spec on the already-
270
+ // running dev server by checking its branch out in the primary checkout.
271
+ // `migrations` is a list of globs (`**`, `*`, `?`) marking migration files; a
272
+ // branch that changes any of them is treated as STATEFUL and `live take`
273
+ // refuses it (code-only v1 — use `/spec-connect` for those). Default: none.
274
+ "live": {
275
+ "migrations": []
276
+ },
277
+
278
+ // Hotfix landing (`spec-env hotfix land` / `/spec-complete` on a Type: Hotfix
279
+ // spec). A hotfix forks from a release tag and lands by release + push + carry,
280
+ // not fast-forward; `hotfix land` runs those steps itself, in that order.
281
+ //
282
+ // `releaseWith` is a command run IN THE HOTFIX WORKTREE that cuts the release —
283
+ // its version commit and its tag — and prints ONE JSON line on stdout:
284
+ // {"tag","version","baseTag","branch","versionCommit","pushed"}. skittership
285
+ // 2.1+ ships one: "node scripts/release-hotfix.cjs --json" (call node directly
286
+ // rather than through `pnpm run`, so no run banner reaches stdout; anything but
287
+ // the JSON line is a refusal). skitterspec never passes it `--push`, since it
288
+ // owns the push. skittership refuses a branch outside its `hotfix.branchPrefix`
289
+ // (default `hotfix/`) — the default `branch.pattern` already gives
290
+ // `hotfix/<slug>` — and refuses a package.json with a `postversion` script
291
+ // unless the command says `--allow-postversion`; add that yourself if you have
292
+ // one. Empty (the default) = the built-in tagger.
293
+ //
294
+ // `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
297
+ // when `hotfix land` runs. Tags order X.Y.Z-rc.N < X.Y.Z < X.Y.Z-hotfix.N <
298
+ // X.Y.(Z+1), the same as skittership.
299
+ //
300
+ // `cherryPickMain` carries the fix onto the base branch for the next release
301
+ // (default true) — its commits minus the version commit and minus anything the
302
+ // base already has. `targets` is an optional default list of extra base tags to
303
+ // also patch (test/demo lines); `--also <tag>` adds more at run time. Targets
304
+ // are TAG-ONLY: they get the built-in tagger, never `releaseWith`, and are not
305
+ // pushed. The prod release is pushed (branch, then tag, to origin) only on
306
+ // `hotfix land --push`; without it the land holds before the carry. The
307
+ // worktree stays until `spec-env hotfix finish`. Default: built-in patch tag,
308
+ // carry to main, no targets.
309
+ "hotfix": {
310
+ "releaseWith": "",
311
+ "bump": "patch",
312
+ "cherryPickMain": true,
313
+ "targets": []
314
+ },
315
+
316
+ // Reading a spec's diff (`spec-env review`, `/spec-diff`).
317
+ //
318
+ // `reader` decides how the page's LOCATION IS WORDED. It no longer decides
319
+ // the bind — `allowNetwork` does, see below — and it no longer decides which
320
+ // tiers are offered, because every tier is now listed whatever it says.
321
+ // WHAT THE SERVER
322
+ // BINDS TO — and nothing else. It never decides whether to serve (`serve`
323
+ // does) and never decides to PUBLISH. Three values:
324
+ // "local" — you are at the machine holding the page; it binds 127.0.0.1.
325
+ // "remote" — you are not; it binds every interface so the page opens.
326
+ // "detect" — work it out (the default); cannot-tell binds 127.0.0.1.
327
+ // An explicit "local"/"remote" is BELIEVED WITHOUT SNIFFING: you know where
328
+ // you are reading, and no signal outranks being told. Detection is only the
329
+ // default, and it has three outcomes rather than two — local, remote, and
330
+ // unknown. Unknown behaves exactly as the tool did before any of this existed
331
+ // (the file:// URL, no warning), because a wrong "local" prints a dead link
332
+ // and a wrong "remote" warns at someone whose link works fine. An
333
+ // unrecognised value falls through to "detect", so a typo cannot become a
334
+ // confident answer. Default: detect.
335
+ //
336
+ // `servePort` is the port for `spec-env review serve`, which renders every
337
+ // spec's diff per request on one local server. Two forms:
338
+ //
339
+ // "auto" (the default) — derive it from this repo's path, as
340
+ // 7700 + hash(realpath(repoRoot)) % 100. Two repos on one machine stop
341
+ // competing for one shared port without anyone configuring anything, and
342
+ // — the part that matters — THE SAME REPO GETS THE SAME PORT EVERY TIME.
343
+ // The derivation reads nothing on disk, so the port survives a restart, a
344
+ // reboot and a `--stop`, which is what lets a link handed out yesterday
345
+ // still resolve. A symlinked spelling of the tree resolves first, so one
346
+ // repo never lands on two ports.
347
+ // <a number> — pin it. An explicit number always wins, and pinning is what
348
+ // you do when you want a port you can memorise, or when two repos derive
349
+ // the same one.
350
+ //
351
+ // A derived port CAN still collide — a hundred slots is a small chance, not
352
+ // no chance — and the server refuses rather than moving itself aside. The fix
353
+ // it names is `servePort`, because that is what the next link is built from;
354
+ // `--port` moves one run and leaves every link already handed out pointing at
355
+ // the busy port. `spec-env review serve --status` prints the port and which
356
+ // of the three chose it.
357
+ //
358
+ // An unrecognised value falls through to "auto", like every other typed key
359
+ // here. Default: "auto".
360
+ //
361
+ // The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which mints
362
+ // an unguessable path token and prints the LAN URL including it — anyone
363
+ // holding that URL can read every spec's diff while it runs.
364
+ //
365
+ // `serve` is whether a render stands the local server up at all:
366
+ // "always" — every render does (the default), so `local:` and `network:`
367
+ // are both http URLs the page can POST a verdict back to. The
368
+ // LAN address comes first with the rest listed under it, because
369
+ // the guess reads interface names and a VPN or an unusual
370
+ // adapter will fool it.
371
+ // "never" — `local:` is a file:// URL instead. Note what that costs: a
372
+ // file:// page has no server to POST to, so its verdict buttons
373
+ // copy a command for you to paste rather than sending anything.
374
+ // It replaced `serveOnRemote`, which gated serving on the reader — and so
375
+ // handed a local machine a page it could read and not answer. A legacy
376
+ // `serveOnRemote: false` is still read as `serve: "never"`.
377
+ // Either way NOTHING IS PUBLISHED on a detection: a server is one process
378
+ // ended by one flag, while a published page is one this tooling cannot
379
+ // remove, so that half stays an explicit ask. Teardown names a server that
380
+ // served the last spec, and `spec-env prune` reaps a pidfile whose process is
381
+ // gone. Default: true.
382
+ //
383
+ // `allowNetwork` and `allowRemote` decide WHICH TIERS a render offers, and
384
+ // they replaced the engine guessing where the reader was sitting. It guessed
385
+ // for a while and got it wrong three separate ways in one day: a file:// page
386
+ // on a session detected `unknown`, a LAN URL for a phone that had left the
387
+ // network, and an address that changed underneath a reader mid-session. So
388
+ // every tier is listed, labelled, and either a URL or the one command that
389
+ // turns it on — and you pick the one that reaches you.
390
+ //
391
+ // `allowNetwork` — whether the review server binds EVERY INTERFACE, so the
392
+ // page opens on your phone, or loopback only. THIS IS WHAT
393
+ // CHOOSES THE BIND; `reader` no longer does. Default: true,
394
+ // which matches what the engine already did.
395
+ // `allowRemote` — whether PUBLISHING is permitted for EVERY spec. It
396
+ // permits it; it publishes nothing. Default: FALSE,
397
+ // because a published page is one skitterspec cannot
398
+ // delete, so it must never happen unasked. This is the
399
+ // PROJECT DEFAULT: `/spec-remote-review` permits one spec
400
+ // at a time, in a gitignored sidecar beside its page, and
401
+ // never edits this file — `review allow remote --project`
402
+ // is what does.
403
+ //
404
+ // `local` and `network` are two doors into ONE ROOM — the page POSTs to
405
+ // `location.pathname`, so both reach the same server and the same waiting
406
+ // verdict, and one wait covers both. `remote` is a second store: a verdict
407
+ // pressed on a published page needs `/spec-reviewed`, because nothing pushes
408
+ // from an artifact's store into a conversation.
409
+ //
410
+ // Both are toggled by `spec-env review allow <tier> --set [on|off]`, where an
411
+ // empty value toggles — which is what `/spec-remote-review` runs. It writes
412
+ // THIS FILE in the primary checkout, so it changes for everyone who pulls and
413
+ // leaves that tree dirty; the engine says so when it does.
414
+ //
415
+ // `commitWith` names the skill a COMMITTING verdict hands off to. A review
416
+ // page ends in a verdict — commit, commit & continue, request changes,
417
+ // discuss — and the point of the first two is that the commit follows from
418
+ // the reading rather than costing a separate decision. Skitterspec never
419
+ // commits through a skill it vendored: `/commit` ships with skittership, a
420
+ // different package, and a copy living here would fork it. Two shapes:
421
+ // "/commit" — the default; hand off to skittership's commit skill.
422
+ // "<name>" — any other skill your project installs.
423
+ // With the named skill unavailable, the committing branch commits directly —
424
+ // stage, typecheck, test, conventional message — and SAYS it took that path,
425
+ // because a commit made under rules nobody configured must not read as one
426
+ // made under /commit.
427
+ //
428
+ // THERE IS NO OFF SWITCH. `"none"` existed and was removed: it produced a
429
+ // verdict that records itself and does nothing, which is the one thing a
430
+ // review page must not offer — a review is the guard in front of an action.
431
+ // Recording an approval for SOMEONE ELSE to act on is a separate mechanism,
432
+ // not a value of this key. Default: "/commit".
433
+ //
434
+ // `required` decides whether a phase that has ended owes a verdict before its
435
+ // work is committed or the next phase is built. The gate is armed when a
436
+ // phase's page is rendered and cleared by exactly two things: a COMMITTING
437
+ // verdict, or `spec-env review skip "<reason>"` — allowed, and on the record,
438
+ // which is the whole difference between skipping and drifting.
439
+ // true — the default. A review is the normal exit from a phase.
440
+ // false — nothing is ever owed; `review gate` answers "cannot tell" and
441
+ // the commit hook (if installed) defers to it, so this one key
442
+ // turns the whole thing off.
443
+ // Only a literal `false` opts out: a typo leaves a check that REFUSES in
444
+ // place rather than quietly disabling it. Default: true.
445
+ //
446
+ // `reviewers` lists EXTERNAL CODE REVIEWERS whose findings render on the page
447
+ // as CHECKS, beside (or instead of) a written review. Empty by default, and
448
+ // `init` never writes one.
449
+ //
450
+ // READ THIS BEFORE ADDING ONE. A reviewer is a command run in the spec's
451
+ // worktree, and every hosted one sends that worktree's diff to a third
452
+ // party. Nothing else in skitterspec leaves your machine — the page is a
453
+ // local file, the server binds your own network, and the diff never reaches
454
+ // a model. Configuring a reviewer changes that, deliberately and only
455
+ // because you asked. Every finding on the page is badged with the reviewer
456
+ // that produced it, so it stays visible where the code went.
457
+ //
458
+ // Two entry shapes, told apart by which key is present:
459
+ // { "use": "<adapter>" }
460
+ // A bundled adapter. The engine owns its command line and its parser,
461
+ // so this is the whole configuration. One ships today: "coderabbit",
462
+ // which drives `cr review --agent` (a free tier, rate-limited per
463
+ // hour; `cr auth login` first). A `use` this build does not have is
464
+ // reported on stderr and does not run.
465
+ // Its adapter also does the one thing a shell script cannot: it tells
466
+ // ITS OWN refusals — not authenticated, rate limited, no network —
467
+ // apart from a review that ran and found nothing.
468
+ // { "name": "<label>", "command": "<shell>", "format": "rdjsonl" }
469
+ // Bring your own. `name` is what the page badges findings with, and
470
+ // `command` is a shell line run with the worktree as its cwd.
471
+ // Both take an optional `timeout` in seconds (default 180).
472
+ //
473
+ // `format` is the output contract, and today there is one: `rdjsonl` — one
474
+ // JSON object per line, reviewdog's diagnostic shape:
475
+ // {"path":"src/a.js","range":{"start":{"line":12}},"severity":"ERROR","message":"…"}
476
+ // A flat `"line": 12` is read too. Most linters already emit this, so a
477
+ // project gets non-AI checks on the same page for free, and an adapter for
478
+ // anything else is a twenty-line script that prints rdjsonl.
479
+ //
480
+ // `command` may carry `${scope}` (`working` | `branch`), `${base}`,
481
+ // `${spec}` and `${worktree}`. `scope` matches the render's own two modes —
482
+ // a phase-end render is `working` (uncommitted + untracked in the worktree)
483
+ // and `--branch` is the whole spec against its base — so what the page shows
484
+ // is what was reviewed. An unknown `${…}` is left verbatim rather than
485
+ // blanked, so a typo shows up in the command that failed instead of silently
486
+ // becoming an empty argument.
487
+ //
488
+ // `severity` maps to the page's levels: `ERROR` → `flag`, everything else →
489
+ // `confirm`. Never `good` — that means "I read this and it is right", which
490
+ // is a thing a person says.
491
+ //
492
+ // FINDINGS NEVER GATE A COMMIT. They are checks, and `judgeVerdict` refuses a
493
+ // committing verdict only while a COMMENT is unresolved. A comment is
494
+ // something a person asked for; twelve machine findings are not. Reply to one
495
+ // on the page and that reply IS a comment — which does gate, because now
496
+ // someone asked. There is deliberately no severity threshold that blocks:
497
+ // that is the counting gate this whole design exists against, and it makes a
498
+ // reviewer's bad day into a wall.
499
+ //
500
+ // A REVIEWER THAT COULD NOT RUN SAYS SO, rather than staying silent — the one
501
+ // place `.claude/rules/negative-checks.md` inverts. Silence is normally the
502
+ // safe branch; here a rate-limited or unauthenticated reviewer would render
503
+ // identically to one that read the diff and found nothing, on the page a
504
+ // commit decision is made from. So every configured reviewer gets a line:
505
+ // `12 findings` · `clean` · `cached` · `did not run — <why>`. Nothing exits
506
+ // non-zero and nothing refuses — a reviewer that could not run never blocks a
507
+ // render, a verdict or a commit.
508
+ //
509
+ // They run in SEQUENCE, at the end of a phase and on demand (`/spec-diff
510
+ // --reviewers`), and the results are cached against a hash of the diff — so
511
+ // re-rendering unchanged work reuses them rather than spending another review
512
+ // against an hourly limit. An entry that is neither shape is dropped and
513
+ // REPORTED on stderr; it cannot be caught by the unknown-key check, because
514
+ // this default is an array. Default: [].
515
+ "review": {
516
+ "reader": "detect",
517
+ "servePort": "auto",
518
+ "serve": "always",
519
+ "commitWith": "/commit",
520
+ "required": true,
521
+ "reviewers": []
522
+ }
523
+ }
524
+ ```
525
+
526
+ ## Token expansion
527
+
528
+ - `{repo}` — primary checkout dir basename (e.g. `skitterspec`).
529
+ - `{repoSlug}` — `{repo}` lower-cased, non-alphanumerics collapsed to `-`
530
+ (safe for a `COMPOSE_PROJECT_NAME`).
531
+ - `{slug}` — the spec slug (folder name minus its `feat-`/`bug-` prefix).
532
+
533
+ ## Pruning orphaned test-DB volumes
534
+
535
+ `spec-env down` drops the finished spec's own Docker volume, but volumes leak
536
+ when that path is skipped — a declined/guard-aborted teardown, a manual
537
+ `git worktree remove`, or `--keep-volumes`. Over many worktrees these orphaned
538
+ DB volumes pile up and eat disk.
539
+
540
+ `skitterspec spec-env prune` reconciles the live volumes in the
541
+ `{repoSlug}_*` namespace against the specs that still have a **worktree** and
542
+ lists the orphans (volumes owned by no live spec) plus the `docker volume rm`
543
+ commands to remove them. It plans only — you run the printed commands — and it
544
+ frees any stale registry slot for a reaped spec.
545
+
546
+ - **Liveness = an existing worktree, not the registry** (the registry is what
547
+ goes stale). A spec with a live worktree — in any bucket, including one checked
548
+ out only on its branch — is always protected.
549
+ - **No backup.** Unlike `spec-env down`, prune does **not** run
550
+ `backupCommand`: an orphan has no running DB to dump. Add `--older-than <days>`
551
+ to only reap volumes older than a cutoff (volumes of unknown age are kept).
552
+ - `/spec-complete` and `/spec-cancel` run prune (confirm-first) as their last
553
+ teardown step, so orphans get swept as specs finish. You can also run it by
554
+ hand at any time to clear an existing backlog.
@@ -0,0 +1,4 @@
1
+ {
2
+ "guidance": "",
3
+ "default": "none: <reason>"
4
+ }
@@ -0,0 +1,81 @@
1
+ # `gating.config.json` — release-gating config
2
+
3
+ Opt-in config for **release gating**: whether each spec records a decision about
4
+ shipping behind a feature flag.
5
+
6
+ The problem it solves is not "we forgot to use a flag" — it is that
7
+ **nobody can tell whether the question was asked**. A spec can go from `/spec`
8
+ through
9
+ implementation to `/spec-complete` with no flag and no mention of one, and that
10
+ is indistinguishable from "we considered it and decided against". Prose guidance
11
+ in a project rule has already been shown not to close that gap; a required header
12
+ does, because its absence is visible.
13
+
14
+ **Skitterspec bakes in the offer, never the mechanism.** How your project does
15
+ flags — a resolver, an admin toggle, env precedence, a vendor SDK — is none of
16
+ its business, and it never reads your flag code. It asks the question, cites your
17
+ own documentation, and records the answer.
18
+
19
+ **Adopt it** by copying `gating.config.json.example` → `gating.config.json` here
20
+ (or `skitterspec init --gating`). While this file is absent the feature is
21
+ entirely unused: no grill question, no header, nothing to check — which is read
22
+ as "this project does not use feature flags".
23
+
24
+ ## Fields
25
+
26
+ ```jsonc
27
+ {
28
+ // Where THIS project documents how its flags work — a repo-relative path,
29
+ // cited when the question is asked so the answer is an informed one.
30
+ // Skitterspec never reads the file; it only names it. Empty = say nothing.
31
+ "guidance": ".claude/rules/feature-flags.md",
32
+
33
+ // The value written when the user declines a flag, so a project can
34
+ // standardise its wording. Must keep the `none: <reason>` shape — the reason
35
+ // half is the whole point (see below). Empty falls back to `none: <reason>`.
36
+ "default": "none: <reason>"
37
+ }
38
+ ```
39
+
40
+ ## The header it drives
41
+
42
+ With this file present, every spec `/spec`, `/spec-bug` and `/spec-hotfix` write
43
+ carries a `Gating:` field beside `Stack:` in `00-overview.md`:
44
+
45
+ ```
46
+ > **Gating:** search-ranking-v2
47
+ > **Gating:** none: additive, nothing to revert
48
+ ```
49
+
50
+ Two valid shapes, and one that is not:
51
+
52
+ | Value | Meaning |
53
+ |-------|---------|
54
+ | a flag name | ships behind that flag |
55
+ | `none: <reason>` | deliberately not flagged, and why |
56
+ | *missing, empty, or a bare `none`* | **not a decision** — nobody answered |
57
+
58
+ The reason half is load-bearing. `none: additive, nothing to revert` is a
59
+ decision a reviewer can disagree with; a bare `none` is a shrug, and a missing
60
+ line is an oversight. Distinguishing those three is the entire feature.
61
+
62
+ ## What it never does
63
+
64
+ - **It never blocks.** `skitterspec gating check` reports and exits 0;
65
+ `/spec-start` and `/spec-complete` mention a missing header and carry on.
66
+ Nothing here can stop a spec being started, completed or landed.
67
+ - **It never accuses an old spec.** Only `specs/backlog/` and
68
+ `specs/in-progress/` are read. Specs finished or abandoned before you adopted
69
+ gating are out of range by construction, not by a filter someone has to
70
+ remember.
71
+ - **It never learns your flag system.** `guidance` is a path it prints, nothing
72
+ more.
73
+
74
+ ## Checking
75
+
76
+ ```
77
+ skitterspec gating check # the spec in flight here
78
+ skitterspec gating check <spec> # one named spec
79
+ skitterspec gating check --all # every active spec
80
+ skitterspec gating check --json # for tooling
81
+ ```