create-agent-rig 0.4.0 → 0.6.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 (118) hide show
  1. package/CHANGELOG.md +316 -11
  2. package/README.md +58 -19
  3. package/package.json +2 -1
  4. package/packages/cli/dist/commands/create.js +8 -3
  5. package/packages/cli/dist/commands/init.js +85 -34
  6. package/packages/cli/dist/commands/upgrade.js +112 -30
  7. package/packages/cli/dist/index.js +81 -19
  8. package/packages/cli/dist/lib/copy-tree.js +35 -6
  9. package/packages/cli/dist/lib/init-settings.js +12 -0
  10. package/packages/cli/dist/lib/install-set.js +6 -8
  11. package/packages/cli/dist/lib/manifest.js +31 -9
  12. package/packages/cli/dist/lib/safe-path.js +30 -0
  13. package/scripts/prepare.mjs +1 -1
  14. package/templates/agent-os/init/AGENTS.md +199 -0
  15. package/templates/agent-os/init/CLAUDE.md +69 -9
  16. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +105 -0
  17. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
  18. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +41 -3
  19. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
  20. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +41 -0
  21. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
  22. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
  23. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +236 -0
  24. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +958 -0
  25. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
  26. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
  27. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
  28. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +396 -0
  29. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
  30. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +40 -1
  31. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +71 -1
  32. package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
  33. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +301 -26
  34. package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
  35. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
  36. package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +127 -0
  37. package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
  38. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
  39. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
  40. package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +503 -0
  41. package/templates/agent-os/universal/.claude/rules/autonomy.md +69 -1
  42. package/templates/agent-os/universal/.claude/rules/invariants.md +104 -19
  43. package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
  44. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1260 -0
  45. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
  46. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +351 -0
  47. package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
  48. package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
  49. package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +28 -0
  50. package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
  51. package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +462 -0
  52. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +33 -17
  53. package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
  54. package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +149 -0
  55. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +809 -28
  56. package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
  57. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +95 -16
  58. package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +543 -15
  59. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +457 -47
  60. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +289 -25
  61. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
  62. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +316 -0
  63. package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +180 -0
  64. package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
  65. package/templates/agent-os/universal/.claude/scripts/run-state.mjs +539 -0
  66. package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
  67. package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +239 -0
  68. package/templates/agent-os/universal/.claude/scripts/verdict.mjs +198 -0
  69. package/templates/agent-os/universal/.claude/settings.json +11 -2
  70. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +118 -7
  71. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +667 -38
  72. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +359 -26
  73. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
  74. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
  75. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
  76. package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
  77. package/templates/agent-os/universal/.codex/hooks.json +70 -0
  78. package/templates/agent-os/universal/AGENTS.md +166 -0
  79. package/templates/agent-os/universal/CLAUDE.md +49 -14
  80. package/templates/agent-os/universal/PLAN.md +7 -40
  81. package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
  82. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
  83. package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
  84. package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
  85. package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
  86. package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
  87. package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
  88. package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
  89. package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +74 -0
  90. package/templates/agent-os/universal/journal/README.md +101 -0
  91. package/templates/agent-os/universal/layers.json +45 -2
  92. package/templates/hash-history.json +310 -49
  93. package/templates/release-ledger.json +9 -0
  94. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
  95. package/templates/skeleton/aws-serverless/README.md +91 -9
  96. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
  97. package/templates/skeleton/aws-serverless/gitignore +37 -0
  98. package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
  99. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
  100. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
  101. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
  102. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
  103. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
  104. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
  105. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
  106. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
  107. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
  108. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
  109. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
  110. package/templates/skeleton/node-service/README.md +11 -1
  111. package/templates/skeleton/node-service/gitignore +34 -0
  112. package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
  113. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
  114. package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
  115. package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
  116. package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
  117. package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
  118. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +28 -0
@@ -6,13 +6,15 @@ argument-hint: [max-tasks]
6
6
  ---
7
7
 
8
8
  You drive an unattended session. `.claude/rules/autonomy.md` sets the behaviour
9
- boundaries; the **queue** holds the work; `PLAN.md` holds state, standing
10
- decisions and the journal. This skill is the driver in between: what gets picked,
9
+ boundaries; the **queue** holds the work; the journal is `journal/YYYY-MM.md`,
10
+ one file per month, newest-on-top; `PLAN.md` holds state and standing
11
+ decisions. This skill is the driver in between: what gets picked,
11
12
  what keeps the loop going, what stops it, and where the report goes.
12
13
 
13
- Per-task procedure is unchanged: (worktree if another session may run) →
14
- `check-premises` → failing test first → implement → `pr-ship` merge on the
15
- named criterion → verify the deployed surface if one changed.
14
+ Per-task procedure: (worktree if another session may run) → `check-premises` on the
15
+ item → failing test first → implement → **`check-premises` again, on your own prose**
16
+ → `pr-ship` → merge on the named criterion → verify the deployed surface if one
17
+ changed.
16
18
 
17
19
  ## 0. The queue is behind an adapter
18
20
 
@@ -23,7 +25,7 @@ Selection never reads a tracker directly. It goes through
23
25
  ```bash
24
26
  node .claude/scripts/queue/index.mjs next # the item to take, and why the rest were skipped
25
27
  node .claude/scripts/queue/index.mjs next --json
26
- node .claude/scripts/queue/index.mjs hygiene # stale labels, link anomalies
28
+ node .claude/scripts/queue/index.mjs hygiene # stale labels, link anomalies, overtaken proposals
27
29
  ```
28
30
 
29
31
  - **`plan-md`** (default) — the Agent queue in `PLAN.md`. The only adapter that
@@ -51,13 +53,13 @@ state-vs-queue split exists to prevent.
51
53
  node .claude/scripts/preflight.mjs
