@skitterbyte/skitterspec 21.0.0 → 22.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 CHANGED
@@ -1,5 +1,203 @@
1
1
  # Migration guide
2
2
 
3
+ ## `@skitterbyte/skitterspec` v21 → v22 (a review port per repo, and every render serves)
4
+
5
+ ### Breaking change 1 — `review.serveOnRemote` is now `review.serve`
6
+
7
+ **Every render now stands the review server up**, and hands back an `http://`
8
+ URL instead of a `file://` one. It used to serve only when it detected that you
9
+ were reading from somewhere else — and that detection reads three environment
10
+ variables, so a plain local terminal got a `file://` page. A `file://` page has
11
+ **no server to POST to**, so the verdict buttons on it had nowhere to go: the
12
+ press-a-button-and-the-work-continues loop was missing from exactly the sessions
13
+ that are easiest to use.
14
+
15
+ **Which interfaces it listens on is `review.allowNetwork`** — see breaking
16
+ change 3, below. The change here is narrower than it looks: `file://` →
17
+ `http://127.0.0.1`, a page that opens on the machine holding it and, unlike
18
+ `file://`, can answer. Serving more never means listening wider; that is one
19
+ setting, and it is not this one.
20
+
21
+ **What to change.** `review.serveOnRemote: false` is still read as the new
22
+ `review.serve: "never"`, so a config that turned serving off keeps working. The
23
+ key is renamed because the old name would now claim to govern remote renders
24
+ while governing every one of them:
25
+
26
+ ```jsonc
27
+ // specs/.core/env.config.json — before
28
+ "review": { "serveOnRemote": false }
29
+ // after
30
+ "review": { "serve": "never" }
31
+ ```
32
+
33
+ `serveOnRemote: true` needs no translation — `"always"` is the default and does
34
+ what it did. Note what `"never"` costs you: the page falls back to `file://`,
35
+ and its buttons copy a command for you to paste rather than sending anything.
36
+
37
+ **A render that could not serve now says why** — `review.serve is "never"`, or
38
+ the failure naming the port to free — on the `open:` line and in `--json` as
39
+ `notServed`. A `file://` link with nothing said about it used to be the ordinary
40
+ outcome; now it can only mean the ask did not land.
41
+
42
+ **A spec with no worktree is served too.** A spec you have only just written has
43
+ no branch of its own, and the served route used to refuse it — so its page
44
+ existed and 404'd. It is now served as its own documents, listed in the index,
45
+ and accepts a verdict.
46
+
47
+ ### Breaking change 2 — a review port per repo
48
+
49
+ **`review.servePort` now defaults to `"auto"`, not `7777`.** A repo that does
50
+ not pin the key moves to a port derived from its own path —
51
+ `7700 + hash(realpath(repoRoot)) % 100` — the first time `spec-env review serve`
52
+ starts after the upgrade.
53
+
54
+ **The one-time break is a link, and only a link.** A review page open on a
55
+ phone at that moment points at `7777`, and `7777` is no longer this repo's
56
+ server. The verdict you press there goes nowhere. **Re-render the page** —
57
+ `/spec-diff`, or `/spec-next`'s own render — and the new link works. Nothing on
58
+ disk is lost, no pass is dropped, and this happens once.
59
+
60
+ **Why not simply keep 7777.** It was shared by every repo on the machine, so the
61
+ second repo to start was refused, whoever hit that passed `--port`, and the
62
+ links they had already handed out pointed at another repo's daemon — a `404`,
63
+ reported in a grey line in the page footer. A derived port is stable for a given
64
+ tree across a restart, a reboot and a `--stop`, which is what a link handed out
65
+ yesterday needs.
66
+
67
+ ### To keep a fixed port
68
+
69
+ Pin it, and nothing derives:
70
+
71
+ ```jsonc
72
+ // specs/.core/env.config.json
73
+ "review": { "servePort": 7777 }
74
+ ```
75
+
76
+ An explicit number always wins, exactly as it did before. Pin one when you want
77
+ a port you can memorise, or when two repos happen to derive the same one — a
78
+ hundred slots is a small chance of that, not no chance, and the server still
79
+ refuses a busy port rather than moving itself aside.
80
+
81
+ `spec-env review serve --status` now prints the port **and which of the three chose it** —
82
+ `--port`, `review.servePort`, or the derivation.
83
+
84
+ ### Breaking change 3 — the reader no longer decides the bind: `review.allowNetwork` does
85
+
86
+ **Two new keys, and one of them changes a default.** The engine used to guess
87
+ where you were reading and pick a surface for you. It guessed wrong three
88
+ separate ways in one day — a `file://` page on a session detected `unknown`, a
89
+ LAN URL for a phone that had left the network, and an address that changed
90
+ underneath a reader when detection flipped mid-session. So it stopped guessing:
91
+ every render now lists every tier, labelled, in a fixed order.
92
+
93
+ ```jsonc
94
+ // specs/.core/env.config.json — the new keys, with their defaults
95
+ "review": {
96
+ "allowNetwork": true, // the server binds every interface, so the page opens on your phone
97
+ "allowRemote": false // publishing is permitted at all — it permits, it never publishes
98
+ }
99
+ ```
100
+
101
+ **`allowNetwork` defaults to `true`, and that is the behaviour change to read before upgrading.**
102
+ Before, a local or unknown reader got a loopback bind and a
103
+ remote one got every interface; now the setting decides, and its default is the
104
+ wide bind — which is what a remote reader already got, applied to every render.
105
+ The URL still carries the **serve token**, 48 random bits minted per server,
106
+ which remains the only thing deciding who can POST a verdict. A machine you
107
+ would rather not expose sets `"allowNetwork": false`, and `network:` then reads
108
+ `off` with the command that turns it back on.
109
+
110
+ **`allowRemote` defaults to `false`** and permits publishing without performing
111
+ it — a published page is one skitterspec cannot delete, so it stays an explicit
112
+ ask either way.
113
+
114
+ **`review.reader` still exists** and now decides only how a page's location is
115
+ *worded*. It no longer chooses the bind and no longer chooses which tiers appear.
116
+
117
+ ### `/spec-remote-review` — a new slash command
118
+
119
+ Toggles `review.allowRemote`, and takes `on`/`off` when you would rather say
120
+ which. It exists because a render's `remote:` line has to name something you can
121
+ act on, and `skitterspec spec-env review allow remote` is not what anyone
122
+ reconstructs from a page they are reading on a phone. `init` and `update`
123
+ install it with the other commands; like them it is user-only, so nothing but you
124
+ can run it.
125
+
126
+ **It writes `specs/.core/env.config.json` in the primary checkout** — a
127
+ committed file, so it changes for everyone who pulls and leaves that tree dirty.
128
+ The engine says so when it does.
129
+
130
+ ## `@skitterbyte/skitterspec-linear` v16 → v17 (a review port per repo, and every render serves)
131
+
132
+ The same two changes as `@skitterbyte/skitterspec` v21 → v22 above — this
133
+ distribution composes the same engine. Read that entry; nothing here is
134
+ Linear-specific.
135
+
136
+ **If you are coming from v15, read the v15 → v16 entry below as well.** 16.0.0
137
+ was bumped in the repo and never published, so npm went 15 → 17 and both
138
+ entries apply to you.
139
+
140
+ ## `@skitterbyte/skitterspec-linear` v15 → v16 (assignment is on by default)
141
+
142
+ ### Breaking change
143
+
144
+ **A spec's Linear issue is now assigned to whoever is building it.**
145
+ `sync.fieldOwnership.assignee` defaults to `"push"`; it was absent
146
+ before, and absent meant inert. From this version `/spec-start` stamps the
147
+ developer on the spec and the push assigns the issue, and `/spec-complete`
148
+ releases it — on every repo that has not said otherwise.
149
+
150
+ **To decline it**, one line in `specs/.core/linear.config.json`:
151
+
152
+ ```jsonc
153
+ "sync": {
154
+ "fieldOwnership": {
155
+ "assignee": "none"
156
+ }
157
+ }
158
+ ```
159
+
160
+ `none` is a new value in the ownership enum (`both|pull|push|none`) and means
161
+ *this repo does not own the field*: nothing is written, no hash is recorded in a
162
+ snapshot, and `spec-sync status` prints no assignee line — identical in every
163
+ respect to the old behaviour of never listing it. It has to be a value rather
164
+ than an omitted key, because the map merges **per key** onto the defaults: once
165
+ a field is owned by default, there is nothing an absent key can subtract.
166
+
167
+ **An explicit `"assignee": "push"` you already had is now redundant and harmless.**
168
+ Nothing rewrites it and nothing needs to.
169
+
170
+ ### It cannot overwrite a PM's triage, and that is by construction
171
+
172
+ The concern this default has to answer is whether upgrading re-assigns or
173
+ un-assigns issues someone else owns. It cannot, for two reasons that predate it:
174
+
175
+ - **A spec that records no assignee sends none.** Only a spec the repo itself
176
+ stamped is ever pushed, so an issue a PM assigned in Linear is left alone.
177
+ - **A snapshot with no assignee key means "never pushed", not "was null".** Every
178
+ spec linked before this existed has exactly such a snapshot, and the diff emits
179
+ a *clear* only against an assignee it has a recorded hash for.
180
+
181
+ So the flip widens who gets **assigned** — specs you start from now on — and
182
+ cannot widen who gets **unassigned**. There is a regression test pinning this
183
+ (`sync-assignee-projection.test.js`, "THE UPGRADE CASE").
184
+
185
+ ### `spec-sync init-config --assign` is gone; `--no-assign` replaces it
186
+
187
+ The flag's meaning inverted with the default, so it is removed rather than kept
188
+ as a silent no-op — a flag that still parses and does nothing would read as
189
+ "assignment configured" to anyone who wrote it before v16. `--assign` now fails
190
+ as an unknown flag, which is the louder and more useful answer. `--no-assign`
191
+ writes the `"none"` opt-out above.
192
+
193
+ ### `spec-sync assign` now refuses a field the repo does not own
194
+
195
+ Previously it stamped the frontmatter in any repo and printed
196
+ `next: push it, so Linear agrees` — while `toFieldSet` dropped the field, so the
197
+ push never carried it. It now refuses, naming `sync.fieldOwnership.assignee` and
198
+ the value that enables it. Only `/spec-claim` enforced this before, so a script
199
+ or a direct CLI call got the untrue sentence.
200
+
3
201
  ## `@skitterbyte/skitterspec` v20 → v21 (the gate installs, and asking implies waiting)
