@skitterbyte/skitterspec-linear 15.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.
Files changed (44) hide show
  1. package/MIGRATION.md +218 -0
  2. package/README.md +34 -1
  3. package/assets/claude-md-section.md +29 -18
  4. package/assets/commands/spec-remote-review.md +22 -0
  5. package/assets/core/SETUP.md +10 -6
  6. package/assets/core/env.config.json.example +4 -2
  7. package/assets/core/env.config.md +100 -23
  8. package/assets/core/linear.config.json.example +2 -1
  9. package/assets/core/linear.config.md +49 -22
  10. package/assets/review/page.html +1044 -101
  11. package/assets/rules/spec-planning.md +35 -3
  12. package/assets/rules/spec-reports.md +131 -20
  13. package/assets/skills/spec/SKILL.md +161 -4
  14. package/assets/skills/spec-bug/SKILL.md +68 -30
  15. package/assets/skills/spec-cancel/SKILL.md +2 -2
  16. package/assets/skills/spec-claim/SKILL.md +12 -4
  17. package/assets/skills/spec-complete/SKILL.md +2 -2
  18. package/assets/skills/spec-diff/SKILL.md +131 -36
  19. package/assets/skills/spec-hotfix/SKILL.md +61 -25
  20. package/assets/skills/spec-linear-setup/SKILL.md +19 -11
  21. package/assets/skills/spec-next/SKILL.md +121 -58
  22. package/assets/skills/spec-push/SKILL.md +45 -0
  23. package/assets/skills/spec-review/SKILL.md +89 -2
  24. package/assets/skills/spec-reviewed/SKILL.md +31 -5
  25. package/assets/skills/spec-start/SKILL.md +26 -3
  26. package/assets/skills/spec-status/SKILL.md +20 -6
  27. package/assets/skills/spec-sync/SKILL.md +1 -0
  28. package/package.json +2 -2
  29. package/src/cli.js +913 -116
  30. package/src/env/classify.js +87 -2
  31. package/src/env/config.js +214 -17
  32. package/src/env/live.js +94 -0
  33. package/src/env/resolve.js +36 -2
  34. package/src/env/review.js +542 -21
  35. package/src/env/serve.js +298 -19
  36. package/src/env/supervise.js +8 -1
  37. package/src/init.js +60 -9
  38. package/src/vendor/linear/api.js +111 -2
  39. package/src/vendor/linear/cli-sync.js +661 -11
  40. package/src/vendor/linear/config.js +41 -13
  41. package/src/vendor/linear/doctor.js +6 -5
  42. package/src/vendor/sync-core/index.js +11 -3
  43. package/src/vendor/sync-core/src/compare.js +65 -0
  44. package/src/vendor/sync-core/src/normalize.js +26 -0
@@ -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* `assignee` in its
20
- `sync.fieldOwnership`. If either is missing, say which one and stop without the
21
- field the stamp would sit in the file doing nothing.
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.** With `assignee` in
99
- `sync.fieldOwnership`, the projection derives it from the spec's lifecycle
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 two ways, and **both are ordinary**:
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 pasted JSON blob** — a `file://` page has no server to talk to, so it
50
- copies. Not legacy: it is the whole story for a local reader.
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
- **The engine handles the switch.** A file when the reader is at this machine, a
371
- served URL when they are not: on a `remote` reader it stands its own server up
372
- and puts a URL the reader can open on the `open:` line. Both are free and neither
373
- publishes anything. You are not choosing between them; you are relaying whichever
374
- one the engine printed.
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 started for them sets
377
- `review.serveOnRemote: false` in `env.config.json`, and the `file://` link with
378
- its *will not open where you are reading* marker comes back.
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
@@ -428,21 +503,29 @@ committing verdict or a recorded skip.
428
503
 
429
504
  **On `--page-only`, stop here** and report the path.
430
505
 