52
54
  ```
53
55
 
54
- Three items are scripted (kill switch absent · local default branch matches the
55
- remote · the last deploy concluded successfully) and the script **prints the ones
56
- it did not check, every time**. Paste the block into the journal: a checklist that
56
+ Four items are scripted (kill switch absent · `RIG_RUN_DIR` not already
57
+ exported · local default branch matches the remote · the last deploy concluded
58
+ successfully) and the script **prints the ones it did not check, every time**. Paste the block into the journal: a checklist that
57
59
  leaves no record cannot tell you it was skipped.
58
60
 
59
61
  Verdicts: **STOP** → do not start, deal with the cause. **CAUTION** → start,
60
- knowing which ground is soft. **GO** → the scripted three are clean; the rest are
62
+ knowing which ground is soft. **GO** → the scripted four are clean; the rest are
61
63
  still yours.
62
64
 
63
65
  **An `unknown` never becomes a `pass`.** A probe that could not run tells you
@@ -67,6 +69,67 @@ nothing.
67
69
  unattended only after the escalation path and the post-deploy verdict have each
68
70
  been seen working at least once.
69
71
 
72
+ ### Declare the run directory here, before the first selection
73
+
74
+ 🔴 **This is not optional, and it is not only about the trace.** The run's
75
+ **stop conditions** live in that directory too (§3) — the escalation streak, the
76
+ deploy verdict, the budget flag. With `RIG_RUN_DIR` unset the escalation count
77
+ is recorded nowhere, silently, so an undeclared run is not a run with a missing
78
+ journal: it is a run whose main brake is off and which looks exactly like a
79
+ healthy one (`docs/decisions/run-directory.md`).
80
+
81
+ The machine trace (§7) is the other half, and its first call site is
82
+ **selection**, which runs before every task. Declared later, it misses
83
+ everything that already happened — so this goes in preflight or not at all:
84
+
85
+ ```bash
86
+ export RIG_RUN_DIR="$PWD/.claude/runs/$(date +%Y%m%d-%H%M%S)" # one per run
87
+ mkdir -p "$RIG_RUN_DIR"
88
+ ```
89
+
90
+ ⚠ **That export reaches the commands THIS shell runs and nothing else.** A
91
+ `PreToolUse` hook is spawned by the harness with the harness's own environment,
92
+ never with a variable the session exported — pinned in the generator's
93
+ `test/template/guard-rulebook.test.ts` (absent in a generated rig) › "only a
94
+ flag arms it — an exported RIG_UNATTENDED=1 with no flag changes nothing" —
95
+ and in some harnesses the export does not even survive to the next Bash call,
96
+ which is why every command in this skill can also take the run directory per
97
+ invocation. What a hook CAN see is a file, so the unattended signal is one:
98
+
99
+ ```bash
100
+ # at claim time, from the paths the item names (repo-relative prefixes, with
101
+ # their trailing slash); the guard refuses every other rulebook edit while it is on
102
+ node .claude/scripts/unattended-flag.mjs on --item <item-id> --run-dir "$RIG_RUN_DIR" --allow <prefix> [<prefix>…]
103
+ ```
104
+
105
+ `guard-rulebook` reads it (`.claude/rules/autonomy.md`, "Never"): with the flag
106
+ on, a Write/Edit/MultiEdit/NotebookEdit/`apply_patch` under `.claude/hooks/`,
107
+ `.claude/settings.json`, `.claude/queue.json`, `.claude/scripts/queue/`, the
108
+ router, the gate sweep, `.claude/rules/` or `CLAUDE.md` is refused unless its
109
+ path starts with an allowed prefix; with no flag the guard does nothing. An
110
+ item that needs a rulebook path names it here — a decision made at claim
111
+ time, never a default — and the stop step below turns the flag off. Pinned in
112
+ the generator's `test/template/guard-rulebook.test.ts` — absent in a generated
113
+ rig — › "blocks a hook-config edit with an empty allow-list, naming path, item and the rule".
114
+
115
+ ⚠ **The export outlives the run's own calls.** Everything the session spawns
116
+ inherits it — and a test suite that spawns the queue CLI would write fixture
117
+ records into this run's trace (AR-139: 38 fixture selections and 22 fixture
118
+ revalidation events in one session, two tests exiting 1). So preflight refuses
119
+ to start on a `RIG_RUN_DIR` already exported. The generator's own test harness
120
+ also scrubs the variable before any test file loads — its `test/setup-env.ts`,
121
+ pinned by its `test/template/rig-run-dir-scrub.test.ts` › "holds with the
122
+ variable exported around the whole vitest process" — and **neither file ships
123
+ into a generated rig**: here, nothing scrubs it, and a rig's own test setup is
124
+ the place to do the same.
125
+
126
+ 🔴 **One directory per run, never shared and never reused — and the journal
127
+ cannot enforce this for you.** A collision or an already-ended directory is
128
+ refused loudly; two runs whose records merely do not collide are merged into one
129
+ seamless trace with nothing able to say so. A fresh directory per run is the
130
+ only thing that prevents it, and it is yours to do — the exact boundary is in
131
+ `docs/decisions/run-directory.md`.
132
+
70
133
  ## 2. Selection — filters in order, then the sort
71
134
 
72
135
  The queue is queried **fresh before every task**, never from a cached list: the
@@ -87,21 +150,168 @@ metadata destroys the evidence that the metadata is unreliable.
87
150
 
88
151
  🔴 **A missing trigger marker means unconditional, not missing data.** Work that is
89
152
  genuinely conditional says so. A `trigger-human` item — a "security pass", a
90
- "window", "user demand" without a named metric — is **never self-taken**; the human
91
- hands it over explicitly. A `trigger-auto` item needs its trigger verified *this
92
- run*: unverified is not fired, and rationalising a trigger into firing builds for
93
- scale that does not exist.
153
+ "window", "user demand" without a named metric — is **never taken on that marker
154
+ alone**; the human hands it over explicitly. A `trigger-auto` item needs its
155
+ trigger verified *this run*: unverified is not fired, and rationalising a
156
+ trigger into firing builds for scale that does not exist.
157
+
158
+ ⚠ **An item carrying BOTH markers is taken as `trigger-auto`.** Every adapter
159
+ resolves `auto` first, nothing refuses the combination, and no hygiene check
160
+ reports it — so one recorded declaration takes an item whose author also marked
161
+ it human-gated. The reachable path is an owner tightening an auto-gated item and
162
+ not deleting the old marker, and the silent resolution goes to the **less**
163
+ restrictive gate. Until that is fixed, treat a double-marked item as
164
+ human-gated by hand.
165
+
166
+ 🔴 **An item marked for another repository is held, never taken.** A label
167
+ `owner-<name>` names the repository an item belongs to; the checkout names
168
+ itself in `options.owner` of `.claude/queue.json`, and a mismatch — or an
169
+ owned item in a checkout that declares no owner — is the holding cause `owner`,
170
+ reported by `hygiene` as `owner-mismatch`. It clears the way `trigger-human`
171
+ does: a human moves the item or re-marks it. An unmarked item is unconditional.
172
+ Two items of another product once entered this queue as normal spacers and
173
+ escalated `PREMISE FALSE` back to back — a run-level stop spent on work that
174
+ was never this checkout's (AR-132). Pinned in the generator's
175
+ `test/template/queue-owner.test.ts` — absent in a generated rig — › "holds an
176
+ item whose owner is another repository, with the cause named".
177
+
178
+ 🔴 **An item's lifecycle is a label a human wrote, and the loop infers none of
179
+ it** (AR-144). Four words, read by `lifecycleOf` in `core.mjs` so every adapter
180
+ means the same thing — three of lifecycle, one of scheduling:
181
+
182
+ - `keep-core` — the problem and the responsibility are valid and the item is
183
+ executable as written. A statement about the item, never a condition on taking
184
+ it: a bare `keep-core` item is selected like an unmarked one.
185
+ - `re-scope` — the problem is valid but the item is **not executable
186
+ literally**: a path, a mechanism, a boundary or an acceptance criterion has
187
+ drifted. It is the holding cause `re-scope`, reported by `hygiene` as
188
+ `re-scope-pending` until it clears — and it clears **only** by a human
189
+ re-reading the code, rewriting the item and removing the label. It is a
190
+ short-lived quarantine, not a backlog category, and never a synonym for
191
+ obsolete, low priority or parked. The loop surfaces it and never rewrites it:
192
+ a re-aimed item is work the agent authored (§8).
193
+ - `obsolete` — the responsibility is gone or fully superseded by a proven
194
+ mechanism. A **human verdict, terminal**: the loop never applies it and never
195
+ closes an item because it believes another mechanism supersedes it. Closing as
196
+ obsolete needs external evidence and a comment naming the replacement or the
197
+ reason — so an obsolete item is out of play (the cause `obsolete`), not held,
198
+ and the stop line says so.
199
+ - `parked` — valid work deliberately not active now. The **scheduling** axis,
200
+ orthogonal to the three above: `keep-core + parked` is the ordinary shape of a
201
+ deferred item and means "still needed, not now". It is the holding cause
202
+ **`deferred`**, freed by a human un-parking it. The cause is not spelled
203
+ `parked` on purpose: §3 already uses that word for the **out-of-play pile** —
204
+ items waiting on a human that no session will take — and a parked-labelled
205
+ item is the opposite, held and takeable. So the stop line reads "held by
206
+ deferred" for the label, and "are parked" for the pile; `obsolete` lands in
207
+ the pile, the `parked` label never does.
208
+
209
+ The rule under all four: **nothing infers `obsolete`** — not age, not a key
210
+ range, old terminology, `parked`, absence from a roadmap, or a migration marker.
211
+ `legacy-backlog` is that marker, retired: `hygiene` reports an open item still
212
+ carrying it (`stale-legacy-backlog-label`), and two lifecycle labels on one item
213
+ (`contradictory-lifecycle-labels`) — selection meanwhile reads the most
214
+ restrictive one. Reported, never corrected: which label is wrong is the human's
215
+ call. Pinned in the generator's `test/template/queue-lifecycle.test.ts` — absent
216
+ in a generated rig — › "holds a re-scope item and says a human rewrites it" and
217
+ › "refuses an obsolete item and says a human closes it with a comment naming the
218
+ evidence".
219
+
220
+ **For a `trigger-auto` item, record the declaration** — it has to outlive the
221
+ turn it was made in, or the next selection holds the item back again:
222
+
223
+ ```bash
224
+ node .claude/scripts/run-state.mjs trigger <item-id>
225
+ ```
226
+
227
+ 🔴 **This does nothing for a `trigger-human` item, and the command will not tell
228
+ you so.** Selection refuses that kind outright — it never consults the record —
229
+ so the only thing that makes one takeable is a human changing the item's own
230
+ marker. Recording a "declaration" against it succeeds, prints, and leaves the
231
+ item exactly as unselectable as before.
232
+
233
+ ⚠ **It is keyed by the item's id — and under `plan-md` that id is the item's
234
+ POSITION in the list.** So a declaration made for the third bullet transfers to
235
+ whatever occupies the third slot after someone edits `PLAN.md`, for the rest of
236
+ the run. Re-check the item the declaration names before acting on it, or use an
237
+ adapter whose ids are stable (`github-issues`, `jira`). There is no un-fire
238
+ word: a new run starts with a clean state, which is the same remedy the budget
239
+ stop relies on.
94
240
 
95
241
  **The elevated tier is rationed by spacing, not by counting** — a per-run count is
96
- meaningless when the run has no end. Never two elevated items back to back: land a
97
- normal item on a healthy runtime in between. One unreviewed permissions or schema
98
- change is recoverable; a chain of them compounding overnight is not.
242
+ meaningless when the run has no end. Never two elevated items back to back **when
243
+ the first one touched a mechanism** (next paragraph): land a normal item on a
244
+ healthy runtime in between. One unreviewed permissions or schema change is
245
+ recoverable; a chain of them compounding overnight is not.
246
+
247
+ **Only a change that EXECUTES spaces the next item.** The close records
248
+ `normal` | `elevated-prose` | `elevated-mechanism`, computed from the diff's
249
+ paths (`queue/state.mjs`): an elevated change whose elevated paths are all
250
+ documents is `elevated-prose` and clears the ration, because a document cannot
251
+ compound into a broken runtime overnight. Prose keeps its tier everywhere it is
252
+ *reviewed* — model lane, cold readers, `human-review`, the gate sweep — and loses
253
+ it only here. A tier outside that vocabulary **holds**, never releases
254
+ (`docs/decisions/spacing-rations-mechanisms.md`).
99
255
 
100
256
  **The tier marker is a pre-filter, not the authority.** If an item passed as normal
101
257
  and the work turns out to touch an elevated path (`CLAUDE.md` →
102
258
  `elevated-paths`), run the gate anyway, record the verdict on the PR, and treat it
103
259
  as this run's elevated item for spacing.
104
260
 
261
+ **Selection also revalidates the item against its last take-up.** `next`
262
+ records the selected item's `updatedAt` marker in the run state (`takeUps`) and,
263
+ when the item is offered again, compares the two — against this run's take-up
264
+ when it has one, otherwise against the newest earlier run's under `.claude/runs/`
265
+ (AR-138: before that, an item taken up yesterday compared against nothing and
266
+ read as a first sight). A proposal the loop files carries its own baseline: the
267
+ `jira` and `github-issues` adapters record the filed item's marker as a take-up
268
+ in the run that filed it. The event names which it used — `baseline:
269
+ this-run | previous-run | null` — pinned in the generator's
270
+ `test/template/revalidation-baseline.test.ts` — absent in a generated rig — ›
271
+ "holds when the marker moved past the earlier run’s take-up, and names that
272
+ baseline". A marker that
273
+ moved prints a `revalidate:` line and the JSON carries `revalidation.changed:
274
+ true`: **re-read the item before acting on it**, then record what the re-read
275
+ concluded — whether the change altered the action is the evidence this exists to
276
+ collect, and the comparison alone cannot supply it:
277
+
278
+ ```bash
279
+ node .claude/scripts/revalidate.mjs outcome --point SELECT --ticket <item-id> --action-changed <true | false> --note '<what changed, or why it changes nothing>'
280
+ ```
281
+
282
+ It appends one `revalidation-outcome` record whose `answers` names the
283
+ revalidation it resolves, so the report can pair the two without guessing. The
284
+ note is stored verbatim from argv, so keep it in single quotes: inside double
285
+ quotes the shell expands a backtick or a `$` before the command sees it.
286
+ Nothing forces this record — a `revalidation` event with no matching outcome is
287
+ counted as `unresolved`, which is the honest word for a re-read the run skipped.
288
+
289
+ Under a declared run directory, every selection logs one `revalidation` event
290
+ `{ticket, point: SELECT, changed, source, action, task}` — the same shape the
291
+ BEFORE_PR and BEFORE_CLOSE points write. **No-change is always recorded**, one
292
+ line per selection and no sampling: the rule is explicit so the report's
293
+ `opportunities` is a count and not an estimate. An adapter with no marker
294
+ (`plan-md`) logs `changed: null`, never "unchanged". ⚠ The marker also moves on
295
+ the run's own claim and comments. The `jira` and `github-issues` adapters
296
+ re-record the take-up after each write they make — claim, comment, close,
297
+ escalate — so a move made through the adapter is not a hold (AR-140, from the
298
+ journal's RX3/RX4 entry: every BEFORE_PR catch of that run was the run's own
299
+ comment, counted by `revalidation-report.mjs`); a
300
+ comment posted by any other route — a REST call by hand, a connector — still
301
+ moves it like anyone else's, and a `true` can still be self-inflicted that way
302
+ — the re-read decides, which is why the outcome is recorded separately, and a
303
+ hold the re-read overturns is counted as a false hold with its source named.
304
+ Pinned in the generator's `test/template/self-inflicted-marker.test.ts` — absent
305
+ in a generated rig — › "%s leaves the take-up at the marker the write produced",
306
+ an `it.each` over claim, comment, close and escalate. The
307
+ four-week view is `node .claude/scripts/revalidation-report.mjs --since <date>`,
308
+ over this rig's `.claude/runs/` (or a `--runs <dir>`). The behaviour is pinned in the
309
+ generator's `test/template/queue-revalidation.test.ts` — absent in a generated
310
+ rig — › "an adapter with no marker records a blind spot, not \"unchanged\"", ›
311
+ "a moved marker holds on task:updatedAt, re-snapshots, and journals the change"
312
+ and › "the loop skill's outcome command records what the re-read concluded", and
313
+ in `test/template/revalidation-evidence.test.ts`.
314
+
105
315
  **Then, before the Red step: `check-premises`.** The item was written by someone
106
316
  who was not reading the code at the time, and everything downstream — the failing
107
317
  test, the implementation, the reviewer comparing diff to item — inherits its
@@ -109,17 +319,130 @@ claims rather than checking them. On `PREMISE FALSE` the item is escalated (§6)
109
319
  not repaired in place: a run that silently re-aims its own task has authored work
110
320
  for itself, which is the one thing this loop does not do (§8).
111
321
 
322
+ 🔴 **And again at the other end, before `pr-ship`: `check-premises` on the prose the
323
+ task itself wrote** — the rulebook prose the diff touches (the skill defines that set,
324
+ and it is the one `workflow.md` already uses for the `prose-reviewer` trigger), plus
325
+ the PR description once one exists. The verdict is `UNMEASURED`, with two exits:
326
+ delete the sentence, or make it a pointer to the test that proves it. Run it before
327
+ the gate: a reviewer reaches the same sentence only after loading the whole diff, and
328
+ that is a round spent on what an edit would have fixed.
329
+
330
+ `PREMISE FALSE` stays with the first pass. At the second one the claims are your own
331
+ and the remedy is an edit, so nothing escalates.
332
+
333
+ **Both passes end in a block, and you check it before you act on it** — the skill
334
+ returns one fenced `json` verdict like every other gate, and this loop is its caller:
335
+
336
+ ```sh
337
+ node .claude/scripts/verdict.mjs check <report> check-premises
338
+ ```
339
+
340
+ **`<report>` is a file you write, not one the harness leaves behind.** The
341
+ subagent's answer arrives as text in the conversation; its transcript on disk is
342
+ a JSONL file whose last fenced block does not parse, so pointing the check at it
343
+ exits 1 whatever the reviewer said. Save the whole answer to a file under the run
344
+ directory — `$RIG_RUN_DIR/check-premises.md`, one file per gate so two answers
345
+ never overwrite each other — and pass that path (`-` reads stdin instead). The
346
+ same holds for every `<report>` in this skill, as `pr-ship` already does for its
347
+ reviewers (AR-117). Pinned in the generator's `test/template/loop-report-file.test.ts`
348
+ — absent in a generated rig — › "states that the report is a file the session
349
+ writes from the subagent answer, before the first check".
350
+
351
+ Exit 1 means it did not answer: a stop verdict naming no premise, or no block at all.
352
+ That is `incomplete` — neither "the premises hold" nor a reason to escalate — so run
353
+ the pass again rather than reading the silence as a pass.
354
+
355
+ **Then journal the block that parsed**, exactly as `pr-ship` journals a reviewer's —
356
+ the paths a premise check names are what `revalidate.mjs` reads at BEFORE_PR as the
357
+ task's cited paths, and a verdict held only in context cites nothing after a
358
+ compaction:
359
+
360
+ ```sh
361
+ node --input-type=module -e '
362
+ const runDir = process.env.RIG_RUN_DIR;
363
+ if (!runDir) process.exit(0); // an undeclared run has no trace to write
364
+ if (!process.argv[1]) process.exit(0); // `check` printed nothing: nothing to record
365
+ const journal = await import("./.claude/scripts/run-journal.mjs");
366
+ const v = JSON.parse(process.argv[1]);
367
+ try {
368
+ console.log(journal.recordDecision({
369
+ runDir,
370
+ gate: "check-premises",
371
+ verdict: v.verdict,
372
+ blockers: v.blockers,
373
+ headSha: v.headSha,
374
+ now: new Date().toISOString(),
375
+ }));
376
+ } catch (error) {
377
+ // The same split pr-ship makes: an exhausted trace is over, the task is not.
378
+ if (!journal.isTraceExhausted?.(error)) throw error;
379
+ process.stderr.write(`run journal: ${error.message}\n the premise verdict above was NOT recorded.\n`);
380
+ }
381
+ ' "$(node .claude/scripts/verdict.mjs check <report> check-premises)"
382
+ ```
383
+
112
384
  ## 3. What keeps the loop running, and what stops it
113
385
 
114
386
  Per-task stops (three strikes, attempt budget, invariant conflict, a blocking
115
- reviewer verdict, a false premise in the item itself) **do not end the run**:
116
- escalate that item (§5) and take the next one.
387
+ reviewer verdict, an exhausted gate-round cap, a false premise in the item itself)
388
+ **do not end the run**: escalate that item (§6) and take the next one.
117
389
 
118
390
  The run-level conditions are in `stopConditionOf` in `core.mjs`, checked in
119
391
  severity order: **queue unreadable** · **runtime regression** · **kill switch** ·
120
- **two escalations in a row** · **budget** · **queue empty**.
392
+ **two escalations in a row** · **budget** · **nothing selectable** · **queue
393
+ empty**.
394
+
395
+ 🔴 **Their inputs come from a file, not from your memory — and that is why they
396
+ fire at all.** `escalations` and `lastDeployVerdict` live in
397
+ `<RIG_RUN_DIR>/state.json`, written by `run-state.mjs`. A stop condition held in
398
+ a session's memory is absent exactly when it is needed, because compaction is
399
+ what a long run does (`docs/decisions/stop-conditions-in-a-file.md`).
400
+
401
+ **One of the three writes itself; two you write.** The escalation count needs
402
+ nothing from you beyond using the documented calls — but it has two writers, and
403
+ both have to be the documented one:
404
+
405
+ - it **rises** through every adapter's `escalate()` (§6), which is why
406
+ escalating by hand-labelling the item counts nothing;
407
+ - it **resets** through the close step's `recordCompletedTier`, and **only when
408
+ you pass it `runDir`** (§9 has the command; it is one of that call's
409
+ load-bearing arguments, not an optional extra). Omit it and the count is
410
+ monotonic: two escalations an hour apart end the run however many tasks
411
+ landed in between.
121
412
 
122
- Three of them deserve their reasons repeated:
413
+ The other two are yours, after the checks that produce them:
414
+
415
+ ```bash
416
+ # the post-deploy check answers in a block like every other gate, and the word
417
+ # below is retyped out of it — so check the block before you trust the word
418
+ node .claude/scripts/verdict.mjs check <report> post-deploy-verify
419
+
420
+ # after the post-deploy check (`.claude/rules/autonomy.md`, "Post-deploy
421
+ # verification") — REGRESSION stops the next selection, HEALTHY clears it
422
+ node .claude/scripts/run-state.mjs deploy REGRESSION
423
+
424
+ # when the declared budget (§4) cannot fit another task
425
+ node .claude/scripts/run-state.mjs budget exhausted
426
+ ```
427
+
428
+ Both **refuse** a word outside their vocabulary and refuse to run with no
429
+ `RIG_RUN_DIR`, rather than writing something the stop conditions cannot match: a
430
+ file that looks recorded and stops nothing is worse than no file.
431
+
432
+ **Only the deploy verdict can be taken back**, and the asymmetry is deliberate:
433
+ `HEALTHY` names a real later event — the revert landed — while un-exhausting a
434
+ budget would name only a decision taken by the run that declared the stop
435
+ (`docs/decisions/stop-conditions-in-a-file.md`). A new run gets a clean state;
436
+ that is the way back from both.
437
+
438
+ Selection itself — `node .claude/scripts/queue/index.mjs next`, §0 — is what
439
+ *reads* these and stops on them. It is still the command you run to get work.
440
+
441
+ ⚠ **The kill switch is not among them.** It stays mechanical in `guard-bash`
442
+ and scripted in preflight; `next` does not check for the flag, so **keep
443
+ checking it between tasks** — the brake block later in this section says how.
444
+
445
+ Four of them deserve their reasons repeated:
123
446
 
124
447
  - **Runtime regression** → deploy the revert first, diagnose second, start no new
125
448
  work on top. A regression compounds into everything built above it.
@@ -130,7 +453,30 @@ Three of them deserve their reasons repeated:
130
453
  sprees, no polish, no pre-emptive optimisation. An empty filtered queue is a
131
454
  legitimate, successful end of session; refilling it is the owner's job.
132
455
  **Expect this to be the most common ending** — the queue is finite and the loop
133
- drains it. That is the system working.
456
+ drains it. That is the system working. The stop line also names the **parked**
457
+ pile if there is one, by cause and count: items out of play, waiting on a
458
+ human. They are reported next to the verdict, never swept into it. 🔴 Under
459
+ `plan-md` an escalation leaves no mark on the queue at all — `escalate`
460
+ returns `ok: false` with the instruction to move the item to the Operator
461
+ queue **in the same edit**, and skipping that move means the next run takes
462
+ the stuck item straight back.
463
+ - **Nothing selectable** → also a clean stop, and **not the same finding**.
464
+ Takeable work is still there and every piece of it is **held back by a
465
+ condition that clears when something else happens, not by refilling the
466
+ queue**: the elevated spacing (a normal or prose-only item lands), a blocker (its item
467
+ closes), in-progress (the other session finishes), a trigger (a human
468
+ declares it — and for a `trigger-auto` item that declaration is **written**,
469
+ §2, so this is the one hold that needs a command rather than only time), and
470
+ an owner (§2: the item is another repository's, and a human moves it or
471
+ re-marks it — neither time nor interleaving frees it). The
472
+ stop line names how many and by which, because the two endings ask the owner
473
+ for opposite things: an empty queue wants refilling, a held one wants
474
+ interleaving, time, or — for a trigger or an owner — a human act the line
475
+ names. 🔴 **A parked cause outranks a holding one on the
476
+ same item** — an escalated item is left claimed on purpose, so it arrives
477
+ carrying `in-progress` too. **Neither ending is an invitation to refill the
478
+ queue or invent work.** Why the two are split, and how the parked pile grows
479
+ per adapter: `docs/decisions/two-empty-endings.md`.
134
480
 
135
481
  🔴 **The kill switch is a real file, not an intention:**
136
482
 
@@ -162,6 +508,21 @@ cost column **will be believed** — by the next reader, and by the next run
162
508
  reasoning about its own budget. A field the loop cannot observe stays **visibly
163
509
  empty, never estimated.**
164
510
 
511
+ **Where the budget lives, and what is honest about it.** The **decision** is
512
+ recorded, not the arithmetic:
513
+
514
+ ```bash
515
+ node .claude/scripts/run-state.mjs budget exhausted
516
+ ```
517
+
518
+ That sets the flag `stopConditionOf` reads, so the next selection stops with
519
+ `queue: budget` — and it survives a compaction, which is the whole reason it is
520
+ a file. **Nothing computes it for you**: the counters above are the ones the
521
+ session observes, and judging that the remaining allowance cannot fit another
522
+ task stays yours. There is deliberately no field holding a spend figure, for the
523
+ same reason the journal has no currency column — a number the run cannot
524
+ observe, written where a stop condition reads, is a fiction with authority.
525
+
165
526
  ## 5. Every task carries an outcome state
166
527
 
167
528
  The stop conditions say why the loop stopped. None of them says whether what it
@@ -174,6 +535,20 @@ perfectly good reason having produced something nobody should build on.
174
535
  | `documented-stall` | it stopped at a real wall, and the diagnosis names **which stage needed what, and which upstream stage should have supplied it** |
175
536
  | `incomplete` | it stopped and the record does not explain where or why |
176
537
 
538
+ 🔴 **What `documented-stall` requires is the STAGE and the wall, not a full
539
+ inventory of findings** — and this had to be settled, because the two readings
540
+ disagreed the first time a stop arrived without an inventory. An exhausted
541
+ gate-round cap names its stage (the gate) and its wall (the branch's rounds are
542
+ spent — the count, not a verdict on whether the fixes were converging, which the
543
+ counter never measured; AR-115), while the individual blockers behind it are not persisted anywhere until
544
+ per-round verdicts exist. That is a `documented-stall`: the record locates the wall
545
+ and the next reader knows where to look.
546
+
547
+ `incomplete` is for a record that cannot say **where** it stopped — not for one that
548
+ can say where but not everything about it. Widening `incomplete` to cover a thin
549
+ diagnosis would make it the common case, and it is meant to be the rare one: it is
550
+ the only failing state, and a state that fires on honest stops stops being read.
551
+
177
552
  🔴 **`documented-stall` is a success, and reading it as a failure is how this stops
178
553
  working.** A stall that names its under-supply is the most useful thing an
179
554
  unattended run produces: it converts a vague gap into a located, fixable defect.
@@ -211,22 +586,61 @@ mechanises fully (`missed`, `.claude/rules/autonomy.md`) needs no self-report.
211
586
  ## 6. Escalation — two channels, by scope
212
587
 
213
588
  **Task-scoped — the item is the home, and the loop continues.** Three strikes, the
214
- attempt budget, an invariant conflict, a blocking reviewer verdict, or a
215
- `PREMISE FALSE` verdict from `check-premises` the last one is a
216
- `documented-stall` (§5), and its diagnosis is already written: what the item
217
- claimed, what the code says, and the citation:
218
-
219
- 1. Comment the diagnosis on the queue item: what fails, what was tried, the
589
+ attempt budget, an invariant conflict, a blocking reviewer verdict, an **exhausted
590
+ gate-round cap**, or a `PREMISE FALSE` verdict from `check-premises` **on the queue
591
+ item**. The last two are
592
+ a `documented-stall` (§5) and their diagnoses differ, so take the one that matches the
593
+ stop: a false premise writes what the item claimed, what the code says, and the
594
+ citation; an exhausted cap writes the round count and what the last gate reported.
595
+ Both then follow the same three steps:
596
+
597
+ 1. Comment the diagnosis on the queue item, in the shape
598
+ `.claude/rules/autonomy.md` ("Escalation format") sets — **cite it rather
599
+ than working from this list**, which is a reminder and is short by two of
600
+ its clauses: what was *observed* (verbatim errors, not summaries), and the
601
+ single question whose answer unblocks the work. So: what fails, what was tried, the
220
602
  current hypothesis, and links to the PR and the failing run where they exist
221
603
  — a premise stop has neither, and its citation stands in for both. **Name the outcome
222
- state in the same comment** — `incomplete` if the diagnosis cannot say which
223
- stage needed what. Writing `incomplete` on your own task is uncomfortable and
604
+ state in the same comment** — `incomplete` if the diagnosis cannot say **where** it
605
+ stopped (§5: a thin diagnosis that still locates the wall is a `documented-stall`).
606
+ Writing `incomplete` on your own task is uncomfortable and
224
607
  is the point: the run that produced it is the only witness.
225
608
  2. Mark it `escalated` and leave it claimed — **not** back to a selectable state,
226
609
  or the next query picks it up and works it twice.
227
610
  3. Journal it. 4. **Take the next item.** One stuck task does not end a run; two
228
611
  in a row does (§3).
229
612
 
613
+ 🔴 **The gate-round cap is the stop a run will not reach on its own.** Every other
614
+ stop here has a red thing behind it; a gate that keeps finding fixable prose is all
615
+ green, so three strikes never fires and the run has no reason to stop re-entering it.
616
+ `pr-ship` step 0 counts the round per branch in `.claude/gate-rounds.json` and exits 2
617
+ past the cap. The count outlives the session, which is the point — a counter held in
618
+ context is one the next context does not have.
619
+
620
+ ⚠ **What the cap does not carry, stated because the gap decides what you can write in
621
+ the escalation:** the counter records the round and nothing about it. Findings travel
622
+ separately, and the two do not cover the same ground — the count is **per branch, in
623
+ the main checkout, across runs**, while a verdict is journalled into **this run's**
624
+ directory. So `decisions.jsonl` holds the rounds this run spent and nothing about a
625
+ round spent before it: a branch on its third round in its second run has one round's
626
+ findings here, and the earlier ones in whichever directory that run declared, or
627
+ nowhere if it declared none.
628
+
629
+ Write the diagnosis from what you can actually read — the round count, plus this run's
630
+ records where there are any — and say which of the rounds that leaves unaccounted for
631
+ rather than reconstructing them from memory. `pr-ship` is told to journal each verdict
632
+ that parsed, and like every gate here that is a step in a skill rather than a
633
+ mechanism: a round whose session skipped it left no record either.
634
+
635
+ 🔴 **Escalate through the adapter, never by hand-labelling the item.** Every
636
+ adapter's `escalate()` counts the escalation into the run state as it marks the
637
+ item, which is what makes "two in a row" a condition the next selection can
638
+ check rather than one you have to remember across a compaction. Adding the
639
+ label yourself marks the item and counts nothing — and the run then grinds past
640
+ the wall this rule exists to stop it at. (`plan-md` still returns `ok: false`,
641
+ because a flat list has no per-item state to mark; the count is recorded all the
642
+ same, and moving the item to the Operator queue is still yours.)
643
+
230
644
  **Run-scoped — the run itself is broken, and it ends.** A runtime regression, two
231
645
  escalations in a row, a systemic wall, a queue-data anomaly: open an escalation
232
646
  issue with the diagnosis and links, notify the owner if the harness can, and write
@@ -238,7 +652,74 @@ a run still working.
238
652
 
239
653
  Write a checkpoint entry **every few completed items and at every stop**, not only
240
654
  at the end: a run that dies unexpectedly must not take its history with it. The
241
- field list is in `PLAN.md` under `## Journal`.
655
+ entry goes at the TOP of `journal/YYYY-MM.md` this month's file, newest-on-top
656
+ — and the field list is in `journal/README.md` next to it.
657
+
658
+ **Behind that entry there is a machine trace, and it is a different artifact.**
659
+ `.claude/scripts/run-journal.mjs` writes gate verdicts to `decisions.jsonl` and
660
+ everything else to `events.jsonl`, both append-only, inside the run directory
661
+ declared in §1. Five things about it are worth knowing before relying on it:
662
+
663
+ - **The run declares the directory; nothing invents one.** With `RIG_RUN_DIR`
664
+ unset, every call site stays silent — the *trace* is opt-in, and a run that
665
+ never declared one has no journal rather than a journal in a guessed place.
666
+ 🔴 **The stop conditions in the same directory are not opt-in** (§1): an
667
+ undeclared run also stops counting escalations, and that half is silent too.
668
+ Read "opt-in" as describing this file, never the declaration.
669
+ - **It answers *what the run decided and on what basis*, never *was that
670
+ right*.** It replaces neither the month file above nor `PLAN.md`; it is the
671
+ evidence a reader checks those against. It is also **oldest-first**, where the
672
+ month file is newest-on-top — reading one as the other is how a reader
673
+ concludes a run did nothing.
674
+ - **A record after the run-end marker is refused, and a broken sequence is
675
+ refused on both write and read.** The order is asserted rather than described,
676
+ so a stale record cannot read as the current one — which is the whole failure a
677
+ journal exists to prevent.
678
+ - ⚠ **The trace can stop before the run does, and the two failures part ways
679
+ here.** A journal that can no longer accept records — a sequence already
680
+ broken, a file that will not parse, a run already marked ended — is a lost
681
+ trace, **not** a reason to withhold work the queue can still hand out: the
682
+ selection prints, stderr carries a `run journal:` line, the exit code stays 0.
683
+ The refusals are the ones where nothing has happened yet and a second fixes
684
+ it, and there are **four**: the declaration is empty, its directory does not
685
+ exist, the path is not a directory, or the journal module is missing. Each
686
+ exits 1 with nothing on stdout.
687
+ - 🔴 A `run journal:` line on stderr is **not** the queue failing. That one is
688
+ `queue: queue-unreadable` on stdout (§0) and it ends the run; this one does
689
+ not.
690
+
691
+ **The marker is written by the stop, and the stop is a step in this skill.** A
692
+ journal whose end nobody writes leaves every run reading as still-running, which
693
+ is exactly the ambiguity the marker exists to remove.
694
+
695
+ 🔴 **At a stop — never at a checkpoint — and after the proposals below, not
696
+ before them.** The marker closes the journal to further records, so a run that
697
+ writes it mid-way keeps working while every later record is refused: a trace
698
+ truncated quietly, which is worse than one that stops loudly. It is the last
699
+ thing the run does, in document order and in wall-clock order both:
700
+
701
+ ```bash
702
+ node --input-type=module -e '
703
+ const { endRun } = await import("./.claude/scripts/run-journal.mjs");
704
+ console.log(endRun({
705
+ runDir: process.env.RIG_RUN_DIR,
706
+ stop: "<the stop condition from §3: queue-empty | budget | kill-switch | …>",
707
+ now: new Date().toISOString(),
708
+ }));
709
+ '
710
+ ```
711
+
712
+ If no run directory was declared, there is nothing to close and this step is
713
+ skipped — say so in the journal entry rather than leaving the reader to guess
714
+ which of the two happened.
715
+
716
+ **And turn the unattended flag off** — it outlives the run otherwise, and the
717
+ next attended session would find its rulebook edits refused in the name of an
718
+ item nobody is working:
719
+
720
+ ```bash
721
+ node .claude/scripts/unattended-flag.mjs off
722
+ ```
242
723
 