4
202
 
5
203
  If you upgraded to v20 and the commit gate never once fired, this is why. Two
@@ -21,6 +219,26 @@ The upgrade migrates you: your existing `PreToolUse` entry has its path
21
219
  added all survive — and the retired `review-gate.js` is deleted. If you edited
22
220
  that file yourself it is **kept**, with a warning, and left for you to remove.
23
221
 
222
+ **Check whether you already had a `review-gate.cjs` of your own.** `.cjs` is the
223
+ obvious way to work around the v20 crash, so anyone who fixed it by hand most
224
+ likely picked this exact filename — and this upgrade is the moment skitterspec
225
+ starts managing that path. A file already sitting there is **kept**, the real
226
+ hook is therefore never installed, and `review-gate.js` is pruned out from under
227
+ it. A hand-written shim that only `require`d the old script then fails open, so
228
+ the gate is silently absent under an update that reported success.
229
+
230
+ Two lines settle it:
231
+
232
+ ```
233
+ head -3 .claude/hooks/review-gate.cjs # ours opens with a 'use strict' + a doc comment
234
+ npx @skitterbyte/skitterspec update --check # a line naming this path means yours was kept
235
+ ```
236
+
237
+ If it is yours, take ours — `npx @skitterbyte/skitterspec update --force`, or
238
+ delete the file and re-run `update`. Later versions report this case in its own
239
+ words rather than as `your edit — kept`, but the manifest records the path after
240
+ the first upgrade, so that wording cannot reach anyone who has already run one.
241
+
24
242
  ### Bug fix
