@skitterbyte/skitterspec-linear 12.0.0 → 14.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.
- package/MIGRATION.md +296 -10
- package/README.md +32 -2
- package/assets/claude-md-section.md +38 -2
- package/assets/commands/spec-connect.md +2 -2
- package/assets/commands/spec-live.md +2 -2
- package/assets/core/SETUP.md +21 -3
- package/assets/core/env.config.json.example +7 -3
- package/assets/core/env.config.md +90 -30
- package/assets/core/linear.config.md +58 -0
- package/assets/hooks/review-gate.js +141 -0
- package/assets/review/page.html +1787 -0
- package/assets/rules/spec-planning.md +250 -10
- package/assets/rules/spec-reports.md +321 -0
- package/assets/skills/spec/SKILL.md +33 -5
- package/assets/skills/spec-bug/SKILL.md +172 -9
- package/assets/skills/spec-cancel/SKILL.md +98 -21
- package/assets/skills/spec-claim/SKILL.md +114 -0
- package/assets/skills/spec-complete/SKILL.md +94 -25
- package/assets/skills/spec-diff/SKILL.md +678 -0
- package/assets/skills/spec-hotfix/SKILL.md +172 -11
- package/assets/skills/spec-init/SKILL.md +56 -7
- package/assets/skills/spec-linear-setup/SKILL.md +55 -1
- package/assets/skills/spec-list/SKILL.md +218 -0
- package/assets/skills/spec-next/SKILL.md +419 -7
- package/assets/skills/spec-push/SKILL.md +32 -8
- package/assets/skills/spec-review/SKILL.md +40 -5
- package/assets/skills/spec-reviewed/SKILL.md +258 -0
- package/assets/skills/spec-start/SKILL.md +386 -106
- package/assets/skills/spec-status/SKILL.md +24 -2
- package/assets/skills/spec-sync/SKILL.md +40 -4
- package/assets/skills/spec-to-main/SKILL.md +28 -6
- package/package.json +11 -7
- package/src/cli.js +1808 -89
- package/src/env/building.js +143 -0
- package/src/env/commitcmd.js +108 -0
- package/src/env/config.js +58 -9
- package/src/env/hooks.js +117 -0
- package/src/env/provision.js +54 -15
- package/src/env/proxy.js +34 -1
- package/src/env/render.js +3 -12
- package/src/env/resolve.js +295 -9
- package/src/env/review.js +1536 -0
- package/src/env/serve.js +573 -0
- package/src/env/teardown.js +13 -6
- package/src/init.js +150 -1
- package/src/vendor/linear/api.js +104 -1
- package/src/vendor/linear/cli-sync.js +854 -17
- package/src/vendor/linear/config.js +8 -0
- package/src/vendor/linear/credentials.js +94 -0
- package/src/vendor/linear/doctor.js +35 -0
- package/src/vendor/linear/identity.js +105 -0
- package/src/vendor/linear/mcp.js +26 -0
- package/src/vendor/sync-core/index.js +6 -2
- package/src/vendor/sync-core/src/compare.js +49 -3
- package/src/vendor/sync-core/src/normalize.js +30 -0
- package/src/vendor/sync-core/src/push.js +11 -1
- package/src/vendor/sync-core/src/write.js +38 -0
- package/LICENSE +0 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Spec Planning
|
|
2
2
|
|
|
3
|
-
Spec-driven development is driven by
|
|
3
|
+
Spec-driven development is driven by ten lifecycle skills (plus the
|
|
4
4
|
`/spec-connect` and `/spec-live` **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:** …`):
|
|
@@ -13,6 +13,7 @@ and lifecycle stay consistent. Each sets a status on the spec header
|
|
|
13
13
|
| `/spec-review` | Re-validate a spec against the codebase; refresh stale parts | `—` | (unchanged) |
|
|
14
14
|
| `/spec-start` | Put a spec in flight — provision its branch, then build phase 1 | `In Progress` | `specs/in-progress/` |
|
|
15
15
|
| `/spec-next` | Build the next phase of the spec this session is in (re-run per phase) | `In Progress` (unchanged) | (unchanged) |
|
|
16
|
+
| `/spec-reviewed` | Pick up a review approved on the page — offered by its code, claimed on your word | (unchanged) | (unchanged) |
|
|
16
17
|
| `/spec-to-main` | Land the branch on the base (rebase + ff) **without** finishing — for running the work in CI / a shared env mid-spec; repeatable | `In Progress` (unchanged) | (unchanged) |
|
|
17
18
|
| `/spec-complete` | Verify all phases done + tests green; land + tear down | `Complete` | `specs/complete/` |
|
|
18
19
|
| `/spec-cancel` | Record progress, stamp a reason on the header; tear down | `Cancelled` | `specs/cancelled/` |
|
|
@@ -26,6 +27,20 @@ finding one. They are marked `disable-model-invocation`, meaning
|
|
|
26
27
|
**only you can run them** — a skill that wants one will tell you to type it
|
|
27
28
|
rather than invoking it.
|
|
28
29
|
|
|
30
|
+
`/spec-reviewed` is a **skill** and **user-only**, and there the marking is not
|
|
31
|
+
convenience — it is the enforcement of `/spec-diff` step 0's rule that a waiting
|
|
32
|
+
review pass is never claimed unasked. Because the model cannot invoke this
|
|
33
|
+
skill, a pass named this way is only ever picked up because a person typed the
|
|
34
|
+
command, and typing it **is** the human signal. Prose alone did not hold that
|
|
35
|
+
line once already.
|
|
36
|
+
|
|
37
|
+
It is **no longer the only way in**, and the difference is worth stating
|
|
38
|
+
precisely. A phase that ends now *waits* on its page, and a pass arriving inside
|
|
39
|
+
that wait is claimed by the engine (`--claim-since`) without anyone typing
|
|
40
|
+
anything — scoped to the window, refusing when two arrive. `/spec-reviewed` is
|
|
41
|
+
what answers everything outside it: a pass sent when nobody was waiting, two
|
|
42
|
+
passes to choose between, and every harness with no file-watch to wait with.
|
|
43
|
+
|
|
29
44
|
`/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
|
|
30
45
|
real judgment (green tests before a land; an MCP fetch and a team-key check; ten
|
|
31
46
|
subcommands) — but they are marked user-only too, since nobody reaches them
|
|
@@ -44,11 +59,15 @@ mid-spec (so the work can run in CI / a shared test env) while the spec stays
|
|
|
44
59
|
|
|
45
60
|
**Two workspace modes.** `specs/.core/env.config.json` → `mode` decides where a
|
|
46
61
|
spec's branch is built. **`worktree`** (the default) gives each spec its own
|
|
47
|
-
checkout — several specs at once and `main` left free
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
checkout — several specs at once and `main` left free. `/spec-start` builds the
|
|
63
|
+
branch there, **moves your session into it** with a plain `cd`, and then asks:
|
|
64
|
+
build phase 1 now? Say yes and it carries on into a bare `/spec-next`; say no and
|
|
65
|
+
you are already standing in the worktree, so `/spec-next` typed later does the
|
|
66
|
+
same thing. The primary checkout stays on the base branch throughout — the
|
|
67
|
+
session followed the spec, nothing was checked out anywhere.
|
|
68
|
+
**`checkout`** builds the branch in the primary checkout instead: one spec at a
|
|
69
|
+
time, nowhere else to stand, and the build always carries straight on.
|
|
70
|
+
The difference is how many specs can be in flight at once.
|
|
52
71
|
Pick it for how you work rather than for what the project contains — a repo with
|
|
53
72
|
no dev servers may still want several specs in flight. In `checkout` mode
|
|
54
73
|
`/spec-connect` and `/spec-live` do not apply and say so: both exist to reach
|
|
@@ -68,8 +87,11 @@ your canonical `localhost` ports so you can test it at the normal URL
|
|
|
68
87
|
move, header edits, the code) happens on the spec's branch in the worktree; `main`
|
|
69
88
|
changes only when it merges. Teardown is folded into `/spec-complete` ·
|
|
70
89
|
`/spec-cancel`. Beneath the skills, `skitterspec spec-env
|
|
71
|
-
<up|down|prune|dev|connect|integrate|hotfix|live|status|resolve>` is the CLI
|
|
72
|
-
engine
|
|
90
|
+
<up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
|
|
91
|
+
engine. **Omit the spec name anywhere and it uses the worktree you are standing in**,
|
|
92
|
+
else the sole provisioned spec — with no exceptions left: a bare
|
|
93
|
+
`/spec-connect` connects, and a bare `/spec-live` takes, where both once meant
|
|
94
|
+
`main`. Teardown drops
|
|
73
95
|
the finished spec's own test-DB volume; `spec-env prune` additionally reaps
|
|
74
96
|
**orphaned** volumes left by declined/aborted teardowns, so `/spec-complete` and
|
|
75
97
|
`/spec-cancel` also sweep orphans (confirm-first). A **hotfix** is the one
|
|
@@ -81,9 +103,12 @@ when `env.config.json` is absent — every skill then behaves as it does today.
|
|
|
81
103
|
**Live overlay (`/spec-live`, a command) — the light way to test a spec.** `/spec-connect`
|
|
82
104
|
runs a spec's *own* dev stack and proxies the canonical ports to it (one stack per
|
|
83
105
|
spec). **Live overlay** instead reuses the one dev server you already have running:
|
|
84
|
-
`/spec-live <spec>`
|
|
106
|
+
`/spec-live <spec>` — or a bare `/spec-live`, which takes the spec you are
|
|
107
|
+
standing on — rebases the branch onto base, frees it from its worktree, and
|
|
85
108
|
checks it out **in the primary checkout**, so your running server hot-reloads the
|
|
86
|
-
feature at the normal URL — no second stack, no proxy.
|
|
109
|
+
feature at the normal URL — no second stack, no proxy. Bare only acts when there
|
|
110
|
+
is exactly one answer *and* the workbench is free; anything else prints the
|
|
111
|
+
status report rather than guessing. The branch checked out in
|
|
87
112
|
the primary checkout **is** the lock: exactly one spec is live at a time, and
|
|
88
113
|
`/spec-live main` hands the instance back (fixes you make while live commit
|
|
89
114
|
straight onto the branch; `/spec-complete` is live-aware and lands them). Rule of
|
|
@@ -93,6 +118,203 @@ stateful ones (`Stack: worktree + docker`, or a branch touching migrations) —
|
|
|
93
118
|
`/spec-connect` + a Docker stack for those, and for genuinely parallel testing.
|
|
94
119
|
Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engine.
|
|
95
120
|
|
|
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.
|
|
126
|
+
`skitterspec spec-env stage [<spec>] [--json]` is how a skill asks instead of
|
|
127
|
+
guessing: it splits the uncommitted tree into the paths that are this spec's —
|
|
128
|
+
its folder in **any** bucket, so a tree mid-`git mv` is handled, plus each
|
|
129
|
+
`spec.companionPaths` entry the project declares — and the paths that are not.
|
|
130
|
+
It reads **the tree you are standing in**, which is the worktree inside one and
|
|
131
|
+
the primary checkout outside, and prints which. The `owned` half is the spec's
|
|
132
|
+
**documents**, never its code: a phase's own implementation is `foreign`, because
|
|
133
|
+
the commits this bounds are the lifecycle ones. Nothing here refuses, writes, or
|
|
134
|
+
exits non-zero — `foreign` is a list of paths to leave alone, not an accusation.
|
|
135
|
+
|
|
136
|
+
**Naming the paths is necessary and not sufficient**, which is why every spec
|
|
137
|
+
commit is also pathspec-limited: `git add -- <paths>` **and**
|
|
138
|
+
`git commit -m "…" -- <paths>`. A checkout has one `.git/index` and every session
|
|
139
|
+
standing in it shares that index, so a bare `git commit` takes whatever another
|
|
140
|
+
session has already staged no matter how carefully this one staged its own. The
|
|
141
|
+
`add` is still needed — `git commit` cannot take a path git has never seen, which
|
|
142
|
+
is every brand-new spec folder — but it is the `--` on the **commit** that bounds
|
|
143
|
+
what lands. The two failures are different and the fix needs both halves.
|
|
144
|
+
|
|
145
|
+
**Reading a spec's diff (`/spec-diff`) — seeing the work, not running it.**
|
|
146
|
+
`/spec-connect` and `/spec-live` both exist to reach a *running app*.
|
|
147
|
+
**`/spec-diff`** answers the other question: what did this actually change? A
|
|
148
|
+
phase is built in its own worktree, so `git diff` in your terminal answers about
|
|
149
|
+
the base branch — and a 350-line diff read as terminal text is scrolling, not
|
|
150
|
+
review. The engine collects the worktree's changes with `git -C` and writes a
|
|
151
|
+
self-contained HTML page (`.spec-env/reviews/<spec>.html`, gitignored) with
|
|
152
|
+
whole-file context that folds away, a file tree, and new files included. You open
|
|
153
|
+
it locally, or publish it and read it on a phone.
|
|
154
|
+
**The diff never passes through the model**, so the page is free to produce
|
|
155
|
+
however large it is; the *written*
|
|
156
|
+
review — a short read plus `flag`/`confirm`/`good` notes — is the part that costs
|
|
157
|
+
tokens, and it is offered rather than assumed. It is a **skill**, model-invocable,
|
|
158
|
+
and it is gated on nothing: half a phase, a hand edit and a colleague's branch are
|
|
159
|
+
all ordinary inputs. `/spec-next` renders the page at the end of a phase and
|
|
160
|
+
offers the review; `skitterspec spec-env review <spec> [--branch]` is the engine.
|
|
161
|
+
|
|
162
|
+
**The page is not read-only — it takes a review pass back.** Tick `✓ accept` per
|
|
163
|
+
file as you read, write notes against a line or a whole file, answer the
|
|
164
|
+
questions a written review asked, then **end it in a decision** and hand it
|
|
165
|
+
back. Three buttons, each carrying its own verdict:
|
|
166
|
+
**`✓ Approve`** (commit it), **`↺ Request changes`** (work them now),
|
|
167
|
+
**`… Discuss first`** (report and talk). `/spec-diff` stores it (`--notes`),
|
|
168
|
+
plays back what it read, and routes on the verdict — `changes` **is** the
|
|
169
|
+
go-ahead, so it works the commented files immediately, while `discuss`, a
|
|
170
|
+
refused approval and a pass carrying no verdict at all report and wait, which
|
|
171
|
+
is what a bare paste has always done. Accepted files stay unopened either way.
|
|
172
|
+
|
|
173
|
+
**Two ways back, and both are ordinary.** A **served** page POSTs the pass to
|
|
174
|
+
its own URL; the engine holds it in a gitignored `.pending.json` beside the
|
|
175
|
+
notes sidecar and answers with a **six-digit code**. Read the code out and
|
|
176
|
+
`/spec-diff` claims it (`--claim`), merging exactly the pass that code names.
|
|
177
|
+
A `file://` page has no server to talk to — the File System Access API is
|
|
178
|
+
Chrome-only and absent on iOS Safari — so it copies to the clipboard and you
|
|
179
|
+
paste, exactly as before. The clipboard path is **not legacy**: it is the whole
|
|
180
|
+
story for a local reader.
|
|
181
|
+
|
|
182
|
+
The code is **not a secret** — it is printed on the page, and it cannot be a
|
|
183
|
+
gate against Claude either, because the store is a file Claude can read.
|
|
184
|
+
|
|
185
|
+
It was once true that a device reaching your page
|
|
186
|
+
**could not reach your conversation**, and that fact was the whole guard: a pass sat in the holding
|
|
187
|
+
area until you typed `/spec-reviewed`. It is no longer true, deliberately —
|
|
188
|
+
a phase that ends **waits** for its verdict, so the button you press on the page
|
|
189
|
+
is what carries the work on. What replaced the guard is two mechanisms and one
|
|
190
|
+
rule. The **serve token** — 48 bits of randomness in the URL path, minted per
|
|
191
|
+
server — decides who can POST at all. The **wait window** decides which pass may
|
|
192
|
+
be claimed without you naming it: the engine's `--claim-since` takes the one
|
|
193
|
+
pass that arrived *while this session was waiting*, acts on nothing when none
|
|
194
|
+
did, and refuses to choose when two did. And outside that window the rule stands
|
|
195
|
+
unchanged and absolute: **Claude never claims a pass it was not asked to**
|
|
196
|
+
(`/spec-diff` step 0, because nothing enforces it).
|
|
197
|
+
|
|
198
|
+
What you give up is real and worth naming: the page can now act. What you get is
|
|
199
|
+
the loop closing without anyone remembering a command — which is the failure the
|
|
200
|
+
old design traded it for.
|
|
201
|
+
|
|
202
|
+
What the code *does* is let you **tell two passes apart** — it is an address,
|
|
203
|
+
never a password. One waiting pass is claimed and acted on the moment you type
|
|
204
|
+
`/spec-reviewed`, with nothing read back: the round-trip verified *which* pass
|
|
205
|
+
in a design where the page pushed and the agent went looking, and that design is
|
|
206
|
+
gone. Two waiting is a refusal to guess — the one case where a stranger's pass
|
|
207
|
+
sits beside yours, and the one case where six digits are worth anyone's time.
|
|
208
|
+
Pasting the code (`/spec-reviewed 324199`) skips even that. A pass you disown is `--drop <code>`; left there it is reported on every
|
|
209
|
+
render until you stop reading the line.
|
|
210
|
+
|
|
211
|
+
A code that matches nothing refuses, **names nothing**, and never falls back to
|
|
212
|
+
"the only one" — that fallback is precisely what would let an unread pass
|
|
213
|
+
through. Claiming **consumes**: a code works once. A second pass from the same
|
|
214
|
+
page render supersedes the first, so the code on screen is always the pass on
|
|
215
|
+
screen; passes from different renders stand alongside each other.
|
|
216
|
+
|
|
217
|
+
**And a claimed pass never enters the model's context.** The engine holds it,
|
|
218
|
+
merges it, and reports the counts — six digits is what reaches Claude, where a
|
|
219
|
+
pasted blob costs context in proportion to how much you wrote. That is the same
|
|
220
|
+
rule the diff already follows, applied to the one place it used to break. What it did comes back as a **resolution** (`--resolve`),
|
|
221
|
+
so the next render shows each note struck through with a one-line account and you
|
|
222
|
+
verify the fix instead of trusting it. An accept is keyed to the file's
|
|
223
|
+
**content hash**, so it survives the commit that ends the phase and lapses by
|
|
224
|
+
itself when that file changes again — announced as `accepted earlier — changed since`, never
|
|
225
|
+
silently. All of it lives beside the page in gitignored `.spec-env/`, and
|
|
226
|
+
**the marks are information, never a gate**: nothing counts them and nothing
|
|
227
|
+
refuses on them.
|
|
228
|
+
|
|
229
|
+
**The verdict is chosen, never derived.** A verdict is one person's conclusion,
|
|
230
|
+
sent once per pass; a counting gate is a refusal computed from how many boxes
|
|
231
|
+
are ticked, and it stays forbidden. The one refusal here is not a count of
|
|
232
|
+
ticks: **committing** is unavailable while any **comment** is unresolved,
|
|
233
|
+
because you asked for something and it therefore cannot also be fine. Files you
|
|
234
|
+
never ticked block nothing — an unticked file is something you said nothing
|
|
235
|
+
about, and requiring every one of them would be the tally this design exists to
|
|
236
|
+
avoid.
|
|
237
|
+
|
|
238
|
+
**The gate — a phase that ended owes an answer.** That is the second refusal,
|
|
239
|
+
and it is a different kind: not a count, and not about the marks at all.
|
|
240
|
+
`/spec-next` **arms** it when a phase ends and renders its page, and exactly two
|
|
241
|
+
things clear it — a **committing verdict**, or
|
|
242
|
+
`skitterspec spec-env review skip "<reason>"`. Until one of them happens,
|
|
243
|
+
`/spec-next` refuses to build the next phase and (where the hook is installed)
|
|
244
|
+
`git commit` refuses in that worktree. `skitterspec spec-env review gate
|
|
245
|
+
[--check] [--json]` is what both ask.
|
|
246
|
+
|
|
247
|
+
**The hook is the half that cannot be talked past.** `skitterspec init` installs
|
|
248
|
+
`.claude/hooks/review-gate.js` and registers it in the project's committed
|
|
249
|
+
`.claude/settings.json`, so a `git commit` is refused by the harness rather than
|
|
250
|
+
by prose — which is what covers a bare `git commit`, a chained command, and
|
|
251
|
+
skittership's own `/commit` without skitterspec ever editing it. It decides
|
|
252
|
+
nothing itself: it hands the command line to
|
|
253
|
+
`spec-env review gate --check --for-command` and turns one exit status into an
|
|
254
|
+
answer. It refuses only where the commit is running
|
|
255
|
+
**inside that spec's own worktree** — a commit on the base branch, or in
|
|
256
|
+
another spec's tree, is not this obligation's business — and it fails **open** on everything else: no engine, an
|
|
257
|
+
unreadable payload, a crash, a timeout, a repo with no isolation.
|
|
258
|
+
|
|
259
|
+
Three things keep it a push rather than a wall. It is armed
|
|
260
|
+
**only by a phase ending**, so reading your own half-finished work mid-phase
|
|
261
|
+
owes nothing.
|
|
262
|
+
It has an **exit that is always one command**, and one of them is
|
|
263
|
+
*"I am moving on"* — with a reason, because `none: additive, nothing to revert` is a decision
|
|
264
|
+
a reviewer can argue with while silence is an oversight. And it accuses
|
|
265
|
+
**only on a positive signal**: an unreadable sidecar, a project that set
|
|
266
|
+
`review.required: false`, a spec the engine could not resolve — every
|
|
267
|
+
cannot-tell exits 0 and says nothing (`.claude/rules/negative-checks.md`).
|
|
268
|
+
|
|
269
|
+
It is on by default wherever isolation is configured. That is the point: the
|
|
270
|
+
push toward reading the diff is the normal path, and stepping off it is the
|
|
271
|
+
thing you have to do deliberately.
|
|
272
|
+
|
|
273
|
+
**The verdict names the action**, and that is why it is `commit` rather than
|
|
274
|
+
`approve`: a review is the guard in front of an action, and an approval that
|
|
275
|
+
only recorded itself was the one control on the page that did not describe what
|
|
276
|
+
it does. Both committing verdicts hand off to the project's own commit skill
|
|
277
|
+
(`review.commitWith` in `env.config.json`, `/commit` by default) — skitterspec
|
|
278
|
+
never vendors one, because `/commit` belongs to skittership.
|
|
279
|
+
**There is no off switch**: `"none"` existed and was removed, since it produced
|
|
280
|
+
exactly the record-and-do-nothing verdict this design is against.
|
|
281
|
+
`Commit & Continue` then runs `/spec-next` and **stops there** — it never
|
|
282
|
+
completes, lands or tears anything down.
|
|
283
|
+
|
|
284
|
+
A verdict is **consumed**, not stored: a commit verdict is spent by the commit
|
|
285
|
+
and a changes one by the work, so neither can go stale and later act on
|
|
286
|
+
something nobody read. What survives is a one-line outcome log, shown on the
|
|
287
|
+
next render as history.
|
|
288
|
+
|
|
289
|
+
**One ending, every skill (`.claude/rules/spec-reports.md`).** Every skill in
|
|
290
|
+
the table above finishes with the same block — a verdict, then a table of only
|
|
291
|
+
the fields that skill declares — and says nothing while it runs beyond a
|
|
292
|
+
question it cannot answer itself or a failure at the moment it happens. The
|
|
293
|
+
shape is defined once in that rule; each skill's `## Report` section names only
|
|
294
|
+
its verdicts and its fields.
|
|
295
|
+
|
|
296
|
+
✅ **Phase 2 built** — `feat-orders`, 2 of 4
|
|
297
|
+
|
|
298
|
+
| | |
|
|
299
|
+
|---|---|
|
|
300
|
+
| **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-orders` · phase 2 moved |
|
|
301
|
+
| **Branch** | `spec/feat-orders` · 3 commits, clean |
|
|
302
|
+
| **Built** | POST /orders handler, orders schema |
|
|
303
|
+
| **Tests** | 128 passed · npm test |
|
|
304
|
+
| **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
|
|
305
|
+
| **Follow-ups** | none |
|
|
306
|
+
| **Next** | `/spec-next` → phase 3 (Auth) |
|
|
307
|
+
|
|
308
|
+
Four verdicts, and the last two are different facts about the repo: `✅` done ·
|
|
309
|
+
`⚠️` done with caveats · `❌` failed part-way, so there is a mess to clear ·
|
|
310
|
+
`⏸` refused before acting, so nothing changed.
|
|
311
|
+
**A refusal emits the block too** — "nothing happened" is a reported outcome
|
|
312
|
+
rather than an absent one.
|
|
313
|
+
`Follow-ups` is always present, because a recorded `none` is a decision where a
|
|
314
|
+
missing line is an oversight. And the block covers **that run only**: what else
|
|
315
|
+
is in flight is another question, and volunteering it here leaves the reader
|
|
316
|
+
unable to tell what followed from the run they just watched.
|
|
317
|
+
|
|
96
318
|
**Ticketing-provider sync (opt-in, a separate package).** The base is
|
|
97
319
|
tracker-free: it knows nothing about any specific ticketing system. A
|
|
98
320
|
ticketing provider is installed as its own distribution that plugs into named
|
|
@@ -111,6 +333,24 @@ against a committed last-pushed snapshot and applies it) and `/spec-status`
|
|
|
111
333
|
workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
|
|
112
334
|
the tracker is never read back or merged.
|
|
113
335
|
|
|
336
|
+
A provider may also ship a **read-only listing** of what the tracker holds. That
|
|
337
|
+
is not a pull: nothing is merged back and no spec file is written. It exists
|
|
338
|
+
because the folder buckets are only the truth *on the branch you are standing
|
|
339
|
+
on* — `/spec-start` moves a spec to
|
|
340
|
+
`specs/in-progress/` on that spec's own branch, so on the base branch an
|
|
341
|
+
in-flight spec still reads `backlog` and a teammate's unlanded spec is not on
|
|
342
|
+
disk at all. The tracker knows both, and each row carries the spec's folder name
|
|
343
|
+
so `/spec-start <name>` is a copy-paste away. See the provider's own docs for
|
|
344
|
+
what it calls the command.
|
|
345
|
+
|
|
346
|
+
A provider may also mirror **who is building a spec**, as one more field the repo
|
|
347
|
+
owns. Where it does, `/spec-start` records the developer and the tracker's ticket
|
|
348
|
+
is assigned to them, the assignment is released when the spec completes, and
|
|
349
|
+
`/spec-claim` moves ownership mid-flight — take it, hand it back, or hand it to a
|
|
350
|
+
teammate. Like everything else here it is opt-in and one-way: the spec file is
|
|
351
|
+
the record, and the ticket is the mirror. See the provider's own docs for how to
|
|
352
|
+
turn it on.
|
|
353
|
+
|
|
114
354
|
**Every skill that moves a spec through the lifecycle carries a seam**, so the
|
|
115
355
|
mirror keeps up without anyone remembering to push: `/spec`, `/spec-bug` and
|
|
116
356
|
`/spec-hotfix` link the spec they create; `/spec-next` refreshes it as work starts;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Spec Reports
|
|
2
|
+
|
|
3
|
+
Every lifecycle skill ends with the same block. This file is its whole
|
|
4
|
+
definition — a skill's `## Report` section names only which fields it emits and
|
|
5
|
+
any behaviour that was always there.
|
|
6
|
+
|
|
7
|
+
Two things it buys: a run's outcome is scannable in one shape rather than
|
|
8
|
+
composed fresh each time, and "nothing happened" becomes a reported outcome
|
|
9
|
+
instead of an absent one.
|
|
10
|
+
|
|
11
|
+
## Silence during the run
|
|
12
|
+
|
|
13
|
+
**Do not narrate.** Not what you are about to do, not why, not which step you
|
|
14
|
+
are on. The tool calls are already on screen, so a line per step re-states the
|
|
15
|
+
transcript in words that never vary.
|
|
16
|
+
|
|
17
|
+
Speak mid-run for exactly two reasons:
|
|
18
|
+
|
|
19
|
+
- **A question you cannot answer yourself** — a decision the spec left open, a
|
|
20
|
+
confirmation a guard demands, an ambiguity with more than one defensible
|
|
21
|
+
answer.
|
|
22
|
+
- **A failure, at the moment it happens** — a red test, a refused command, a
|
|
23
|
+
conflict. Say it where it occurs; do not hold it back for the block.
|
|
24
|
+
|
|
25
|
+
Everything else waits.
|
|
26
|
+
|
|
27
|
+
## The block
|
|
28
|
+
|
|
29
|
+
A verdict sentence, a blank line, then a two-column table:
|
|
30
|
+
|
|
31
|
+
✅ **Phase 2 built** — `feat-foo`, 2 of 4
|
|
32
|
+
|
|
33
|
+
| | |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-foo` · phase 2 moved |
|
|
36
|
+
| **Branch** | `spec/feat-foo` · 3 commits, clean |
|
|
37
|
+
| **Built** | POST /orders handler, orders schema |
|
|
38
|
+
| **Tests** | 128 passed · npm test |
|
|
39
|
+
| **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
|
|
40
|
+
| **Follow-ups** | none |
|
|
41
|
+
| **Next** | `/spec-next` → phase 3 (Auth) |
|
|
42
|
+
|
|
43
|
+
**A table, and never a fenced block.** An aligned label column is only
|
|
44
|
+
achievable inside a fence, because markdown collapses runs of spaces outside
|
|
45
|
+
one — so alignment and being recognised as a report are mutually exclusive, and
|
|
46
|
+
the report wins. A fence renders as the same grey box as a code sample, and a
|
|
47
|
+
reader who has been shown code all session reads one more box as code: the first
|
|
48
|
+
person to receive one of these did not register it as output at all, and missed
|
|
49
|
+
the `Next` row inside it. That is the exact scanning failure this contract exists
|
|
50
|
+
to fix, so producing it in the contract's own format is not a cosmetic mistake.
|
|
51
|
+
|
|
52
|
+
**The verdict sentence** is `<state> **<one clause>** — <spec>, <where it got
|
|
53
|
+
to>`. The clause says what happened; the tail says to what, and how far. Name
|
|
54
|
+
the skill in the clause when the run's identity is not obvious from the
|
|
55
|
+
conversation — a refusal, or a skill invoked by another. A run with no spec to
|
|
56
|
+
name drops that segment rather than inventing one.
|
|
57
|
+
|
|
58
|
+
**The header row is empty.** The labels are the left column, and a header over
|
|
59
|
+
them would only be able to say "field", which no one needs told.
|
|
60
|
+
|
|
61
|
+
**The block is the last thing in the message.** Nothing follows it — not a
|
|
62
|
+
closing line, not a next step, not the offer. Everything the reader must act on
|
|
63
|
+
is a row, which is what earns the block its position.
|
|
64
|
+
|
|
65
|
+
**And that is a rule about the destination, not only the prohibition.** It was
|
|
66
|
+
stated plainly and broken repeatedly, by runs that had something worth saying
|
|
67
|
+
and nowhere in the block to say it — so the paragraph went after the table,
|
|
68
|
+
where it buried the rows it was meant to sit beside. A missing destination reads
|
|
69
|
+
as a rule that does not fit the work, and a rule that does not fit gets ignored.
|
|
70
|
+
So: **if it is worth telling the reader, it is a row.**
|
|
71
|
+
**If it is not a row, it is not worth telling them.**
|
|
72
|
+
`Notes` is the row most of that content belongs in.
|
|
73
|
+
|
|
74
|
+
**Two controls may follow the block, and nothing else ever.** The ban exists
|
|
75
|
+
because *prose* after the block competes with it for the reader's attention and
|
|
76
|
+
loses them the rows. A control does not compete: it is a row made actionable, in
|
|
77
|
+
the place the reader's eye already finishes. Prose after either of them is the
|
|
78
|
+
same violation wearing a control's clothes.
|
|
79
|
+
|
|
80
|
+
**The first is a picker.** Where the run ends in a choice the reader must make,
|
|
81
|
+
the options may be offered after the table.
|
|
82
|
+
|
|
83
|
+
**The second is the review call-to-action**, and only where the run is actually
|
|
84
|
+
**waiting** on a verdict. It is a banner, not a sentence:
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## ⏸ Review ready — 7 files, +212 −18
|
|
89
|
+
|
|
90
|
+
**[Open the page](http://…)** · I'm holding here until you send a verdict.
|
|
91
|
+
|
|
92
|
+
`/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
A rule, a heading naming the state and the size, the link in bold, one line
|
|
97
|
+
saying the run is stopped, and the two exits. Nothing else, and never a
|
|
98
|
+
paragraph explaining it.
|
|
99
|
+
|
|
100
|
+
**Only promise a wait the transport can deliver.** A page the engine SERVES
|
|
101
|
+
hands its pass to the local store, which a file-watch sees — that is the one
|
|
102
|
+
case where *"I'm holding here"* is true. A **published** page writes to the
|
|
103
|
+
artifact's own store, and nothing pushes from there into the conversation, so a
|
|
104
|
+
verdict pressed on it is invisible until someone asks for it. Its banner says
|
|
105
|
+
so instead:
|
|
106
|
+
|
|
107
|
+
**[Open the page](https://…)** · press a verdict, then type `/spec-reviewed` — I cannot see it until you do.
|
|
108
|
+
|
|
109
|
+
**Exactly one link, never two.** The banner offers the page the reader can
|
|
110
|
+
actually use, and the engine has already decided which that is — a served URL
|
|
111
|
+
when it could serve, a published one only when it could not. Handing over both
|
|
112
|
+
asks the reader to know which door the run is standing behind, and they cannot:
|
|
113
|
+
it was done, and a verdict was pressed on the published page three times while
|
|
114
|
+
each one sat unread under a line claiming the run was waiting. Two links is not
|
|
115
|
+
a convenience with a caveat; it is the caveat existing at all.
|
|
116
|
+
|
|
117
|
+
**This is not the old failure returning**, and the difference is the whole
|
|
118
|
+
justification. The offer used to be two quoted lines in the tail of a long
|
|
119
|
+
report, addressed to nobody, under a closing line that told the reader to move
|
|
120
|
+
on — and it was never once taken. It was moved into a labelled row to make it
|
|
121
|
+
findable, and that worked. What changed since is that the run now *waits*: the
|
|
122
|
+
reader is not being offered something optional, they are being told the work has
|
|
123
|
+
stopped until they answer. A row cannot carry that, because a row is scanned at
|
|
124
|
+
the same weight as every other row. So the state gets the loudest shape on
|
|
125
|
+
screen, at the end, where reading finishes.
|
|
126
|
+
|
|
127
|
+
**Where the run is not waiting, it stays a `Review` row.** A render nobody is
|
|
128
|
+
being held for — a mid-phase `/spec-diff`, a page produced alongside other work
|
|
129
|
+
— is a convenience, and giving it a banner teaches the reader to scroll past
|
|
130
|
+
banners. The shape marks the difference between *here is a page* and *nothing
|
|
131
|
+
proceeds until you answer*.
|
|
132
|
+
|
|
133
|
+
**Never fence a message to the reader.** A fenced block is for a command to
|
|
134
|
+
run, code, or engine output quoted verbatim — things the reader copies or
|
|
135
|
+
compares. A question, an offer, a hand-off is prose. The same mechanism that
|
|
136
|
+
made the old report unreadable applies to anything else in a grey box: it is
|
|
137
|
+
read as an artefact to skim rather than as something addressed to someone, so a
|
|
138
|
+
question inside one goes unanswered and an instruction inside one goes unread.
|
|
139
|
+
|
|
140
|
+
**It reports this run and nothing else.** Not what else is in flight, not the
|
|
141
|
+
other worktrees, not the backlog, not the state of the repo at large. A reader
|
|
142
|
+
finishing one piece of work cannot tell whether a line about some other spec is
|
|
143
|
+
a consequence of what just happened or an unrelated aside, and has to stop and
|
|
144
|
+
work it out — which is the cost this block exists to remove. If they want the
|
|
145
|
+
wider picture there is a skill that answers it; volunteering it here muddies the
|
|
146
|
+
one thing they asked about.
|
|
147
|
+
|
|
148
|
+
`Next` is the single next action **for this work**, not a menu of what else
|
|
149
|
+
could be done.
|
|
150
|
+
|
|
151
|
+
**And it must be runnable from the state the run actually leaves behind.** Most
|
|
152
|
+
of these skills deliberately do not commit — they finish, verify, and wait — so
|
|
153
|
+
the tree they hand back is dirty, and several of the commands they would name
|
|
154
|
+
next refuse on a dirty tree. A `Next` that omits the commit therefore sends the
|
|
155
|
+
reader to a refusal: `/spec-next` leaves a phase uncommitted and its own §2
|
|
156
|
+
refuses on exactly that, and `/spec-bug` leaves a fix uncommitted where
|
|
157
|
+
`/spec-complete` refuses the same way. Where the skill does not commit, the row
|
|
158
|
+
says `/commit, then <the thing>`.
|
|
159
|
+
|
|
160
|
+
This is not a style rule. `Next` is the one row a reader is meant to act on, and
|
|
161
|
+
it closes the block because the closing row should be the one that moves the
|
|
162
|
+
work on. A row that cannot be run spends the trust the position was designed to
|
|
163
|
+
earn — and the two skills that got it wrong got it wrong independently, which is
|
|
164
|
+
what makes it a missing constraint rather than two slips.
|
|
165
|
+
|
|
166
|
+
## Field vocabulary, in this order
|
|
167
|
+
|
|
168
|
+
A skill emits only the fields it declares, in this order, skipping the rest.
|
|
169
|
+
`Follow-ups` is the exception: always present, second to last.
|
|
170
|
+
|
|
171
|
+
`Tracker` is the conditional one: a skill may declare it and still never emit
|
|
172
|
+
it, because no ticketing provider is installed. That is an absence with nothing
|
|
173
|
+
behind it — say nothing rather than reporting that there was nothing to report.
|
|
174
|
+
|
|
175
|
+
| Field | Carries |
|
|
176
|
+
|-------|---------|
|
|
177
|
+
| `Tracker` | **First.** The ticket id, linked where the spec has a url, the spec's folder name beside it, and what changed there. |
|
|
178
|
+
| `Why` | **Non-`✅` only.** What stopped it, in one clause. |
|
|
179
|
+
| `Branch` | Branch name, commit count, clean or dirty. |
|
|
180
|
+
| `Spec` | The spec document's own state: status, bucket, phase. |
|
|
181
|
+
| `Cause` | The root cause, for work that diagnosed one. |
|
|
182
|
+
| `Built` | What the run produced — the code, the edits, the spec written. |
|
|
183
|
+
| `Tests` | The result and the command that produced it. |
|
|
184
|
+
| `Notes` | What this run hit and handled — one short paragraph. Named `Snags` once, which announced a problem before the reader had read one; most of what belongs here is neither good nor bad, just worth knowing. A wrong turn, a guard that fired, a check of yours that turned out to be a false negative. Not a caveat on the outcome (`⚠️` and `Why` carry those) and not future work (`Follow-ups` carries that): this is what happened on the way. |
|
|
185
|
+
| `Landed` | A fast-forward, a tag, a cherry-pick. |
|
|
186
|
+
| `Worktree` | A worktree provisioned, entered, or torn down. |
|
|
187
|
+
| `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
|
|
188
|
+
| `Review` | The rendered diff page: files, `+`/`−`, the link — and the offer of a written review, in the same row. **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
|
|
189
|
+
| `Follow-ups` | **Always.** `none`, or one line each. |
|
|
190
|
+
| `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
|
|
191
|
+
|
|
192
|
+
The order runs identity → context → what happened → where it went → what to do.
|
|
193
|
+
**`Tracker` is first** because the id is what addresses this work outside the
|
|
194
|
+
repo, and **`Next` is last** because it is the only row the reader acts on: the
|
|
195
|
+
closing row should be the one that moves the work on.
|
|
196
|
+
|
|
197
|
+
**`Untouched` is a fact, not a caveat.** A run that provisions beside another
|
|
198
|
+
spec's uncommitted work has done nothing wrong and nothing partial — it left
|
|
199
|
+
alone something that was never its business — so it stays `✅` with a row, and
|
|
200
|
+
does not become `⚠️`. Several specs in flight at once is what the worktree mode
|
|
201
|
+
is *for*; spending the "something is worth knowing" verdict on the normal case
|
|
202
|
+
is how that verdict stops meaning anything. It is still reported rather than
|
|
203
|
+
silent, because "I provisioned, and your four files are still sitting there" is
|
|
204
|
+
a different sentence from "I provisioned".
|
|
205
|
+
|
|
206
|
+
**`Review` is one row, not two** — and where the run is waiting, it is no rows
|
|
207
|
+
at all. The page and the offer to read it are the same subject, and splitting
|
|
208
|
+
them made the reader resolve a distinction before acting on either. A waiting
|
|
209
|
+
run promotes the whole subject into the banner; keeping the row as well would
|
|
210
|
+
recreate that split with the two halves further apart than ever.
|
|
211
|
+
|
|
212
|
+
## The four verdicts
|
|
213
|
+
|
|
214
|
+
| State | Means |
|
|
215
|
+
|-------|-------|
|
|
216
|
+
| `✅` | Done. Everything the skill set out to do happened. |
|
|
217
|
+
| `⚠️` | Done with caveats — it finished, but something is worth knowing. |
|
|
218
|
+
| `❌` | Failed part-way. It acted, and the repo is mid-something. |
|
|
219
|
+
| `⏸` | Refused before acting. Nothing changed. |
|
|
220
|
+
|
|
221
|
+
**`❌` and `⏸` are different facts about the repo**, and the difference decides
|
|
222
|
+
what happens next. A failed landing leaves a conflicted rebase and a standing
|
|
223
|
+
worktree — there is a mess to clear. A refusal leaves nothing at all, and the
|
|
224
|
+
only thing to do is supply what was missing. Collapsing them sends people
|
|
225
|
+
looking for wreckage that is not there, or past wreckage that is.
|
|
226
|
+
|
|
227
|
+
**A refusal still emits the block.** That is the point of having four states:
|
|
228
|
+
every invocation ends the same way, so an outcome is never inferred from
|
|
229
|
+
silence.
|
|
230
|
+
|
|
231
|
+
## Where the block may grow
|
|
232
|
+
|
|
233
|
+
One place only. A non-`✅` verdict adds a `Why` row, and quotes the output that
|
|
234
|
+
justifies it — the failing assertion, the conflict, the engine's refusal — in a
|
|
235
|
+
fenced block **above** the table, where a fence means what a fence should mean:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
✖ rejects a negative quantity
|
|
239
|
+
expected 422, got 500
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
❌ **Phase 2 stopped — tests red** — `feat-foo`, 2 of 4
|
|
243
|
+
|
|
244
|
+
| | |
|
|
245
|
+
|---|---|
|
|
246
|
+
| **Why** | 3 assertions fail in `orders.test.js`; nothing committed |
|
|
247
|
+
| **Branch** | `spec/feat-foo` · 2 commits, dirty |
|
|
248
|
+
| **Built** | POST /orders handler, orders schema |
|
|
249
|
+
| **Tests** | 125 passed, 3 failed · npm test |
|
|
250
|
+
| **Follow-ups** | none |
|
|
251
|
+
| **Next** | fix the failures, then `/spec-next` to finish phase 2 |
|
|
252
|
+
|
|
253
|
+
Everywhere else the block stays the size it is. Prose after it, an extra row of
|
|
254
|
+
your own invention, a second block — all three are the shape drifting back to
|
|
255
|
+
what this replaced.
|
|
256
|
+
|
|
257
|
+
## Follow-ups
|
|
258
|
+
|
|
259
|
+
**Always present.** A recorded `none` is a decision; a missing line is an
|
|
260
|
+
oversight — the same logic as the `Gating` header.
|
|
261
|
+
|
|
262
|
+
The bar is deliberately high. A follow-up is something *this* work surfaced
|
|
263
|
+
that *this* spec will not fix — a wrong assumption, a missing guard, a decision
|
|
264
|
+
the spec deferred and the code then leaned on. It is **not** "could be tidier",
|
|
265
|
+
and it is **not** anything already sitting in the spec's Open questions.
|
|
266
|
+
|
|
267
|
+
When there is one, **offer to `/spec` it on the spot**. If the user does not
|
|
268
|
+
take the offer, write one dated line into the spec's Changelog before you
|
|
269
|
+
finish, so it outlives the session:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
- 2026-01-09 — Follow-up surfaced: connect's port picker assumes a free
|
|
273
|
+
canonical port; not in scope here.
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Say where the spec would be written.** These skills run inside the spec's own
|
|
277
|
+
worktree, and a spec authored there physically lives on that branch — invisible
|
|
278
|
+
from the base branch, and cancelled along with its host if the host is
|
|
279
|
+
cancelled. So the offer names the primary checkout as the place to write it.
|
|
280
|
+
`spec-planning.md` and `commit-trailers.md` carry the reasoning.
|
|
281
|
+
|
|
282
|
+
## Worked examples
|
|
283
|
+
|
|
284
|
+
**`⚠️` — it finished, with something worth knowing.**
|
|
285
|
+
|
|
286
|
+
⚠️ **Phase 3 built; tracker not updated** — `feat-foo`, 3 of 4
|
|
287
|
+
|
|
288
|
+
| | |
|
|
289
|
+
|---|---|
|
|
290
|
+
| **Branch** | `spec/feat-foo` · 4 commits, clean |
|
|
291
|
+
| **Built** | Auth middleware, session table |
|
|
292
|
+
| **Tests** | 140 passed · npm test |
|
|
293
|
+
| **Follow-ups** | Session expiry is read from two places; phase 4 leans on one |
|
|
294
|
+
| **Next** | `/spec-next` → phase 4 (Docs) |
|
|
295
|
+
|
|
296
|
+
**`⏸` — refused, nothing changed.**
|
|
297
|
+
|
|
298
|
+
⏸ **`/spec-next` refused — no spec in flight**
|
|
299
|
+
|
|
300
|
+
| | |
|
|
301
|
+
|---|---|
|
|
302
|
+
| **Why** | Not standing in a worktree, and 2 specs are provisioned |
|
|
303
|
+
| **Follow-ups** | none |
|
|
304
|
+
| **Next** | `/spec-start <name>`, or `cd` into `../repo-wt/feat-orders` or `../repo-wt/feat-auth` |
|
|
305
|
+
|
|
306
|
+
**`✅` — a spec finished and landed.**
|
|
307
|
+
|
|
308
|
+
✅ **Landed and torn down** — `feat-foo`, complete
|
|
309
|
+
|
|
310
|
+
| | |
|
|
311
|
+
|---|---|
|
|
312
|
+
| **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-foo` · moved to its done state |
|
|
313
|
+
| **Spec** | Complete · `specs/complete/feat-foo` |
|
|
314
|
+
| **Tests** | 131 passed · npm test |
|
|
315
|
+
| **Landed** | base fast-forwarded to 4a1c9e2 |
|
|
316
|
+
| **Worktree** | removed · `../repo-wt/feat-foo` |
|
|
317
|
+
| **Follow-ups** | none |
|
|
318
|
+
| **Next** | pick the next spec from `specs/backlog/` |
|
|
319
|
+
|
|
320
|
+
The `❌` example is in **Where the block may grow** above, since the failing
|
|
321
|
+
output it quotes is the thing that example exists to show.
|