243
724
  At every **stop** — not at a checkpoint — turn the run's findings into **at most
244
725
  three** improvement proposals. **The cap is the mechanism, not a budget:** an
@@ -252,8 +733,10 @@ four things, and a proposal missing any of them is not ready to file:
252
733
  4. how the next run would prove it worked — the observation that would differ.
253
734
 
254
735
  Filing is the adapter's `proposeTriage`, which the CLI deliberately does **not**
255
- expose — `index.mjs` is read-only (`next`, `list`, `hygiene`) so that no accidental
256
- invocation can write to the queue. Call it directly:
736
+ expose — `index.mjs` never writes to the QUEUE (`next`, `list`, `hygiene` only), so
737
+ that no accidental invocation can change what the next run is handed. Its one
738
+ write is to the run journal above, and only into a directory the run declared —
739
+ a trace of the selection, never a change to it. Call `proposeTriage` directly:
257
740
 
258
741
  ```bash
259
742
  node --input-type=module -e '
@@ -263,12 +746,69 @@ node --input-type=module -e '
263
746
  part: "<skill | agent | hook | rule | CLAUDE.md | workflow>",
264
747
  change: "<concretely enough to diff>",
265
748
  proof: "<the observation that would differ next run>",
266
- }));
749
+ // a pair: what the probe touched, and what is concluded from it. The
750
+ // mechanism accepts a proposal without them; this procedure does not.
751
+ measured: "<the paths the probe actually exercised>",
752
+ inferred: "<the conclusion, citing only surfaces named in measured>",
753
+ }, { project: "<KEY>" })); // jira only — the project key from .claude/queue.json;
754
+ // plan-md and github-issues take no second argument
267
755
  '
268
756
  ```
