@skitterbyte/skitterspec-jira 0.0.0-stage → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/MIGRATION.md +1266 -0
  2. package/README.md +44 -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 +606 -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 +88 -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 +702 -0
  23. package/assets/rules/spec-reports.md +572 -0
  24. package/assets/skills/no-spec/SKILL.md +249 -0
  25. package/assets/skills/spec/SKILL.md +714 -0
  26. package/assets/skills/spec-bug/SKILL.md +585 -0
  27. package/assets/skills/spec-cancel/SKILL.md +261 -0
  28. package/assets/skills/spec-complete/SKILL.md +451 -0
  29. package/assets/skills/spec-diff/SKILL.md +870 -0
  30. package/assets/skills/spec-hotfix/SKILL.md +607 -0
  31. package/assets/skills/spec-init/SKILL.md +178 -0
  32. package/assets/skills/spec-jira-setup/SKILL.md +221 -0
  33. package/assets/skills/spec-list/SKILL.md +173 -0
  34. package/assets/skills/spec-next/SKILL.md +678 -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 +114 -0
  40. package/assets/skills/spec-sync/SKILL.md +220 -0
  41. package/assets/skills/spec-to-main/SKILL.md +141 -0
  42. package/bin/skitterspec-jira.js +68 -0
  43. package/package.json +40 -4
  44. package/src/cli.js +5967 -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 +850 -0
  51. package/src/env/dev.js +63 -0
  52. package/src/env/glob.js +34 -0
  53. package/src/env/hooks.js +226 -0
  54. package/src/env/hotfix.js +413 -0
  55. package/src/env/hotfixland.js +302 -0
  56. package/src/env/integrate.js +167 -0
  57. package/src/env/live.js +479 -0
  58. package/src/env/mainguard.js +299 -0
  59. package/src/env/provision.js +466 -0
  60. package/src/env/proxy.js +225 -0
  61. package/src/env/prune.js +119 -0
  62. package/src/env/registry.js +159 -0
  63. package/src/env/render.js +17 -0
  64. package/src/env/resolve.js +716 -0
  65. package/src/env/review.js +2622 -0
  66. package/src/env/reviewers/coderabbit.js +228 -0
  67. package/src/env/reviewers.js +517 -0
  68. package/src/env/serve.js +1044 -0
  69. package/src/env/supervise.js +157 -0
  70. package/src/env/teardown.js +248 -0
  71. package/src/env/trust.js +87 -0
  72. package/src/gating.js +155 -0
  73. package/src/init.js +1150 -0
  74. package/src/lines-diff.js +114 -0
  75. package/src/prompts.js +129 -0
  76. package/src/vendor/jira/adf.js +414 -0
  77. package/src/vendor/jira/api.js +295 -0
  78. package/src/vendor/jira/cli-sync.js +1252 -0
  79. package/src/vendor/jira/commands.js +44 -0
  80. package/src/vendor/jira/config.js +286 -0
  81. package/src/vendor/jira/credentials.js +108 -0
  82. package/src/vendor/jira/dialect.js +45 -0
  83. package/src/vendor/jira/mcp.js +129 -0
  84. package/src/vendor/provider-kit/index.js +41 -0
  85. package/src/vendor/provider-kit/src/adapter.js +23 -0
  86. package/src/vendor/provider-kit/src/apply.js +207 -0
  87. package/src/vendor/provider-kit/src/args.js +68 -0
  88. package/src/vendor/provider-kit/src/assignment.js +277 -0
  89. package/src/vendor/provider-kit/src/config.js +251 -0
  90. package/src/vendor/provider-kit/src/credentials.js +404 -0
  91. package/src/vendor/provider-kit/src/doctor.js +35 -0
  92. package/src/vendor/provider-kit/src/http.js +63 -0
  93. package/src/vendor/provider-kit/src/identity.js +93 -0
  94. package/src/vendor/provider-kit/src/list-assignee.js +71 -0
  95. package/src/vendor/provider-kit/src/list-rows.js +35 -0
  96. package/src/vendor/provider-kit/src/md-compare.js +39 -0
  97. package/src/vendor/provider-kit/src/md-tables.js +94 -0
  98. package/src/vendor/provider-kit/src/prompt.js +50 -0
  99. package/src/vendor/provider-kit/src/released.js +219 -0
  100. package/src/vendor/provider-kit/src/specs.js +248 -0
  101. package/src/vendor/provider-kit/src/transport.js +22 -0
  102. package/src/vendor/sync-core/index.js +92 -0
  103. package/src/vendor/sync-core/src/base.js +81 -0
  104. package/src/vendor/sync-core/src/compare.js +338 -0
  105. package/src/vendor/sync-core/src/dialect.js +51 -0
  106. package/src/vendor/sync-core/src/fields.js +36 -0
  107. package/src/vendor/sync-core/src/legacy.js +100 -0
  108. package/src/vendor/sync-core/src/normalize.js +971 -0
  109. package/src/vendor/sync-core/src/push.js +85 -0
  110. package/src/vendor/sync-core/src/retarget.js +278 -0
  111. package/src/vendor/sync-core/src/task-block.js +308 -0
  112. package/src/vendor/sync-core/src/verify.js +67 -0
  113. package/src/vendor/sync-core/src/write.js +191 -0