431
- ## 4a. Read the `reader:` line — never sniff for it yourself
432
-
433
- `spec-env review` reports where it believes the reader is, and
434
- **that is the only place this question is answered.** Three states:
435
-
436
- | `reader:` | What to offer |
437
- |-----------|---------------|
438
- | absent (`unknown`) | the `file://` URL, exactly as always. **Do not warn** — unknown is the ordinary state of a local machine |
439
- | `local` | the `file://` URL |
440
- | `remote` | the `open:` line as printed — the engine already served it. Pass on any `also:` lines too |
441
-
442
- **Never read an environment variable to decide this.** Not `SSH_CONNECTION`, not
443
- `CLAUDE_CODE_*`, not a tty check the engine already did it, reports the answer
444
- on that line and in `--json`, and a second implementation here could not be
445
- tested and would drift from the first. The ranking and the traps
506
+ ## 4a. Relay the stackevery 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 itor `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
446
529
  (`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
447
530
  a tty under Claude Code) live in `review.js` beside the code, which is where they
448
531
  belong.
@@ -461,8 +544,9 @@ answer left.
461
544
 
462
545
  `review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
463
546
  operator settle where they are reading, and an explicit value is believed without
464
- sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
465
- act on it.
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.
466
550
 
467
551
  ## 4b. Wait for the verdict — because asking for one means waiting for it
468
552
 
@@ -483,8 +567,8 @@ said the page was **ready** rather than that it was **waiting**.
483
567
  **So the question is not "can I watch a file".** It is "am I asking?" — and the
484
568
  transport only decides what carries the answer back:
485
569
 
486
- - **A served page** posts to the local store; a file-watch sees it, and the
487
- steps below are that watch.
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.
488
572
  - **A `file://` page** has nothing to post to, so the reader pastes the pass and
489
573
  their next message carries it. Say you are holding and **end the turn** — that
490
574
  is the same wait, carried by the conversation. It is not a lesser one.
@@ -498,9 +582,20 @@ already arrived. Those get the `Review` row, no question, and nothing is owed.
498
582
 
499
583
  1. **Note the moment**, as an ISO timestamp, before you start. That instant is
500
584
  the entire scope of what may be claimed without a person naming it.
501
- 2. **Watch** `.spec-env/reviews/<spec>.pending.json` in the primary checkout,
502
- and **end the turn**. Not a poll and not a held-open turn: the operator has
503
- their terminal back, and the session costs nothing while they read.
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.
504
599
  3. **On waking, let the engine choose:**
505
600
 
506
601
  ```
@@ -711,8 +806,8 @@ consumed rather than stored.
711
806
  purpose; someone who forgot they had voted has not, and only the report tells
712
807
  them apart.
713
808
 
714
- `Review` carries the files and `+`/`−`, the page's `open:` line, and the
715
- published URL when there is one. Where the page holds a review pass, it also
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
716
811
  carries the three totals — files accepted, comments open, comments answered.
717
812
  `--json` reports those under `notes.totals`; read that, never the diff.
718
813
 
@@ -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. The reporter's words are not lost:
117
- they are quoted in the spec's **Problem** (or **Symptom**) section, and Linear
118
- keeps the original in the issue's history.
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, its
337
- `sync.fieldOwnership` includes `assignee`, and the spec carries a
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 has not opted in must see no trace of assignment.
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
@@ -517,30 +550,33 @@ this worktree is refused until a verdict is sent or
517
550
  `skitterspec spec-env review skip "<reason>"` records the decision to move on.
518
551
  The exit is always one command, and one of them is *"I am moving on"*.
519
552
 
520
- Relay the **`open:`** line rather than the bare path: a path is not clickable in
521
- any terminal, and a page nobody can open is a page nobody reads.
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.
522
559
 
523
560
  - **Never write the review unasked**, and **never publish**. Publishing leaves
524
561
  something behind that this tooling cannot remove, so it is always an ask. A
525
562
  `file://` link is no use on a phone, and saying so **is** the ask —
526
563
  `/spec-diff` §6 owns how.
527
- **Follow the `reader:` line the engine printed do not sniff for it.** It
528
- answers where the person reading this is sitting, and the offer changes with it:
529
-
530
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
531
- unknown is the ordinary state of a local machine, and a warning there is an
532
- accusation against a healthy session.
533
- - **`local`** the `file://` URL.
534
- - **`remote`** — the engine has already stood its local server up and put a URL
535
- the reader can open on `open:`. So there is **nothing special to say**: relay
536
- that line like any other. Any `also:` lines under it are the other addresses
537
- this machine has, offered because the best-guess one can be wrong — pass them
538
- on rather than editing them out.
539
-
540
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
541
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
542
- and in `--json`, and a second implementation here could not be tested and would
543
- 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.
544
580
 
545
581
  **Serving is the engine's to do; publishing is never.** A `remote` reader
546
582
  authorises a local server — one process, ended by one flag, leaving nothing
@@ -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. Should a spec's issue be assigned to whoever is building it? (optional)
181
+ ## 7b. Assignment is on does this team want it off?
182
182
 
183
- Ask it plainly, in one line: with this on, `/spec-start` records the developer
184
- and their Linear issue is assigned to them, released again when the spec
185
- completes. Off, nothing about assignment happens at all.
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
- **Offer once and take "no" for an answer.** It is off by default deliberately —
188
- a team whose PM owns assignment in Linear does not want the repo writing that
189
- field, and the cost of guessing wrong is somebody else's triage being
190
- overwritten.
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
- If they want it, pass **`--assign`** in step 8. Then settle who *they* are, so
193
- `/spec-start` never has to stop and ask:
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