269
757
 
270
758
  A proposal missing any of the four parts is refused rather than filed half-formed.
271
759
 
760
+ **A finding can say what it measured and what it inferred, as two paired fields**
761
+ (AR-142). A proposal whose premise was never true had no check at filing, only at
762
+ take-up — AR-124 was filed, promoted and claimed before its platform conclusion
763
+ was traced to a probe that had touched one hook. So `measured` and `inferred`
764
+ are separate, and `validateProposal` refuses an `inferred` that cites a path
765
+ `measured` does not, naming both fields and the path; one field without the
766
+ other is refused too, and neither files as before. The surface is a cited path
767
+ (`citedPathsOf`), so a conclusion that names no path passes this check — it
768
+ catches the path-shaped overreach and nothing subtler.
769
+
770
+ ⚠ **The pair is how a proposal opts into the check, and a proposal filed without
771
+ it is not checked at all** — `validateProposal` keeps the four-part contract, so
772
+ the AR-124 shape with neither field still files as it always did. That is the
773
+ stated limit, not an oversight: making the fields mandatory would refuse every
774
+ proposal the three adapters already file, and the loop is the author this rule
775
+ is for. So **every proposal this loop files carries both fields** — the snippet
776
+ above supplies them, and a stop that cannot say what it measured has nothing
777
+ to propose. A reviewer reading a filed proposal without the pair reads a
778
+ proposal that skipped this procedure. Pinned in the generator's
779
+ `test/template/queue.test.ts` — absent in a generated rig — › "refuses a
780
+ proposal whose inference names a surface its measurement did not touch", ›
781
+ "files a proposal whose inference stays inside what it measured", › "refuses one
782
+ of the two fields without the other" and › "a proposal with neither field files
783
+ as today".
784
+
785
+ **The filed item also records the commit it was measured against** — an `asOf:`
786
+ line, HEAD of this checkout unless the call passes its own `asOf` (`null` files
787
+ without one). It is there for `hygiene`, which lists the proposals on file and
788
+ reports one whose cited paths changed since its `asOf` as
789
+ `proposal-possibly-overtaken`, one with no `asOf` as `proposal-asof-missing`, and
790
+ one git cannot diff from as `proposal-asof-unanswerable` — never as clean. Two
791
+ proposals in a row once escalated `PREMISE FALSE` because the merge that
792
+ falsified each landed after it was filed, and selection hands out the oldest
793
+ first (AR-116). The behaviour is pinned in the generator's
794
+ `test/template/proposal-asof.test.ts` — absent in a generated rig — ›
795
+ "names the overtaken one, the unanswerable one, and stays silent on the current one".
796
+
797
+ **All three adapters write it themselves** — `jira` and `github-issues` create a
798
+ `triage`-labelled issue, `plan-md` appends a bullet to the **Operator queue**, and
799
+ each increments an existing proposal carrying the same fingerprint rather than
800
+ filing a second. `ok: true` means it is filed: there is no "I noted it in the
801
+ summary" version of filing.
802
+
803
+ `ok: false` is the one case that still needs you, and it is a structural fault
804
+ rather than a step in the procedure: `plan-md` returns it when the plan file has
805
+ no `## Operator queue` heading, because a proposal then has nowhere to land that
806
+ the selection query cannot reach. Add the heading — never the Agent queue.
807
+
808
+ One adapter needs the second argument the snippet above carries: `jira` requires
809
+ `options.project` and throws rather than filing without it — loudly, so nothing
810
+ is lost, but a call that drops it files nothing (AR-117).
811
+
272
812
  🔴 **The loop proposes; the owner patches.** Self-applying a change to its own