25
243
 
26
244
  **`skitterspec update` now registers the hook.** v20's notes said `init` and
package/README.md CHANGED
@@ -9,11 +9,31 @@ Spec-driven development for [Claude Code](https://claude.com/claude-code) — a
9
9
  ↳ /spec-diff — read what the phase changed
10
10
  ```
11
11
 
12
- Ships the spec-lifecycle skills (`/spec`, `/spec-start`, `/spec-next`, `/spec-complete`,
13
- `/spec-cancel`, `/spec-bug`, `/spec-hotfix`, `/spec-review`, `/spec-diff`,
14
- `/spec-init`) plus per-spec **isolation** — a git worktree per in-progress spec,
15
- Docker on demand, host dev servers on reserved ports, and `/spec-connect` to test
16
- a worktree at your normal `localhost` URL.
12
+ Ships per-spec **isolation** a git worktree per in-progress spec, Docker on
13
+ demand, host dev servers on reserved ports — and everything below.
14
+
15
+ <!-- commands:start -->
16
+ | | |
17
+ |---|---|
18
+ | `/spec` | Grill to a shared understanding, then write a groomed spec |
19
+ | `/spec-bug` | Reproduce with a failing test, then drive it red→green |
20
+ | `/spec-hotfix` | Fix the released version: fork from a tag, land by tag + cherry-pick |
21
+ | `/spec-review` | Re-validate a spec against the code; refresh what drifted |
22
+ | `/spec-start` | Put a spec in flight — provision its branch, then build phase 1 |
23
+ | `/spec-next` | Build the next phase of the spec in flight |
24
+ | `/spec-diff` | Read what a phase changed, on a page you can mark up |
25
+ | `/spec-reviewed` | Pick up a verdict pressed when nothing was waiting for it |
26
+ | `/spec-to-main` | Land the branch mid-spec, without finishing it |
27
+ | `/spec-complete` | Verify, land, tear down |
28
+ | `/spec-cancel` | Record why, stamp the header, tear down |
29
+ | `/spec-init` | Bootstrap or repair the workflow in a project |
30
+ | `/spec-connect` | Point your canonical `localhost` ports at one spec's stack |
31
+ | `/spec-live` | Check one spec out in the primary checkout, so your running server reloads it |
32
+ | `/spec-remote-review` | Permit (or forbid) reviewing from off your network |
33
+ <!-- commands:end -->
34
+
35
+ The last three are **slash commands** rather than skills: each pre-executes one
36
+ `skitterspec spec-env` verb and relays it, so only you can run them.
17
37
 
18
38
  ```sh
19
39
  npx @skitterbyte/skitterspec init
@@ -78,6 +98,93 @@ is; the optional *written* review (a short read plus `flag`/`confirm`/`good`
78
98
  notes) is the part that costs, and it is offered rather than assumed. Publishing
79
99
  is always opt-in, and one page per spec — later phases update the same link.
80
100
 
101
+ ### The page takes a verdict back, and the phase waits for it
102
+
103
+ Reading is half of it. The page also takes marks — tick `✓ accept` per file as
104
+ you read, write a note against a line or a whole file, answer the questions a
105
+ written review asked — and then **ends in a decision**:
106
+
107
+ | Button | Does |
108
+ |--------|------|
109
+ | `✓ Commit` | commits the phase, and stops |
110
+ | `✓ Commit & Continue` | commits, then builds the next phase — and stops there |
111
+ | `✓ Commit & Start` | on a freshly written spec: commits it and puts it in flight. The `commit && /spec-start` you would otherwise type |
112
+ | `↺ Request changes` | sends your notes straight back to be worked |
113
+ | `… Discuss first` | reports what you wrote and asks what's up |
114
+
115
+ The last one appears only on a spec's own page — a spec with no phase in flight
116
+ has nothing to continue — and `▶ Put it live` sits above them all, which is not
117
+ a verdict: it commits the phase, checks the branch out where your dev server can
118
+ see it, and hands you back the same page. It clears no gate.
119
+
120
+ **`/spec-next` ends a phase by rendering the page and then watching for that press**,
121
+ so the button is what carries the work on — there is no command to
122
+ remember. A press on a page the engine served reaches it directly; a `file://`
123
+ page has no server to talk to, so it copies the pass and you paste it.
124
+
125
+ **A verdict sent when nothing was watching is not lost.** Type
126
+ **`/spec-reviewed`** to pick it up — bare when one is waiting, or
127
+ `/spec-reviewed 324199` to name one exactly, which matters only when two are.
128
+ It is the only path the harness itself enforces: the model cannot run it, so a
129
+ pass picked up that way is only ever picked up because a person asked.
130
+
131
+ Fixes come back as **resolutions**, so the next render shows each note struck
132
+ through with a one-line account of what changed — you verify the fix rather than
133
+ trusting it. An accept remembers the file's content, so it lapses by itself when
134
+ that file changes again, announced rather than silent.
135
+ **The marks are information, never a gate**:
136
+ nothing counts them, and the verdict is the one thing you choose, once.
137
+
138
+ ### A phase that ended owes an answer
139
+
140
+ The **gate** is armed when a phase ends and its page is rendered, and exactly two
141
+ things clear it: a committing verdict, or
142
+ `skitterspec spec-env review skip "<reason>"`. While it is armed `/spec-next`
143
+ refuses to build the next phase, and — where the hook is installed —
144
+ `git commit` refuses in that worktree. Mid-phase renders arm nothing, every
145
+ cannot-tell exits 0 and says nothing, and `review.required: false` turns it off
146
+ for a project. The exit is always one command, and one of them is *"I am moving
147
+ on"* with the reason on the record.
148
+
149
+ ### Where the page can be read
150
+
151
+ Every render prints one line per surface, labelled, so you pick the one that
152
+ reaches you rather than the engine guessing:
153
+
154
+ ```
155
+ local: http://127.0.0.1:7760/7e9e123e7540/feat-orders
156
+ network: http://192.168.0.136:7760/7e9e123e7540/feat-orders
157
+ remote: off — turn on with: /spec-remote-review
158
+ live: off — /spec-live to put it live
159
+ ```
160
+
161
+ `local` and `network` are two doors into one room — the same server, the same
162
+ waiting verdict — so the wait covers both. `remote` is a published page, which
163
+ is a separate store: a verdict there needs `/spec-reviewed`, and it is off until
164
+ you type `/spec-remote-review`. The `live:` line is the other way to judge a
165
+ change: whether it is also **running**, and the page has a button to put it
166
+ there.
167
+
168
+ The random-looking segment in the URL is a **serve token** — 48 bits minted per
169
+ server, and the only thing deciding who can POST a verdict at all. That, and why
170
+ `/spec-reviewed` is user-only, are written up in
171
+ `.claude/rules/spec-reports.md` and `.claude/rules/spec-planning.md`, which
172
+ `init` installs into your project.
173
+
174
+ ### The `review.*` config keys
175
+
176
+ All optional, in `specs/.core/env.config.json`:
177
+
178
+ | Key | Default | Does |
179
+ |-----|---------|------|
180
+ | `review.serve` | `"always"` | whether a render stands the local server up. `"never"` gives you a `file://` page, whose buttons copy a command instead of sending |
181
+ | `review.allowNetwork` | `true` | whether that server binds every interface, so the page opens on your phone |
182
+ | `review.allowRemote` | `false` | whether publishing is permitted at all. `/spec-remote-review` toggles it; it permits publishing, it does not publish |
183
+ | `review.required` | `true` | whether a finished phase owes a verdict |
184
+ | `review.commitWith` | `"/commit"` | the skill a committing verdict hands off to |
185
+ | `review.reader` | `"detect"` | where you are reading — `local`, `remote`, or detect it |
186
+ | `review.servePort` | derived | the port, normally derived from the repo's path |
187
+
81
188
  `/spec-next` writes the page at the end of every phase. Nothing about it depends
82
189
  on where your shell is.
83
190
 
@@ -93,7 +200,7 @@ a question it cannot answer itself or a failure at the moment it happens:
93
200
  | **Branch** | `spec/feat-orders` · 3 commits, clean |
94
201
  | **Built** | POST /orders handler, orders schema |
95
202
  | **Tests** | 128 passed · npm test |
96
- | **Review** | 7 files, +212 −18 · [open the page](file:///…) want a written review before you commit? |
203
+ | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) · **remote** off |
97
204
  | **Follow-ups** | none |
98
205
  | **Next** | `/spec-next` → phase 3 (Auth) |
99
206
 
@@ -20,24 +20,35 @@ every phase.
20
20
 
21
21
  **Handing the review back.** The page takes marks: tick `✓ accept` per file as
22
22
  you read, note anything against a line or a whole file, answer the questions a
23
- written review asked — then **end it in a decision**. Three buttons, each
24
- carrying its own verdict:
25
- `✓ Commit` commits it, `✓ Commit & Continue` commits and builds the next phase,
26
- `↺ Request changes` sends it straight back to be worked, `… Discuss first` asks
27
- you what's up. A **served** page hands the pass to
28
- the engine, which **holds** it and shows a six-digit code. Type `/spec-reviewed`
29
- and the waiting pass is picked up and acted on; paste the code after it
30
- (`/spec-reviewed 324199`) to name one exactly, which matters only when two are
31
- waiting. **Nothing pushes** a device that reaches your page cannot reach this
32
- conversation, which is what keeps a stray approval out of your review, and is
33
- why the code is an address rather than a password. A `file://` page has no server to talk to, so it
34
- copies and you paste, as before. Approve is unavailable while a note is open —
35
- you asked for something, so it cannot also be fine — and it hands off to your
36
- own commit skill (`review.commitWith`, `/commit` by default) rather than a copy
37
- living here. Fixes come back as resolutions, so the next render shows each note
38
- struck through with what changed. An accept remembers the file's content, so it
39
- lapses by itself when that file changes again. The marks are information
40
- nothing gates on them; the verdict is the one thing you choose, once.
23
+ written review asked — then **end it in a decision**. At the end of a phase the
24
+ buttons are `✓ Commit`, `✓ Commit & Continue` (commit, then build the next
25
+ phase), `↺ Request changes` and `… Discuss first`. A spec that has just been
26
+ written gets `✓ Commit & Start` instead the `commit-start` verdict, which is
27
+ the `commit && /spec-start` you would otherwise type. A **served** page hands
28
+ the pass to the engine, which **holds** it and shows a six-digit code. Committing
29
+ is unavailable while a note is open you asked for something, so it cannot also
30
+ be fine and it hands off to your own commit skill (`review.commitWith`,
31
+ `/commit` by default) rather than a copy living here. Fixes come back as
32
+ resolutions, so the next render shows each note struck through with what changed.
33
+ An accept remembers the file's content, so it lapses by itself when that file
34
+ changes again. The marks are information nothing gates on them; the verdict is
35
+ the one thing you choose, once.
36
+
37
+ **Above the verdicts sits one line that is not a verdict.** `▶ Put it live`
38
+ commits the phase, checks the branch out where your dev server can see it, and
39
+ hands you back the same page with the same options so you can judge the change
40
+ by *using* it, not only by reading it. It clears no gate: you have looked at it
41
+ running and concluded nothing, so the phase still owes an answer.
42
+
43
+ **Every render lists where the page can be read**, labelled, in a fixed order —
44
+ `local`, `network`, `remote` — each either a URL or the one command that turns it
45
+ on, plus a `live:` line saying whether the change is also running. `local` and
46
+ `network` are two doors into one room: the same server and the same waiting
47
+ verdict, so one wait covers both. `remote` is a published page, a second store —
48
+ a verdict there needs `/spec-reviewed`, and it stays off until someone types
49
+ **`/spec-remote-review`**, which toggles it. Publishing is permitted by that
50
+ command, never performed by it: a published page is one this tooling cannot
51
+ remove.
41
52
 
42
53
  **One ending.** Every spec skill finishes with the same block — a verdict
43
54
  (`✅` · `⚠️` · `❌` · `⏸`), then a table of the fields that skill declares,
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Permit or forbid publishing a review for a reader off your network — bare toggles it, `on`/`off` set it explicitly
3
+ argument-hint: "[on | off]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env review allow remote:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env review allow remote --set "$ARGUMENTS"`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ **Bare toggles.** That is the common case: you are looking at a render whose
12
+ `remote:` line says which way it currently is, and what you want is the other
13
+ one. `on` and `off` are there for when you are not looking at it.
14
+
15
+ **It permits publishing; it publishes nothing.** A published page is one
16
+ skitterspec cannot delete, so publishing stays an explicit ask
17
+ (`/spec-diff` §6) — and a verdict pressed on one needs `/spec-reviewed`, because
18
+ nothing pushes from an artifact's store into a conversation.
19
+
20
+ **It writes a committed file.** `specs/.core/env.config.json` in the primary
21
+ checkout, whichever tree you ran this from — so it changes for everyone who
22
+ pulls and leaves that tree dirty. The engine says so; do not soften it.
@@ -40,8 +40,10 @@
40
40
  },
