@sleepy_panda_srl/portulan 0.1.3 → 0.3.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 (89) hide show
  1. package/README.md +7 -6
  2. package/cli/README.md +145 -358
  3. package/cli/advisory.mjs +354 -0
  4. package/cli/comments.mjs +932 -0
  5. package/cli/compile.mjs +1719 -1699
  6. package/cli/context.mjs +883 -0
  7. package/cli/control-chars.mjs +12 -301
  8. package/cli/discover.mjs +17 -574
  9. package/cli/doctor.mjs +269 -1080
  10. package/cli/drills.mjs +92 -504
  11. package/cli/feedback.mjs +16 -237
  12. package/cli/finish.mjs +437 -0
  13. package/cli/form.mjs +644 -0
  14. package/cli/fuzz-shell.mjs +20 -305
  15. package/cli/gate.mjs +7 -199
  16. package/cli/goldens.mjs +26 -184
  17. package/cli/index.mjs +305 -655
  18. package/cli/init.mjs +243 -566
  19. package/cli/inside.mjs +3 -28
  20. package/cli/instructions.mjs +829 -0
  21. package/cli/ledger.mjs +882 -0
  22. package/cli/librarian.mjs +135 -599
  23. package/cli/manifest.mjs +2 -39
  24. package/cli/mutants.mjs +9 -170
  25. package/cli/new.mjs +12 -243
  26. package/cli/pack-identity.mjs +5 -74
  27. package/cli/pack-version.mjs +13 -255
  28. package/cli/plugin-lint.mjs +42 -658
  29. package/cli/portulan.mjs +6 -125
  30. package/cli/recipe-set.mjs +15 -245
  31. package/cli/release-eval.mjs +28 -348
  32. package/cli/review-meter.mjs +29 -265
  33. package/cli/rule-carriers.mjs +12 -175
  34. package/cli/sessions.mjs +82 -0
  35. package/cli/skill-goldens.mjs +10 -176
  36. package/cli/skills-set.mjs +12 -414
  37. package/cli/stop-gate.mjs +83 -514
  38. package/cli/symbols.mjs +1132 -0
  39. package/cli/telemetry.mjs +82 -505
  40. package/cli/upgrade.mjs +171 -314
  41. package/cli/vendor.mjs +146 -476
  42. package/cli/version-carriers.mjs +8 -86
  43. package/core/README.md +1 -1
  44. package/core/engine.md +3 -9
  45. package/core/operating/autonomy.md +1 -1
  46. package/core/operating/context.md +258 -0
  47. package/core/operating/evolution.md +3 -3
  48. package/core/operating/loop.md +13 -20
  49. package/core/operating/memory.md +12 -3
  50. package/core/operating/sessions.md +100 -0
  51. package/core/personas/README.md +33 -0
  52. package/core/personas/librarian.md +2 -2
  53. package/core/skills/clarify/SKILL.md +1 -1
  54. package/core/skills/consolidate/SKILL.md +5 -3
  55. package/core/templates/handoff.md +28 -15
  56. package/core/templates/memory-entry.md +1 -0
  57. package/evals/README.md +271 -845
  58. package/evals/releases/0.2.0.json +146 -0
  59. package/evals/releases/0.2.0.md +67 -0
  60. package/evals/releases/0.3.0.json +150 -0
  61. package/evals/releases/0.3.0.md +68 -0
  62. package/evals/releases/README.md +3 -3
  63. package/package.json +5 -2
  64. package/packs/.claude-plugin/plugin.json +2 -1
  65. package/packs/README.md +7 -4
  66. package/packs/rituals/checkpoints/README.md +1 -1
  67. package/packs/tools/README.md +8 -8
  68. package/packs/tools/github/verify/actions-pinned.sh +10 -55
  69. package/packs/tools/local-llm/README.md +136 -0
  70. package/packs/tools/local-llm/pack.json +32 -0
  71. package/packs/tools/local-llm/personas/local-worker.md +44 -0
  72. package/packs/tools/local-llm/runner/local-llm.mjs +1665 -0
  73. package/packs/tools/local-llm/skills/delegate-local/SKILL.md +64 -0
  74. package/packs/tools/local-llm/verify/README.md +18 -0
  75. package/spec/README.md +113 -6
  76. package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
  77. package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
  78. package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
  79. package/spec/migrations/0004-changelog-fragments.mjs +53 -0
  80. package/spec/migrations/0005-session-log-retired.mjs +69 -0
  81. package/spec/migrations/0006-boot-card.mjs +77 -0
  82. package/spec/migrations/0007-guidance-compiled.mjs +31 -0
  83. package/spec/migrations/0008-card-reading.mjs +55 -0
  84. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
  85. package/spec/migrations/0010-comment-rail.mjs +57 -0
  86. package/spec/migrations/README.md +45 -10
  87. package/spec/pack.schema.json +1 -1
  88. package/spec/slots.md +343 -27
  89. package/spec/workspace.schema.json +145 -4
package/cli/drills.mjs CHANGED
@@ -1,94 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  // The forced-red drill harness — every rail broken on purpose, and required to fire.
3
- //
4
- // Milestone 8, clause (d): *scheduled forced-red drills — every rail forced red on a calendar and
5
- // required to fire.* The argument for the clause is in `../docs/milestones/m08.md`; the calendar is
6
- // `../.github/workflows/drills.yml`; what belongs here is what a drill is and why the shape is this
7
- // one.
8
- //
9
- // ## What it answers, and what its two siblings answer
10
- //
11
- // `./goldens.mjs` asks whether every compiled gate has adversarial fixtures — **presence**.
12
- // `./mutants.mjs` asks whether those fixtures can tell a working matcher from a broken one —
13
- // **discrimination**. Neither asks the question one level out: **does the rail still fire at all?**
14
- // A recipe whose precondition quietly started exiting 0 over an empty file list, a hook that fails
15
- // open on a crash, a check whose enumeration went empty — each reports green and each has stopped
16
- // being a rail. Nothing in this repository could see that, and the way it was found until now was an
17
- // incident.
18
- //
19
- // ## The clause's provenance is two sessions doing this by hand
20
- //
21
- // `../.portulan/proposals/0007-every-watcher-ships-with-its-observation-procedure.md` — *a watcher
22
- // earns its place by being watched* — asks for a procedure *"run once and its result recorded"*. The
23
- // 2026-07-28 amendment generalises it along both axes: from one demonstration at adoption **to a
24
- // calendar**, and from **watchers to every rail**. Milestone 8 sessions 0 and 1 ran eleven forced-red
25
- // drills by hand and recorded them in their handoffs, which is precisely the state this file exists to
26
- // replace: two sessions running them by hand is evidence for the clause, not a substitute for it.
27
- //
28
- // **And both sessions had a drill that did not fire.** One anchored substitution missed by four spaces
29
- // of indentation; one patch script's quoting broke. Both times the recipe ran green against an
30
- // unmodified file and the drill reported on nothing — the same false green the whole milestone is
31
- // about, inside the instrument built to detect it. Every guard below traces to one of those two.
32
- //
33
- // ## A drill is a PAIR, and the pair is the oracle
34
- //
35
- // Per rail: a **control** run on a pristine tree, then a **fire** run after the perturbation. Both are
36
- // required, because drill 1 of 2026-07-30 recorded the reason — *"a rail that only ever reds proves
37
- // nothing about its green: a recipe hard-wired to fail would have produced the identical red
38
- // transcript."*
39
- //
40
- // The control does a second job the drills of the two hand sessions could not do: it catches an
41
- // **environment** difference. A rail that is red for a reason having nothing to do with the
42
- // perturbation — a path-sensitive check, a clean checkout that is not a working copy — makes its drill
43
- // exit 2 rather than report a fire it did not cause.
44
- //
45
- // ## Three guards, each from a defect this repository has already shipped
46
- //
47
- // 1. **A `tell` is required, never optional.** A perturbation can red a rail for the wrong reason — a
48
- // syntax error instead of the finding. So every drill declares a substring the rail's own output
49
- // must carry when it fires, and the harness requires it **absent in the control and present in the
50
- // fire**. `../.portulan/verify/README.md` already records one instance caught *"only because the
51
- // drill asserts the message"*.
52
- // 2. **A perturbation may not no-op.** An anchored substitution must place **exactly once** —
53
- // `./mutants.mjs`'s discipline, adopted rather than re-derived — *and* the bytes on disk are
54
- // hashed before and after and must differ. A missing or ambiguous anchor is could-not-run, never
55
- // a skip.
56
- // 3. **`--only` narrows what RUNS, not what must be well formed.** The whole table is validated in
57
- // every mode. Session 1's round 1 found the opposite in `./mutants.mjs`: `--only` skipped
58
- // validation in exactly the mode a person reaches for when something is already wrong.
59
- //
60
- // ## Isolation: one throwaway `git worktree` per drill
61
- //
62
- // `git worktree add --detach <tmp> <tree>` shares the object store, so there is no clone and no
63
- // network, and history is complete — which `pack-version` and `pack-identity` both need. The
64
- // developer's working tree is never perturbed. Both hand sessions perturbed in place and restored by
65
- // hand, which is one crash away from leaving a repository broken.
66
- //
67
- // ## Which tree is drilled, said out loud on every run
68
- //
69
- // A worktree is a **commit**, so this tool reports on a commit and prints which one. By default it
70
- // refuses a dirty tree rather than quietly drilling `HEAD` while the reader believes it drilled what
71
- // they are looking at — the shape of *the gate allows in silence when it reads the wrong tree*.
72
- // `--working-copy` synthesizes a commit from the working copy with `git stash create` and prints
73
- // **that** sha; because `stash create` does not carry untracked-and-unstaged files, that mode refuses
74
- // while any exist, naming them, rather than drilling a tree missing the very file under review.
75
- //
76
- // ## Why the sweep is NOT a verify recipe, and what is
77
- //
78
- // `../.portulan/dod.md` condition 1 asks that every recipe ran green *in this working copy*. A recipe
79
- // that reported on `HEAD` would be a green about a different tree. So the **sweep** belongs to the
80
- // calendar and to `--working-copy` by hand, and what is declared as the `drills` recipe is `--check`:
81
- // the correspondence pass over the working tree — every yielded rail has a drill, every drill names a
82
- // declared rail, every anchor still places exactly once. That is the half that can drift on any commit,
83
- // it runs no rail, and it costs milliseconds.
84
- //
85
- // ## This module SPAWNS, and its siblings assert they do not
86
- //
87
- // `./goldens.mjs`, `./mutants.mjs` and `./fuzz-shell.mjs` are each held by their suites to importing no
88
- // process-spawning API, because a corpus of `git push --force` spellings must never reach a shell.
89
- // This module is the opposite by nature: running a rail *is* spawning it. What its suite pins instead
90
- // is that nothing it spawns is composed from a payload — every command comes from the yielded recipe
91
- // set or from this file's own declarations, and no drill's `perturb` value is ever passed to a shell.
92
3
 