273
813
  rulebook is how an unattended run drifts irreversibly, and it collides head-on
274
814
  with the rule that the agent authors no work for itself.
@@ -288,10 +828,10 @@ three poisons the only channel by which this project learns.
288
828
  | **Re-aim an item whose premise turned out false** | Same rule wearing a disguise: an item silently rewritten into "what it should have said" is a work item the agent authored. Escalate it (§6) |
289
829
  | Take items needing a human decision | It cannot unblock itself; those wait in the Operator queue |
290
830
  | Take a `trigger-human` item | It would build for scale that does not exist |
291
- | Take two elevated items back to back | One unreviewed schema/permissions change is recoverable; a chain overnight is not |
831
+ | Take two mechanism-touching elevated items back to back | One unreviewed schema/permissions change is recoverable; a chain overnight is not (a prose-only elevated close clears it) |
292
832
  | Merge past a blocking reviewer verdict | The reviewer gate is what replaced the human merge |
293
833
  | Trust a `blocked` label over the links | The label is a snapshot; the links are the dependency |
294
- | "Improve" on an empty queue | An empty filtered queue is the end of the run, not an invitation |
834
+ | "Improve" on a queue that hands out nothing | Whether it stopped as empty or as held back, a run with no item is at its end, not at an invitation |
295
835
  | Start new work on an unhealthy runtime | The regression compounds into everything above it |
