create-agent-rig 0.4.0 → 0.5.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 (105) hide show
  1. package/CHANGELOG.md +207 -8
  2. package/README.md +50 -17
  3. package/package.json +2 -1
  4. package/packages/cli/dist/commands/create.js +8 -3
  5. package/packages/cli/dist/commands/init.js +82 -34
  6. package/packages/cli/dist/commands/upgrade.js +112 -30
  7. package/packages/cli/dist/index.js +38 -14
  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 +21 -9
  12. package/packages/cli/dist/lib/safe-path.js +30 -0
  13. package/templates/agent-os/init/AGENTS.md +191 -0
  14. package/templates/agent-os/init/CLAUDE.md +61 -9
  15. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +98 -0
  16. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
  17. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +34 -3
  18. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
  19. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +34 -0
  20. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
  21. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +233 -0
  22. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +676 -0
  23. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
  24. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
  25. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
  26. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +338 -0
  27. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
  28. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +33 -1
  29. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +64 -1
  30. package/templates/agent-os/universal/.claude/agents/security-scanner.md +33 -0
  31. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +274 -24
  32. package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
  33. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
  34. package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
  35. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
  36. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
  37. package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +480 -0
  38. package/templates/agent-os/universal/.claude/rules/autonomy.md +61 -1
  39. package/templates/agent-os/universal/.claude/rules/invariants.md +71 -16
  40. package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
  41. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1242 -0
  42. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
  43. package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
  44. package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
  45. package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +433 -0
  46. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +6 -16
  47. package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +89 -0
  48. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +334 -23
  49. package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
  50. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +7 -2
  51. package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +413 -8
  52. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +75 -13
  53. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +222 -21
  54. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
  55. package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
  56. package/templates/agent-os/universal/.claude/scripts/run-state.mjs +441 -0
  57. package/templates/agent-os/universal/.claude/scripts/verdict.mjs +101 -0
  58. package/templates/agent-os/universal/.claude/settings.json +7 -2
  59. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +115 -7
  60. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +378 -31
  61. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +300 -25
  62. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
  63. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
  64. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
  65. package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
  66. package/templates/agent-os/universal/.codex/hooks.json +65 -0
  67. package/templates/agent-os/universal/AGENTS.md +164 -0
  68. package/templates/agent-os/universal/CLAUDE.md +47 -14
  69. package/templates/agent-os/universal/PLAN.md +7 -40
  70. package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
  71. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
  72. package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
  73. package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
  74. package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
  75. package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
  76. package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
  77. package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +62 -0
  78. package/templates/agent-os/universal/journal/README.md +101 -0
  79. package/templates/agent-os/universal/layers.json +36 -2
  80. package/templates/hash-history.json +2 -1
  81. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
  82. package/templates/skeleton/aws-serverless/README.md +91 -9
  83. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
  84. package/templates/skeleton/aws-serverless/gitignore +37 -0
  85. package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
  86. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
  87. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
  88. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
  89. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
  90. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
  91. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
  92. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
  93. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
  94. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
  95. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
  96. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
  97. package/templates/skeleton/node-service/README.md +11 -1
  98. package/templates/skeleton/node-service/gitignore +34 -0
  99. package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
  100. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
  101. package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
  102. package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
  103. package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
  104. package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
  105. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +28 -0