41
41
  "review": {
42
42
  "reader": "detect",
43
- "servePort": 7777,
44
- "serveOnRemote": true,
43
+ "servePort": "auto",
44
+ "serve": "always",
45
+ "allowNetwork": true,
46
+ "allowRemote": false,
45
47
  "commitWith": "/commit",
46
48
  "required": true
47
49
  },
@@ -21,8 +21,27 @@ port block, and no `.env`.
21
21
  exactly as it does today.
22
22
 
23
23
  The loader (`src/env/config.js` → `loadEnvConfig`) merges your file over the
24
- frozen defaults below and returns `{ config, present }`; `present:false` means
25
- no live `env.config.json` was found.
24
+ frozen defaults below and returns `{ config, present, unknown }`;
25
+ `present:false` means no live `env.config.json` was found.
26
+
27
+ **A key not listed below is ignored — and says so.** Every `spec-env` command
28
+ prints one advisory line per unrecognised key, top-level or nested:
29
+
30
+ ```
31
+ spec-env: env.config.json — unknown key "docker.portbase" is ignored.
32
+ ```
33
+
34
+ It is advisory in the strongest sense: the key is dropped exactly as it always
35
+ was, nothing refuses, and the exit status is unchanged. It exists because the
36
+ two things an unknown key can be — a deliberate forward-compat entry and a typo
37
+ — are indistinguishable from here, and only one of them is a mistake you would
38
+ want to hear about. A mis-typed `review.required` leaves the commit gate on and
39
+ a mis-typed `teardown.deleteRemoteBranch` reverts to `prompt`; before this, the
40
+ only signal either gave was that nothing happened.
41
+
42
+ A **known** key whose value is rejected — `mode: "Checkout"`,
43
+ `teardown.deleteRemoteBranch: "yes"` — is not reported here. Each of those falls
44
+ through to a documented conservative default; see the field notes below.
26
45
 