296
836
  | Act on the "Never" tier | A hard stop, enforced by hooks |
297
837
 
@@ -301,8 +841,97 @@ three poisons the only channel by which this project learns.
301
841
  that creates the branch or worktree. Not when the PR opens. An item being worked
302
842
  while it still reads as available is invisible to the human and re-selectable by
303
843
  the very next query.
304
- - **Closing:** close it with the merged PR linked, immediately after the
305
- post-merge verdict not in a cleanup pass.
844
+ - **Closing:** first ask whether the item is still the item you took up — a
845
+ late comment or a status somebody else moved is not published as `Done`
846
+ underneath it (AR-135):
847
+
848
+ ```bash
849
+ node .claude/scripts/revalidate.mjs --point BEFORE_CLOSE --ticket <item-id>
850
+ ```
851
+
852
+ It compares the item's marker against the newer of this run's last
853
+ validation and its take-up — an adapter re-records the take-up after each
854
+ write of its own (§2, AR-140), so a comment posted after BEFORE_PR does not
855
+ hold the close; pinned in the generator's
856
+ `test/template/self-inflicted-marker.test.ts` › "continues when the run’s own
857
+ write moved the marker after the last validation" — and its
858
+ state against the `in-progress` a close expects, journals one `revalidation`
859
+ event at `point: BEFORE_CLOSE`, and lists the item's dependants with each
860
+ one's state re-read for the write-back below — pinned in the generator's
861
+ `test/template/revalidate.test.ts` (absent in a generated rig) › "appends
862
+ exactly one BEFORE_CLOSE revalidation event after the BEFORE_PR one, and does
863
+ not end the run", › "runs the BEFORE_CLOSE revalidation before the close call
864
+ and reads a hold as a stop" and › "re-reads each dependant's state, and names
865
+ one the tracker no longer offers". On a `github-issues` queue that list is
866
+ empty: a single `gh issue view` carries no cross-index, so `find` answers no
867
+ `blocks` there (`test/template/close-transitioned.test.ts` › "github asks `gh
868
+ issue view` with the full field list and maps CLOSED to closed"). A
869
+ hold (exit 2) stops the close: re-read the item, record the outcome with
870
+ `node .claude/scripts/revalidate.mjs outcome --point BEFORE_CLOSE --ticket
871
+ <item-id> --action-changed <true | false> --note '…'`, and close only if the
872
+ re-read leaves the action standing. Then call the
873
+ adapter's `close(ticket, { prUrl, transitionId })` with the merged PR linked,
874
+ immediately after the post-merge verdict — not in a cleanup pass — and read
875
+ its answer: `ok: true` says the call ran, and only `transitioned` set to
876
+ `true` says the close landed, because every adapter reads the item back after
877
+ the transition — `jira` the status category after the POST, `github-issues`
878
+ `gh issue view --json state`, `plan-md` the line being there and then gone
879
+ (the generator's `test/template/close-transitioned.test.ts` › "GETs the issue
880
+ status after the transition POST and reports transitioned: true when the
881
+ category is done", › "runs `issue view <id> --json state` after `issue close`
882
+ and reports transitioned: true on CLOSED", › "reports transitioned: true once
883
+ the item's line is gone"). A close whose result says `transitioned: false` is
884
+ not a close: report it, and leave the item as the adapter left it — `jira` its
885
+ status, `github-issues` its `in-progress` label, which comes off only after a
886
+ read-back that says CLOSED (› "leaves the in-progress label on an issue whose
887
+ close did not land"), `plan-md` nothing, because the line was never there. The
888
+ tier below is recorded only for a close that transitioned; a close that did
889
+ not is not the "something landed" the escalation streak resets on. **Record
890
+ the tier in the same step**, because the next selection rations on it:
891
+
892
+ ```bash
893
+ node --input-type=module -e '
894
+ const { recordCompletedTier } = await import("./.claude/scripts/queue/state.mjs");
895
+ const { withoutGitLocation } = await import("./.claude/scripts/git-env.mjs");
896
+ const { execFileSync } = await import("node:child_process");
897
+ const merge = "<merge-sha>";
898
+ // The merge commit against its first parent: what the PR actually added.
899
+ const changedFiles = execFileSync(
900
+ "git", ["diff", "--name-only", "-z", `${merge}^1`, merge],
901
+ { encoding: "utf8", env: withoutGitLocation() },
902
+ ).split("\0").filter(Boolean);
903
+ console.log(recordCompletedTier({
904
+ changedFiles,
905
+ projectRoot: process.cwd(),
906
+ runDir: process.env.RIG_RUN_DIR,
907
+ }));
908
+ '
909
+ ```
910
+
911
+ Five details in that command are load-bearing — copy it, do not re-derive it:
912
+
913
+ - **`runDir`**, or the escalation streak (§3) never resets;
914
+ - **`<merge-sha>^1 <merge-sha>`**, never `origin/<default>...<merge-sha>`;
915
+ - **`-z`, and split on `\0`**, or a quoted path records the wrong tier;
916
+ - **`execFileSync` with an argument array**, never a shell string;
917
+ - **`env: withoutGitLocation()`**, or under a git hook it diffs another repo.
918
+
919
+ 🔴 One of them — `-z` — fails **silently and permissively**: it records
920
+ `normal` for an elevated change rather than refusing. The wrong diff form
921
+ refuses loudly, and omitting `runDir` fails quietly toward a stop nobody can
922
+ clear. Which fails which way, measured rather than assumed, is in
923
+ `docs/decisions/closing-a-task.md`.
924
+
925
+ 🔴 **The tier comes from the diff, never from the item's marker.** The marker
926
+ is a pre-filter (§2); `autonomy.md` decides the tier by what the change
927
+ *touches*, and rationing on the marker would mean one written a tier low
928
+ silently buys a second elevated item in a row. A marker that disagrees with
929
+ the paths is queue hygiene to report, not the value to ration on.
930
+
931
+ It **refuses** rather than guessing when the file list is empty or missing:
932
+ an absence is not a normal-tier change, and the permissive answer written
933
+ confidently is exactly how this seam went unnoticed in the first place. If it
934
+ refuses, find the file list — do not pass one to make it quiet.
306
935
  - **Write-back:** with the close, record what it **unblocked** — the items that
307
936
  were waiting on this one, by name. It is the journal's `unblocked` field, and
308
937
  it is **required, not a step for when it applies**: an absent line and an