@@ -0,0 +1,676 @@
1
+ ---
2
+ name: loop
3
+ description: The unattended work driver. Picks the next queue item through the queue adapter, runs it under the autonomy rules, journals at checkpoints, and ends the session on a stated stop condition — never inventing work. Use at the start of every autonomous run and between tasks.
4
+ allowed-tools: Read, Grep, Glob, Write, Edit, Bash, Task
5
+ argument-hint: [max-tasks]
6
+ ---
7
+
8
+ You drive an unattended session. `.claude/rules/autonomy.md` sets the behaviour
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,
12
+ what keeps the loop going, what stops it, and where the report goes.
13
+
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.
18
+
19
+ ## 0. The queue is behind an adapter
20
+
21
+ Selection never reads a tracker directly. It goes through
22
+ `.claude/scripts/queue/index.mjs`, which resolves the adapter named in
23
+ `.claude/queue.json`:
24
+
25
+ ```bash
26
+ node .claude/scripts/queue/index.mjs next # the item to take, and why the rest were skipped
27
+ node .claude/scripts/queue/index.mjs next --json
28
+ node .claude/scripts/queue/index.mjs hygiene # stale labels, link anomalies
29
+ ```
30
+
31
+ - **`plan-md`** (default) — the Agent queue in `PLAN.md`. The only adapter that
32
+ works in a freshly generated project. Its limit is real and stated in the
33
+ adapter: a flat list carries **no dependency links**, so the blocker filter is
34
+ absent rather than satisfied.
35
+ - **`github-issues`** — the upgrade once the project has a remote. Per-item state,
36
+ a comment thread, and dependencies written as `Blocked by #7` in the body.
37
+ - **`jira`** — for a team that already lives there. Native issue links, so the
38
+ dependency needs no convention. Credentials come from the environment
39
+ (`JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN`) and never from a file in the
40
+ repo; the project or the JQL goes in `.claude/queue.json`.
41
+
42
+ Adding a fourth is an adapter, not a rewrite: `core.mjs` holds every selection
43
+ decision and each adapter only maps its tracker's records onto the neutral shape.
44
+
45
+ 🔴 **If the queue cannot be read, stop the run and say so.** Never fall back to
46
+ memory, to a stale copy, or to "what I remember was next". A remembered queue is
47
+ how a loop works items that no longer exist, and it is exactly the rot the
48
+ state-vs-queue split exists to prevent.
49
+
50
+ ## 1. Preflight — once, before the first task
51
+
52
+ ```bash
53
+ node .claude/scripts/preflight.mjs
54
+ ```
55
+
56
+ Three items are scripted (kill switch absent · local default branch matches the
57
+ remote · the last deploy concluded successfully) and the script **prints the ones
58
+ it did not check, every time**. Paste the block into the journal: a checklist that
59
+ leaves no record cannot tell you it was skipped.
60
+
61
+ Verdicts: **STOP** → do not start, deal with the cause. **CAUTION** → start,
62
+ knowing which ground is soft. **GO** → the scripted three are clean; the rest are
63
+ still yours.
64
+
65
+ **An `unknown` never becomes a `pass`.** A probe that could not run tells you
66
+ nothing.
67
+
68
+ **First-ever run: attended and short** — one normal item, owner watching. Go
69
+ unattended only after the escalation path and the post-deploy verdict have each
70
+ been seen working at least once.
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
+ 🔴 **One directory per run, never shared and never reused — and the journal
91
+ cannot enforce this for you.** A collision or an already-ended directory is
92
+ refused loudly; two runs whose records merely do not collide are merged into one
93
+ seamless trace with nothing able to say so. A fresh directory per run is the
94
+ only thing that prevents it, and it is yours to do — the exact boundary is in
95
+ `docs/decisions/run-directory.md`.
96
+
97
+ ## 2. Selection — filters in order, then the sort
98
+
99
+ The queue is queried **fresh before every task**, never from a cached list: the
100
+ loop itself closes items and unblocks their dependents, so a list read at the
101
+ start of the run is wrong by the second task. Re-resolve after every close.
102
+
103
+ The filters, in order, are implemented in `.claude/scripts/queue/core.mjs` — read
104
+ them there rather than re-deriving them here. Two of them are load-bearing enough
105
+ to restate:
106
+
107
+ 🔴 **Blockers resolve from links, never from labels.** A `ready`/`blocked` label is
108
+ a hand-maintained snapshot; the links are the dependency. Nothing updates a
109
+ dependent's label when its blocker lands — and in continuous mode the loop is what
110
+ landed it. A label-driven loop stalls on work it just unblocked itself, and takes
111
+ work whose blocker is still open. Both directions happen. Stale labels are
112
+ **reported as queue hygiene, never silently corrected**: quietly fixing the
113
+ metadata destroys the evidence that the metadata is unreliable.
114
+
115
+ 🔴 **A missing trigger marker means unconditional, not missing data.** Work that is
116
+ genuinely conditional says so. A `trigger-human` item — a "security pass", a
117
+ "window", "user demand" without a named metric — is **never taken on that marker
118
+ alone**; the human hands it over explicitly. A `trigger-auto` item needs its
119
+ trigger verified *this run*: unverified is not fired, and rationalising a
120
+ trigger into firing builds for scale that does not exist.
121
+
122
+ ⚠ **An item carrying BOTH markers is taken as `trigger-auto`.** Every adapter
123
+ resolves `auto` first, nothing refuses the combination, and no hygiene check
124
+ reports it — so one recorded declaration takes an item whose author also marked
125
+ it human-gated. The reachable path is an owner tightening an auto-gated item and
126
+ not deleting the old marker, and the silent resolution goes to the **less**
127
+ restrictive gate. Until that is fixed, treat a double-marked item as
128
+ human-gated by hand.
129
+
130
+ **For a `trigger-auto` item, record the declaration** — it has to outlive the
131
+ turn it was made in, or the next selection holds the item back again:
132
+
133
+ ```bash
134
+ node .claude/scripts/run-state.mjs trigger <item-id>
135
+ ```
136
+
137
+ 🔴 **This does nothing for a `trigger-human` item, and the command will not tell
138
+ you so.** Selection refuses that kind outright — it never consults the record —
139
+ so the only thing that makes one takeable is a human changing the item's own
140
+ marker. Recording a "declaration" against it succeeds, prints, and leaves the
141
+ item exactly as unselectable as before.
142
+
143
+ ⚠ **It is keyed by the item's id — and under `plan-md` that id is the item's
144
+ POSITION in the list.** So a declaration made for the third bullet transfers to
145
+ whatever occupies the third slot after someone edits `PLAN.md`, for the rest of
146
+ the run. Re-check the item the declaration names before acting on it, or use an
147
+ adapter whose ids are stable (`github-issues`, `jira`). There is no un-fire
148
+ word: a new run starts with a clean state, which is the same remedy the budget
149
+ stop relies on.
150
+
151
+ **The elevated tier is rationed by spacing, not by counting** — a per-run count is
152
+ meaningless when the run has no end. Never two elevated items back to back **when
153
+ the first one touched a mechanism** (next paragraph): land a normal item on a
154
+ healthy runtime in between. One unreviewed permissions or schema change is
155
+ recoverable; a chain of them compounding overnight is not.
156
+
157
+ **Only a change that EXECUTES spaces the next item.** The close records
158
+ `normal` | `elevated-prose` | `elevated-mechanism`, computed from the diff's
159
+ paths (`queue/state.mjs`): an elevated change whose elevated paths are all
160
+ documents is `elevated-prose` and clears the ration, because a document cannot
161
+ compound into a broken runtime overnight. Prose keeps its tier everywhere it is
162
+ *reviewed* — model lane, cold readers, `human-review`, the gate sweep — and loses
163
+ it only here. A tier outside that vocabulary **holds**, never releases
164
+ (`docs/decisions/spacing-rations-mechanisms.md`).
165
+
166
+ **The tier marker is a pre-filter, not the authority.** If an item passed as normal
167
+ and the work turns out to touch an elevated path (`CLAUDE.md` →
168
+ `elevated-paths`), run the gate anyway, record the verdict on the PR, and treat it
169
+ as this run's elevated item for spacing.
170
+
171
+ **Then, before the Red step: `check-premises`.** The item was written by someone
172
+ who was not reading the code at the time, and everything downstream — the failing
173
+ test, the implementation, the reviewer comparing diff to item — inherits its
174
+ claims rather than checking them. On `PREMISE FALSE` the item is escalated (§6),
175
+ not repaired in place: a run that silently re-aims its own task has authored work
176
+ for itself, which is the one thing this loop does not do (§8).
177
+
178
+ 🔴 **And again at the other end, before `pr-ship`: `check-premises` on the prose the
179
+ task itself wrote** — the rulebook prose the diff touches (the skill defines that set,
180
+ and it is the one `workflow.md` already uses for the `prose-reviewer` trigger), plus
181
+ the PR description once one exists. The verdict is `UNMEASURED`, with two exits:
182
+ delete the sentence, or make it a pointer to the test that proves it. Run it before
183
+ the gate: a reviewer reaches the same sentence only after loading the whole diff, and
184
+ that is a round spent on what an edit would have fixed.
185
+
186
+ `PREMISE FALSE` stays with the first pass. At the second one the claims are your own
187
+ and the remedy is an edit, so nothing escalates.
188
+
189
+ **Both passes end in a block, and you check it before you act on it** — the skill
190
+ returns one fenced `json` verdict like every other gate, and this loop is its caller:
191
+
192
+ ```sh
193
+ node .claude/scripts/verdict.mjs check <report> check-premises
194
+ ```
195
+
196
+ Exit 1 means it did not answer: a stop verdict naming no premise, or no block at all.
197
+ That is `incomplete` — neither "the premises hold" nor a reason to escalate — so run
198
+ the pass again rather than reading the silence as a pass.
199
+
200
+ ## 3. What keeps the loop running, and what stops it
201
+
202
+ Per-task stops (three strikes, attempt budget, invariant conflict, a blocking
203
+ reviewer verdict, an exhausted gate-round cap, a false premise in the item itself)
204
+ **do not end the run**: escalate that item (§6) and take the next one.
205
+
206
+ The run-level conditions are in `stopConditionOf` in `core.mjs`, checked in
207
+ severity order: **queue unreadable** · **runtime regression** · **kill switch** ·
208
+ **two escalations in a row** · **budget** · **nothing selectable** · **queue
209
+ empty**.
210
+
211
+ 🔴 **Their inputs come from a file, not from your memory — and that is why they
212
+ fire at all.** `escalations` and `lastDeployVerdict` live in
213
+ `<RIG_RUN_DIR>/state.json`, written by `run-state.mjs`. A stop condition held in
214
+ a session's memory is absent exactly when it is needed, because compaction is
215
+ what a long run does (`docs/decisions/stop-conditions-in-a-file.md`).
216
+
217
+ **One of the three writes itself; two you write.** The escalation count needs
218
+ nothing from you beyond using the documented calls — but it has two writers, and
219
+ both have to be the documented one:
220
+
221
+ - it **rises** through every adapter's `escalate()` (§6), which is why
222
+ escalating by hand-labelling the item counts nothing;
223
+ - it **resets** through the close step's `recordCompletedTier`, and **only when
224
+ you pass it `runDir`** (§9 has the command; it is one of that call's
225
+ load-bearing arguments, not an optional extra). Omit it and the count is
226
+ monotonic: two escalations an hour apart end the run however many tasks
227
+ landed in between.
228
+
229
+ The other two are yours, after the checks that produce them:
230
+
231
+ ```bash
232
+ # the post-deploy check answers in a block like every other gate, and the word
233
+ # below is retyped out of it — so check the block before you trust the word
234
+ node .claude/scripts/verdict.mjs check <report> post-deploy-verify
235
+
236
+ # after the post-deploy check (`.claude/rules/autonomy.md`, "Post-deploy
237
+ # verification") — REGRESSION stops the next selection, HEALTHY clears it
238
+ node .claude/scripts/run-state.mjs deploy REGRESSION
239
+
240
+ # when the declared budget (§4) cannot fit another task
241
+ node .claude/scripts/run-state.mjs budget exhausted
242
+ ```
243
+
244
+ Both **refuse** a word outside their vocabulary and refuse to run with no
245
+ `RIG_RUN_DIR`, rather than writing something the stop conditions cannot match: a
246
+ file that looks recorded and stops nothing is worse than no file.
247
+
248
+ **Only the deploy verdict can be taken back**, and the asymmetry is deliberate:
249
+ `HEALTHY` names a real later event — the revert landed — while un-exhausting a
250
+ budget would name only a decision taken by the run that declared the stop
251
+ (`docs/decisions/stop-conditions-in-a-file.md`). A new run gets a clean state;
252
+ that is the way back from both.
253
+
254
+ Selection itself — `node .claude/scripts/queue/index.mjs next`, §0 — is what
255
+ *reads* these and stops on them. It is still the command you run to get work.
256
+
257
+ ⚠ **The kill switch is not among them.** It stays mechanical in `guard-bash`
258
+ and scripted in preflight; `next` does not check for the flag, so **keep
259
+ checking it between tasks** — the brake block later in this section says how.
260
+
261
+ Four of them deserve their reasons repeated:
262
+
263
+ - **Runtime regression** → deploy the revert first, diagnose second, start no new
264
+ work on top. A regression compounds into everything built above it.
265
+ - **Two escalations in a row** → stop. If two consecutive tasks hit walls, the
266
+ third likely will too: the wall is systemic, not task-local. This is the main
267
+ guard against grinding a broken assumption for hours.
268
+ - **Queue empty after the filters** → **stop; do not invent work.** No refactoring
269
+ sprees, no polish, no pre-emptive optimisation. An empty filtered queue is a
270
+ legitimate, successful end of session; refilling it is the owner's job.
271
+ **Expect this to be the most common ending** — the queue is finite and the loop
272
+ drains it. That is the system working. The stop line also names the **parked**
273
+ pile if there is one, by cause and count: items out of play, waiting on a
274
+ human. They are reported next to the verdict, never swept into it. 🔴 Under
275
+ `plan-md` an escalation leaves no mark on the queue at all — `escalate`
276
+ returns `ok: false` with the instruction to move the item to the Operator
277
+ queue **in the same edit**, and skipping that move means the next run takes
278
+ the stuck item straight back.
279
+ - **Nothing selectable** → also a clean stop, and **not the same finding**.
280
+ Takeable work is still there and every piece of it is **held back by a
281
+ condition that clears when something else happens, not by refilling the
282
+ queue**: the elevated spacing (a normal or prose-only item lands), a blocker (its item
283
+ closes), in-progress (the other session finishes), a trigger (a human
284
+ declares it — and for a `trigger-auto` item that declaration is **written**,
285
+ §2, so this is the one hold that needs a command rather than only time). The
286
+ stop line names how many and by which, because the two endings ask the owner
287
+ for opposite things: an empty queue wants refilling, a held one wants
288
+ interleaving or simply time. 🔴 **A parked cause outranks a holding one on the
289
+ same item** — an escalated item is left claimed on purpose, so it arrives
290
+ carrying `in-progress` too. **Neither ending is an invitation to refill the
291
+ queue or invent work.** Why the two are split, and how the parked pile grows
292
+ per adapter: `docs/decisions/two-empty-endings.md`.
293
+
294
+ 🔴 **The kill switch is a real file, not an intention:**
295
+
296
+ ```bash
297
+ touch ~/.claude/__PROJECT_NAME__-loop-STOP # brake on
298
+ rm ~/.claude/__PROJECT_NAME__-loop-STOP # brake off
299
+ ```
300
+
301
+ While it exists, `guard-bash.mjs` **denies the merge at the tool layer**, so
302
+ nothing lands even if this file is never read. Everything else stays allowed on
303
+ purpose: finish the current task, push the branch, open the PR, write the journal,
304
+ stop. Losing in-flight work is not what stopping cleanly means. Check for the flag
305
+ between tasks rather than waiting to be denied.
306
+
307
+ ## 4. Budget — a rate, not a task count
308
+
309
+ There is no "max N tasks" ceiling by default; the bound is **cost per run or per
310
+ rolling day**, declared at the start. Meter it **before** picking the next task,
311
+ not after. If the remaining allowance cannot plausibly fit the next task's size,
312
+ stop now rather than starting something that will be abandoned half-done.
313
+
314
+ The journal's `cost` block carries only what the session **observes**: reviewer
315
+ subagents run, CI runs consumed (re-runs included — the cheapest signal that a task
316
+ fought its tests), deploys triggered.
317
+
318
+ ⚠ **There is deliberately no token or currency column.** The harness does not
319
+ reliably expose per-subagent accounting to the agent, and a plausible number in a
320
+ cost column **will be believed** — by the next reader, and by the next run
321
+ reasoning about its own budget. A field the loop cannot observe stays **visibly
322
+ empty, never estimated.**
323
+
324
+ **Where the budget lives, and what is honest about it.** The **decision** is
325
+ recorded, not the arithmetic:
326
+
327
+ ```bash
328
+ node .claude/scripts/run-state.mjs budget exhausted
329
+ ```
330
+
331
+ That sets the flag `stopConditionOf` reads, so the next selection stops with
332
+ `queue: budget` — and it survives a compaction, which is the whole reason it is
333
+ a file. **Nothing computes it for you**: the counters above are the ones the
334
+ session observes, and judging that the remaining allowance cannot fit another
335
+ task stays yours. There is deliberately no field holding a spend figure, for the
336
+ same reason the journal has no currency column — a number the run cannot
337
+ observe, written where a stop condition reads, is a fiction with authority.
338
+
339
+ ## 5. Every task carries an outcome state
340
+
341
+ The stop conditions say why the loop stopped. None of them says whether what it
342
+ produced is trustworthy, and those are different questions — a run can stop for a
343
+ perfectly good reason having produced something nobody should build on.
344
+
345
+ | State | Means |
346
+ | --- | --- |
347
+ | `clean-pass` | every stage produced its artifact from its **documented inputs**; checks green; the deployed surface healthy where one changed |
348
+ | `documented-stall` | it stopped at a real wall, and the diagnosis names **which stage needed what, and which upstream stage should have supplied it** |
349
+ | `incomplete` | it stopped and the record does not explain where or why |
350
+
351
+ 🔴 **What `documented-stall` requires is the STAGE and the wall, not a full
352
+ inventory of findings** — and this had to be settled, because the two readings
353
+ disagreed the first time a stop arrived without an inventory. An exhausted
354
+ gate-round cap names its stage (the gate) and its wall (two rounds of fixes did not
355
+ converge), while the individual blockers behind it are not persisted anywhere until
356
+ per-round verdicts exist. That is a `documented-stall`: the record locates the wall
357
+ and the next reader knows where to look.
358
+
359
+ `incomplete` is for a record that cannot say **where** it stopped — not for one that
360
+ can say where but not everything about it. Widening `incomplete` to cover a thin
361
+ diagnosis would make it the common case, and it is meant to be the rare one: it is
362
+ the only failing state, and a state that fires on honest stops stops being read.
363
+
364
+ 🔴 **`documented-stall` is a success, and reading it as a failure is how this stops
365
+ working.** A stall that names its under-supply is the most useful thing an
366
+ unattended run produces: it converts a vague gap into a located, fixable defect.
367
+ `incomplete` is the only failing state — and it fails on the **record**, not the
368
+ outcome. A task that shipped nothing but explained exactly where it hit the wall
369
+ did better work than one that shipped something nobody can retrace.
370
+
371
+ ### 5.1 The no-hand-feeding rule
372
+
373
+ > **When a stage asks for context an earlier stage should have supplied, do not
374
+ > answer it with new facts.** Record what it needed and which stage should have
375
+ > carried it, then either continue from the documented inputs plus a **labelled
376
+ > assumption**, or stop. **The stall is the finding.**
377
+
378
+ An unattended run is structurally biased the other way: a later stage asks for
379
+ something nobody supplied, the driver answers from its own head because it happens
380
+ to know, and the line keeps moving. The output looks clean and is fiction —
381
+ assembled from context no documented input contains and no reader can retrace.
382
+
383
+ Two corollaries, because they are the ones rationalised away mid-run:
384
+
385
+ - **A stage that finishes without producing its named artifact is an under-supply
386
+ finding, not a retry.** Never rerun it until the output looks clean — that is
387
+ re-running CI until it goes green, one level up.
388
+ - **"I already know this" is the signal, not the exemption.** The question is not
389
+ whether the fact is true; it is whether the pipeline supplied it. A true fact
390
+ injected by hand still leaves the next run starving in the same place.
391
+
392
+ ⚠ **Known limit, documented rather than trusted:** both halves rely on the run
393
+ reporting on itself, and hand-feeding is ordinary helpful completion rather than a
394
+ deliberate act a model catches itself performing. A stated rule still shifts
395
+ behaviour and costs nothing per run — but it is precisely why the one status that
396
+ mechanises fully (`missed`, `.claude/rules/autonomy.md`) needs no self-report.
397
+
398
+ ## 6. Escalation — two channels, by scope
399
+
400
+ **Task-scoped — the item is the home, and the loop continues.** Three strikes, the
401
+ attempt budget, an invariant conflict, a blocking reviewer verdict, an **exhausted
402
+ gate-round cap**, or a `PREMISE FALSE` verdict from `check-premises` **on the queue
403
+ item**. The last two are
404
+ a `documented-stall` (§5) and their diagnoses differ, so take the one that matches the
405
+ stop: a false premise writes what the item claimed, what the code says, and the
406
+ citation; an exhausted cap writes the round count and what the last gate reported.
407
+ Both then follow the same three steps:
408
+
409
+ 1. Comment the diagnosis on the queue item, in the shape
410
+ `.claude/rules/autonomy.md` ("Escalation format") sets — **cite it rather
411
+ than working from this list**, which is a reminder and is short by two of
412
+ its clauses: what was *observed* (verbatim errors, not summaries), and the
413
+ single question whose answer unblocks the work. So: what fails, what was tried, the
414
+ current hypothesis, and links to the PR and the failing run where they exist
415
+ — a premise stop has neither, and its citation stands in for both. **Name the outcome
416
+ state in the same comment** — `incomplete` if the diagnosis cannot say **where** it
417
+ stopped (§5: a thin diagnosis that still locates the wall is a `documented-stall`).
418
+ Writing `incomplete` on your own task is uncomfortable and
419
+ is the point: the run that produced it is the only witness.
420
+ 2. Mark it `escalated` and leave it claimed — **not** back to a selectable state,
421
+ or the next query picks it up and works it twice.
422
+ 3. Journal it. 4. **Take the next item.** One stuck task does not end a run; two
423
+ in a row does (§3).
424
+
425
+ 🔴 **The gate-round cap is the stop a run will not reach on its own.** Every other
426
+ stop here has a red thing behind it; a gate that keeps finding fixable prose is all
427
+ green, so three strikes never fires and the run has no reason to stop re-entering it.
428
+ `pr-ship` step 0 counts the round per branch in `.claude/gate-rounds.json` and exits 2
429
+ past the cap. The count outlives the session, which is the point — a counter held in
430
+ context is one the next context does not have.
431
+
432
+ ⚠ **What the cap does not carry, stated because the gap decides what you can write in
433
+ the escalation:** the counter records the round and nothing about it. Findings travel
434
+ separately, and the two do not cover the same ground — the count is **per branch, in
435
+ the main checkout, across runs**, while a verdict is journalled into **this run's**
436
+ directory. So `decisions.jsonl` holds the rounds this run spent and nothing about a
437
+ round spent before it: a branch on its third round in its second run has one round's
438
+ findings here, and the earlier ones in whichever directory that run declared, or
439
+ nowhere if it declared none.
440
+
441
+ Write the diagnosis from what you can actually read — the round count, plus this run's
442
+ records where there are any — and say which of the rounds that leaves unaccounted for
443
+ rather than reconstructing them from memory. `pr-ship` is told to journal each verdict
444
+ that parsed, and like every gate here that is a step in a skill rather than a
445
+ mechanism: a round whose session skipped it left no record either.
446
+
447
+ 🔴 **Escalate through the adapter, never by hand-labelling the item.** Every
448
+ adapter's `escalate()` counts the escalation into the run state as it marks the
449
+ item, which is what makes "two in a row" a condition the next selection can
450
+ check rather than one you have to remember across a compaction. Adding the
451
+ label yourself marks the item and counts nothing — and the run then grinds past
452
+ the wall this rule exists to stop it at. (`plan-md` still returns `ok: false`,
453
+ because a flat list has no per-item state to mark; the count is recorded all the
454
+ same, and moving the item to the Operator queue is still yours.)
455
+
456
+ **Run-scoped — the run itself is broken, and it ends.** A runtime regression, two
457
+ escalations in a row, a systemic wall, a queue-data anomaly: open an escalation
458
+ issue with the diagnosis and links, notify the owner if the harness can, and write
459
+ the journal entry. In continuous mode the notification matters more than it does
460
+ in a bounded run — nobody is watching, so a silent stop is indistinguishable from
461
+ a run still working.
462
+
463
+ ## 7. The journal, and closing the loop
464
+
465
+ Write a checkpoint entry **every few completed items and at every stop**, not only
466
+ at the end: a run that dies unexpectedly must not take its history with it. The
467
+ entry goes at the TOP of `journal/YYYY-MM.md` — this month's file, newest-on-top
468
+ — and the field list is in `journal/README.md` next to it.
469
+
470
+ **Behind that entry there is a machine trace, and it is a different artifact.**
471
+ `.claude/scripts/run-journal.mjs` writes gate verdicts to `decisions.jsonl` and
472
+ everything else to `events.jsonl`, both append-only, inside the run directory
473
+ declared in §1. Five things about it are worth knowing before relying on it:
474
+
475
+ - **The run declares the directory; nothing invents one.** With `RIG_RUN_DIR`
476
+ unset, every call site stays silent — the *trace* is opt-in, and a run that
477
+ never declared one has no journal rather than a journal in a guessed place.
478
+ 🔴 **The stop conditions in the same directory are not opt-in** (§1): an
479
+ undeclared run also stops counting escalations, and that half is silent too.
480
+ Read "opt-in" as describing this file, never the declaration.
481
+ - **It answers *what the run decided and on what basis*, never *was that
482
+ right*.** It replaces neither the month file above nor `PLAN.md`; it is the
483
+ evidence a reader checks those against. It is also **oldest-first**, where the
484
+ month file is newest-on-top — reading one as the other is how a reader
485
+ concludes a run did nothing.
486
+ - **A record after the run-end marker is refused, and a broken sequence is
487
+ refused on both write and read.** The order is asserted rather than described,
488
+ so a stale record cannot read as the current one — which is the whole failure a
489
+ journal exists to prevent.
490
+ - ⚠ **The trace can stop before the run does, and the two failures part ways
491
+ here.** A journal that can no longer accept records — a sequence already
492
+ broken, a file that will not parse, a run already marked ended — is a lost
493
+ trace, **not** a reason to withhold work the queue can still hand out: the
494
+ selection prints, stderr carries a `run journal:` line, the exit code stays 0.
495
+ The refusals are the ones where nothing has happened yet and a second fixes
496
+ it, and there are **four**: the declaration is empty, its directory does not
497
+ exist, the path is not a directory, or the journal module is missing. Each
498
+ exits 1 with nothing on stdout.
499
+ - 🔴 A `run journal:` line on stderr is **not** the queue failing. That one is
500
+ `queue: queue-unreadable` on stdout (§0) and it ends the run; this one does
501
+ not.
502
+
503
+ **The marker is written by the stop, and the stop is a step in this skill.** A
504
+ journal whose end nobody writes leaves every run reading as still-running, which
505
+ is exactly the ambiguity the marker exists to remove.
506
+
507
+ 🔴 **At a stop — never at a checkpoint — and after the proposals below, not
508
+ before them.** The marker closes the journal to further records, so a run that
509
+ writes it mid-way keeps working while every later record is refused: a trace
510
+ truncated quietly, which is worse than one that stops loudly. It is the last
511
+ thing the run does, in document order and in wall-clock order both:
512
+
513
+ ```bash
514
+ node --input-type=module -e '
515
+ const { endRun } = await import("./.claude/scripts/run-journal.mjs");
516
+ console.log(endRun({
517
+ runDir: process.env.RIG_RUN_DIR,
518
+ stop: "<the stop condition from §3: queue-empty | budget | kill-switch | …>",
519
+ now: new Date().toISOString(),
520
+ }));
521
+ '
522
+ ```
523
+
524
+ If no run directory was declared, there is nothing to close and this step is
525
+ skipped — say so in the journal entry rather than leaving the reader to guess
526
+ which of the two happened.
527
+
528
+ At every **stop** — not at a checkpoint — turn the run's findings into **at most
529
+ three** improvement proposals. **The cap is the mechanism, not a budget:** an
530
+ unbounded improvement list is another diary, and three forces a choice. Each names
531
+ four things, and a proposal missing any of them is not ready to file:
532
+
533
+ 1. the finding it came from, cited as the journal line it appears on;
534
+ 2. the part to change — a skill, an agent spec, a hook, a rule file, `CLAUDE.md`,
535
+ the CI workflow;
536
+ 3. the change, concretely enough to diff;
537
+ 4. how the next run would prove it worked — the observation that would differ.
538
+
539
+ Filing is the adapter's `proposeTriage`, which the CLI deliberately does **not**
540
+ expose — `index.mjs` never writes to the QUEUE (`next`, `list`, `hygiene` only), so
541
+ that no accidental invocation can change what the next run is handed. Its one
542
+ write is to the run journal above, and only into a directory the run declared —
543
+ a trace of the selection, never a change to it. Call `proposeTriage` directly:
544
+
545
+ ```bash
546
+ node --input-type=module -e '
547
+ const a = await import("./.claude/scripts/queue/plan-md.mjs"); // or github-issues / jira
548
+ console.log(await a.proposeTriage({
549
+ finding: "<the journal line it came from>",
550
+ part: "<skill | agent | hook | rule | CLAUDE.md | workflow>",
551
+ change: "<concretely enough to diff>",
552
+ proof: "<the observation that would differ next run>",
553
+ }));
554
+ '
555
+ ```
556
+
557
+ A proposal missing any of the four parts is refused rather than filed half-formed.
558
+
559
+ **All three adapters write it themselves** — `jira` and `github-issues` create a
560
+ `triage`-labelled issue, `plan-md` appends a bullet to the **Operator queue**, and
561
+ each increments an existing proposal carrying the same fingerprint rather than
562
+ filing a second. `ok: true` means it is filed: there is no "I noted it in the
563
+ summary" version of filing.
564
+
565
+ `ok: false` is the one case that still needs you, and it is a structural fault
566
+ rather than a step in the procedure: `plan-md` returns it when the plan file has
567
+ no `## Operator queue` heading, because a proposal then has nowhere to land that
568
+ the selection query cannot reach. Add the heading — never the Agent queue.
569
+
570
+ One adapter needs more than the snippet above carries: `jira` requires
571
+ `options.project` and throws rather than filing without it. It fails loudly, so
572
+ nothing is lost — but called exactly as written, it does not file.
573
+
574
+ 🔴 **The loop proposes; the owner patches.** Self-applying a change to its own
575
+ rulebook is how an unattended run drifts irreversibly, and it collides head-on
576
+ with the rule that the agent authors no work for itself.
577
+
578
+ 🔴 **Proposals land in `triage`, never in the queue the loop selects from.** A
579
+ triage item is unselectable twice over — no ready marker, and `triage` is excluded
580
+ outright. **Deduplicate by fingerprint:** twenty "queue empty" stops must produce
581
+ one proposal with a count of twenty, not twenty proposals. **A stop with nothing
582
+ worth proposing files nothing** — zero is a legitimate number, and padding to
583
+ three poisons the only channel by which this project learns.
584
+
585
+ ## 8. What the loop does NOT do
586
+
587
+ | Does not | Why |
588
+ | --- | --- |
589
+ | **Create its own work items** | The queue is human-filled. Self-authored work drifts scope, and unattended it drifts unwatched |
590
+ | **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) |
591
+ | Take items needing a human decision | It cannot unblock itself; those wait in the Operator queue |
592
+ | Take a `trigger-human` item | It would build for scale that does not exist |
593
+ | 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) |
594
+ | Merge past a blocking reviewer verdict | The reviewer gate is what replaced the human merge |
595
+ | Trust a `blocked` label over the links | The label is a snapshot; the links are the dependency |
596
+ | "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 |
597
+ | Start new work on an unhealthy runtime | The regression compounds into everything above it |
598
+ | Act on the "Never" tier | A hard stop, enforced by hooks |
599
+
600
+ ## 9. State updates bracket the task, they are not a follow-up
601
+
602
+ - **Opening:** claim the item **before the first file is edited** — the same turn
603
+ that creates the branch or worktree. Not when the PR opens. An item being worked
604
+ while it still reads as available is invisible to the human and re-selectable by
605
+ the very next query.
606
+ - **Closing:** close it with the merged PR linked, immediately after the
607
+ post-merge verdict — not in a cleanup pass. **Record the tier in the same
608
+ step**, because the next selection rations on it:
609
+
610
+ ```bash
611
+ node --input-type=module -e '
612
+ const { recordCompletedTier } = await import("./.claude/scripts/queue/state.mjs");
613
+ const { withoutGitLocation } = await import("./.claude/scripts/git-env.mjs");
614
+ const { execFileSync } = await import("node:child_process");
615
+ const merge = "<merge-sha>";
616
+ // The merge commit against its first parent: what the PR actually added.
617
+ const changedFiles = execFileSync(
618
+ "git", ["diff", "--name-only", "-z", `${merge}^1`, merge],
619
+ { encoding: "utf8", env: withoutGitLocation() },
620
+ ).split("\0").filter(Boolean);
621
+ console.log(recordCompletedTier({
622
+ changedFiles,
623
+ projectRoot: process.cwd(),
624
+ runDir: process.env.RIG_RUN_DIR,
625
+ }));
626
+ '
627
+ ```
628
+
629
+ Five details in that command are load-bearing — copy it, do not re-derive it:
630
+
631
+ - **`runDir`**, or the escalation streak (§3) never resets;
632
+ - **`<merge-sha>^1 <merge-sha>`**, never `origin/<default>...<merge-sha>`;
633
+ - **`-z`, and split on `\0`**, or a quoted path records the wrong tier;
634
+ - **`execFileSync` with an argument array**, never a shell string;
635
+ - **`env: withoutGitLocation()`**, or under a git hook it diffs another repo.
636
+
637
+ 🔴 One of them — `-z` — fails **silently and permissively**: it records
638
+ `normal` for an elevated change rather than refusing. The wrong diff form
639
+ refuses loudly, and omitting `runDir` fails quietly toward a stop nobody can
640
+ clear. Which fails which way, measured rather than assumed, is in
641
+ `docs/decisions/closing-a-task.md`.
642
+
643
+ 🔴 **The tier comes from the diff, never from the item's marker.** The marker
644
+ is a pre-filter (§2); `autonomy.md` decides the tier by what the change
645
+ *touches*, and rationing on the marker would mean one written a tier low
646
+ silently buys a second elevated item in a row. A marker that disagrees with
647
+ the paths is queue hygiene to report, not the value to ration on.
648
+
649
+ It **refuses** rather than guessing when the file list is empty or missing:
650
+ an absence is not a normal-tier change, and the permissive answer written
651
+ confidently is exactly how this seam went unnoticed in the first place. If it
652
+ refuses, find the file list — do not pass one to make it quiet.
653
+ - **Write-back:** with the close, record what it **unblocked** — the items that
654
+ were waiting on this one, by name. It is the journal's `unblocked` field, and
655
+ it is **required, not a step for when it applies**: an absent line and an
656
+ unpaid debt are the same observation from outside, so the empty case has to
657
+ be written to mean anything. Which empty case matters — "nothing was waiting"
658
+ is an answer, "this queue has no dependency links" is the absence of one
659
+ (§0), and a queue that cannot be asked must never be reported as asked.
660
+
661
+ 🔴 It is a **report, not an edit to those items.** Blocked state is
662
+ re-resolved from the blocker itself on every selection (§2), so nothing is
663
+ stuck waiting to be corrected — and a label fixed by hand is evidence
664
+ destroyed, which §2 forbids by name. What the write-back buys is the thing no
665
+ query can answer: whether anyone **looked**. Where the close changed a fact
666
+ rather than a state — an Operator-queue item it settles — the paragraph
667
+ closing this section applies instead, and that edit lands in the same PR.
668
+
669
+ Between the opening and the close the item keeps absorbing what happens **as it happens** —
670
+ a decision, a deviation, a defect found in passing, a tier discovered mid-work. A
671
+ run that dies mid-task leaves its whole trail on the item; a run that batches its
672
+ comments to the end leaves nothing.
673
+
674
+ If the task changed a **fact** in `PLAN.md` — a state row, a standing decision, an
675
+ Operator-queue item it unblocked — that edit lands **in the same PR that changed
676
+ the fact**, never a docs-only follow-up.