@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
|
@@ -111,11 +111,40 @@ it happens differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step
|
|
|
111
111
|
description (an **update** to the existing issue, plus a sub-issue per phase).
|
|
112
112
|
Recording a snapshot here would declare the mirror already in sync and strand
|
|
113
113
|
the issue showing the raw report forever.
|
|
114
|
+
- **Preserve the original, before anything replaces it.** Run this the moment
|
|
115
|
+
the spec file exists and **before the linking push**:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
skitterspec spec-sync preserve <spec>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
It posts the issue's current description onto the issue as a comment — the one
|
|
122
|
+
surface one-way sync never touches, so no push can clobber it. On the API path
|
|
123
|
+
it reads the description itself; without a key it prints the body and the
|
|
124
|
+
marker, and you post it with the Linear comment tool after checking the
|
|
125
|
+
comments for that marker.
|
|
126
|
+
|
|
127
|
+
**The ordering is the whole correctness condition.** Run before the push, this
|
|
128
|
+
keeps what the reporter filed. Run after it, it would keep the generated spec
|
|
129
|
+
and report success — which is why it is a step of its own here rather than a
|
|
130
|
+
line in the push sequence. The engine warns when it can tell it is late, but it
|
|
131
|
+
cannot always tell.
|
|
132
|
+
|
|
133
|
+
**It never fails the adoption.** A project that set
|
|
134
|
+
`intake.preserveOriginal: false`, an unreachable Linear, an issue with no
|
|
135
|
+
description, a comment Linear refused — every one of them exits 0 and says so.
|
|
136
|
+
Relay what it printed and carry on.
|
|
114
137
|
- **Say what will happen** in the finish-up message. The linking step runs right
|
|
115
138
|
after the spec is written, so the issue's description is replaced by the spec
|
|
116
|
-
**then** — not on some later manual push.
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
**then** — not on some later manual push. Say both halves: the description
|
|
140
|
+
becomes the spec, **and** the original is preserved as a comment on the same
|
|
141
|
+
issue, so the reporter can still read what they wrote.
|
|
142
|
+
|
|
143
|
+
**Do not send anyone to the issue's history for it.** This used to say Linear
|
|
144
|
+
keeps the original there, and that is true and useless: history is a diff
|
|
145
|
+
viewer nobody opens, it is not quotable, and it degrades to "it is in there
|
|
146
|
+
somewhere". The comment is the answer, and the other half is the spec's own
|
|
147
|
+
**Problem** (or **Symptom**) section quoting the reporter.
|
|
119
148
|
|
|
120
149
|
## 1. Establish the base version (the tag)
|
|
121
150
|
|
|
@@ -333,10 +362,14 @@ and **never auto-push git** — Linear's own automation reacts to real branch/PR
|
|
|
333
362
|
events later. Report the Linear issue URL as part of the skill's finish-up
|
|
334
363
|
message.
|
|
335
364
|
|
|
336
|
-
**Only when all three hold**: `specs/.core/linear.config.json` exists,
|
|
337
|
-
`
|
|
365
|
+
**Only when all three hold**: `specs/.core/linear.config.json` exists, the repo
|
|
366
|
+
**owns** `assignee` in its `sync.fieldOwnership`, and the spec carries a
|
|
338
367
|
`linear_identifier`. Any one missing → skip this step silently and carry on; a
|
|
339
|
-
project that
|
|
368
|
+
project that declined must see no trace of assignment.
|
|
369
|
+
|
|
370
|
+
Ownership is the **value**, not the key: `assignee` is owned by default, and
|
|
371
|
+
`"none"` is how a project declines it. Testing whether the key is *present* would
|
|
372
|
+
read as opted-in everywhere, since the default puts it in every config.
|
|
340
373
|
|
|
341
374
|
**Never blocks, never fails the skill.** Everything below is best-effort: the
|
|
342
375
|
branch is provisioned and the spec is moving either way, and an unassigned issue
|
|
@@ -473,7 +506,7 @@ sub-issue jump from Backlog straight to Done, with nothing visible in between.
|
|
|
473
506
|
- **Say what happened** in the skill's report: mirror updated, skipped as
|
|
474
507
|
unlinked, or failed with the reason.
|
|
475
508
|
|
|
476
|
-
## 6b. Render the page
|
|
509
|
+
## 6b. Render the page, arm the gate, then wait for the verdict
|
|
477
510
|
|
|
478
511
|
**Only when the project has per-spec isolation** (`specs/.core/env.config.json`
|
|
479
512
|
present). Without it there is no worktree to read and this step does not exist —
|
|
@@ -489,53 +522,61 @@ skitterspec spec-env review <spec>
|
|
|
489
522
|
**This is free.** The engine reads git and splices the patches into a template;
|
|
490
523
|
the diff never passes through you, so a 266KB patch costs nothing.
|
|
491
524
|
|
|
492
|
-
**Then
|
|
493
|
-
costs — roughly **700 output tokens**, because writing it means reading the diff
|
|
494
|
-
— and that spend is the operator's call, not a default.
|
|
495
|
-
|
|
496
|
-
**The offer is the `Review` row of step 6's block** — the counts, the page link
|
|
497
|
-
and a question, in one row:
|
|
498
|
-
|
|
499
|
-
| **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
|
|
500
|
-
|
|
501
|
-
**It ends in a question, addressed to someone.** It was once a fenced block of
|
|
502
|
-
engine output, and it fired on every phase and was never once taken: two quoted
|
|
503
|
-
lines under the test counts, addressed to nobody, with the report then closing
|
|
504
|
-
on *"commit this first"* — the last instruction the reader got was to move on,
|
|
505
|
-
so they did. A row in a labelled table is findable; a question in it is
|
|
506
|
-
answerable. Both halves are load-bearing.
|
|
507
|
-
|
|
508
|
-
**Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
|
|
509
|
-
the page and the question stay in the same row: two adjacent rows about one page
|
|
510
|
-
make the reader resolve a distinction before acting on either. A later edit that
|
|
511
|
-
moves it out of the block, or separates the link from the question, undoes this
|
|
512
|
-
and should be read as a regression rather than tidying.
|
|
525
|
+
**Then arm the gate**, so the fix now owes a verdict:
|
|
513
526
|
|
|
527
|
+
```
|
|
528
|
+
skitterspec spec-env review arm <spec>
|
|
529
|
+
```
|
|
514
530
|
|
|
515
|
-
|
|
516
|
-
|
|
531
|
+
**Then wait**, and `/spec-next` §5 owns the sequence — follow it there rather
|
|
532
|
+
than reading a second copy here: note the moment you start waiting, watch the
|
|
533
|
+
pending store, end your turn, and let `--claim-since` pick the one pass that
|
|
534
|
+
arrived inside the window. The banner it describes is what this skill emits in
|
|
535
|
+
place of a `Review` row, and the routing on the verdict is `/spec-diff` §2 and
|
|
536
|
+
§4, as it is everywhere.
|
|
537
|
+
|
|
538
|
+
**Why this skill arms as well as waits.** Waiting is what any offer does;
|
|
539
|
+
**arming** asserts an obligation that outlives the turn, and belongs only to
|
|
540
|
+
work that is finished. A hotfix is a completed unit — red→green against a
|
|
541
|
+
released tag — so it qualifies, and a wait with nothing owed behind it is a
|
|
542
|
+
suggestion rather than a gate.
|
|
543
|
+
|
|
544
|
+
**A hotfix is the case where reading it matters most.** This change is about to
|
|
545
|
+
be tagged and shipped to production from a release line, not merged into a
|
|
546
|
+
branch someone else will read first. The verdict is the only review it gets.
|
|
547
|
+
|
|
548
|
+
**It is user-visible, and that is deliberate.** Once armed, a `git commit` in
|
|
549
|
+
this worktree is refused until a verdict is sent or
|
|
550
|
+
`skitterspec spec-env review skip "<reason>"` records the decision to move on.
|
|
551
|
+
The exit is always one command, and one of them is *"I am moving on"*.
|
|
552
|
+
|
|
553
|
+
Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
|
|
554
|
+
three — rather than the bare `page:` path: a path is not clickable in any
|
|
555
|
+
terminal, and a page nobody can open is a page nobody reads. Relay the `live:`
|
|
556
|
+
line with it where the engine printed one, and nothing where it did not.
|
|
557
|
+
`.claude/rules/spec-reports.md` carries the shape, including why a rigid
|
|
558
|
+
contract took that line.
|
|
517
559
|
|
|
518
560
|
- **Never write the review unasked**, and **never publish**. Publishing leaves
|
|
519
561
|
something behind that this tooling cannot remove, so it is always an ask. A
|
|
520
562
|
`file://` link is no use on a phone, and saying so **is** the ask —
|
|
521
563
|
`/spec-diff` §6 owns how.
|
|
522
|
-
**
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
drift.
|
|
564
|
+
**The `reader:` line no longer decides anything here, and that is the point.**
|
|
565
|
+
It is still printed, and it is still the only place that question is answered —
|
|
566
|
+
but the offer does not change with it, because the stack lists every tier
|
|
567
|
+
whatever it says. Three reader states used to mean three different offers, and
|
|
568
|
+
that branching is exactly what produced a `file://` page on a session detected
|
|
569
|
+
`unknown`, a LAN URL for a phone off the network, and an address that changed
|
|
570
|
+
underneath a reader when detection flipped mid-session.
|
|
571
|
+
|
|
572
|
+
So: **relay all three tier lines, every time.** Any `also:` lines sit under
|
|
573
|
+
`network` — the other addresses this machine has, offered because the
|
|
574
|
+
best-guess one can be wrong — so pass them on rather than editing them out.
|
|
575
|
+
|
|
576
|
+
**Never read an environment variable to decide anything about the offer** — not
|
|
577
|
+
`SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
|
|
578
|
+
here for a detection to decide, and a second implementation of one could not be
|
|
579
|
+
tested and would drift.
|
|
539
580
|
|
|
540
581
|
**Serving is the engine's to do; publishing is never.** A `remote` reader
|
|
541
582
|
authorises a local server — one process, ended by one flag, leaving nothing
|
|
@@ -578,10 +619,16 @@ the shape; this section carries only what is specific here.
|
|
|
578
619
|
**Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
|
|
579
620
|
`Review` · `Follow-ups` · `Next`
|
|
580
621
|
|
|
622
|
+
**`Review` is emitted only where step 6b did not run** — a project with no
|
|
623
|
+
isolation, or a render that failed. Where the run is waiting, the banner carries
|
|
624
|
+
the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
|
|
625
|
+
|
|
581
626
|
**The base tag goes in the verdict clause** — `✅ /spec-hotfix · hotfix-foo ·
|
|
582
627
|
green on v2.3.1`. Which released version this was fixed against is the first
|
|
583
628
|
thing anyone needs, and it is not a field: the clause is where the run says
|
|
584
629
|
where it got to.
|
|
585
630
|
|
|
586
|
-
Step 6b
|
|
587
|
-
|
|
631
|
+
Step 6b ends in the **banner**, not a paragraph and not a row — the run is
|
|
632
|
+
waiting on a verdict, and the banner is the shape that says so. Nothing follows
|
|
633
|
+
it. Where the run is not waiting it asks nothing at all: a `Review` row carries
|
|
634
|
+
the counts and the link, and no question (*asking implies waiting*).
|
|
@@ -105,12 +105,24 @@ Ensure it exists. If missing, create it documenting:
|
|
|
105
105
|
Read a sibling spec skill (e.g. `spec`, `spec-next`) for the canonical shapes
|
|
106
106
|
rather than inventing them. If the rule already exists, leave it unless stale.
|
|
107
107
|
|
|
108
|
-
## 4a. The review-gate hook (`.claude/hooks/review-gate.
|
|
109
|
-
|
|
110
|
-
`skitterspec init`
|
|
111
|
-
**committed** `.claude/settings.json` as a
|
|
112
|
-
one engine call per Bash tool call and
|
|
113
|
-
whose phase is still awaiting a verdict.
|
|
108
|
+
## 4a. The review-gate hook (`.claude/hooks/review-gate.cjs`)
|
|
109
|
+
|
|
110
|
+
`skitterspec init` **and `skitterspec update`** install the hook script and
|
|
111
|
+
register it in the project's **committed** `.claude/settings.json` as a
|
|
112
|
+
`PreToolUse` hook on `Bash`. It runs one engine call per Bash tool call and
|
|
113
|
+
refuses a `git commit` in a worktree whose phase is still awaiting a verdict.
|
|
114
|
+
|
|
115
|
+
**Both commands, and that is not a detail.** Copying the script and registering
|
|
116
|
+
it are one operation, and they were once split across two code paths — so
|
|
117
|
+
`update` landed the file, reported `created:`, and wired nothing, on every
|
|
118
|
+
project that upgraded. Landing the script is not installing the hook.
|
|
119
|
+
|
|
120
|
+
**`.cjs`, and the extension is load-bearing.** The script lands inside the
|
|
121
|
+
target project, where *that* project's `package.json` decides how node parses a
|
|
122
|
+
`.js` — so CommonJS shipped as `.js` crashes in any `"type": "module"` project,
|
|
123
|
+
on every Bash tool call. `.cjs` settles it at the file. A registration left over
|
|
124
|
+
from the release that named `.js` is rewritten in place, keeping any wrapping
|
|
125
|
+
the operator added, rather than gaining a second entry beside it.
|
|
114
126
|
|
|
115
127
|
**Committed, not machine-local**, and the difference is the point: the trusted
|
|
116
128
|
worktree root is one machine's absolute path, while *a phase that ended owes an
|
|
@@ -178,19 +178,27 @@ a state from the `list_issue_statuses` names you already have:
|
|
|
178
178
|
automation closes them and a problem otherwise — `spec-sync doctor` warns about
|
|
179
179
|
it either way, so say which it is.
|
|
180
180
|
|
|
181
|
-
## 7b.
|
|
181
|
+
## 7b. Assignment is on — does this team want it off?
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
Say what it does, in one line: `/spec-start` records the developer and their
|
|
184
|
+
Linear issue is assigned to them, released again when the spec completes. Then
|
|
185
|
+
ask whether to turn it **off**.
|
|
186
186
|
|
|
187
|
-
**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
**The question inverted at v16, and the reason is which mistake is recoverable.**
|
|
188
|
+
While it was opt-in, a repo that simply never added the line was
|
|
189
|
+
indistinguishable from one that had decided against it — so the only signal was
|
|
190
|
+
someone noticing weeks later that nobody was on the issue. On by default, a team
|
|
191
|
+
that does not want it says so once, here, and the answer is on the record.
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
**It cannot overwrite a PM's triage either way**, which is what makes the default
|
|
194
|
+
safe rather than merely convenient: a spec that records no assignee sends none,
|
|
195
|
+
and a snapshot with no assignee key means "never pushed" rather than "was null".
|
|
196
|
+
Assignment only ever writes specs *this repo* started.
|
|
197
|
+
|
|
198
|
+
If they want it off, pass **`--no-assign`** in step 8, and skip the rest of this
|
|
199
|
+
step — there is no identity to settle for a repo that does not assign.
|
|
200
|
+
|
|
201
|
+
Otherwise settle who *they* are, so `/spec-start` never has to stop and ask:
|
|
194
202
|
|
|
195
203
|
```
|
|
196
204
|
skitterspec spec-sync whoami
|
|
@@ -210,7 +218,7 @@ setup.
|
|
|
210
218
|
skitterspec spec-sync init-config \
|
|
211
219
|
--team-id <uuid> [--team-key KEY] [--project-id <uuid>] \
|
|
212
220
|
[--intake-label <name>] [--bug-labels a,b] [--hotfix-labels a,b] \
|
|
213
|
-
[--state <bucket>=<name> …] [--assign] \
|
|
221
|
+
[--state <bucket>=<name> …] [--no-assign] \
|
|
214
222
|
--states <statesfile> [--force] [--json]
|
|
215
223
|
```
|
|
216
224
|
|
|
@@ -154,6 +154,24 @@ Before writing any code for this phase, get the workspace clean:
|
|
|
154
154
|
being *"I am moving on"*, which is what keeps this a push rather than a wall —
|
|
155
155
|
a gate with no exit gets switched off wholesale instead of answered.
|
|
156
156
|
|
|
157
|
+
- **Report anything left waiting, and claim none of it.** Ask the engine once:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
skitterspec spec-env review waiting
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A pass listed here arrived when nothing was watching — a wait that never ran,
|
|
164
|
+
a session cleared, a terminal closed overnight — and no watcher can recover
|
|
165
|
+
those, however good. Relay what it prints: the spec, the code, the verdict and
|
|
166
|
+
the age, with `/spec-reviewed <code>` to pick one up and `--drop <code>` to
|
|
167
|
+
disown it.
|
|
168
|
+
|
|
169
|
+
**It is information, not a gate.** It never refuses, nothing here is
|
|
170
|
+
conditional on it, and **you never claim one** — `/spec-diff` §0 stands
|
|
171
|
+
unchanged, and a pass sitting there when you arrived was not sent to you.
|
|
172
|
+
Silent when nothing is waiting, which is the usual case: say nothing rather
|
|
173
|
+
than reporting that there was nothing to report.
|
|
174
|
+
|
|
157
175
|
## 3. Implement the phase
|
|
158
176
|
|
|
159
177
|
Identify the **first unfinished phase** from the `00-overview.md` phase index,
|
|
@@ -184,8 +202,8 @@ once it is over. Without a provider this is a no-op and nothing below changes.
|
|
|
184
202
|
them. Skip it and a started spec stays mirrored as a phase-less issue.
|
|
185
203
|
- **Never mint the spec issue.** An unlinked spec is skipped, not created —
|
|
186
204
|
`/spec-push` is how someone opts in.
|
|
187
|
-
- **Backfill a missing assignee silently; never ask.** Only when
|
|
188
|
-
`
|
|
205
|
+
- **Backfill a missing assignee silently; never ask.** Only when the repo owns
|
|
206
|
+
`assignee` (it does by default; `"none"` declines it). If the spec records nobody and
|
|
189
207
|
`skitterspec spec-sync whoami --json` answers, stamp it with
|
|
190
208
|
`skitterspec spec-sync assign <spec> --to <id> --name "<name>"` and let the
|
|
191
209
|
push above carry it. If identity is unknown, skip it in silence.
|
|
@@ -323,6 +341,12 @@ skitterspec spec-env review <spec>
|
|
|
323
341
|
the diff never passes through you, so a 266KB patch costs nothing. Report the
|
|
324
342
|
path it prints and move on.
|
|
325
343
|
|
|
344
|
+
**This render takes the committing button set**, which is the default — so
|
|
345
|
+
`--buttons` is not passed. That is a statement about the work, not about the
|
|
346
|
+
gate: the phase is finished, so `Commit` and `Commit & Continue` are the right
|
|
347
|
+
verbs for it. A render part-way through a run takes `--buttons midrun` and
|
|
348
|
+
offers `Continue` instead; `/spec-diff` owns that case.
|
|
349
|
+
|
|
326
350
|
**Then arm the gate**, so the phase now owes a verdict:
|
|
327
351
|
|
|
328
352
|
```
|
|
@@ -347,41 +371,72 @@ so one subject lives in one place:
|
|
|
347
371
|
|
|
348
372
|
## ⏸ Review ready — <N> files, +<a> −<d>
|
|
349
373
|
|
|
350
|
-
**
|
|
374
|
+
- **local** — <the `local:` URL>
|
|
375
|
+
- **network** — <the `network:` URL, or off with the command that turns it on>
|
|
376
|
+
- **remote** — <the `remote:` URL, or off with the command that turns it on>
|
|
351
377
|
|
|
352
|
-
|
|
378
|
+
<the `live:` line, exactly as the engine printed it — omitted when it printed none>
|
|
353
379
|
|
|
354
|
-
|
|
355
|
-
banner carries the served URL and the wait is real. Where it could not serve —
|
|
356
|
-
a busy port, a machine with no network address — that is the case publishing
|
|
357
|
-
exists for, and then the banner carries the published URL with what is true of
|
|
358
|
-
it: *press a verdict, then type `/spec-reviewed`*, because nothing pushes from
|
|
359
|
-
the artifact store into this conversation.
|
|
380
|
+
I'm holding here until you send a verdict — the wait covers local and network.
|
|
360
381
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
382
|
+
`/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
|
|
383
|
+
|
|
384
|
+
**The stack is the engine's — copy it, do not compose it.** `spec-env review`
|
|
385
|
+
printed one line per tier, in that order, and `--json` carries the same thing as
|
|
386
|
+
`tiers`. Relay all three whatever they say, including the ones that are off with
|
|
387
|
+
the command that turns them on: a reader who has left the house cannot ask for a
|
|
388
|
+
remote page they were never shown.
|
|
389
|
+
|
|
390
|
+
**Do not pick one for them.** The engine used to, and the guess failed three
|
|
391
|
+
separate ways in one day. `.claude/rules/spec-reports.md` carries the shape and
|
|
392
|
+
why the *one link, never two* rule now reads
|
|
393
|
+
**one link per reachable store, each labelled** —
|
|
394
|
+
local and network are two doors into one room, so the wait
|
|
395
|
+
covers both. `remote` is a second store, so its own line carries its own caveat:
|
|
396
|
+
*a verdict here needs `/spec-reviewed`*, because nothing pushes from
|
|
397
|
+
the artifact store into this conversation. Never let the holding line promise a
|
|
398
|
+
wait over that tier.
|
|
399
|
+
|
|
400
|
+
**Publishing is still never yours to do.** `remote` reading `off` is the normal
|
|
401
|
+
state, and the fix for it is the command on that line — typed by the operator,
|
|
402
|
+
or pressed on the page, because a published page is one this tooling cannot
|
|
403
|
+
remove and permitting it is not publishing it.
|
|
404
|
+
|
|
405
|
+
**The `live:` line goes with the stack**, and it is the engine's line too —
|
|
406
|
+
copied as printed, and **absent when the engine printed none**. The page can act
|
|
407
|
+
on it: a press puts the spec live or takes it down and hands the reader back the
|
|
408
|
+
same page, which is `/spec-diff` §2b's routing and not this skill's. What
|
|
409
|
+
matters here is that an action **never clears the gate this phase armed** — the
|
|
410
|
+
reader has looked at it running and concluded nothing, so the phase still owes a
|
|
411
|
+
verdict.
|
|
367
412
|
|
|
368
413
|
---
|
|
369
414
|
|
|
370
|
-
**Where you are not waiting, it stays the `Review` row** — the counts
|
|
371
|
-
link and
|
|
415
|
+
**Where you are not waiting, it stays the `Review` row** — the counts and the
|
|
416
|
+
page link, and **no question**:
|
|
417
|
+
|
|
418
|
+
| **Review** | <N> files, +<a> −<d> · **local** <URL> · **network** <URL> · **remote** off |
|
|
372
419
|
|
|
373
|
-
|
|
420
|
+
At the end of a phase you are always waiting, so this shape belongs to the
|
|
421
|
+
renders that are not this step: a mid-phase `/spec-diff`, a page produced
|
|
422
|
+
alongside other work. A row cannot be waited on, so a question in one is
|
|
423
|
+
unanswerable however findable it is.
|
|
374
424
|
|
|
375
425
|
**Both shapes are addressed to someone, and that is the constraint.** The offer
|
|
376
426
|
was once a fenced block of engine output: two quoted lines under the test
|
|
377
427
|
counts, addressed to nobody, with the report then closing on *"commit this
|
|
378
428
|
first"* — the last instruction the reader got was to move on, so they did. A row
|
|
379
|
-
in a labelled table is findable; a
|
|
380
|
-
|
|
381
|
-
|
|
429
|
+
in a labelled table is findable; a banner says the work has stopped. What must
|
|
430
|
+
never come back is something unaddressed, unfindable, or fenced.
|
|
431
|
+
|
|
432
|
+
**The row asks nothing, and that is not a weakening of the above.** A row cannot
|
|
433
|
+
be waited on, so a question in one is unanswerable however findable it is —
|
|
434
|
+
which is the failure `spec-reports.md` records under *asking implies waiting*.
|
|
435
|
+
The row names the page; the banner is what asks, because the banner is the shape
|
|
436
|
+
the run is standing behind.
|
|
382
437
|
|
|
383
438
|
**Never bury it and never split it.** The row sits above the last two rows of
|
|
384
|
-
the block, and the page and
|
|
439
|
+
the block, and the page and its counts stay in the same row; the banner
|
|
385
440
|
replaces the row rather than joining it. Two places naming one page make the reader
|
|
386
441
|
resolve a distinction before acting on either — which is the same failure
|
|
387
442
|
whether the two places are adjacent rows or a row and a banner.
|
|
@@ -390,16 +445,25 @@ whether the two places are adjacent rows or a row and a banner.
|
|
|
390
445
|
|
|
391
446
|
The row is findable, but a row cannot make the continuation follow from the
|
|
392
447
|
reading — and that is the gap the whole gate exists to close.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
reader pressed is what carries the work on.
|
|
448
|
+
|
|
449
|
+
**The wait is a command. Do not write one.**
|
|
396
450
|
|
|
397
451
|
1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
|
|
398
452
|
the whole scope of what you may claim.
|
|
399
|
-
2. **
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
453
|
+
2. **Run the engine's wait in the background, and end your turn:**
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
skitterspec spec-env review wait <spec> --since <the timestamp>
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
It returns when a pass arrives inside that window, and
|
|
460
|
+
**it takes no timeout unless you pass one** —
|
|
461
|
+
the wait lasts as long as your session, because a reader who walks away from
|
|
462
|
+
a diff is the normal case rather than the edge one. Do not give it a duration
|
|
463
|
+
of your own.
|
|
464
|
+
|
|
465
|
+
Then **end your turn**. Do not poll it and do not hold the turn open: the
|
|
466
|
+
point is that the reader has the terminal back while they read.
|
|
403
467
|
3. **On waking, let the engine pick**:
|
|
404
468
|
|
|
405
469
|
```
|
|
@@ -407,11 +471,26 @@ reader pressed is what carries the work on.
|
|
|
407
471
|
```
|
|
408
472
|
|
|
409
473
|
It claims the one pass that arrived inside the window, and acts on nothing
|
|
410
|
-
at all when none did (the
|
|
411
|
-
when two did (two sittings, or two people — the operator has the
|
|
474
|
+
at all when none did (the wait can be woken by something that was not a
|
|
475
|
+
pass) or when two did (two sittings, or two people — the operator has the
|
|
476
|
+
codes).
|
|
412
477
|
4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
|
|
413
478
|
restate that routing here.
|
|
414
479
|
|
|
480
|
+
**WHY A COMMAND RATHER THAN A LOOP YOU COMPOSE.** This step used to say "watch
|
|
481
|
+
the pending store" and stop, so every run invented its own watcher in shell —
|
|
482
|
+
and three failed in two days, each reaching the operator as *"I pressed the
|
|
483
|
+
button and nothing happened"*. The worst wrote
|
|
484
|
+
`until [ -f "$P" ] && [ "$x" \> "$y" ]`: valid bash, a syntax error in zsh, a
|
|
485
|
+
predicate that could never be true. It spun for five minutes writing to a
|
|
486
|
+
stderr nobody reads.
|
|
487
|
+
|
|
488
|
+
**The bug is not the lesson — the silence is.** A watcher that can never fire
|
|
489
|
+
and one patiently working are indistinguishable from outside, so nothing about
|
|
490
|
+
that run looked wrong until the operator asked. `review wait` says it has
|
|
491
|
+
started, is written once, and is tested against a store that gains a pass
|
|
492
|
+
mid-flight. A predicate composed fresh each time is proven by nothing.
|
|
493
|
+
|
|
415
494
|
**WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
|
|
416
495
|
reaching your page could not reach your conversation, and that fact was the
|
|
417
496
|
whole guard: a pass sat in the holding area until a person typed
|
|
@@ -425,46 +504,52 @@ is that the page can now act, so the token has become a credential rather than
|
|
|
425
504
|
a convenience — and `--claim-since` refusing to choose between two passes is
|
|
426
505
|
what stops a race becoming a wrong commit.
|
|
427
506
|
|
|
428
|
-
**Where the harness cannot
|
|
429
|
-
|
|
430
|
-
|
|
507
|
+
**Where the harness cannot run something in the background, you still wait** —
|
|
508
|
+
the turn ending is the wait. Say the page is rendered and that you are holding
|
|
509
|
+
for the pass, then end your turn; the reader's next message is what carries it,
|
|
510
|
+
and `/spec-diff` picks it up from the paste exactly as it always has. The gate
|
|
511
|
+
holds either way: it is the engine's, not the wait's.
|
|
512
|
+
|
|
513
|
+
This once read *"change nothing"* — keep the row, keep the question, do not
|
|
514
|
+
wait — and that exemption is the hatch a whole class of unanswerable questions
|
|
515
|
+
came through. Every harness can end a turn.
|
|
431
516
|
|
|
432
517
|
**It still does not break a chained run.** `/commit && /spec-next` is typed as
|
|
433
518
|
one line; by the time this step is reached the chain has finished, so waiting
|
|
434
519
|
here stops nothing that was still going to happen.
|
|
435
520
|
|
|
436
|
-
Relay the
|
|
437
|
-
|
|
521
|
+
Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines —
|
|
522
|
+
never the bare `page:` path: a path is not clickable in any terminal, and a page
|
|
523
|
+
nobody can open is a page nobody reads.
|
|
438
524
|
|
|
439
525
|
- **Never write the review unasked**, and **never publish**. Publishing leaves
|
|
440
526
|
something behind that this tooling cannot remove, so it is always something
|
|
441
527
|
someone asks for. A `file://` link is no use on a phone, and saying so **is**
|
|
442
528
|
the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
|
|
443
|
-
**
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
`file://` URL with its marker, and that is when publishing is worth naming.
|
|
529
|
+
**The `reader:` line no longer decides anything here, and that is the point.**
|
|
530
|
+
It is still printed, and it is still the only place that question is answered —
|
|
531
|
+
but the offer does not change with it, because the stack lists every tier
|
|
532
|
+
whatever it says. Three reader states used to mean three different offers, and
|
|
533
|
+
that branching is exactly what produced a `file://` page on a session detected
|
|
534
|
+
`unknown`, a LAN URL for a phone off the network, and an address that changed
|
|
535
|
+
underneath a reader when detection flipped mid-session.
|
|
536
|
+
|
|
537
|
+
So: **relay all three tier lines, every time.** Any `also:` lines sit under
|
|
538
|
+
`network` — the other addresses this machine has, offered because the
|
|
539
|
+
best-guess one can be wrong — so pass them on rather than editing them out.
|
|
540
|
+
|
|
541
|
+
**Never read an environment variable to decide anything about the offer** — not
|
|
542
|
+
`SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
|
|
543
|
+
here for a detection to decide, and a second implementation of one could not be
|
|
544
|
+
tested and would drift.
|
|
545
|
+
|
|
546
|
+
**Serving is the engine's to do; publishing is never.** The server is one
|
|
547
|
+
process, ended by one flag, leaving nothing behind, and `review.allowNetwork`
|
|
548
|
+
decides whether it binds wide — no detection involved. A reader the server CAN
|
|
549
|
+
reach is not a reason to publish as well: the tiers they can already open are on
|
|
550
|
+
the stack. Publishing leaves a page this tooling cannot remove, so it stays an
|
|
551
|
+
ask in every case, always — which is why `remote` shows the command rather than
|
|
552
|
+
a URL until someone types it.
|
|
468
553
|
|
|
469
554
|
- **Never fatal.** A failed render — no worktree, a git error — is one line and
|
|
470
555
|
the phase is still done. The page is a convenience; the repo is the record.
|
|
@@ -155,6 +155,24 @@ the chosen project through as `--project <id>`.
|
|
|
155
155
|
|
|
156
156
|
## 4a. Apply it yourself — the MCP path (order matters)
|
|
157
157
|
|
|
158
|
+
**First, check whether you are about to overwrite someone.** On the API path
|
|
159
|
+
`apply` does this for itself, off the read-back it already makes. On the MCP
|
|
160
|
+
path nothing has read the issue yet, so ask before you write:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
skitterspec spec-sync status <spec> --remote <issuefile>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
using the same issue JSON step 2 fetched — it must carry the `description`. A
|
|
167
|
+
`drift: … description was edited on Linear since the last push` line means a
|
|
168
|
+
person wrote something there that step 1 below replaces.
|
|
169
|
+
|
|
170
|
+
**It is a warning and never a gate.** The repo is the source of truth and the
|
|
171
|
+
push still goes ahead; relay the line, name the issue, and carry on. What is
|
|
172
|
+
forbidden is doing it *silently* — someone's paragraph disappearing with nothing
|
|
173
|
+
said about it is the failure this exists to end. Nothing printed means nothing to
|
|
174
|
+
say, which includes every case the engine could not tell.
|
|
175
|
+
|
|
158
176
|
1. **Spec issue** → if the overview has no `linear_identifier`, this push
|
|
159
177
|
**mints** it: run the picker in **Picking the Linear Project** below, then
|
|
160
178
|
create it with `save_issue` (`team` = `linear.teamId`; `project` = the picked
|
|
@@ -233,6 +251,33 @@ This writes the last-pushed snapshot from the now-stamped files, so the next
|
|
|
233
251
|
`/spec-push` produces an empty plan. Commit the stamped spec + snapshot into the
|
|
234
252
|
branch so the mirror-link rides in the PR.
|
|
235
253
|
|
|
254
|
+
## 5b. A refused mint, and the way back
|
|
255
|
+
|
|
256
|
+
Two refusals reach you from `apply`, and neither is a failure to retry.
|
|
257
|
+
|
|
258
|
+
**`N already exists and no spec claims it`** — the push was about to mint, and
|
|
259
|
+
an issue with **exactly** this title is sitting there unheld. That is almost
|
|
260
|
+
always the spec's own issue, whose link was lost: a bad merge, a hand edit, a
|
|
261
|
+
stray `sed` over the frontmatter. Adopt it rather than creating a twin:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
skitterspec spec-sync reattach <spec> --to <ISSUE-REF>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Bare, `reattach` searches by title itself and stamps the one unclaimed match;
|
|
268
|
+
it refuses when several match and names them, because choosing between two is
|
|
269
|
+
a guess. `--force-new` is the escape when the duplicate title is genuine.
|
|
270
|
+
|
|
271
|
+
**`N exists in Linear but could not be recorded`** — the create landed and the
|
|
272
|
+
stamp did not, so Linear holds an issue the repo does not point at.
|
|
273
|
+
**Do not re-run**:
|
|
274
|
+
the plan still reads the spec as unlinked, so a re-run mints a second.
|
|
275
|
+
`reattach --to <the named id>` is the fix.
|
|
276
|
+
|
|
277
|
+
Everything else — a rate limit, a refusal that says waiting will not help — is
|
|
278
|
+
reported with Linear's own explanation and whether retrying can help. Relay it;
|
|
279
|
+
do not paraphrase.
|
|
280
|
+
|
|
236
281
|
## 6. Report
|
|
237
282
|
|
|
238
283
|
End with the block defined in `.claude/rules/spec-reports.md`. That file carries
|