27
46
  ## Fields
28
47
 
@@ -222,13 +241,15 @@ no live `env.config.json` was found.
222
241
 
223
242
  // Reading a spec's diff (`spec-env review`, `/spec-diff`).
224
243
  //
225
- // `reader` decides how the page's LOCATION IS WORDED, and through
226
- // `serveOnRemote` belowwhether the engine stands its local server up so a
227
- // remote reader gets a link that opens. It never decides to PUBLISH. Three
228
- // values:
229
- // "local" — you are at the machine holding the page; a file:// URL opens.
230
- // "remote" — you are not; it does not, so the page is served instead.
231
- // "detect" work it out (the default).
244
+ // `reader` decides how the page's LOCATION IS WORDED. It no longer decides
245
+ // the bind`allowNetwork` does, see below and it no longer decides which
246
+ // tiers are offered, because every tier is now listed whatever it says.
247
+ // WHAT THE SERVER
248
+ // BINDS TO and nothing else. It never decides whether to serve (`serve`
249
+ // does) and never decides to PUBLISH. Three values:
250
+ // "local" you are at the machine holding the page; it binds 127.0.0.1.
251
+ // "remote" — you are not; it binds every interface so the page opens.
252
+ // "detect" — work it out (the default); cannot-tell binds 127.0.0.1.
232
253
  // An explicit "local"/"remote" is BELIEVED WITHOUT SNIFFING: you know where