93
4
  import fs from "node:fs";
94
5
  import os from "node:os";
@@ -101,37 +12,11 @@ import { CouldNotRun } from "./goldens.mjs";
101
12
  import { isInside } from "./inside.mjs";
102
13
  import { recipeSet, resolverFor } from "./recipe-set.mjs";
103
14
 
104
- /** How long any one rail may take before the drill calls it a could-not-run rather than a verdict. */
105
15
  const RAIL_TIMEOUT_MS = 10 * 60 * 1000;
106
16
 
107
- /**
108
- * The prefix every session id this harness invents carries.
109
- *
110
- * It is load-bearing twice. A hook keyed by session id must not share state between runs, and — because
111
- * `./stop-gate.mjs` writes its refusal counter into the OS temp directory under
112
- * `portulan-stopgate-<readable-session-id>-…` — a prefix this harness owns is what lets it retire its
113
- * own counter files without touching a real session's. Measured before it was relied on: each sweep was
114
- * leaving one 45-byte file behind, found at the pre-commit checkpoint.
115
- */
116
17
  export const DRILL_SESSION_PREFIX = "portulan-drill";
117
18
 
118
- /**
119
- * The rails that are NOT recipes, declared as an allow-list.
120
- *
121
- * **The allow-list is the point rather than the convenience.** Without it a drill whose `rail` is a
122
- * misspelled recipe id — `doc` for `docs` — would be silently reclassified as a rail of its own, and
123
- * the coverage check would pass while the real recipe went undrilled. A rail is a yielded recipe id or
124
- * a member of this list, and nothing else.
125
- *
126
- * Both members are hooks: they exit 0 whether they refuse or allow, because a hook communicates through
127
- * its stdout JSON and not through its status. So their drills declare `exit: 0` and lean entirely on the
128
- * tell — which is why the tell is mandatory rather than a nicety.
129
- *
130
- * `tellStream: "stdout"` for both, measured rather than assumed: `stop-gate` writes git's own
131
- * diagnostics to stderr when it probes an upstream in a detached worktree (`fatal: HEAD does not point
132
- * to a branch`), so a tell read from the combined streams would be read out of noise the rail did not
133
- * choose to say.
134
- */
19
+ /** Hooks answer in their stdout JSON and exit 0 either way, so their drills judge by a tell read from stdout. */
135
20
  export const NON_RECIPE_RAILS = [
136
21
  {
137
22
  id: "stop-gate",
@@ -147,17 +32,10 @@ export const NON_RECIPE_RAILS = [
147
32
  },
148
33
  ];
149
34
 
150
- /**
151
- * The rails this harness does NOT drill, each with the reason.
152
- *
153
- * Printed on every run, green included. The clause's whole subject is the honesty of the word *every*,
154
- * and a scope claim with no carrier is the defect `../.portulan/verify/README.md`'s register was
155
- * already carrying by hand. These are rails; they are simply not rails a local process can force.
156
- */
157
35
  export const NOT_DRILLED = [
158
36
  {
159
37
  rail: "the platform floor — branch protection, the required checks, `enforce_admins`",
160
- why: "forcing it red means a direct push to `main` or a merge of a red pull request: outward, Gated, and the maintainer's. `../.portulan/verify/README.md` records that `enforce_admins` is what stands behind the last inch and that no pull request can demonstrate it about itself.",
38
+ why: "forcing it red means a direct push to `main` or a merge of a red pull request: outward, Gated, and the maintainer's. `../.portulan/gate-map/platform-floor.md` records what `enforce_admins` holds and why the test that would force it past is deliberately not run.",
161
39
  },
162
40
  {
163
41
  rail: "the `permissions` layer the compiler emits",
@@ -176,7 +54,7 @@ export const NOT_DRILLED = [
176
54
  why: "deliberately not a verify recipe — `../.portulan/identity.md` argues why: CI installs nothing, so declaring it would make a recipe that exits 2 on every run. It is checked at the supervised checkpoints and before a release, which is a rail on no calendar, and that cost is already stated in `../.portulan/verify/README.md`.",
177
55
  },
178
56
  {
179
- rail: "Dependabot, the `copilot auto-review` ruleset, and `pr-labels`",
57
+ rail: "Dependabot and `pr-labels`",
180
58
  why: "platform watchers whose triggering condition is an event this repository cannot manufacture on a schedule. `0007`'s own honest limit applies — not every watcher can be forced red safely — and each carries its own recorded observation instead.",
181
59
  },
182
60
  {
@@ -185,39 +63,15 @@ export const NOT_DRILLED = [
185
63
  },
186
64
  ];
187
65
 
188
- /**
189
- * The drills. One per rail, and the harness refuses a yielded rail that has none.
190
- *
191
- * **One table, in this module** — `./mutants.mjs`'s argument, unchanged: a drill is an anchor into a
192
- * file plus the outcome expected of it, and splitting those across code and JSON would make two
193
- * carriers of one drill.
194
- *
195
- * Fields:
196
- * `rail` a yielded recipe id, or a `NON_RECIPE_RAILS` member's id
197
- * `perturb` `{file, find, replace}` — anchored, must place exactly once; or `{create, content}`;
198
- * or `null` for a rail whose control and fire differ by INPUT rather than by tree
199
- * `stage` `git add -A` in the drill worktree before the fire, for a rail that reads the index
200
- * `exit` the status the fire must return
201
- * `tell` a substring the rail's own output must carry when it fires, and must not carry before
202
- * `why` what this drill proves about this rail — not what the perturbation does
203
- *
204
- * **Prefer an in-place edit of a tracked file to a creation.** Measured: `docs.sh` and
205
- * `control-chars.sh` both walk `--cached --others --exclude-standard` and see a new file, while a pass
206
- * reading `--cached` alone does not — so a creation-shaped perturbation is invisible to some rails and
207
- * would read as a rail that did not fire. Where a creation is the only honest shape, `stage` makes it
208
- * visible and says so.
209
- */
66
+ /** A rail that reads only the index misses a created file unless the drill sets `stage`. */
210
67
  export const DRILLS = [
211
68
  // ------------------------------------------------------------------------- the yielded recipes
212
- // (Including the composed one at the end of this section. This read *the eighteen yielded recipes*,
213
- // which was both a count in prose and wrong — the section holds the workspace's own plus the pack's.
214
- // The number has one carrier and it is the runner. Copilot, round 2's notes.)
215
69
  {
216
70
  rail: "docs",
217
71
  perturb: {
218
72
  file: "CONTRIBUTING.md",
219
- find: "- [`docs/plan.md`](docs/plan.md) — the milestones, and the Session log every session appends to.",
220
- replace: "- [`docs/plan.md`](docs/plan-the-drill-broke-this.md) — the milestones, and the Session log every session appends to.",
73
+ find: "- [`docs/plan.md`](docs/plan.md) — the milestones. A change's record is its commit message.",
74
+ replace: "- [`docs/plan.md`](docs/plan-the-drill-broke-this.md) — the milestones. A change's record is its commit message.",
221
75
  },
222
76
  exit: 1,
223
77
  tell: "link(s) that do not resolve in the repository",
@@ -246,35 +100,13 @@ export const DRILLS = [
246
100
  why: "`doctor` resolving a slot path is the check the Workspace Definition rests on: a manifest naming a file that is not there must never validate.",
247
101
  },
248
102
  {
249
- // ---- the SECOND drill on the `doctor` rail, and the reason there are two
250
- //
251
- // `check()` counts a rail as drilled once any drill names it, so a second entry adds coverage
252
- // rather than a duplicate — the DRILLS docblock's "one per rail" is a floor, not a cap. This
253
- // arm needs its own because the one above perturbs a slot path and this one perturbs the gate
254
- // POLICY: different file, different check inside `doctor`, and nothing about the first would
255
- // have noticed if this one stopped firing.
256
- //
257
- // **Why it is a `doctor` drill and not a `compile` one.** The refusal it forces is a
258
- // `CompileError`, so `compile` exits **2** — and `check()` refuses any drill declaring
259
- // `exit: 2`, correctly, because it "would read a refusal as a verdict". `doctor` runs the
260
- // backends inside its own try and turns the same refusal into a `fail`, which is exit 1 and is
261
- // a verdict. Measured on this tree, 2026-09-03: with `edit-on-a-working-branch` flipped to
262
- // `gated`, `compile` exits 2, `goldens` stays 0, and `doctor` exits 1 printing the sentence
263
- // below.
264
103
  rail: "doctor",
265
104
  perturb: {
266
105
  file: ".portulan/gates.json",
267
- // The flip is the whole hazard in one word: this rule's target is `./`, which matches no
268
- // path a host submits, and at `auto` that is harmless — the tier is refused before any
269
- // target is read. At `gated` it becomes a hollow gate, reported compiled and enforcing
270
- // nothing. So the perturbation manufactures exactly the state hole 8 describes, from the
271
- // policy this repository actually ships.
272
106
  find: '"id": "edit-on-a-working-branch",\n "tier": "auto",',
273
107
  replace: '"id": "edit-on-a-working-branch",\n "tier": "gated",',
274
108
  },
275
109
  exit: 1,
276
- // Scoped to the sentence only this refusal prints. `doctor` has other enforcement failures and
277
- // a tell like "enforcement" would be satisfied by any of them.
278
110
  tell: "matches no path a host can submit",
279
111
  why: "A gate whose target can never match is the one defect this repository cannot see by reading a policy: the compiler reports it COMPILED and `doctor` counts it covered, so the gate map, the artifact and the report all agree about a gate that enforces nothing. Hole 8 of the gate map, closed by #337's option 3 — and the only rail that can watch the closure is the one whose exit is a verdict rather than a refusal.",
280
112
  },
@@ -326,16 +158,12 @@ export const DRILLS = [
326
158
  perturb: {
327
159
  file: ".github/workflows/pr-labels.yml",
328
160
  find: "if ! declared=$(jq -er '.labels[].name' \"$POLICY\"); then",
329
- // The program's OUTPUT is changed rather than its shape: dropping a character made the
330
- // fixture's anchor match no program at all, and the rail then exited 2 — a legitimate
331
- // refusal, but the arm this drill is not for. Appending a suffix keeps the anchor findable
332
- // and makes the bytes the surrounding shell branches on differ, which is the exit-1 arm.
333
- // Measured both ways before this line was written.
161
+ // Appends, so the fixture's anchor still finds the program and the rail exits 1 rather than 2.
334
162
  replace: "if ! declared=$(jq -er '.labels[].name + \"-drilled\"' \"$POLICY\"); then",
335
163
  },
336
164
  exit: 1,
337
165
  tell: "fixture(s) failed",
338
- why: "The jq programs the merge gates branch on are executed rather than described. A filter that silently changed shape would take the label gate down with it.",
166
+ why: "The jq programs the label gate branches on are executed rather than described. A filter that silently changed shape would take the label gate down with it.",
339
167
  },
340
168
  {
341
169
  rail: "index",
@@ -348,19 +176,63 @@ export const DRILLS = [
348
176
  tell: "is out of date against the store",
349
177
  why: "The index is generated and byte-compared, which is the only thing that makes `core/operating/memory.md`'s *generated, never hand-maintained* a fact rather than a preference.",
350
178
  },
179
+ {
180
+ rail: "context",
181
+ perturb: {
182
+ file: "core/engine.md",
183
+ find: "# Portulan engine",
184
+ // 408 bytes: over the engine rail's headroom, under the boot rails', so only the engine rail fires.
185
+ replace: `# Portulan engine${" (moved back from an on-read file)".repeat(12)}`,
186
+ },
187
+ exit: 1,
188
+ tell: "over its rail of",
189
+ why: "Every boot in every adopting repository reads the kernel and the boot skill in full, so a demotion undone there costs every context everywhere. The rail is the only thing that turns that growth from a quiet diff into a red.",
190
+ },
191
+ {
192
+ rail: "ledger",
193
+ perturb: {
194
+ file: "cli/ledger.mjs",
195
+ find: "const prior = request.id === null ? undefined : byId.get(request.id);",
196
+ replace: "const prior = undefined;",
197
+ },
198
+ exit: 1,
199
+ tell: "does not reproduce",
200
+ why: "The host writes one usage record per content block, so a reader that stops deduplicating by message id multiplies every figure by the blocks per request and still prints plausible numbers. The fixture's known totals are the only thing that tells those numbers from the true ones.",
201
+ },
202
+ {
203
+ rail: "ledger",
204
+ perturb: {
205
+ file: "cli/ledger.mjs",
206
+ find: "if (!calls.has(result.call)) continue;",
207
+ replace: "if (result.call === null) continue;",
208
+ },
209
+ exit: 1,
210
+ tell: "✗ fixture: local is",
211
+ why: "A local worker's subagent reports its receipt back to the session that delegated it, so a reader that takes a receipt from any tool result counts the same local work twice, once from the Bash call that ran it and once from the report quoting it, and the hybrid line still reads like a figure. Only the hybrid fixture's known receipt count tells the two apart.",
212
+ },
351
213
  {
352
214
  rail: "control-chars",
353
215
  perturb: {
354
216
  file: "CONTRIBUTING.md",
355
217
  find: "# Contributing",
356
- // A real NUL, written as an escape so THIS file carries no control byte of its own. Session 0
357
- // shipped a literal NUL as a literal character while writing prose about storing bytes escaped.
218
+ // A real NUL, written as an escape so this file carries no control byte of its own.
358
219
  replace: "# Contributing\u0000",
359
220
  },
360
221
  exit: 1,
361
222
  tell: "control character(s) — first at line",
362
223
  why: "A raw NUL shipped here once inside a template literal, and the tool most likely to have shown it — `grep` — is the tool the byte silences. Only a byte reader can fire on this, so only a byte reader's silence would hide it.",
363
224
  },
225
+ {
226
+ rail: "comments",
227
+ perturb: {
228
+ file: "cli/comments.mjs",
229
+ find: 'const figure = (n) => n.toLocaleString("en-US");',
230
+ replace: 'const figure = (n) => n.toLocaleString("en-US"); // Added 2026-09-24.',
231
+ },
232
+ exit: 1,
233
+ tell: "is over the limit of",
234
+ why: "A comment carrying a change's history is read again by every session that opens its file, and a model matching the surrounding code copies it. Only a reader of the comments themselves can tell one from the code around it.",
235
+ },
364
236
  {
365
237
  rail: "rule-carriers",
366
238
  perturb: {
@@ -387,8 +259,8 @@ export const DRILLS = [
387
259
  rail: "pack-identity",
388
260
  perturb: {
389
261
  file: "README.md",
390
- find: "**Current release: `0.1.3`**",
391
- replace: "**Current release: `0.1.2`** <!-- the drill edited this and did not stage it -->",
262
+ find: "**Current release: `0.3.0`**",
263
+ replace: "**Current release: `0.1.3`** <!-- the drill edited this and did not stage it -->",
392
264
  },
393
265
  stage: false,
394
266
  exit: 1,
@@ -413,10 +285,7 @@ export const DRILLS = [
413
285
  "",
414
286
  ].join("\n"),
415
287
  },
416
- // **NOT staged, and that is the drill's second subject.** `npm pack` reads the WORKING TREE, so an
417
- // untracked module ships — measured before this drill was written. A drill that staged first would
418
- // pass while leaving the likelier arrival untested: a module written, packed and published before
419
- // anyone committed it. `pack-identity` above is the mirror case and stages for the opposite reason.
288
+ // Not staged: `npm pack` reads the working tree, so an untracked module ships too.
420
289
  stage: false,
421
290
  exit: 1,
422
291
  tell: "SHIPS and is classified by nothing",
@@ -474,14 +343,10 @@ export const DRILLS = [
474
343
  rail: "version-carriers",
475
344
  perturb: {
476
345
  file: "README.md",
477
- find: "**Current release: `0.1.3`**",
478
- replace: "**Current release: `0.2.0`**",
346
+ find: "**Current release: `0.3.0`**",
347
+ replace: "**Current release: `0.1.3`**",
479
348
  },
480
- // **Staged, because this rail reads the INDEX** — `git show :<path>`, the same way
481
- // `pack-identity` does and for the reason that module states: enumerating from the index and
482
- // then reading the worktree lets a staged drift with a reverted worktree copy report green.
483
- // Measured: unstaged, the perturbation is invisible and the rail reports its ordinary green,
484
- // which would have read as a rail that stopped firing.
349
+ // Staged: this rail reads the index (`git show :<path>`), so an unstaged edit is invisible to it.
485
350
  stage: true,
486
351
  exit: 1,
487
352
  tell: "but package.json declares",
@@ -491,15 +356,6 @@ export const DRILLS = [
491
356
  rail: "skill-goldens",
492
357
  perturb: {
493
358
  file: "core/skills/clarify/SKILL.md",
494
- // **The perturbation ADDS A MANDATE**, which puts this rail's central claim under test: the
495
- // denominator is derived from the skill's own pass, so a step nobody wrote a case for is a
496
- // red rather than a default. A corpus whose author picks which mandates to answer for
497
- // reports "5 of 5" of a set it drew, and that is the version this drill exists to refuse.
498
- //
499
- // **Rewording a mandate was the first spelling and it is the wrong arm.** An anchor that
500
- // stops placing is could-not-run, and this harness refuses `exit: 2` as a fire outright —
501
- // *"counting it as a fire would read a refusal as a verdict"*. Measured rather than
502
- // reasoned about: `--check` rejected the first version of this entry for exactly that.
503
359
  find: "## Why it earns its tokens",
504
360
  replace: "5. **A step the drill added** — bound to nothing, answered by no case.\n\n## Why it earns its tokens",
505
361
  },
@@ -516,10 +372,6 @@ export const DRILLS = [
516
372
  },
517
373
  exit: 1,
518
374
  tell: "is out of date against the snapshot",
519
- // The perturbation edits the REGISTER rather than the snapshot, and that is the direction the
520
- // rail exists for. Editing the snapshot would make both sides move together in a rerun and
521
- // prove nothing; editing the published figures is the failure the recipe is there to catch,
522
- // since a committed Markdown table is the one artifact here a person can quietly correct.
523
375
  why: "The register is the review loop's published figures, and this clause exists because the figures it replaces were hand-maintained. A register that could drift from its own snapshot would be that hand-maintained tally wearing a generated file's clothes.",
524
376
  },
525
377
  {
@@ -531,16 +383,6 @@ export const DRILLS = [
531
383
  },
532
384
  exit: 1,
533
385
  tell: "invokes cli/review-meter.mjs --fetch",
534
- // **The perturbation is a network mode reaching a recipe, which is the rail's actual subject —
535
- // not a drifted payload, which would have been the cheaper drill.** Byte-drift is already the
536
- // shape `review-loop` and `index` drill; what nothing else here watches is the offline
537
- // property, so that is what this forces.
538
- //
539
- // And it forces it through the mode this session did NOT introduce. `--fetch` predates the
540
- // telemetry clause by a session, so a drill that fired on `--export` would prove only that the
541
- // checker recognises its author's own module — while the whole argument for railing a class
542
- // rather than a file is that the OLDER network mode was the unrailed one. A drill on the new
543
- // mode would have passed while leaving exactly the gap the class exists to close.
544
386
  why: "A verify recipe that can reach the network goes red about the world rather than about the tree, and this repository prohibits it in three carriers while nothing checked it. The drill proves the checker sees a real invocation — spread across continued lines, the spelling a recipe actually uses — rather than only the literal string a test would hand it.",
545
387
  },
546
388
  {
@@ -559,13 +401,6 @@ export const DRILLS = [
559
401
  ].join("\n"),
560
402
  },
561
403
  exit: 1,
562
- // **The perturbation ADDS a file rather than editing the table, and that is the rail's actual
563
- // subject.** Deleting a disposition would fire the *stale declaration* arm — a legitimate
564
- // refusal, and exit 2, which is the arm this drill is not for. What the rail exists to catch is
565
- // the opposite direction: a file arriving in `.portulan/` that nobody classified, which is
566
- // precisely how customer zero's `memory-index.md`, `handoffs-index.md`, `rule-carriers.json`
567
- // and `labels.json` reached an arm built to `evals/ab/arm.md`'s six rows with `doctor` GREEN
568
- // over all of it. The drill reproduces the arrival, not the forgetting.
569
404
  tell: "classified by no disposition",
570
405
  why:
571
406
  "The A/B treatment arm is built by removing things from customer zero's workspace, and `cli/vendor.mjs` carries " +
@@ -577,16 +412,6 @@ export const DRILLS = [
577
412
  rail: "ab-grade",
578
413
  perturb: {
579
414
  file: "cli/ab-grade.mjs",
580
- // **The perturbation makes a grader a CONSTANT, which is the rail's actual subject.** Drifting
581
- // the register would have been the cheaper drill and would have proved only the byte compare —
582
- // a shape `review-loop`, `index` and `ab` already force. What nothing else here watches is a
583
- // grader that has stopped reading the arm, and that is the failure this instrument was built
584
- // around: the prototype returned "30 records, each with its own provenance and retirement
585
- // condition" against an arm that had done nothing at all.
586
- //
587
- // It is planted in `gradeCuratedLayer` deliberately — the prototype's own scenario family —
588
- // and it is an early `return` rather than an edited branch, so it fires whatever the delta,
589
- // which is what a constant is.
590
415
  find: ' const scenario = "curated-layer";\n const anchor = anchored(root, scenario, nonce);',
591
416
  replace:
592
417
  ' const scenario = "curated-layer";\n' +
@@ -594,10 +419,6 @@ export const DRILLS = [
594
419
  " const anchor = anchored(root, scenario, nonce);",
595
420
  },
596
421
  exit: 1,
597
- // The tell is level 2's, not level 1's. A constant about the base ALSO fails level 1 and
598
- // inertness here, and naming one of those would let a later edit that fixed only the cheap half
599
- // pass the drill — while the property this rail is for is that the inverted fixture cannot be
600
- // inverted by a grader that ignores the delta.
601
422
  tell: "did not invert the figures",
602
423
  why:
603
424
  "A grader that has stopped reading the arm produces a baseline about file copying, and it does so silently: the " +
@@ -609,25 +430,10 @@ export const DRILLS = [
609
430
  rail: "release-eval",
610
431
  perturb: {
611
432
  file: "cli/release-eval.mjs",
612
- // **The perturbation moves the CLAUSE BOUNDARY, which is the rail's actual subject.**
613
- // Editing a committed record would have been the cheaper drill and would have proved only
614
- // the byte compare — a shape `review-loop`, `index`, `ab` and `ab-run` already force. What
615
- // nothing else here watches is the check this clause exists for: *a release the clause
616
- // governs, carrying no eval result*. Every governed release this repository has cut carries its
617
- // record, so that arm is unreachable from the committed tree — until the boundary moves. Dropping it
618
- // to `0.1.2` makes an already-cut release governed, and the rail must then say the record
619
- // is missing.
620
- //
621
- // It is the boundary rather than a record because a boundary that silently widened is the
622
- // failure mode with teeth: a release would be graded that nobody captured for, or — the
623
- // direction that matters — one that should be graded would fall outside and the rail would
624
- // report green having looked at nothing.
625
433
  find: 'export const FIRST_GOVERNED_VERSION = "0.1.3";',
626
434
  replace: 'export const FIRST_GOVERNED_VERSION = "0.1.2";',
627
435
  },
628
436
  exit: 1,
629
- // The tell is the missing-record arm specifically, not the boundary arithmetic. A fix that
630
- // restored only the constant while leaving the presence check broken would pass a looser tell.
631
437
  tell: "is a release from milestone 8 onward and there is no",
632
438
  why:
633
439
  "Milestone 8's ninth clause is that a release carries an eval result, and the check that carries it is *a governed " +
@@ -641,23 +447,10 @@ export const DRILLS = [
641
447
  rail: "ab-run",
642
448
  perturb: {
643
449
  file: "evals/ab/baseline.json",
644
- // **The perturbation edits a PUBLISHED FIGURE, which is the rail's actual subject.** The
645
- // baseline is the one record here that cannot be re-derived — its events do not repeat — so
646
- // what holds it honest is that the register agrees with the capture beside it, and that the
647
- // capture folds to the figures it publishes. Editing a cell is exactly the drift
648
- // `review-loop.sh` names: *"a published figure that can drift from its own data is the
649
- // hand-maintained tally in a new costume"*.
650
- //
651
- // A drifted REGISTER would have been the cheaper drill and would prove only the byte
652
- // compare, which `review-loop`, `index` and `ab-grade` already force between them. This
653
- // forces the arithmetic instead.
654
450
  find: '"operatorEnv": "isolated"',
655
451
  replace: '"operatorEnv": "inherit"',
656
452
  },
657
453
  exit: 1,
658
- // The tell is the isolation clause, not the byte compare: a baseline recorded over an unisolated
659
- // arm is the one thing `evals/ab/corpus.md` forbids outright, and a repair that fixed only the
660
- // rendering would leave that hole open.
661
454
  tell: "no baseline may be recorded under an unisolated arm",
662
455
  why:
663
456
  "A baseline is the only record in this repository derived from events rather than from the tree, so it cannot be " +
@@ -676,20 +469,24 @@ export const DRILLS = [
676
469
  tell: "is a tag or branch, not a commit",
677
470
  why: "The composed pack's own rail, drilled from the adopting workspace exactly as a workspace-owned one is — which is the property milestone 7's composition amendment bought and nothing else here re-checks.",
678
471
  },
472
+ {
473
+ rail: "tools/local-llm:runner-rails",
474
+ perturb: {
475
+ file: "packs/tools/local-llm/runner/local-llm.mjs",
476
+ find: " if (!allowRemote && !LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) {",
477
+ replace: " if (!allowRemote && false) {",
478
+ },
479
+ exit: 1,
480
+ tell: "non-loopback endpoint was not refused by assertLoopback",
481
+ why: "Loopback is the rail that keeps a local worker's context on the machine, and the runner holds it in code rather than in any permission rule the compiler emits, so a guard that stopped refusing would leave every other rail green. The drill removes the refusal and requires the pack's own recipe to see it.",
482
+ },
679
483
 
680
484
  // ------------------------------------------------------- the coverage rail, which is itself a rail
681
485
  {
682
486
  rail: "drills",
683
487
  perturb: {
684
488
  file: "cli/drills.mjs",
685
- // **The perturbation is ADDITIVE and the anchor is CONCATENATED, and neither is a style
686
- // choice.** A drill perturbing its own module meets two traps, and the first draft met
687
- // both. Renaming an existing declaration destroys the very anchor some drill depends on —
688
- // measured: the child `--check` then exited 2 saying the drill no longer places, instead
689
- // of 1 reporting the coverage hole. And an anchor written as one literal would place
690
- // TWICE, once at its target and once in this line, so `--check` would refuse the whole
691
- // roster as ambiguous. Splitting the literal keeps this line from being what it searches
692
- // for; inserting rather than editing leaves every other anchor standing.
489
+ // Split so this line is not its own anchor; additive so every other anchor still places.
693
490
  find: "export const DRILL" + "S = [",
694
491
  replace:
695
492
  "export const DRILL" +
@@ -703,41 +500,22 @@ export const DRILLS = [
703
500
  // ------------------------------------------------------------------------- the two non-recipe rails
704
501
  {
705
502
  rail: "stop-gate",
706
- // The same perturbation the `docs` drill uses, because `docs` is this workspace's DEFAULT recipe
707
- // and the default is what this hook runs. The tell is scoped to the recipe reason on purpose:
708
- // this gate also blocks for a missing same-day handoff, and whether it does depends on the
709
- // calendar date and on whether HEAD's patches are on a remote — measured at the session-open
710
- // checkpoint, which saw the identical tree allow on one date and block on the next. A drill whose
711
- // control demanded *no block at all* would therefore be green or red by the day of the week.
712
503
  perturb: {
713
504
  file: "CONTRIBUTING.md",
714
- find: "- [`docs/plan.md`](docs/plan.md) — the milestones, and the Session log every session appends to.",
715
- replace: "- [`docs/plan.md`](docs/plan-the-drill-broke-this.md) — the milestones, and the Session log every session appends to.",
505
+ find: "- [`docs/plan.md`](docs/plan.md) — the milestones. A change's record is its commit message.",
506
+ replace: "- [`docs/plan.md`](docs/plan-the-drill-broke-this.md) — the milestones. A change's record is its commit message.",
716
507
  },
717
508
  stdin: {
718
509
  hook_event_name: "Stop",
719
- // **A session id unique per run**, completed by `runRail` from the worktree's own name. The
720
- // constant this replaced was harmless and not obviously so, which is the reason it moved:
721
- // `stop-gate`'s counter file is keyed by session id **and** by the tree, and the tree is a
722
- // fresh `mkdtemp` every drill, so two runs could not share a counter either way. Measured
723
- // at the pre-commit checkpoint. Relying on that is relying on another module's key, and the
724
- // prefix below is also what lets this harness retire the counter file it leaves behind.
725
510
  session_id: DRILL_SESSION_PREFIX,
726
511
  },
727
512
  exit: 0,
513
+ // The recipe reason only: whether this hook also blocks for a missing handoff depends on the date.
728
514
  tell: "PORTULAN STOP-GATE (recipe",
729
515
  why: "This gate is what makes a red unmissable rather than merely recorded. It fails open on a crash by design, so a crashed Stop-gate and a green one are indistinguishable from outside — and only a drill that requires the refusal can tell them apart.",
730
516
  },
731
517
  {
732
518
  rail: "gate",
733
- // No perturbation: this rail's control and fire differ by INPUT. The control is a benign command
734
- // and the fire is a gated one.
735
- //
736
- // **The control is byte-identical to this runner having crashed**, and that is worth knowing
737
- // rather than fixing: `./gate.mjs` fails open deliberately — any internal error exits 0 emitting
738
- // nothing, so the permission layer governs unchanged. Empty stdout therefore means *allowed* or
739
- // *crashed*, indistinguishably. What separates them is the FIRE: a crashing runner is silent on
740
- // both inputs, so the pair is the oracle even though neither half is.
741
519
  perturb: null,
742
520
  stdinControl: { tool_name: "Bash", tool_input: { command: "git status --short" } },
743
521
  stdin: { tool_name: "Bash", tool_input: { command: "git push --force origin main" } },
@@ -747,35 +525,13 @@ export const DRILLS = [
747
525
  },
748
526
  ];
749
527
 
750
- /** The digest used to prove a perturbation moved bytes. Not security — just enough to differ. */
751
528
  const digest = (buffer) => crypto.createHash("sha256").update(buffer).digest("hex");
752
529
 
753
- /**
754
- * Apply a drill's perturbation inside `worktree`.
755
- *
756
- * Refuses — could-not-run, never a skip — when the anchor is absent, when it places more than once, or
757
- * when the bytes on disk did not move. The third is not redundant with the first two: both hand sessions
758
- * had a drill whose substitution placed and changed nothing that mattered, and `count === 1` alone would
759
- * have passed a replacement identical to what it replaced.
760
- */
530
+ /** Throws `CouldNotRun`, never skips, when the anchor is absent or ambiguous or the bytes do not move. */
761
531
  export function perturb(worktree, drill) {
762
532
  if (!drill.perturb) return null;
763
533
 
764
- /**
765
- * Refuse a target that does not stay inside the worktree, **before anything is written**.
766
- *
767
- * The isolation guarantee is the whole reason a drill runs in a throwaway tree, and nothing checked
768
- * it: a `..` in a declaration, or a symlinked parent in the checkout, would have let `writeFileSync`
769
- * reach the caller's own repository — the tree this harness promises never to perturb. `isInside` is
770
- * the one carrier of the predicate and `realpathSync` is what makes it answer about the tree rather
771
- * than about two spellings of it; for a path that does not exist yet the nearest existing ancestor
772
- * is resolved instead, since that is what a write would actually follow. Copilot round 2.
773
- */
774
534
  const contained = (rel) => {
775
- // The worktree side is resolved tolerantly too. `realpathSync` on a directory that is not there
776
- // throws a bare ENOENT, and an unguarded throw here would arrive as *could not finish the sweep*
777
- // with a stack trace instead of a sentence — the first version did exactly that, and its own test
778
- // caught it.
779
535
  let root;
780
536
  try {
781
537
  root = fs.realpathSync(worktree);
@@ -787,20 +543,7 @@ export function perturb(worktree, drill) {
787
543
  }
788
544
  const target = path.resolve(worktree, rel);
789
545
 
790
- // **A symlink AT THE TARGET is refused outright, and this is the sibling the first containment
791
- // fix left open.** `writeFileSync` follows a link, and the resolve loop below skips a *broken*
792
- // leaf up to its parent — so a target that is itself a dangling symlink pointing outside passed
793
- // both checks and the write landed outside the worktree. Measured at the final checkpoint, which
794
- // produced a file called `outside/ghost.txt` holding `PWNED`.
795
- //
796
- // `lstat`, not `stat`, because the question is whether *this name* is a link rather than what it
797
- // points at — and it catches a live link and a dangling one with the same test. A drill has no
798
- // business perturbing through a link in either case: the tree it is given is a fresh `git
799
- // worktree` of this repository, which commits none.
800
- //
801
- // The enumerated vectors — `..`, an absolute path, a symlinked *parent* — were all closed by the
802
- // first fix, and this was not one of them. That is `0020` at its most literal: the repair held
803
- // for the spellings its own docblock listed.
546
+ // The realpath loop below skips past a dangling link, so a link at the target is refused here.
804
547
  let leaf = null;
805
548
  try {
806
549
  leaf = fs.lstatSync(target);
@@ -825,8 +568,7 @@ export function perturb(worktree, drill) {
825
568
  probe = parent;
826
569
  }
827
570
  }
828
- // The resolved ancestor must be inside, and so must the unresolved remainder — the first stops a
829
- // symlink hop, the second stops a `..` that never touches an existing directory.
571
+ // The first stops a symlink hop, the second a `..` that never touches an existing directory.
830
572
  if (!isInside(root, probe) || !isInside(path.resolve(worktree), target)) {
831
573
  throw new CouldNotRun(
832
574
  `drill \`${drill.rail}\` names ${JSON.stringify(rel)}, which resolves outside the drill worktree. ` +
@@ -895,14 +637,7 @@ export function perturb(worktree, drill) {
895
637
  return { path: rel, before, after };
896
638
  }
897
639
 
898
- /**
899
- * The yielded recipe set, at a PINNED root, with host discovery refused.
900
- *
901
- * The pin is the same argument `./goldens.sh` and `./compile.sh` state at length: a rail answers *does
902
- * this tree hold its own claims*, so its answer may not move with what happens to be installed on the
903
- * machine running it. `discovery: null` and `forced: false` are the second carrier of that, so a stray
904
- * `--pack-root auto` on the command line cannot reach the host either.
905
- */
640
+ /** Never consults the host for packs, so the set cannot move with what happens to be installed. */
906
641
  export function yieldedRecipes({ workspaceDir, repoRoot, packRoots }) {
907
642
  let manifest;
908
643
  try {
@@ -921,24 +656,13 @@ export function yieldedRecipes({ workspaceDir, repoRoot, packRoots }) {
921
656
  return set.recipes;
922
657
  }
923
658
 
924
- /**
925
- * Validate the whole table against a tree. Returns findings; throws `CouldNotRun` for a drill whose
926
- * anchor no longer places, because that is `./mutants.mjs`'s ruling on the same condition: an anchor
927
- * that has drifted must refuse rather than be skipped.
928
- *
929
- * Runs in **every** mode, `--only` included.
930
- */
659
+ /** Returns findings; throws `CouldNotRun` for an anchor that does not place exactly once. */
931
660
  export function check({ recipes, repoRoot, drills = DRILLS }) {
932
661
  const findings = [];
933
662
  const recipeIds = new Set(recipes.map((r) => r.id));
934
663
  const nonRecipeIds = new Set(NON_RECIPE_RAILS.map((r) => r.id));
935
664
  const drilled = new Set();
936
665
 
937
- // **A yielded recipe sharing an id with a non-recipe rail is refused**, because the sweep's lookup
938
- // map spreads the hooks after the recipes: a workspace recipe called `gate` would be silently
939
- // shadowed by the hook, `check` would count that id as drilled, and the real recipe would never run
940
- // while the sweep reported green. Nothing in `spec/workspace.schema.json` reserves these two slugs,
941
- // so the refusal belongs here. Copilot round 1.
942
666
  for (const rail of NON_RECIPE_RAILS) {
943
667
  if (recipeIds.has(rail.id)) {
944
668
  findings.push({
@@ -950,9 +674,6 @@ export function check({ recipes, repoRoot, drills = DRILLS }) {
950
674
  }
951
675
  }
952
676
 
953
- // `drills` is a seam for the suite and never a selection: the CLI never passes it, so the whole
954
- // declared table is always what gets validated. It exists because a guard nothing can exercise
955
- // positively is a guard nobody has seen work — this module's own subject, one altitude up.
956
677
  for (const drill of drills) {
957
678
  const where = `drill \`${drill.rail}\``;
958
679
  if (!recipeIds.has(drill.rail) && !nonRecipeIds.has(drill.rail)) {
@@ -966,11 +687,6 @@ export function check({ recipes, repoRoot, drills = DRILLS }) {
966
687
  }
967
688
  drilled.add(drill.rail);
968
689
 
969
- // **A hook drill's session id must be the prefix this harness owns.** The completion in
970
- // `runRail` and the retirement in `drillOne` both key on it, so a drill inventing its own id
971
- // would get neither: a counter shared between runs and a file nobody cleans. That is a rail
972
- // with a precondition nobody states, refused here rather than left as a comment. Raised at the
973
- // pre-commit checkpoint's second pass, as a trap the fold had left open.
974
690
  if (nonRecipeIds.has(drill.rail) && drill.stdin?.session_id !== undefined && drill.stdin.session_id !== DRILL_SESSION_PREFIX) {
975
691
  findings.push({
976
692
  where,
@@ -987,13 +703,6 @@ export function check({ recipes, repoRoot, drills = DRILLS }) {
987
703
  if (typeof drill.why !== "string" || drill.why.length === 0) {
988
704
  findings.push({ where, what: "declares no `why`. A drill with no stated claim is a perturbation nobody can review" });
989
705
  }
990
- // **0 or 1, never 2.** Exit 2 is reserved throughout this repository for *could not run*, so a
991
- // drill declaring it would count a rail that could not be judged as a rail that fired — the exact
992
- // inversion `../.portulan/memory/verify-preconditions-fail-closed.md` exists to prevent, inside
993
- // the harness built to detect it. A rail whose only non-green arm IS a refusal needs an argument
994
- // and a different mechanism, not a declaration that quietly reads a refusal as a fire; the
995
- // `workflow-filters` drill was drafted that way and was rewritten to force the exit-1 arm
996
- // instead. Copilot round 1.
997
706
  if (drill.exit !== 0 && drill.exit !== 1) {
998
707
  findings.push({
999
708
  where,
@@ -1002,15 +711,7 @@ export function check({ recipes, repoRoot, drills = DRILLS }) {
1002
711
  "1 (a red); 2 is could-not-run everywhere here, and counting it as a fire would read a refusal as a verdict",
1003
712
  });
1004
713
  }
1005
- // A drill whose control and fire are the same run proves nothing at all. One of the two must
1006
- // differ: the tree, or the input.
1007
- //
1008
- // **The input half is only available to a non-recipe rail, and the test for it was three kinds of
1009
- // weak.** `stdinControl !== undefined` did not ask whether it *differs*; `stdinControl: null`
1010
- // falls back to `stdin` through `drillOne`'s `??`, so it is the same run wearing a different
1011
- // declaration; and `runRail` passes stdin to `rail.argv` only — a **recipe** rail ignores it
1012
- // entirely, so a recipe drill with no perturbation could satisfy this and never change a thing.
1013
- // Raised as a suppressed note by Copilot, round 1.
714
+ // A null `stdinControl` falls back to `stdin` in `drillOne`, so it does not count as differing input.
1014
715
  const inputCanDiffer =
1015
716
  nonRecipeIds.has(drill.rail) &&
1016
717
  drill.stdinControl !== undefined &&
@@ -1026,8 +727,6 @@ export function check({ recipes, repoRoot, drills = DRILLS }) {
1026
727
  });
1027
728
  }
1028
729
 
1029
- // The anchor is checked against the WORKING TREE here, which is the whole value of `--check`
1030
- // running on a pull request: the commit that moves an anchored line is the one that learns it.
1031
730
  if (drill.perturb?.file !== undefined) {
1032
731
  const target = path.join(repoRoot, drill.perturb.file);
1033
732
  let source;
@@ -1082,7 +781,6 @@ export function check({ recipes, repoRoot, drills = DRILLS }) {
1082
781
  return findings;
1083
782
  }
1084
783
 
1085
- /** `git`, run for its stdout, with a could-not-run on failure rather than a throw nobody can read. */
1086
784
  function git(args, { cwd }) {
1087
785
  const result = spawnSync("git", args, { cwd, encoding: "utf8" });
1088
786
  if (result.error) throw new CouldNotRun(`git ${args[0]} could not run — ${result.error.message}`);
@@ -1092,18 +790,6 @@ function git(args, { cwd }) {
1092
790
  return result.stdout;
1093
791
  }
1094
792
 
1095
- /**
1096
- * Which commit the sweep drills, and the refusals that keep the answer honest.
1097
- *
1098
- * A worktree is a commit, so this tool reports on a commit. Two modes:
1099
- * default `HEAD`, and a dirty tree is refused rather than silently drilled around
1100
- * --working-copy a commit synthesized from the working copy by `git stash create`
1101
- *
1102
- * `stash create` carries staged additions and unstaged edits to tracked files and does **not** carry
1103
- * untracked-and-unstaged files — measured at the session-open checkpoint. So that mode refuses while any
1104
- * exist, naming them: drilling a tree that is missing the file under review is the wrong-tree green this
1105
- * whole design is arranged against.
1106
- */
1107
793
  export function treeToDrill({ repoRoot, workingCopy }) {
1108
794
  const head = git(["rev-parse", "HEAD"], { cwd: repoRoot }).trim();
1109
795
  const dirty = git(["status", "--porcelain"], { cwd: repoRoot }).trim();
@@ -1141,11 +827,8 @@ export function treeToDrill({ repoRoot, workingCopy }) {
1141
827
  return { sha: synthesized, kind: "a commit synthesized from the working copy" };
1142
828
  }
1143
829
 
1144
- /** Run one rail in one tree. Returns `{status, stdout, stderr}`. */
1145
830
  function runRail({ rail, worktree, stdin, workspaceRel }) {
1146
831
  if (rail.argv) {
1147
- // The id is completed here rather than in the declaration, so it is distinct per worktree and
1148
- // still carries the prefix this harness cleans up under.
1149
832
  const payload =
1150
833
  stdin?.session_id === DRILL_SESSION_PREFIX
1151
834
  ? { ...stdin, session_id: `${DRILL_SESSION_PREFIX}-${path.basename(worktree)}` }
@@ -1153,31 +836,16 @@ function runRail({ rail, worktree, stdin, workspaceRel }) {
1153
836
  const result = spawnSync(process.execPath, rail.argv, {
1154
837
  cwd: worktree,
1155
838
  encoding: "utf8",
1156
- // **The enforced fields come LAST**, and the order is a finding rather than a style: spread
1157
- // first, a drill's own `cwd` silently overrode it, and `./stop-gate.mjs` resolves the session
1158
- // tree from that field — so drill data could have pointed a control or a fire at another
1159
- // repository entirely while the transcript said the worktree. The harness owns the execution
1160
- // tree, never the declaration. Copilot round 1.
839
+ // After the spread, so a drill's own `cwd` cannot point the hook at another tree.
1161
840
  input: `${JSON.stringify({ ...payload, cwd: worktree })}\n`,
1162
841
  timeout: RAIL_TIMEOUT_MS,
1163
- // The hooks are TOLD their project root rather than deriving it, so a drill that did not set
1164
- // this would grade the repository this session is in and not the throwaway worktree.
1165
- //
1166
- // **And `PORTULAN_WORKSPACE` is set rather than inherited.** Both hooks read
1167
- // `process.env.PORTULAN_WORKSPACE || ".portulan"`, so with `--workspace` naming anything else
1168
- // — or with that variable merely present in the ambient environment — the sweep enumerated one
1169
- // workspace's recipes while the hooks read another workspace's policy. A rail graded against a
1170
- // policy the run did not choose is a verdict about the machine. `run` refuses a workspace that
1171
- // is not inside the repository, so this relative spelling always resolves in the worktree.
1172
- // Copilot round 1.
842
+ // Set, never inherited: the hooks read their tree and workspace from these two.
1173
843
  env: { ...process.env, CLAUDE_PROJECT_DIR: worktree, PORTULAN_WORKSPACE: workspaceRel },
1174
844
  });
1175
845
  if (result.error) throw new CouldNotRun(`rail \`${rail.id}\` could not run — ${result.error.message}`);
1176
846
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1177
847
  }
1178
- // A recipe's `run` is a shell string in the manifest — `bash -c` is how CI runs it and how the
1179
- // Stop-gate runs it, so it is how a drill must run it. Nothing from a drill's declarations reaches
1180
- // this command line: the string comes from the yielded set and the perturbation is applied to a file.
848
+ // `bash -c`, as CI and the Stop-gate run a recipe; no drill declaration may reach this command line.
1181
849
  const result = spawnSync("bash", ["-c", rail.run], {
1182
850
  cwd: worktree,
1183
851
  encoding: "utf8",
@@ -1187,17 +855,8 @@ function runRail({ rail, worktree, stdin, workspaceRel }) {
1187
855
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1188
856
  }
1189
857
 
1190
- /** The text a tell is looked for in — see `NON_RECIPE_RAILS` for why one rail reads stdout alone. */
1191
858
  const tellText = (rail, out) => (rail.tellStream === "stdout" ? out.stdout : `${out.stdout}${out.stderr}`);
1192
859
 
1193
- /**
1194
- * The part of a rail's output a reader needs, for a message that has to be legible in a CI log.
1195
- *
1196
- * **The finding-shaped lines first, and that is a defect this tool shipped once.** The first version
1197
- * printed the last twelve lines, and this repository's recipes end with a long list of `ok` lines — so
1198
- * the one `FAIL` that explained everything was scrolled off the top of the very diagnostic written to
1199
- * explain it. A reader then has to re-run the rail by hand to find out what it said.
1200
- */
1201
860
  const salient = (text, lines = 12) => {
1202
861
  const all = text.split("\n").filter((l) => l.trim() !== "");
1203
862
  const findings = all.filter((l) => /^\s*(FAIL|RED|✗|✖|not ok|UNPINNED)\b/.test(l) || /^RED —/.test(l));
@@ -1205,17 +864,10 @@ const salient = (text, lines = 12) => {
1205
864
  return [...new Set(chosen)].map((l) => ` | ${l}`).join("\n");
1206
865
  };
1207
866
 
1208
- /**
1209
- * Drill one rail: a pristine control, then the perturbation, then the fire.
1210
- *
1211
- * Returns a finding or `null`. Throws `CouldNotRun` when no verdict can be formed — a control that is
1212
- * already red, a tell already present before the perturbation, an anchor that will not place.
1213
- */
867
+ /** A finding, or null when the rail fired as recorded; throws `CouldNotRun` when no verdict can be formed. */
1214
868
  export function drillOne({ drill, rail, repoRoot, sha, say, workspaceRel = ".portulan" }) {
1215
869
  const worktree = fs.mkdtempSync(path.join(os.tmpdir(), "portulan-drill-"));
1216
- // `mkdtemp` creates the directory and `git worktree add` insists on creating it itself, so the
1217
- // reservation is made and then handed back — which is still the right order: it is what guarantees
1218
- // no two concurrent runs pick one path.
870
+ // `git worktree add` creates the directory itself; `mkdtemp` only reserved a unique name for it.
1219
871
  fs.rmSync(worktree, { recursive: true, force: true });
1220
872
  try {
1221
873
  git(["worktree", "add", "--detach", worktree, sha], { cwd: repoRoot });
@@ -1239,11 +891,6 @@ export function drillOne({ drill, rail, repoRoot, sha, say, workspaceRel = ".por
1239
891
 
1240
892
  const fire = runRail({ rail, worktree, stdin: drill.stdin, workspaceRel });
1241
893
  const text = tellText(rail, fire);
1242
- // **A status that is not a verdict is could-not-run, never a rail that failed to fire.**
1243
- // `spawnSync` reports `status: null` for a signal-killed child — a timeout, an OOM — and 2 is
1244
- // could-not-run everywhere here. Both mean no verdict was formed, and reporting either as *this
1245
- // rail did not fire* is a could-not-measure read as a measurement: session 1's round 3, in the
1246
- // guard added to stop exactly that. Raised as a suppressed note by Copilot, round 1.
1247
894
  if (fire.status === null) {
1248
895
  throw new CouldNotRun(
1249
896
  `rail \`${drill.rail}\` was killed by a signal rather than exiting, so no verdict was formed` +
@@ -1275,12 +922,7 @@ export function drillOne({ drill, rail, repoRoot, sha, say, workspaceRel = ".por
1275
922
  say(` fired ${drill.rail.padEnd(28)} exit ${fire.status} · said ${JSON.stringify(drill.tell)}`);
1276
923
  return null;
1277
924
  } finally {
1278
- // **The counter files a hook rail leaves behind, retired.** `./stop-gate.mjs` writes one per
1279
- // (session id, tree) into the OS temp directory, so a sweep that invented a session id and
1280
- // walked away left one 45-byte file per run accumulating forever — the leak #340 names in a
1281
- // sibling module, found here at the pre-commit checkpoint before it could become the same
1282
- // issue. Scoped to this harness's own prefix, which is why the prefix is a constant: a wider
1283
- // glob would retire a live session's counter and quietly disarm its cap.
925
+ // Only this harness's prefix: a wider match would delete a live session's counter and disarm its cap.
1284
926
  try {
1285
927
  const dir = os.tmpdir();
1286
928
  const mine = `portulan-stopgate-${DRILL_SESSION_PREFIX}-${path.basename(worktree)}`;
@@ -1290,7 +932,6 @@ export function drillOne({ drill, rail, repoRoot, sha, say, workspaceRel = ".por
1290
932
  } catch {
1291
933
  /* A counter file this run cannot retire is litter in a temp directory, never a wrong verdict. */
1292
934
  }
1293
- // `--force` because the perturbation left the worktree dirty, which is the whole point of it.
1294
935
  try {
1295
936
  git(["worktree", "remove", "--force", worktree], { cwd: repoRoot });
1296
937
  } catch {
@@ -1363,9 +1004,7 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
1363
1004
 
1364
1005
  const recipes = yieldedRecipes({ workspaceDir, repoRoot, packRoots });
1365
1006
 
1366
- // **The whole table, in every mode.** `--only` narrows what runs and not what must be well
1367
- // formed — session 1's round 1 on `./mutants.mjs`, where `--only` skipped validation in exactly
1368
- // the mode a person reaches for when something is already wrong.
1007
+ // The whole table in every mode: `--only` narrows what runs, not what must be well formed.
1369
1008
  const findings = check({ recipes, repoRoot });
1370
1009
 
1371
1010
  if (checkOnly) {
@@ -1384,10 +1023,7 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
1384
1023
  return 0;
1385
1024
  }
1386
1025
 
1387
- // **The argument is validated before the tree is chosen, and the order is a finding rather than
1388
- // a preference.** With `treeToDrill` first, a typo'd `--only` on a dirty tree reported the dirty
1389
- // tree — a refusal naming a cause it had not established, which sends the reader to look at
1390
- // their working copy when what was wrong was what they typed. Caught by this module's own suite.
1026
+ // Before `treeToDrill`, so a mistyped `--only` on a dirty tree is not reported as the dirty tree.
1391
1027
  const selected = only === null ? DRILLS : DRILLS.filter((d) => d.rail === only);
1392
1028
  if (only !== null && selected.length === 0) {
1393
1029
  throw new CouldNotRun(
@@ -1396,29 +1032,7 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
1396
1032
  );
1397
1033
  }
1398
1034
 
1399
- // **The sweep runs every command from a throwaway worktree, so every path in one must resolve
1400
- // there.** Two refusals, both measured rather than reasoned:
1401
- //
1402
- // * a **pack root outside the repository** makes `recipe-set` relativise `${PACK_ROOT}` against
1403
- // the repo root, which produced `bash ../../../../../../../private/tmp/…/actions-pinned.sh`
1404
- // — a path with `..` hops, executed from a different directory, landing somewhere nobody
1405
- // chose. `--check` stays permissive because it runs no rail.
1406
- // * a **workspace outside the repository** cannot be handed to the hooks, which read
1407
- // `PORTULAN_WORKSPACE` as a path inside their project root.
1408
- //
1409
- // Both are could-not-run rather than a best effort: a rail run against files the sweep cannot
1410
- // name is a verdict about neither tree. Copilot round 1.
1411
- // **`isInside` is imported, not re-spelled.** The predicate this replaced was
1412
- // `!path.relative(a, b).startsWith("..")` plus `rel !== ""` — the exact spelling
1413
- // `./inside.mjs` exists to hold, and its docblock records that two copies of this rule drifted
1414
- // into the identical defect before either shipped. Mine had both halves of it: a directory
1415
- // legitimately named `..packs` reads as outside, and a root *equal* to the repository root was
1416
- // rejected outright, so `--pack-root .` refused a tree that is trivially inside itself. A third
1417
- // copy of a rule twice-corrected elsewhere is `0020` exactly. Copilot round 2.
1418
- //
1419
- // `fs.realpathSync` on both sides, for the reason `./compile.mjs` had to add it: a symlinked
1420
- // checkout — `/var` against `/private/var` on macOS is the one this session already met — makes
1421
- // two names for one directory compare as different trees.
1035
+ // `realpathSync` on both sides: on macOS `/var` is `/private/var`, one directory under two names.
1422
1036
  const real = (dir) => {
1423
1037
  try {
1424
1038
  return fs.realpathSync(dir);
@@ -1442,19 +1056,9 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
1442
1056
  "path inside the tree being drilled. Pass a workspace inside the repository, or use --check",
1443
1057
  );
1444
1058
  }
1445
- // **An empty relative path means *the repository root itself*, and the hooks read it as absent.**
1446
- // `PORTULAN_WORKSPACE=""` falls through `process.env.PORTULAN_WORKSPACE || ".portulan"` in both
1447
- // runners, so a workspace at the repo root would have silently drilled `.portulan` instead — a
1448
- // rail graded against a policy the run did not choose, which is the defect the threading was
1449
- // added to close, surviving in its own edge case. `.` is the spelling that says *here*. Copilot,
1450
- // round 2's notes.
1059
+ // `.`, not "": the hooks' `PORTULAN_WORKSPACE || ".portulan"` reads an empty value as unset.
1451
1060
  const workspaceRel = path.relative(repoRoot, workspaceDir) || ".";
1452
1061
 
1453
- // **A malformed roster is reported here, not met as a throw inside the loop.** `check` had already
1454
- // recorded a drill naming an undeclared rail as a finding, and the sweep then walked into the
1455
- // guard below and turned that documented exit-1 roster failure into an exit-2 could-not-run,
1456
- // abandoning the rest of the transcript. A roster failure is a finding in both modes. Raised as a
1457
- // suppressed note by Copilot, round 1.
1458
1062
  if (findings.length) {
1459
1063
  for (const f of findings) stderr.write(`drills: ${f.where}\n ${f.what}\n`);
1460
1064
  stderr.write(`RED — ${findings.length} finding(s) in the drill roster; no rail was drilled\n`);
@@ -1471,22 +1075,9 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
1471
1075
  say(`drills: ${selected.length} of ${DRILLS.length} drill(s), one throwaway git worktree each`);
1472
1076
  for (const excluded of NOT_DRILLED) say(` not drilled ${excluded.rail}`);
1473
1077
 
1474
- // **One rail that cannot be judged does not end the sweep**, and this is a defect the first run
1475
- // of this tool shipped: a `CouldNotRun` thrown out of one drill aborted the loop and discarded
1476
- // every finding already collected, so the run reported on one rail and said nothing about the
1477
- // twenty behind it. That is the same argument `../.github/workflows/verify.yml` settles with
1478
- // `set +e` — *a red recipe does not abort the loop* — and a sweep whose whole subject is rails
1479
- // nobody has watched fire is the last place to stop looking at the first obstacle.
1480
- //
1481
- // The two outcomes stay distinguishable, per `../.portulan/memory/verify-preconditions-fail-closed.md`:
1482
- // a **finding** is a rail that did not fire, and a **could-not-run** is a rail whose verdict
1483
- // could not be formed. Both are printed; a single could-not-run makes the whole run exit 2,
1484
- // because a set that was not fully judged has not been judged.
1485
1078
  const unjudged = [];
1486
1079
  for (const drill of selected) {
1487
1080
  const rail = byId.get(drill.rail);
1488
- // `check` above has already refused a drill naming an undeclared rail, so this cannot be hit
1489
- // through the CLI — it is the guard that keeps that true if the two ever drift.
1490
1081
  if (!rail) throw new CouldNotRun(`rail \`${drill.rail}\` is not in the yielded set nor declared`);
1491
1082
  try {
1492
1083
  const finding = drillOne({ drill, rail, repoRoot, sha: tree.sha, say, workspaceRel });
@@ -1523,10 +1114,7 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
1523
1114
  }
1524
1115
  }
1525
1116
 
1526
- // The entry guard, in the ONE form `./rule-carriers.mjs` designates. `file://${argv[1]}` is NOT that
1527
- // form: `import.meta.url` percent-encodes, this working copy lives under a path with spaces, and the
1528
- // comparison fails — so the tool exits 0 having run nothing. Copied rather than re-derived, for the
1529
- // reason `./goldens.mjs` states after meeting the false green a third time.
1117
+ // Not `file://${argv[1]}`: `import.meta.url` percent-encodes, so a path with a space would never match.
1530
1118
  function isMain() {
1531
1119
  const invoked = process.argv[1];
1532
1120
  if (!invoked) return false;