@@ -0,0 +1,606 @@
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
+ // An entry may also be { "run": "<cmd>", "when": ["<glob>", ...] }. `when` is
147
+ // IGNORED here — a fresh worktree always runs every setup step — and exists
148
+ // so `afterLand` can re-run that step after a land that changes its inputs
149
+ // (`{ "setup": true }`, below).
150
+ "setup": [],
151
+
152
+ // Commands to run in the PRIMARY checkout after a land — the counterpart of
153
+ // `setup`, for whatever landed code needs before the base branch is usable
154
+ // locally (e.g. ["pnpm db:data-migration"] for imperative data migrations
155
+ // your deploy runs and nothing runs on your dev DB). An array, run in order;
156
+ // [] (or absent) = none, and every plan prints exactly what it always did.
157
+ // `spec-env integrate` prints them after the fast-forward (worktree and
158
+ // checkout mode), and also after "already landed" — a land on another
159
+ // machine leaves them pending here. `spec-env hotfix land` prints them
160
+ // after a carry that moved the base branch, and not at all when it moved
161
+ // nothing (held, already carried, or `hotfix.cherryPickMain` false).
162
+ // The engine only plans: /spec-complete, /spec-to-main and /no-spec run
163
+ // them (a /spec documents-only land does not). They stop at the first
164
+ // failure, report it as a caveat, and never undo the land or block
165
+ // teardown. Literal strings — tokens are NOT expanded, because they run on
166
+ // the base branch where a spec's {slug} or {portOffset} mean nothing.
167
+ //
168
+ // CONDITIONAL ENTRIES. A plain string always runs. An entry may instead be
169
+ // { "run": "<cmd>", "when": ["<glob>", ...] } (a single string `when` is
170
+ // fine), which runs only when the land changes a file one of its globs
171
+ // matches — so a generate or an install costs nothing on a land that does
172
+ // not need it, and is not forgotten on one that does:
173
+ //
174
+ // "afterLand": [
175
+ // { "run": "pnpm install --frozen-lockfile", "when": ["pnpm-lock.yaml", "**/package.json"] },
176
+ // { "run": "pnpm run db:generate", "when": ["prisma/schema.prisma"] },
177
+ // "pnpm db:data-migration"
178
+ // ]
179
+ //
180
+ // Globs are repo-relative: `*` stays within a directory, `**` crosses them
181
+ // (`**/package.json` matches the root one too), `?` is one character. An
182
+ // entry whose `when` holds no usable glob is DROPPED, never widened to
183
+ // always-run.
184
+ // The changed files are what the land brings into the base branch:
185
+ // `base...branch` for `integrate` (read before the rebase, so it is the
186
+ // branch's own changes), and the carried fix commits — minus the release's
187
+ // version commit — for `hotfix land`.
188
+ // When that cannot be known, the conditional entries RUN. That is the
189
+ // "already landed" plan (the land happened elsewhere, so its range is not
190
+ // here) and any diff git would not answer. A redundant generate costs
191
+ // seconds; a skipped one leaves the base red with nothing pointing at why.
192
+ // A land that changed no files is known, not unknown, and skips them.
193
+ // The plan says why for every entry: a matched one ends in
194
+ // `# <path> changed`, an unknown one in `# range unknown` with a line saying
195
+ // so, and the rest are listed under `skipped (nothing in the land matched):`
196
+ // with their globs. The reason is a shell comment, so a line run as printed
197
+ // stays one command. Skipped lines are information — run only the list
198
+ // under `then, … run`.
199
+ //
200
+ // REUSING A SETUP STEP. `{ "setup": true }` expands, in place, to every
201
+ // `setup` entry that declares `when`, so the trigger lives in one list:
202
+ //
203
+ // "setup": [
204
+ // { "run": "pnpm install --frozen-lockfile", "when": ["pnpm-lock.yaml", "**/package.json"] },
205
+ // { "run": "pnpm run db:generate", "when": ["prisma/schema.prisma"] }
206
+ // ],
207
+ // "afterLand": [{ "setup": true }, "pnpm db:data-migration"]
208
+ //
209
+ // Plain setup strings carry no trigger and are never pulled in. The command
210
+ // is copied literally — afterLand expands no tokens — so a setup step that
211
+ // relies on {slug}-style tokens is a poor one to reuse.
212
+ "afterLand": [],
213
+
214
+ // Host dev servers `spec-env dev up <spec>` starts on the spec's port block
215
+ // (for apps that run via `pnpm dev` on the host, not inside the Docker stack).
216
+ // An array so UI + API (or more) are supervised independently; [] = none.
217
+ // Each entry:
218
+ // name label used in logs/pid files (.spec-env/{logs,pids}/<spec>-<name>).
219
+ // command the launch command, run detached in the worktree. {portVar} and
220
+ // {port} expand to the process's resolved port.
221
+ // portVar env var the command reads for its port; injected into its env.
222
+ // Process i in the block gets portBase + slot*portsPerSpec + i.
223
+ // health optional URL polled until it answers before reporting ready;
224
+ // {portVar}/{port} expand here too. Omit for no health gate.
225
+ // frontPort optional canonical origin this process fronts (e.g. 3000 for the
226
+ // UI, 8080 for the API) — used by the proxy to route to it.
227
+ "dev": [],
228
+
229
+ // Front-door proxy for `spec-env connect <spec>` — a small bundled Node
230
+ // reverse proxy (no external install) that exposes ONE connected spec's
231
+ // frontPort dev servers on the canonical ports. Exclusive: one target at a
232
+ // time. `connect main` stops it so the primary checkout owns the ports again.
233
+ "proxy": {
234
+ "enabled": true, // false = the connect command is unavailable
235
+ "host": "127.0.0.1" // bind host for the canonical ports
236
+ },
237
+
238
+ // Machine-local slot registry (spec → slot index). Resolved against the
239
+ // primary checkout root, shared by all worktrees, gitignored.
240
+ "registry": ".spec-env/registry.json",
241
+
242
+ // Git branch naming, provider-neutral. `pattern` expands {type} and {slug}
243
+ // (e.g. "feat/add-widget"). When a ticketing provider is linked and you want
244
+ // tracker ids in branch names, use {identifier} in the pattern and point
245
+ // `identifierField` at the 00-overview.md frontmatter field the provider
246
+ // writes the id into — pushing that branch can then fire the tracker's
247
+ // automation. Empty `identifierField` (or a spec missing that field) makes a
248
+ // pattern with {identifier} fall back to {type}/{slug}.
249
+ "branch": {
250
+ "pattern": "{type}/{slug}",
251
+ "identifierField": ""
252
+ },
253
+
254
+ // Paths that belong to a spec ALONGSIDE its own `specs/<bucket>/<name>/`
255
+ // folder. `/spec-start` uses this to tell "the spec you just wrote, not yet
256
+ // committed" apart from someone else's uncommitted work: if every dirty path
257
+ // belongs to the spec being started it is committed for you, and if a single
258
+ // path does not, the start is refused as before.
259
+ //
260
+ // Provider-neutral by design — the base engine must not know that any
261
+ // particular tracker exists — so you declare the shape here. `{slug}` and
262
+ // `{identifier}` expand exactly as in `branch.pattern` above, `{identifier}`
263
+ // via `branch.identifierField`.
264
+ //
265
+ // A pattern using {identifier} matches NOTHING when no identifier resolves
266
+ // (no `identifierField` set, or a spec never pushed to a tracker). That is
267
+ // deliberate: the file it names then belongs to some other spec, and the safe
268
+ // failure is a refusal you clear with /commit, not a stranger's file swept
269
+ // into your commit. Default: none — a spec owns only its own folder.
270
+ //
271
+ // THIS DISTRIBUTION SEEDS BOTH KEYS. `skitterspec init --isolation` writes an
272
+ // `env.config.json` already carrying
273
+ // `"companionPaths": ["specs/.core/jira-base/{identifier}.base.json"]` and
274
+ // `"identifierField": "jira_identifier"`, because the Jira snapshot is
275
+ // written by this package, and only this package knows where. What follows is
276
+ // the base engine's default — what you get if you clear them.
277
+ //
278
+ // Both or neither: `{identifier}` resolves through `branch.identifierField`,
279
+ // so `companionPaths` on its own expands to nothing and owns nothing.
280
+ "spec": {
281
+ "companionPaths": []
282
+ },
283
+
284
+ // Integration base branch — the branch specs fork from and land back onto
285
+ // (used by the teardown "merged?" guard and, later, the integrate step).
286
+ // Empty = auto-detect: origin/HEAD → main → master. Set it when your default
287
+ // branch isn't discoverable (e.g. no remote) or differs (trunk, develop).
288
+ "baseBranch": "",
289
+
290
+ // Teardown safety. --force overrides both. refuseTeardownIfUnpushed only
291
+ // blocks when the commits are ALSO unmerged into the base branch — a branch
292
+ // already landed on base tears down (and its branch is deleted) without
293
+ // --force, even with no remote.
294
+ "guards": {
295
+ "refuseTeardownIfDirty": true,
296
+ "refuseTeardownIfUnpushed": true
297
+ },
298
+
299
+ // What teardown cleans up beyond this machine. Nothing publishes a spec
300
+ // branch for you, so a remote copy exists only because you pushed it by hand —
301
+ // and without this a completed spec leaves that merged branch on the remote
302
+ // forever. `deleteRemoteBranch`:
303
+ // "prompt" (default) — plan `git push <remote> --delete <branch>` in its own
304
+ // "confirm with the user first" section; /spec-complete and
305
+ // /spec-cancel ask before running it.
306
+ // "never" — omit it; clean the remote up yourself.
307
+ // "always" — fold it into `run these:` and never ask.
308
+ // Only ever planned for a branch that has LANDED (merged into base, or captured
309
+ // by a hotfix's deploy tag) and whose remote-tracking ref this clone can
310
+ // actually see. Until a branch lands, the remote copy is its only backup, so
311
+ // --force does NOT enable this. A branch pushed from another machine has no
312
+ // local ref here and is simply missed — teardown under-cleans rather than
313
+ // deleting something it cannot see. An unrecognised value means "prompt".
314
+ "teardown": {
315
+ "deleteRemoteBranch": "prompt"
316
+ },
317
+
318
+ // Live overlay (`spec-env live` / `/spec-live`): test a spec on the already-
319
+ // running dev server by checking its branch out in the primary checkout.
320
+ // `migrations` is a list of globs (`**`, `*`, `?`) marking migration files; a
321
+ // branch that changes any of them is treated as STATEFUL and `live take`
322
+ // refuses it (code-only v1 — use `/spec-connect` for those). Default: none.
323
+ "live": {
324
+ "migrations": []
325
+ },
326
+
327
+ // Hotfix landing (`spec-env hotfix land` / `/spec-complete` on a Type: Hotfix
328
+ // spec). A hotfix forks from a release tag and lands by release + push + carry,
329
+ // not fast-forward; `hotfix land` runs those steps itself, in that order.
330
+ //
331
+ // `releaseWith` is a command run IN THE HOTFIX WORKTREE that cuts the release —
332
+ // its version commit and its tag — and prints ONE JSON line on stdout:
333
+ // {"tag","version","baseTag","branch","versionCommit","pushed"}. skittership
334
+ // 2.1+ ships one: "node scripts/release-hotfix.cjs --json" (call node directly
335
+ // rather than through `pnpm run`, so no run banner reaches stdout; anything but
336
+ // the JSON line is a refusal). skitterspec never passes it `--push`, since it
337
+ // owns the push. skittership refuses a branch outside its `hotfix.branchPrefix`
338
+ // (default `hotfix/`) — the default `branch.pattern` already gives
339
+ // `hotfix/<slug>` — and refuses a package.json with a `postversion` script
340
+ // unless the command says `--allow-postversion`; add that yourself if you have
341
+ // one. Empty (the default) = the built-in tagger.
342
+ //
343
+ // `bump` is the built-in tagger's strategy: "patch" (v33.16.4 -> v33.16.5) or
344
+ // "hotfix-suffix" (v33.16.4 -> v33.16.4-hotfix.1, then -hotfix.2, ...). Under
345
+ // either, a base that is already a hotfix tag continues its line
346
+ // (v36.2.3-hotfix.4 -> v36.2.3-hotfix.5), so "patch" never takes a number the
347
+ // mainline owns. When the patch tag already exists, the refusal names the
348
+ // free hotfix tag that "hotfix-suffix" would cut. Any other value is refused
349
+ // when `hotfix land` runs. Tags order X.Y.Z-rc.N < X.Y.Z < X.Y.Z-hotfix.N <
350
+ // X.Y.(Z+1), the same as skittership.
351
+ //
352
+ // `cherryPickMain` carries the fix onto the base branch for the next release
353
+ // (default true) — its commits minus the version commit and minus anything the
354
+ // base already has. `targets` is an optional default list of extra base tags to
355
+ // also patch (test/demo lines); `--also <tag>` adds more at run time. Targets
356
+ // are TAG-ONLY: they get the built-in tagger, never `releaseWith`, and are not
357
+ // pushed. The prod release is pushed (branch, then tag, to origin) only on
358
+ // `hotfix land --push`; without it the land holds before the carry. The
359
+ // worktree stays until `spec-env hotfix finish`. Default: built-in patch tag,
360
+ // carry to main, no targets.
361
+ "hotfix": {
362
+ "releaseWith": "",
363
+ "bump": "patch",
364
+ "cherryPickMain": true,
365
+ "targets": []
366
+ },
367
+
368
+ // Reading a spec's diff (`spec-env review`, `/spec-diff`).
369
+ //
370
+ // `reader` decides how the page's LOCATION IS WORDED. It no longer decides
371
+ // the bind — `allowNetwork` does, see below — and it no longer decides which
372
+ // tiers are offered, because every tier is now listed whatever it says.
373
+ // WHAT THE SERVER
374
+ // BINDS TO — and nothing else. It never decides whether to serve (`serve`
375
+ // does) and never decides to PUBLISH. Three values:
376
+ // "local" — you are at the machine holding the page; it binds 127.0.0.1.
377
+ // "remote" — you are not; it binds every interface so the page opens.
378
+ // "detect" — work it out (the default); cannot-tell binds 127.0.0.1.
379
+ // An explicit "local"/"remote" is BELIEVED WITHOUT SNIFFING: you know where
380
+ // you are reading, and no signal outranks being told. Detection is only the
381
+ // default, and it has three outcomes rather than two — local, remote, and
382
+ // unknown. Unknown behaves exactly as the tool did before any of this existed
383
+ // (the file:// URL, no warning), because a wrong "local" prints a dead link
384
+ // and a wrong "remote" warns at someone whose link works fine. An
385
+ // unrecognised value falls through to "detect", so a typo cannot become a
386
+ // confident answer. Default: detect.
387
+ //
388
+ // `servePort` is the port for `spec-env review serve`, which renders every
389
+ // spec's diff per request on one local server. Two forms:
390
+ //
391
+ // "auto" (the default) — derive it from this repo's path, as
392
+ // 7700 + hash(realpath(repoRoot)) % 100. Two repos on one machine stop
393
+ // competing for one shared port without anyone configuring anything, and
394
+ // — the part that matters — THE SAME REPO GETS THE SAME PORT EVERY TIME.
395
+ // The derivation reads nothing on disk, so the port survives a restart, a
396
+ // reboot and a `--stop`, which is what lets a link handed out yesterday
397
+ // still resolve. A symlinked spelling of the tree resolves first, so one
398
+ // repo never lands on two ports.
399
+ // <a number> — pin it. An explicit number always wins, and pinning is what
400
+ // you do when you want a port you can memorise, or when two repos derive
401
+ // the same one.
402
+ //
403
+ // A derived port CAN still collide — a hundred slots is a small chance, not
404
+ // no chance — and the server refuses rather than moving itself aside. The fix
405
+ // it names is `servePort`, because that is what the next link is built from;
406
+ // `--port` moves one run and leaves every link already handed out pointing at
407
+ // the busy port. `spec-env review serve --status` prints the port and which
408
+ // of the three chose it.
409
+ //
410
+ // An unrecognised value falls through to "auto", like every other typed key
411
+ // here. Default: "auto".
412
+ //
413
+ // The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which mints
414
+ // an unguessable path token and prints the LAN URL including it — anyone
415
+ // holding that URL can read every spec's diff while it runs.
416
+ //
417
+ // `serve` is whether a render stands the local server up at all:
418
+ // "always" — every render does (the default), so `local:` and `network:`
419
+ // are both http URLs the page can POST a verdict back to. The
420
+ // LAN address comes first with the rest listed under it, because
421
+ // the guess reads interface names and a VPN or an unusual
422
+ // adapter will fool it.
423
+ // "never" — `local:` is a file:// URL instead. Note what that costs: a
424
+ // file:// page has no server to POST to, so its verdict buttons
425
+ // copy a command for you to paste rather than sending anything.
426
+ // It replaced `serveOnRemote`, which gated serving on the reader — and so
427
+ // handed a local machine a page it could read and not answer. A legacy
428
+ // `serveOnRemote: false` is still read as `serve: "never"`.
429
+ // Either way NOTHING IS PUBLISHED on a detection: a server is one process
430
+ // ended by one flag, while a published page is one this tooling cannot
431
+ // remove, so that half stays an explicit ask. Teardown names a server that
432
+ // served the last spec, and `spec-env prune` reaps a pidfile whose process is
433
+ // gone. Default: true.
434
+ //
435
+ // `allowNetwork` and `allowRemote` decide WHICH TIERS a render offers, and
436
+ // they replaced the engine guessing where the reader was sitting. It guessed
437
+ // for a while and got it wrong three separate ways in one day: a file:// page
438
+ // on a session detected `unknown`, a LAN URL for a phone that had left the
439
+ // network, and an address that changed underneath a reader mid-session. So
440
+ // every tier is listed, labelled, and either a URL or the one command that
441
+ // turns it on — and you pick the one that reaches you.
442
+ //
443
+ // `allowNetwork` — whether the review server binds EVERY INTERFACE, so the
444
+ // page opens on your phone, or loopback only. THIS IS WHAT
445
+ // CHOOSES THE BIND; `reader` no longer does. Default: true,
446
+ // which matches what the engine already did.
447
+ // `allowRemote` — whether PUBLISHING is permitted for EVERY spec. It
448
+ // permits it; it publishes nothing. Default: FALSE,
449
+ // because a published page is one skitterspec cannot
450
+ // delete, so it must never happen unasked. This is the
451
+ // PROJECT DEFAULT: `/spec-remote-review` permits one spec
452
+ // at a time, in a gitignored sidecar beside its page, and
453
+ // never edits this file — `review allow remote --project`
454
+ // is what does.
455
+ //
456
+ // `local` and `network` are two doors into ONE ROOM — the page POSTs to
457
+ // `location.pathname`, so both reach the same server and the same waiting
458
+ // verdict, and one wait covers both. `remote` is a second store: a verdict
459
+ // pressed on a published page needs `/spec-reviewed`, because nothing pushes
460
+ // from an artifact's store into a conversation.
461
+ //
462
+ // Both are toggled by `spec-env review allow <tier> --set [on|off]`, where an
463
+ // empty value toggles — which is what `/spec-remote-review` runs. It writes
464
+ // THIS FILE in the primary checkout, so it changes for everyone who pulls and
465
+ // leaves that tree dirty; the engine says so when it does.
466
+ //
467
+ // `commitWith` names the skill a COMMITTING verdict hands off to. A review
468
+ // page ends in a verdict — commit, commit & continue, request changes,
469
+ // discuss — and the point of the first two is that the commit follows from
470
+ // the reading rather than costing a separate decision. Skitterspec never
471
+ // commits through a skill it vendored: `/commit` ships with skittership, a
472
+ // different package, and a copy living here would fork it. Two shapes:
473
+ // "/commit" — the default; hand off to skittership's commit skill.
474
+ // "<name>" — any other skill your project installs.
475
+ // With the named skill unavailable, the committing branch commits directly —
476
+ // stage, typecheck, test, conventional message — and SAYS it took that path,
477
+ // because a commit made under rules nobody configured must not read as one
478
+ // made under /commit.
479
+ //
480
+ // THERE IS NO OFF SWITCH. `"none"` existed and was removed: it produced a
481
+ // verdict that records itself and does nothing, which is the one thing a
482
+ // review page must not offer — a review is the guard in front of an action.
483
+ // Recording an approval for SOMEONE ELSE to act on is a separate mechanism,
484
+ // not a value of this key. Default: "/commit".
485
+ //
486
+ // `required` decides whether a phase that has ended owes a verdict before its
487
+ // work is committed or the next phase is built. The gate is armed when a
488
+ // phase's page is rendered and cleared by exactly two things: a COMMITTING
489
+ // verdict, or `spec-env review skip "<reason>"` — allowed, and on the record,
490
+ // which is the whole difference between skipping and drifting.
491
+ // true — the default. A review is the normal exit from a phase.
492
+ // false — nothing is ever owed; `review gate` answers "cannot tell" and
493
+ // the commit hook (if installed) defers to it, so this one key
494
+ // turns the whole thing off.
495
+ // Only a literal `false` opts out: a typo leaves a check that REFUSES in
496
+ // place rather than quietly disabling it. Default: true.
497
+ //
498
+ // `reviewers` lists EXTERNAL CODE REVIEWERS whose findings render on the page
499
+ // as CHECKS, beside (or instead of) a written review. Empty by default, and
500
+ // `init` never writes one.
501
+ //
502
+ // READ THIS BEFORE ADDING ONE. A reviewer is a command run in the spec's
503
+ // worktree, and every hosted one sends that worktree's diff to a third
504
+ // party. Nothing else in skitterspec leaves your machine — the page is a
505
+ // local file, the server binds your own network, and the diff never reaches
506
+ // a model. Configuring a reviewer changes that, deliberately and only
507
+ // because you asked. Every finding on the page is badged with the reviewer
508
+ // that produced it, so it stays visible where the code went.
509
+ //
510
+ // Two entry shapes, told apart by which key is present:
511
+ // { "use": "<adapter>" }
512
+ // A bundled adapter. The engine owns its command line and its parser,
513
+ // so this is the whole configuration. One ships today: "coderabbit",
514
+ // which drives `cr review --agent` (a free tier, rate-limited per
515
+ // hour; `cr auth login` first). A `use` this build does not have is
516
+ // reported on stderr and does not run.
517
+ // Its adapter also does the one thing a shell script cannot: it tells
518
+ // ITS OWN refusals — not authenticated, rate limited, no network —
519
+ // apart from a review that ran and found nothing.
520
+ // { "name": "<label>", "command": "<shell>", "format": "rdjsonl" }
521
+ // Bring your own. `name` is what the page badges findings with, and
522
+ // `command` is a shell line run with the worktree as its cwd.
523
+ // Both take an optional `timeout` in seconds (default 180).
524
+ //
525
+ // `format` is the output contract, and today there is one: `rdjsonl` — one
526
+ // JSON object per line, reviewdog's diagnostic shape:
527
+ // {"path":"src/a.js","range":{"start":{"line":12}},"severity":"ERROR","message":"…"}
528
+ // A flat `"line": 12` is read too. Most linters already emit this, so a
529
+ // project gets non-AI checks on the same page for free, and an adapter for
530
+ // anything else is a twenty-line script that prints rdjsonl.
531
+ //
532
+ // `command` may carry `${scope}` (`working` | `branch`), `${base}`,
533
+ // `${spec}` and `${worktree}`. `scope` matches the render's own two modes —
534
+ // a phase-end render is `working` (uncommitted + untracked in the worktree)
535
+ // and `--branch` is the whole spec against its base — so what the page shows
536
+ // is what was reviewed. An unknown `${…}` is left verbatim rather than
537
+ // blanked, so a typo shows up in the command that failed instead of silently
538
+ // becoming an empty argument.
539
+ //
540
+ // `severity` maps to the page's levels: `ERROR` → `flag`, everything else →
541
+ // `confirm`. Never `good` — that means "I read this and it is right", which
542
+ // is a thing a person says.
543
+ //
544
+ // FINDINGS NEVER GATE A COMMIT. They are checks, and `judgeVerdict` refuses a
545
+ // committing verdict only while a COMMENT is unresolved. A comment is
546
+ // something a person asked for; twelve machine findings are not. Reply to one
547
+ // on the page and that reply IS a comment — which does gate, because now
548
+ // someone asked. There is deliberately no severity threshold that blocks:
549
+ // that is the counting gate this whole design exists against, and it makes a
550
+ // reviewer's bad day into a wall.
551
+ //
552
+ // A REVIEWER THAT COULD NOT RUN SAYS SO, rather than staying silent — the one
553
+ // place `.claude/rules/negative-checks.md` inverts. Silence is normally the
554
+ // safe branch; here a rate-limited or unauthenticated reviewer would render
555
+ // identically to one that read the diff and found nothing, on the page a
556
+ // commit decision is made from. So every configured reviewer gets a line:
557
+ // `12 findings` · `clean` · `cached` · `did not run — <why>`. Nothing exits
558
+ // non-zero and nothing refuses — a reviewer that could not run never blocks a
559
+ // render, a verdict or a commit.
560
+ //
561
+ // They run in SEQUENCE, at the end of a phase and on demand (`/spec-diff
562
+ // --reviewers`), and the results are cached against a hash of the diff — so
563
+ // re-rendering unchanged work reuses them rather than spending another review
564
+ // against an hourly limit. An entry that is neither shape is dropped and
565
+ // REPORTED on stderr; it cannot be caught by the unknown-key check, because
566
+ // this default is an array. Default: [].
567
+ "review": {
568
+ "reader": "detect",
569
+ "servePort": "auto",
570
+ "serve": "always",
571
+ "commitWith": "/commit",
572
+ "required": true,
573
+ "reviewers": []
574
+ }
575
+ }
576
+ ```
577
+
578
+ ## Token expansion
579
+
580
+ - `{repo}` — primary checkout dir basename (e.g. `skitterspec`).
581
+ - `{repoSlug}` — `{repo}` lower-cased, non-alphanumerics collapsed to `-`
582
+ (safe for a `COMPOSE_PROJECT_NAME`).
583
+ - `{slug}` — the spec slug (folder name minus its `feat-`/`bug-` prefix).
584
+
585
+ ## Pruning orphaned test-DB volumes
586
+
587
+ `spec-env down` drops the finished spec's own Docker volume, but volumes leak
588
+ when that path is skipped — a declined/guard-aborted teardown, a manual
589
+ `git worktree remove`, or `--keep-volumes`. Over many worktrees these orphaned
590
+ DB volumes pile up and eat disk.
591
+
592
+ `skitterspec spec-env prune` reconciles the live volumes in the
593
+ `{repoSlug}_*` namespace against the specs that still have a **worktree** and
594
+ lists the orphans (volumes owned by no live spec) plus the `docker volume rm`
595
+ commands to remove them. It plans only — you run the printed commands — and it
596
+ frees any stale registry slot for a reaped spec.
597
+
598
+ - **Liveness = an existing worktree, not the registry** (the registry is what
599
+ goes stale). A spec with a live worktree — in any bucket, including one checked
600
+ out only on its branch — is always protected.
601
+ - **No backup.** Unlike `spec-env down`, prune does **not** run
602
+ `backupCommand`: an orphan has no running DB to dump. Add `--older-than <days>`
603
+ to only reap volumes older than a cutoff (volumes of unknown age are kept).
604
+ - `/spec-complete` and `/spec-cancel` run prune (confirm-first) as their last
605
+ teardown step, so orphans get swept as specs finish. You can also run it by
606
+ hand at any time to clear an existing backlog.
@@ -0,0 +1,4 @@
1
+ {
2
+ "guidance": "",
3
+ "default": "none: <reason>"
4
+ }