233
254
  // you are reading, and no signal outranks being told. Detection is only the
234
255
  // default, and it has three outcomes rather than two — local, remote, and
@@ -238,25 +259,81 @@ no live `env.config.json` was found.
238
259
  // unrecognised value falls through to "detect", so a typo cannot become a
239
260
  // confident answer. Default: detect.
240
261
  //
241
- // `servePort` is the default port for `spec-env review serve`, which renders
242
- // every spec's diff per request on one local server. `--port` overrides it per
243
- // run. The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which
244
- // mints an unguessable path token and prints the LAN URL including it —
245
- // anyone holding that URL can read every spec's diff while it runs.
246
- // Default: 7777.
262
+ // `servePort` is the port for `spec-env review serve`, which renders every
263
+ // spec's diff per request on one local server. Two forms:
264
+ //
265
+ // "auto" (the default) derive it from this repo's path, as
266
+ // 7700 + hash(realpath(repoRoot)) % 100. Two repos on one machine stop
267
+ // competing for one shared port without anyone configuring anything, and
268
+ // — the part that matters — THE SAME REPO GETS THE SAME PORT EVERY TIME.
269
+ // The derivation reads nothing on disk, so the port survives a restart, a
270
+ // reboot and a `--stop`, which is what lets a link handed out yesterday
271
+ // still resolve. A symlinked spelling of the tree resolves first, so one
272
+ // repo never lands on two ports.
273
+ // <a number> — pin it. An explicit number always wins, and pinning is what
274
+ // you do when you want a port you can memorise, or when two repos derive
275
+ // the same one.
247
276
  //
