@skitterbyte/skitterspec-linear 14.0.0 → 17.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 +304 -4
- package/README.md +34 -1
- package/assets/claude-md-section.md +29 -18
- package/assets/commands/spec-remote-review.md +22 -0
- package/assets/core/SETUP.md +10 -6
- package/assets/core/env.config.json.example +4 -2
- package/assets/core/env.config.md +103 -25
- package/assets/core/linear.config.json.example +2 -1
- package/assets/core/linear.config.md +49 -22
- package/assets/review/page.html +1101 -108
- package/assets/rules/spec-planning.md +39 -7
- package/assets/rules/spec-reports.md +210 -31
- package/assets/skills/spec/SKILL.md +161 -4
- package/assets/skills/spec-bug/SKILL.md +102 -49
- package/assets/skills/spec-cancel/SKILL.md +2 -2
- package/assets/skills/spec-claim/SKILL.md +12 -4
- package/assets/skills/spec-complete/SKILL.md +2 -2
- package/assets/skills/spec-diff/SKILL.md +183 -39
- package/assets/skills/spec-hotfix/SKILL.md +96 -49
- package/assets/skills/spec-init/SKILL.md +18 -6
- package/assets/skills/spec-linear-setup/SKILL.md +19 -11
- package/assets/skills/spec-next/SKILL.md +147 -62
- package/assets/skills/spec-push/SKILL.md +45 -0
- package/assets/skills/spec-review/SKILL.md +89 -2
- package/assets/skills/spec-reviewed/SKILL.md +31 -5
- package/assets/skills/spec-start/SKILL.md +26 -3
- package/assets/skills/spec-status/SKILL.md +20 -6
- package/assets/skills/spec-sync/SKILL.md +1 -0
- package/package.json +2 -2
- package/src/cli.js +940 -116
- package/src/env/classify.js +87 -2
- package/src/env/config.js +214 -17
- package/src/env/hooks.js +49 -9
- package/src/env/live.js +94 -0
- package/src/env/resolve.js +36 -2
- package/src/env/review.js +581 -21
- package/src/env/serve.js +298 -19
- package/src/env/supervise.js +8 -1
- package/src/init.js +88 -13
- package/src/vendor/linear/api.js +111 -2
- package/src/vendor/linear/cli-sync.js +661 -11
- package/src/vendor/linear/config.js +41 -13
- package/src/vendor/linear/doctor.js +6 -5
- package/src/vendor/sync-core/index.js +11 -3
- package/src/vendor/sync-core/src/compare.js +65 -0
- package/src/vendor/sync-core/src/normalize.js +26 -0
- package/src/vendor/sync-core/src/retarget.js +1 -1
- package/src/vendor/sync-core/src/task-block.js +2 -2
- /package/assets/hooks/{review-gate.js → review-gate.cjs} +0 -0
|
@@ -104,11 +104,40 @@ it happens differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step
|
|
|
104
104
|
description (an **update** to the existing issue, plus a sub-issue per phase).
|
|
105
105
|
Recording a snapshot here would declare the mirror already in sync and strand
|
|
106
106
|
the issue showing the raw report forever.
|
|
107
|
+
- **Preserve the original, before anything replaces it.** Run this the moment
|
|
108
|
+
the spec file exists and **before the linking push**:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
skitterspec spec-sync preserve <spec>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It posts the issue's current description onto the issue as a comment — the one
|
|
115
|
+
surface one-way sync never touches, so no push can clobber it. On the API path
|
|
116
|
+
it reads the description itself; without a key it prints the body and the
|
|
117
|
+
marker, and you post it with the Linear comment tool after checking the
|
|
118
|
+
comments for that marker.
|
|
119
|
+
|
|
120
|
+
**The ordering is the whole correctness condition.** Run before the push, this
|
|
121
|
+
keeps what the reporter filed. Run after it, it would keep the generated spec
|
|
122
|
+
and report success — which is why it is a step of its own here rather than a
|
|
123
|
+
line in the push sequence. The engine warns when it can tell it is late, but it
|
|
124
|
+
cannot always tell.
|
|
125
|
+
|
|
126
|
+
**It never fails the adoption.** A project that set
|
|
127
|
+
`intake.preserveOriginal: false`, an unreachable Linear, an issue with no
|
|
128
|
+
description, a comment Linear refused — every one of them exits 0 and says so.
|
|
129
|
+
Relay what it printed and carry on.
|
|
107
130
|
- **Say what will happen** in the finish-up message. The linking step runs right
|
|
108
131
|
after the spec is written, so the issue's description is replaced by the spec
|
|
109
|
-
**then** — not on some later manual push.
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
**then** — not on some later manual push. Say both halves: the description
|
|
133
|
+
becomes the spec, **and** the original is preserved as a comment on the same
|
|
134
|
+
issue, so the reporter can still read what they wrote.
|
|
135
|
+
|
|
136
|
+
**Do not send anyone to the issue's history for it.** This used to say Linear
|
|
137
|
+
keeps the original there, and that is true and useless: history is a diff
|
|
138
|
+
viewer nobody opens, it is not quotable, and it degrades to "it is in there
|
|
139
|
+
somewhere". The comment is the answer, and the other half is the spec's own
|
|
140
|
+
**Problem** (or **Symptom**) section quoting the reporter.
|
|
112
141
|
|
|
113
142
|
## 1. Reproduce & isolate (light investigation)
|
|
114
143
|
|
|
@@ -309,10 +338,14 @@ and **never auto-push git** — Linear's own automation reacts to real branch/PR
|
|
|
309
338
|
events later. Report the Linear issue URL as part of the skill's finish-up
|
|
310
339
|
message.
|
|
311
340
|
|
|
312
|
-
**Only when all three hold**: `specs/.core/linear.config.json` exists,
|
|
313
|
-
`
|
|
341
|
+
**Only when all three hold**: `specs/.core/linear.config.json` exists, the repo
|
|
342
|
+
**owns** `assignee` in its `sync.fieldOwnership`, and the spec carries a
|
|
314
343
|
`linear_identifier`. Any one missing → skip this step silently and carry on; a
|
|
315
|
-
project that
|
|
344
|
+
project that declined must see no trace of assignment.
|
|
345
|
+
|
|
346
|
+
Ownership is the **value**, not the key: `assignee` is owned by default, and
|
|
347
|
+
`"none"` is how a project declines it. Testing whether the key is *present* would
|
|
348
|
+
read as opted-in everywhere, since the default puts it in every config.
|
|
316
349
|
|
|
317
350
|
**Never blocks, never fails the skill.** Everything below is best-effort: the
|
|
318
351
|
branch is provisioned and the spec is moving either way, and an unassigned issue
|
|
@@ -451,7 +484,7 @@ sub-issue jump from Backlog straight to Done, with nothing visible in between.
|
|
|
451
484
|
- **Say what happened** in the skill's report: mirror updated, skipped as
|
|
452
485
|
unlinked, or failed with the reason.
|
|
453
486
|
|
|
454
|
-
## 5b. Render the page
|
|
487
|
+
## 5b. Render the page, arm the gate, then wait for the verdict
|
|
455
488
|
|
|
456
489
|
**Only when the project has per-spec isolation** (`specs/.core/env.config.json`
|
|
457
490
|
present). Without it there is no worktree to read and this step does not exist —
|
|
@@ -467,53 +500,67 @@ skitterspec spec-env review <spec>
|
|
|
467
500
|
**This is free.** The engine reads git and splices the patches into a template;
|
|
468
501
|
the diff never passes through you, so a 266KB patch costs nothing.
|
|
469
502
|
|
|
470
|
-
**Then
|
|
471
|
-
costs — roughly **700 output tokens**, because writing it means reading the diff
|
|
472
|
-
— and that spend is the operator's call, not a default.
|
|
473
|
-
|
|
474
|
-
**The offer is the `Review` row of step 6's block** — the counts, the page link
|
|
475
|
-
and a question, in one row:
|
|
476
|
-
|
|
477
|
-
| **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
|
|
478
|
-
|
|
479
|
-
**It ends in a question, addressed to someone.** It was once a fenced block of
|
|
480
|
-
engine output, and it fired on every phase and was never once taken: two quoted
|
|
481
|
-
lines under the test counts, addressed to nobody, with the report then closing
|
|
482
|
-
on *"commit this first"* — the last instruction the reader got was to move on,
|
|
483
|
-
so they did. A row in a labelled table is findable; a question in it is
|
|
484
|
-
answerable. Both halves are load-bearing.
|
|
485
|
-
|
|
486
|
-
**Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
|
|
487
|
-
the page and the question stay in the same row: two adjacent rows about one page
|
|
488
|
-
make the reader resolve a distinction before acting on either. A later edit that
|
|
489
|
-
moves it out of the block, or separates the link from the question, undoes this
|
|
490
|
-
and should be read as a regression rather than tidying.
|
|
503
|
+
**Then arm the gate**, so the fix now owes a verdict:
|
|
491
504
|
|
|
505
|
+
```
|
|
506
|
+
skitterspec spec-env review arm <spec>
|
|
507
|
+
```
|
|
492
508
|
|
|
493
|
-
|
|
494
|
-
|
|
509
|
+
**Then wait**, and `/spec-next` §5 owns the sequence — follow it there rather
|
|
510
|
+
than reading a second copy here: note the moment, run
|
|
511
|
+
`skitterspec spec-env review wait <spec> --since <that moment>` in the
|
|
512
|
+
background, end your turn, and let `--claim-since` pick the one pass that
|
|
513
|
+
arrived inside the window. **Do not compose a watcher**, and do not give the
|
|
514
|
+
wait a timeout — the reasoning is in `/spec-next` §5 and is not repeated here.
|
|
515
|
+
The banner it describes is what this skill emits in place of a `Review` row,
|
|
516
|
+
and the routing on the verdict is `/spec-diff` §2 and §4, as it is everywhere.
|
|
517
|
+
|
|
518
|
+
**Why this skill arms as well as waits.** Waiting is what any offer does;
|
|
519
|
+
**arming** asserts an obligation that outlives the turn, and belongs only to
|
|
520
|
+
work that is finished. A bug fix is a completed unit — red→green, suite
|
|
521
|
+
passing — so it qualifies, and a wait with nothing owed behind it is a
|
|
522
|
+
suggestion rather than a gate. A mid-run render waits without arming, and
|
|
523
|
+
walking away from that costs nothing.
|
|
524
|
+
|
|
525
|
+
**This is the gap this step existed inside.** It used to render the page, emit a
|
|
526
|
+
row asking *"want a written review before you commit?"*, and finish — with
|
|
527
|
+
nothing watching. A verdict pressed on that page landed in the holding area and
|
|
528
|
+
stayed there, because the run had said the page was **ready** rather than that
|
|
529
|
+
it was **waiting**. Two were pressed on one spec and both were stranded; the
|
|
530
|
+
second existed only because the first appeared to do nothing.
|
|
531
|
+
|
|
532
|
+
**It is user-visible, and that is deliberate.** Once armed, a `git commit` in
|
|
533
|
+
this worktree is refused until a verdict is sent or
|
|
534
|
+
`skitterspec spec-env review skip "<reason>"` records the decision to move on.
|
|
535
|
+
The exit is always one command, and one of them is *"I am moving on"*.
|
|
536
|
+
|
|
537
|
+
Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
|
|
538
|
+
three — rather than the bare `page:` path: a path is not clickable in any
|
|
539
|
+
terminal, and a page nobody can open is a page nobody reads. Relay the `live:`
|
|
540
|
+
line with it where the engine printed one, and nothing where it did not.
|
|
541
|
+
`.claude/rules/spec-reports.md` carries the shape, including why a rigid
|
|
542
|
+
contract took that line.
|
|
495
543
|
|
|
496
544
|
- **Never write the review unasked**, and **never publish**. Publishing leaves
|
|
497
545
|
something behind that this tooling cannot remove, so it is always an ask. A
|
|
498
546
|
`file://` link is no use on a phone, and saying so **is** the ask —
|
|
499
547
|
`/spec-diff` §6 owns how.
|
|
500
|
-
**
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
drift.
|
|
548
|
+
**The `reader:` line no longer decides anything here, and that is the point.**
|
|
549
|
+
It is still printed, and it is still the only place that question is answered —
|
|
550
|
+
but the offer does not change with it, because the stack lists every tier
|
|
551
|
+
whatever it says. Three reader states used to mean three different offers, and
|
|
552
|
+
that branching is exactly what produced a `file://` page on a session detected
|
|
553
|
+
`unknown`, a LAN URL for a phone off the network, and an address that changed
|
|
554
|
+
underneath a reader when detection flipped mid-session.
|
|
555
|
+
|
|
556
|
+
So: **relay all three tier lines, every time.** Any `also:` lines sit under
|
|
557
|
+
`network` — the other addresses this machine has, offered because the
|
|
558
|
+
best-guess one can be wrong — so pass them on rather than editing them out.
|
|
559
|
+
|
|
560
|
+
**Never read an environment variable to decide anything about the offer** — not
|
|
561
|
+
`SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
|
|
562
|
+
here for a detection to decide, and a second implementation of one could not be
|
|
563
|
+
tested and would drift.
|
|
517
564
|
|
|
518
565
|
**Serving is the engine's to do; publishing is never.** A `remote` reader
|
|
519
566
|
authorises a local server — one process, ended by one flag, leaving nothing
|
|
@@ -550,9 +597,15 @@ the shape; this section carries only what is specific here.
|
|
|
550
597
|
**Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
|
|
551
598
|
`Review` · `Follow-ups` · `Next`
|
|
552
599
|
|
|
600
|
+
**`Review` is emitted only where step 5b did not run** — a project with no
|
|
601
|
+
isolation, or a render that failed. Where the run is waiting, the banner carries
|
|
602
|
+
the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
|
|
603
|
+
|
|
553
604
|
`Cause` is the root cause in one clause and `Built` is the fix — the
|
|
554
605
|
failing→passing test belongs in `Tests`, named, so the evidence is a test name
|
|
555
606
|
rather than an adjective.
|
|
556
607
|
|
|
557
|
-
Step 5b
|
|
558
|
-
|
|
608
|
+
Step 5b ends in the **banner**, not a paragraph and not a row — the run is
|
|
609
|
+
waiting on a verdict, and the banner is the shape that says so. Nothing follows
|
|
610
|
+
it. Where the run is not waiting it asks nothing at all: a `Review` row carries
|
|
611
|
+
the counts and the link, and no question (*asking implies waiting*).
|
|
@@ -76,8 +76,8 @@ Linear API key is set (see `apply.transport` in `linear.config.md`).
|
|
|
76
76
|
Do not roll anything back, and do not stop to ask.
|
|
77
77
|
- **Say what happened** in the skill's report: mirror updated, skipped as
|
|
78
78
|
unlinked, or failed with the reason.
|
|
79
|
-
- **There is no unassign step, and that is not an omission.**
|
|
80
|
-
`
|
|
79
|
+
- **There is no unassign step, and that is not an omission.** Where the repo owns
|
|
80
|
+
`assignee`, the projection derives it from the spec's lifecycle
|
|
81
81
|
bucket — so moving the folder to `complete`/`cancelled` is *itself* what
|
|
82
82
|
releases the issue, and the push above carries it like any other field. The
|
|
83
83
|
`linear_assignee_id` stamp and the `> **Developer:**` header both stay put:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-claim
|
|
3
|
-
description: Take ownership of a spec, hand it back, or give it to a teammate — the spec records who is building it and its Linear issue is assigned to them. Opt-in — needs specs/.core/linear.config.json with assignee in sync.fieldOwnership. Use when the user says "/spec-claim", "claim this spec", "take ownership of this", "I'm picking this up", "hand this back", or "assign this spec to someone".
|
|
3
|
+
description: Take ownership of a spec, hand it back, or give it to a teammate — the spec records who is building it and its Linear issue is assigned to them. Opt-in — needs specs/.core/linear.config.json with the repo owning assignee in sync.fieldOwnership. Use when the user says "/spec-claim", "claim this spec", "take ownership of this", "I'm picking this up", "hand this back", or "assign this spec to someone".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -16,9 +16,17 @@ and the push that follows tells Linear. This skill is how that record changes
|
|
|
16
16
|
after `/spec-start` has set it — a hand-off mid-flight, a spec picked up from
|
|
17
17
|
someone who moved on, or work a lead is distributing.
|
|
18
18
|
|
|
19
|
-
**Opt-in.** Needs `specs/.core/linear.config.json` *and*
|
|
20
|
-
`sync.fieldOwnership
|
|
21
|
-
|
|
19
|
+
**Opt-in.** Needs `specs/.core/linear.config.json` *and* the repo to **own**
|
|
20
|
+
`assignee` in its `sync.fieldOwnership` — which `"none"` declines as surely as
|
|
21
|
+
omitting it does. If either is missing, say which one and stop: without the field
|
|
22
|
+
the stamp would sit in the file doing nothing.
|
|
23
|
+
|
|
24
|
+
**The engine refuses this too, and that is the point.** `spec-sync assign` checks
|
|
25
|
+
the same thing and writes nothing, so the rule holds for a script, a chained
|
|
26
|
+
command, and anyone who reached for the CLI directly. This paragraph was the only
|
|
27
|
+
thing enforcing it for long enough that the engine's own success line —
|
|
28
|
+
`next: push it, so Linear agrees` — was printed over a push that would drop the
|
|
29
|
+
field. Prose in one skill is not a guard; it is a guard's documentation.
|
|
22
30
|
|
|
23
31
|
## 1. Identify the target spec
|
|
24
32
|
|
|
@@ -95,8 +95,8 @@ Linear API key is set (see `apply.transport` in `linear.config.md`).
|
|
|
95
95
|
Do not roll anything back, and do not stop to ask.
|
|
96
96
|
- **Say what happened** in the skill's report: mirror updated, skipped as
|
|
97
97
|
unlinked, or failed with the reason.
|
|
98
|
-
- **There is no unassign step, and that is not an omission.**
|
|
99
|
-
`
|
|
98
|
+
- **There is no unassign step, and that is not an omission.** Where the repo owns
|
|
99
|
+
`assignee`, the projection derives it from the spec's lifecycle
|
|
100
100
|
bucket — so moving the folder to `complete`/`cancelled` is *itself* what
|
|
101
101
|
releases the issue, and the push above carries it like any other field. The
|
|
102
102
|
`linear_assignee_id` stamp and the `> **Developer:**` header both stay put:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-diff
|
|
3
|
-
description: See what a spec's worktree changed — render its diff as a page you can mark up, take that review pass back, and act on it. Answers at any point, including half-way through a phase. Use when the user says "/spec-diff", "show me the diff", "what did this phase change", "review this spec's work", wants to read a worktree's changes away from the terminal, or hands back what the review page produced — a six-digit claim code, or the pasted JSON.
|
|
3
|
+
description: See what a spec's worktree changed — render its diff as a page you can mark up, take that review pass back, and act on it. Answers at any point, including half-way through a phase. Use when the user says "/spec-diff", "show me the diff", "what did this phase change", "review this spec's work", wants to read a worktree's changes away from the terminal, or hands back what the review page produced — a six-digit claim code, a bare verdict word, or the pasted JSON.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /spec-diff — see the phase before you commit it
|
|
@@ -41,13 +41,19 @@ sole candidate or prints the candidates.
|
|
|
41
41
|
The page has marks on it — `✓ accept` per file, notes against a line or a whole
|
|
42
42
|
file, answers to the checks a written review asked — and it
|
|
43
43
|
**ends in a decision**: `✓ Approve`, `↺ Request changes` or `… Discuss first`.
|
|
44
|
-
A review comes back to you one of
|
|
44
|
+
A review comes back to you one of three ways, and **all three are ordinary**:
|
|
45
45
|
|
|
46
46
|
- **A six-digit code** — `418207`, on its own. A *served* page hands its pass
|
|
47
47
|
straight to the engine, which holds it until someone reads the code out. This
|
|
48
48
|
is the usual way on a phone.
|
|
49
|
-
- **A
|
|
50
|
-
|
|
49
|
+
- **A verdict word** — `commit`, `commit-continue`, `continue`, `changes` or
|
|
50
|
+
`discuss`. A `file://` page cannot send anything, so where the reader marked
|
|
51
|
+
nothing it copies a command carrying the conclusion on its own. Send it with
|
|
52
|
+
`skitterspec spec-env review <spec> --verdict <word>`; it joins the same merge
|
|
53
|
+
a claimed pass does, so everything below is unchanged.
|
|
54
|
+
- **A pasted JSON blob** — the same `file://` page, once the reader has marked
|
|
55
|
+
something up: accepts and notes do not fit on a command line, so the whole
|
|
56
|
+
pass travels. Not legacy: it is the whole story for a local reader.
|
|
51
57
|
|
|
52
58
|
Either way, **this is not a request to render anything**: it is a review coming
|
|
53
59
|
back, and these steps replace §3–§5 below.
|
|
@@ -151,6 +157,10 @@ back, and these steps replace §3–§5 below.
|
|
|
151
157
|
absent verdict has always meant "report it and wait" — which is why that is
|
|
152
158
|
what it still means.
|
|
153
159
|
|
|
160
|
+
**A pass may carry an `action:` instead**, and then none of the above
|
|
161
|
+
applies: it concluded nothing. Go to **§2b**. The engine refuses a pass
|
|
162
|
+
carrying both, so there is never a choice to make between them.
|
|
163
|
+
|
|
154
164
|
3. **Say what you read, then ask what's up.** Report the accepted count, then
|
|
155
165
|
each open comment as `file:line — note`, then the files you would touch.
|
|
156
166
|
**Wait — unless the verdict already said otherwise.** Pasting on its own is
|
|
@@ -335,6 +345,69 @@ it — and the paste was the one place it broke. The *work* either authorises is
|
|
|
335
345
|
ordinary phase-sized cost, and step 3 is where the operator decides whether to
|
|
336
346
|
spend it.
|
|
337
347
|
|
|
348
|
+
## 2b. An action changes something, then hands the page back
|
|
349
|
+
|
|
350
|
+
Only on a pass carrying an **action**. Three of them, and
|
|
351
|
+
**all three end the same way**:
|
|
352
|
+
do the thing, re-render (§4), and wait again (§4b) — because the reader
|
|
353
|
+
still has a decision in front of them and has not made it.
|
|
354
|
+
|
|
355
|
+
| Action | Do | Then |
|
|
356
|
+
|--------|----|------|
|
|
357
|
+
| `live-on` | commit first (below), then `skitterspec spec-env live take <spec>` | re-render `--branch`, wait again |
|
|
358
|
+
| `allow-network` | `skitterspec spec-env review allow network` | re-render, wait again |
|
|
359
|
+
| `allow-remote` | `skitterspec spec-env review allow remote --set on` | re-render, wait again |
|
|
360
|
+
|
|
361
|
+
**THERE IS NO `live-off`, and its absence is deliberate.** Putting *this* change
|
|
362
|
+
live is about the diff on screen; handing the whole instance back to `main` is a
|
|
363
|
+
workspace decision with nothing to do with this review. The `live:` line names
|
|
364
|
+
`/spec-live main` for it — a command the operator types, which is also why no
|
|
365
|
+
skill runs it.
|
|
366
|
+
|
|
367
|
+
**AN ACTION IS NOT A VERDICT, and the gate is untouched by all three.** A phase
|
|
368
|
+
that ended armed the gate, and it is discharged by a committing verdict or a
|
|
369
|
+
recorded skip and by nothing else — so after any of these the phase still owes
|
|
370
|
+
an answer, and that is exactly why the run waits again rather than finishing.
|
|
371
|
+
The engine enforces this rather than trusting it: `ACTIONS` is disjoint from
|
|
372
|
+
`VERDICTS` and `COMMITTING`, so nothing that routes on a verdict can see one.
|
|
373
|
+
|
|
374
|
+
**`live-on` commits first, and the commit is a precondition rather than an answer.**
|
|
375
|
+
`live take` refuses a dirty worktree, and even without that guard
|
|
376
|
+
uncommitted work stays behind in the worktree — so what went live would be the
|
|
377
|
+
*previous* commit while the page claimed to be showing this one. Hand off to
|
|
378
|
+
`review.commitWith` exactly as §2a does (do not restate its rules here), then
|
|
379
|
+
take the instance. The reader has not approved anything by pressing it.
|
|
380
|
+
|
|
381
|
+
**It re-renders `--branch` afterwards**, because the working view is now empty:
|
|
382
|
+
the commit just happened, and the branch range is what still answers "what am I
|
|
383
|
+
looking at". §4's clean-tree fallback reaches the same place on its own, so
|
|
384
|
+
passing `--branch` is belt and braces rather than a separate rule.
|
|
385
|
+
|
|
386
|
+
**A mid-phase page does not get the commit.** Where the render took
|
|
387
|
+
`--buttons midrun`, the work is half a phase — committing it to look at it
|
|
388
|
+
running splits one phase across two commits and leaves a mess nobody asked for.
|
|
389
|
+
Say the phase needs to land first, re-render, and wait.
|
|
390
|
+
|
|
391
|
+
**Relay every refusal, and work around none of them.** `live take` refuses a
|
|
392
|
+
workbench another spec holds, a hotfix, a stateful spec, a branch touching
|
|
393
|
+
migrations, and a tree with no dev server listening. Each refusal names its own
|
|
394
|
+
way out. **Never park another spec's live session** to make room — that is
|
|
395
|
+
someone else's work, and freeing it is their decision. Re-render and wait, so
|
|
396
|
+
the reader can choose something else.
|
|
397
|
+
|
|
398
|
+
**`allow` writes a committed file, and that is worth a sentence.** It edits
|
|
399
|
+
`specs/.core/env.config.json` in the **primary checkout**, so it changes
|
|
400
|
+
behaviour for everyone who pulls and leaves that tree dirty — unlike the live
|
|
401
|
+
actions, which move a branch and write a gitignored receipt. The engine prints
|
|
402
|
+
the absolute path and whose tree it dirtied; relay that rather than letting a
|
|
403
|
+
shared setting change land silently. And it is **enable-only**: there is no
|
|
404
|
+
action that turns a tier off, because turning `network` off from a page reached
|
|
405
|
+
over the network kills the page doing the turning.
|
|
406
|
+
|
|
407
|
+
**`allow remote` permits publishing. It does not publish.** Publishing stays an
|
|
408
|
+
explicit ask in every case (§6), because the page it leaves behind is one this
|
|
409
|
+
tooling cannot remove.
|
|
410
|
+
|
|
338
411
|
## 3. Gate it on nothing
|
|
339
412
|
|
|
340
413
|
**This skill has no preconditions and must never grow one.** Not tests passing,
|
|
@@ -367,15 +440,17 @@ skitterspec spec-env review <spec> --branch # everything since the base bran
|
|
|
367
440
|
skitterspec spec-env review serve # every spec, on localhost
|
|
368
441
|
```
|
|
369
442
|
|
|
370
|
-
**
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
443
|
+
**There is no switch left to handle.** The engine serves and prints a **stack** —
|
|
444
|
+
one line per tier, `local`, `network`, `remote`, each either a URL or the one
|
|
445
|
+
command that turns it on. All of it is free and none of it publishes anything.
|
|
446
|
+
You are not choosing between them; you are relaying every line it printed.
|
|
447
|
+
`.claude/rules/spec-reports.md` carries the shape and why.
|
|
375
448
|
|
|
376
|
-
The operator who does not want a LAN listener
|
|
377
|
-
|
|
378
|
-
|
|
449
|
+
The operator who does not want a LAN listener sets `review.allowNetwork: false`
|
|
450
|
+
in `env.config.json` — `network` then reads `off` with the command that turns it
|
|
451
|
+
back on, and `local` is the loopback page. Turning serving off entirely is
|
|
452
|
+
`review.serve: "never"`, and then `local` is the `file://` page, which says on
|
|
453
|
+
its own line that it cannot send a verdict.
|
|
379
454
|
|
|
380
455
|
`serve` renders **per request**, so nothing it shows can be stale, and it lists
|
|
381
456
|
every spec with a worktree rather than one. `--host 0.0.0.0` binds beyond
|
|
@@ -401,23 +476,56 @@ Add `--json` to get the file list, totals and the page path back as data. The
|
|
|
401
476
|
page is written to `.spec-env/reviews/<spec>.html`, which is gitignored — it
|
|
402
477
|
leaves no trace in the branch under review.
|
|
403
478
|
|
|
404
|
-
**
|
|
479
|
+
**Declare the button set when the work is unfinished.** A page rendered part-way
|
|
480
|
+
through a run takes `--buttons midrun`, and offers `Continue` — *I have read it,
|
|
481
|
+
carry on* — in place of `Commit` and `Commit & Continue`:
|
|
482
|
+
|
|
483
|
+
```
|
|
484
|
+
skitterspec spec-env review <spec> --buttons midrun
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**It is a statement about the work, never a reading of the gate.** Ask whether
|
|
488
|
+
the thing you just rendered is *finished*: a phase that ended, a bug fix that is
|
|
489
|
+
green, a spec about to land — committing set, which is the default, so pass
|
|
490
|
+
nothing. Half a phase, a hand edit, a colleague's branch mid-flight, anything
|
|
491
|
+
the operator asked to look at while it is still moving — `midrun`.
|
|
405
492
|
|
|
406
|
-
|
|
493
|
+
**Do not derive it from whether the gate is armed.** That is tidier and wrong: a
|
|
494
|
+
project running `review.required: false` never arms at all, so every one of its
|
|
495
|
+
pages would lose the committing buttons and its reader could never commit from
|
|
496
|
+
the page. The caller knows what it rendered; the gate only knows whether the
|
|
497
|
+
project opted into gating.
|
|
407
498
|
|
|
408
|
-
`
|
|
409
|
-
**
|
|
499
|
+
`Continue` is not the removed `none` verdict. `none` recorded itself and did
|
|
500
|
+
nothing; `Continue` **resumes the run**, so it still names an action. What it
|
|
501
|
+
cannot do is commit, or clear a gate a finished phase armed — that takes a
|
|
502
|
+
committing verdict or a recorded skip.
|
|
410
503
|
|
|
411
|
-
|
|
412
|
-
|-----------|---------------|
|
|
413
|
-
| absent (`unknown`) | the `file://` URL, exactly as always. **Do not warn** — unknown is the ordinary state of a local machine |
|
|
414
|
-
| `local` | the `file://` URL |
|
|
415
|
-
| `remote` | the `open:` line as printed — the engine already served it. Pass on any `also:` lines too |
|
|
504
|
+
**On `--page-only`, stop here** and report the path.
|
|
416
505
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
506
|
+
## 4a. Relay the stack — every tier, never one you picked
|
|
507
|
+
|
|
508
|
+
`spec-env review` prints one line per tier and `--json` carries the same thing
|
|
509
|
+
as `tiers`. **Relay all of them, in that order**, whatever the `reader:` line
|
|
510
|
+
says:
|
|
511
|
+
|
|
512
|
+
| Tier | Carries |
|
|
513
|
+
|------|---------|
|
|
514
|
+
| `local` | the loopback URL — or the `file://` page when nothing served, which says it cannot send a verdict |
|
|
515
|
+
| `network` | the LAN URL and any `also:` alternates under it — or `off` with the command that turns it on |
|
|
516
|
+
| `remote` | the published URL and *a verdict here needs `/spec-reviewed`* — or `off` with the command that turns it on |
|
|
517
|
+
|
|
518
|
+
**The `reader:` line decides nothing here any more.** It is still printed and
|
|
519
|
+
still the only place that question is answered, but the offer no longer branches
|
|
520
|
+
on it — because branching on it is what produced a `file://` page for a session
|
|
521
|
+
detected `unknown`, a LAN URL for a phone off the network, and an address that
|
|
522
|
+
changed underneath a reader mid-session. A tier that is off keeps its line, so a
|
|
523
|
+
reader who has left the house can see the surface exists and ask for it.
|
|
524
|
+
|
|
525
|
+
**Never read an environment variable to decide anything about the offer.** Not
|
|
526
|
+
`SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check — there is nothing left
|
|
527
|
+
for a detection to decide, and a second implementation of one could not be
|
|
528
|
+
tested and would drift. The ranking and the traps
|
|
421
529
|
(`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
|
|
422
530
|
a tty under Claude Code) live in `review.js` beside the code, which is where they
|
|
423
531
|
belong.
|
|
@@ -436,10 +544,11 @@ answer left.
|
|
|
436
544
|
|
|
437
545
|
`review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
|
|
438
546
|
operator settle where they are reading, and an explicit value is believed without
|
|
439
|
-
sniffing.
|
|
440
|
-
|
|
547
|
+
sniffing. What the engine may *do* is settled by the tier settings instead —
|
|
548
|
+
`review.serve` (`always` · `never`), `review.allowNetwork` and
|
|
549
|
+
`review.allowRemote` — not by the reader.
|
|
441
550
|
|
|
442
|
-
## 4b. Wait for the verdict
|
|
551
|
+
## 4b. Wait for the verdict — because asking for one means waiting for it
|
|
443
552
|
|
|
444
553
|
A served page can hand its pass back the moment it is pressed. Without a wait,
|
|
445
554
|
that pass lands in the holding area and stops — nothing happens until someone
|
|
@@ -448,16 +557,45 @@ acts joined only by the operator remembering.
|
|
|
448
557
|
**This is the step that joins them**, and it is this skill's, so `/spec-next` can point here rather than
|
|
449
558
|
keeping a second copy.
|
|
450
559
|
|
|
451
|
-
**
|
|
452
|
-
|
|
453
|
-
wait
|
|
454
|
-
|
|
560
|
+
**The rule is `.claude/rules/spec-reports.md`'s: asking implies waiting.** If
|
|
561
|
+
this render asks the reader for a verdict, it waits for one. If it is not going
|
|
562
|
+
to wait, it does not ask — the report names the page and its size and stops
|
|
563
|
+
there. There is no third option, and in particular there is no *ask now, notice
|
|
564
|
+
later*: that is the shape that stranded two passes on one spec, where the run
|
|
565
|
+
said the page was **ready** rather than that it was **waiting**.
|
|
566
|
+
|
|
567
|
+
**So the question is not "can I watch a file".** It is "am I asking?" — and the
|
|
568
|
+
transport only decides what carries the answer back:
|
|
569
|
+
|
|
570
|
+
- **A served page** posts to the local store; the engine's own wait returns when
|
|
571
|
+
it lands, and the steps below run it.
|
|
572
|
+
- **A `file://` page** has nothing to post to, so the reader pastes the pass and
|
|
573
|
+
their next message carries it. Say you are holding and **end the turn** — that
|
|
574
|
+
is the same wait, carried by the conversation. It is not a lesser one.
|
|
575
|
+
- **A published page** writes to the artifact's own store, which nothing reaches
|
|
576
|
+
from here. That is the one case where the honest sentence is *press a verdict,
|
|
577
|
+
then type `/spec-reviewed`* — see the published-page paragraph below.
|
|
578
|
+
|
|
579
|
+
**Do not ask, and therefore do not wait, on a render nobody is waiting behind** —
|
|
580
|
+
a bare `--page-only`, a page produced alongside other work, a pass that has
|
|
581
|
+
already arrived. Those get the `Review` row, no question, and nothing is owed.
|
|
455
582
|
|
|
456
583
|
1. **Note the moment**, as an ISO timestamp, before you start. That instant is
|
|
457
584
|
the entire scope of what may be claimed without a person naming it.
|
|
458
|
-
2. **
|
|
459
|
-
|
|
460
|
-
|
|
585
|
+
2. **Run the engine's wait in the background, and end the turn:**
|
|
586
|
+
|
|
587
|
+
```
|
|
588
|
+
skitterspec spec-env review wait <spec> --since <timestamp>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Never a watcher of your own, and never a timeout.** It lasts as long as the
|
|
592
|
+
session, because a reader who walks away from a diff is the normal case — and
|
|
593
|
+
a loop composed here would be proven by nothing, which is how one written as
|
|
594
|
+
`[ "$x" \> "$y" ]` spun for five minutes in zsh and looked exactly like
|
|
595
|
+
patience. `/spec-next` §5 carries the full account.
|
|
596
|
+
|
|
597
|
+
Not a poll and not a held-open turn: the operator has their terminal back,
|
|
598
|
+
and the session costs nothing while they read.
|
|
461
599
|
3. **On waking, let the engine choose:**
|
|
462
600
|
|
|
463
601
|
```
|
|
@@ -492,10 +630,16 @@ Where the page is published, say plainly that `/spec-reviewed` is what picks it
|
|
|
492
630
|
up.
|
|
493
631
|
|
|
494
632
|
**`/spec-reviewed` is not replaced by this.** It stays the way in for a pass
|
|
495
|
-
that arrived when nobody was waiting, for the two-passes case, for every
|
|
496
|
-
published page
|
|
633
|
+
that arrived when nobody was waiting, for the two-passes case, and for every
|
|
634
|
+
published page. It is user-only, and that is still the
|
|
497
635
|
enforcement that makes a named claim a person's decision.
|
|
498
636
|
|
|
637
|
+
**A harness with no file-watch is no longer on that list**, and that is the
|
|
638
|
+
change. It used to be — the row and `/spec-reviewed` were "the whole story",
|
|
639
|
+
which read as permission to ask without waiting. Now the turn ending is the
|
|
640
|
+
wait, and every harness can end a turn; `/spec-reviewed` remains available
|
|
641
|
+
there, as it is everywhere, for a pass nobody was holding for.
|
|
642
|
+
|
|
499
643
|
## 5. Offer the written review — say what it costs first
|
|
500
644
|
|
|
501
645
|
The page is free. The **written review is not**, and it costs in two separate
|
|
@@ -662,8 +806,8 @@ consumed rather than stored.
|
|
|
662
806
|
purpose; someone who forgot they had voted has not, and only the report tells
|
|
663
807
|
them apart.
|
|
664
808
|
|
|
665
|
-
`Review` carries the files and `+`/`−`, the
|
|
666
|
-
|
|
809
|
+
`Review` carries the files and `+`/`−`, and the tier stack — labelled, run
|
|
810
|
+
together with `·` because a row is one cell. Where the page holds a review pass, it also
|
|
667
811
|
carries the three totals — files accepted, comments open, comments answered.
|
|
668
812
|
`--json` reports those under `notes.totals`; read that, never the diff.
|
|
669
813
|
|