248
- // `serveOnRemote` is whether a "remote" reader may have that server started
249
- // FOR them. On (the default) the engine brings it up, binds 0.0.0.0, and puts
250
- // the served URL on the `open:` line best-guess network address first, the
251
- // rest listed under it, because the guess reads interface names and a VPN or
252
- // an unusual adapter will fool it. Off, you get the file:// URL with its
253
- // "will not open where you are reading" marker and the command to type.
277
+ // A derived port CAN still collide — a hundred slots is a small chance, not
278
+ // no chance and the server refuses rather than moving itself aside. The fix
279
+ // it names is `servePort`, because that is what the next link is built from;
280
+ // `--port` moves one run and leaves every link already handed out pointing at
281
+ // the busy port. `spec-env review serve --status` prints the port and which
282
+ // of the three chose it.
283
+ //
284
+ // An unrecognised value falls through to "auto", like every other typed key
285
+ // here. Default: "auto".
286
+ //
287
+ // The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which mints
288
+ // an unguessable path token and prints the LAN URL including it — anyone
289
+ // holding that URL can read every spec's diff while it runs.
290
+ //
291
+ // `serve` is whether a render stands the local server up at all:
292
+ // "always" — every render does (the default), so `local:` and `network:`
293
+ // are both http URLs the page can POST a verdict back to. The
294
+ // LAN address comes first with the rest listed under it, because
295
+ // the guess reads interface names and a VPN or an unusual
296
+ // adapter will fool it.
297
+ // "never" — `local:` is a file:// URL instead. Note what that costs: a
298
+ // file:// page has no server to POST to, so its verdict buttons
299
+ // copy a command for you to paste rather than sending anything.
300
+ // It replaced `serveOnRemote`, which gated serving on the reader — and so
301
+ // handed a local machine a page it could read and not answer. A legacy
302
+ // `serveOnRemote: false` is still read as `serve: "never"`.
254
303
  // Either way NOTHING IS PUBLISHED on a detection: a server is one process
255
304
  // ended by one flag, while a published page is one this tooling cannot
256
305
  // remove, so that half stays an explicit ask. Teardown names a server that
257
306
  // served the last spec, and `spec-env prune` reaps a pidfile whose process is
258
307
  // gone. Default: true.
259
308
  //
309
+ // `allowNetwork` and `allowRemote` decide WHICH TIERS a render offers, and
310
+ // they replaced the engine guessing where the reader was sitting. It guessed
311
+ // for a while and got it wrong three separate ways in one day: a file:// page
312
+ // on a session detected `unknown`, a LAN URL for a phone that had left the
313
+ // network, and an address that changed underneath a reader mid-session. So
314
+ // every tier is listed, labelled, and either a URL or the one command that
315
+ // turns it on — and you pick the one that reaches you.
316
+ //
317
+ // `allowNetwork` — whether the review server binds EVERY INTERFACE, so the
318
+ // page opens on your phone, or loopback only. THIS IS WHAT
319
+ // CHOOSES THE BIND; `reader` no longer does. Default: true,
320
+ // which matches what the engine already did.
321
+ // `allowRemote` — whether PUBLISHING is permitted at all. It permits it; it
322
+ // publishes nothing. Default: FALSE, because a published
323
+ // page is one skitterspec cannot delete, so it must never
324
+ // happen unasked.
325
+ //
326
+ // `local` and `network` are two doors into ONE ROOM — the page POSTs to
327
+ // `location.pathname`, so both reach the same server and the same waiting
328
+ // verdict, and one wait covers both. `remote` is a second store: a verdict
329
+ // pressed on a published page needs `/spec-reviewed`, because nothing pushes
330
+ // from an artifact's store into a conversation.
331
+ //
332
+ // Both are toggled by `spec-env review allow <tier> --set [on|off]`, where an
333
+ // empty value toggles — which is what `/spec-remote-review` runs. It writes
334
+ // THIS FILE in the primary checkout, so it changes for everyone who pulls and
335
+ // leaves that tree dirty; the engine says so when it does.
336
+ //
260
337
  // `commitWith` names the skill a COMMITTING verdict hands off to. A review
261
338
  // page ends in a verdict — commit, commit & continue, request changes,
262
339
  // discuss — and the point of the first two is that the commit follows from
@@ -289,8 +366,8 @@ no live `env.config.json` was found.
289
366
  // place rather than quietly disabling it. Default: true.
290
367
  "review": {
291
368
  "reader": "detect",
292
- "servePort": 7777,
293
- "serveOnRemote": true,
369
+ "servePort": "auto",
370
+ "serve": "always",
294
371
  "commitWith": "/commit",
295
372
  "required": true
296
373
  }