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.
- package/CHANGELOG.md +207 -8
- package/README.md +50 -17
- package/package.json +2 -1
- package/packages/cli/dist/commands/create.js +8 -3
- package/packages/cli/dist/commands/init.js +82 -34
- package/packages/cli/dist/commands/upgrade.js +112 -30
- package/packages/cli/dist/index.js +38 -14
- package/packages/cli/dist/lib/copy-tree.js +35 -6
- package/packages/cli/dist/lib/init-settings.js +12 -0
- package/packages/cli/dist/lib/install-set.js +6 -8
- package/packages/cli/dist/lib/manifest.js +21 -9
- package/packages/cli/dist/lib/safe-path.js +30 -0
- package/templates/agent-os/init/AGENTS.md +191 -0
- package/templates/agent-os/init/CLAUDE.md +61 -9
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +98 -0
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +34 -3
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +34 -0
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +233 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +676 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +338 -0
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +33 -1
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +64 -1
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +33 -0
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +274 -24
- package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
- package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +480 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +61 -1
- package/templates/agent-os/universal/.claude/rules/invariants.md +71 -16
- package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1242 -0
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
- package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
- package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
- package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +433 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +6 -16
- package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +89 -0
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +334 -23
- package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +7 -2
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +413 -8
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +75 -13
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +222 -21
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
- package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
- package/templates/agent-os/universal/.claude/scripts/run-state.mjs +441 -0
- package/templates/agent-os/universal/.claude/scripts/verdict.mjs +101 -0
- package/templates/agent-os/universal/.claude/settings.json +7 -2
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +115 -7
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +378 -31
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +300 -25
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
- package/templates/agent-os/universal/.codex/hooks.json +65 -0
- package/templates/agent-os/universal/AGENTS.md +164 -0
- package/templates/agent-os/universal/CLAUDE.md +47 -14
- package/templates/agent-os/universal/PLAN.md +7 -40
- package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
- package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
- package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
- package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
- package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
- package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +62 -0
- package/templates/agent-os/universal/journal/README.md +101 -0
- package/templates/agent-os/universal/layers.json +36 -2
- package/templates/hash-history.json +2 -1
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
- package/templates/skeleton/aws-serverless/README.md +91 -9
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
- package/templates/skeleton/aws-serverless/gitignore +37 -0
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
- package/templates/skeleton/node-service/README.md +11 -1
- package/templates/skeleton/node-service/gitignore +34 -0
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
- package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
- package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
- package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
- 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; `
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
named criterion → verify the deployed surface if one
|
|
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
|
|
|
@@ -67,6 +69,31 @@ 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
|
+
🔴 **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
|
+
|
|
70
97
|
## 2. Selection — filters in order, then the sort
|
|
71
98
|
|
|
72
99
|
The queue is queried **fresh before every task**, never from a cached list: the
|
|
@@ -87,15 +114,54 @@ metadata destroys the evidence that the metadata is unreliable.
|
|
|
87
114
|
|
|
88
115
|
🔴 **A missing trigger marker means unconditional, not missing data.** Work that is
|
|
89
116
|
genuinely conditional says so. A `trigger-human` item — a "security pass", a
|
|
90
|
-
"window", "user demand" without a named metric — is **never
|
|
91
|
-
hands it over explicitly. A `trigger-auto` item needs its
|
|
92
|
-
run*: unverified is not fired, and rationalising a
|
|
93
|
-
scale that does not exist.
|
|
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.
|
|
94
150
|
|
|
95
151
|
**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
|
|
97
|
-
|
|
98
|
-
|
|
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`).
|
|
99
165
|
|
|
100
166
|
**The tier marker is a pre-filter, not the authority.** If an item passed as normal
|
|
101
167
|
and the work turns out to touch an elevated path (`CLAUDE.md` →
|
|
@@ -109,17 +175,90 @@ claims rather than checking them. On `PREMISE FALSE` the item is escalated (§6)
|
|
|
109
175
|
not repaired in place: a run that silently re-aims its own task has authored work
|
|
110
176
|
for itself, which is the one thing this loop does not do (§8).
|
|
111
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
|
+
|
|
112
200
|
## 3. What keeps the loop running, and what stops it
|
|
113
201
|
|
|
114
202
|
Per-task stops (three strikes, attempt budget, invariant conflict, a blocking
|
|
115
|
-
reviewer verdict, a false premise in the item itself)
|
|
116
|
-
escalate that item (§
|
|
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.
|
|
117
205
|
|
|
118
206
|
The run-level conditions are in `stopConditionOf` in `core.mjs`, checked in
|
|
119
207
|
severity order: **queue unreadable** · **runtime regression** · **kill switch** ·
|
|
120
|
-
**two escalations in a row** · **budget** · **queue
|
|
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
|
|
121
239
|
|
|
122
|
-
|
|
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:
|
|
123
262
|
|
|
124
263
|
- **Runtime regression** → deploy the revert first, diagnose second, start no new
|
|
125
264
|
work on top. A regression compounds into everything built above it.
|
|
@@ -130,7 +269,27 @@ Three of them deserve their reasons repeated:
|
|
|
130
269
|
sprees, no polish, no pre-emptive optimisation. An empty filtered queue is a
|
|
131
270
|
legitimate, successful end of session; refilling it is the owner's job.
|
|
132
271
|
**Expect this to be the most common ending** — the queue is finite and the loop
|
|
133
|
-
drains it. That is the system working.
|
|
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`.
|
|
134
293
|
|
|
135
294
|
🔴 **The kill switch is a real file, not an intention:**
|
|
136
295
|
|
|
@@ -162,6 +321,21 @@ cost column **will be believed** — by the next reader, and by the next run
|
|
|
162
321
|
reasoning about its own budget. A field the loop cannot observe stays **visibly
|
|
163
322
|
empty, never estimated.**
|
|
164
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
|
+
|
|
165
339
|
## 5. Every task carries an outcome state
|
|
166
340
|
|
|
167
341
|
The stop conditions say why the loop stopped. None of them says whether what it
|
|
@@ -174,6 +348,19 @@ perfectly good reason having produced something nobody should build on.
|
|
|
174
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** |
|
|
175
349
|
| `incomplete` | it stopped and the record does not explain where or why |
|
|
176
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
|
+
|
|
177
364
|
🔴 **`documented-stall` is a success, and reading it as a failure is how this stops
|
|
178
365
|
working.** A stall that names its under-supply is the most useful thing an
|
|
179
366
|
unattended run produces: it converts a vague gap into a located, fixable defect.
|
|
@@ -211,22 +398,61 @@ mechanises fully (`missed`, `.claude/rules/autonomy.md`) needs no self-report.
|
|
|
211
398
|
## 6. Escalation — two channels, by scope
|
|
212
399
|
|
|
213
400
|
**Task-scoped — the item is the home, and the loop continues.** Three strikes, the
|
|
214
|
-
attempt budget, an invariant conflict, a blocking reviewer verdict,
|
|
215
|
-
`PREMISE FALSE` verdict from `check-premises`
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
220
414
|
current hypothesis, and links to the PR and the failing run where they exist
|
|
221
415
|
— 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
|
|
223
|
-
|
|
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
|
|
224
419
|
is the point: the run that produced it is the only witness.
|
|
225
420
|
2. Mark it `escalated` and leave it claimed — **not** back to a selectable state,
|
|
226
421
|
or the next query picks it up and works it twice.
|
|
227
422
|
3. Journal it. 4. **Take the next item.** One stuck task does not end a run; two
|
|
228
423
|
in a row does (§3).
|
|
229
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
|
+
|
|
230
456
|
**Run-scoped — the run itself is broken, and it ends.** A runtime regression, two
|
|
231
457
|
escalations in a row, a systemic wall, a queue-data anomaly: open an escalation
|
|
232
458
|
issue with the diagnosis and links, notify the owner if the harness can, and write
|
|
@@ -238,7 +464,66 @@ a run still working.
|
|
|
238
464
|
|
|
239
465
|
Write a checkpoint entry **every few completed items and at every stop**, not only
|
|
240
466
|
at the end: a run that dies unexpectedly must not take its history with it. The
|
|
241
|
-
|
|
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.
|
|
242
527
|
|
|
243
528
|
At every **stop** — not at a checkpoint — turn the run's findings into **at most
|
|
244
529
|
three** improvement proposals. **The cap is the mechanism, not a budget:** an
|
|
@@ -252,8 +537,10 @@ four things, and a proposal missing any of them is not ready to file:
|
|
|
252
537
|
4. how the next run would prove it worked — the observation that would differ.
|
|
253
538
|
|
|
254
539
|
Filing is the adapter's `proposeTriage`, which the CLI deliberately does **not**
|
|
255
|
-
expose — `index.mjs`
|
|
256
|
-
invocation can
|
|
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:
|
|
257
544
|
|
|
258
545
|
```bash
|
|
259
546
|
node --input-type=module -e '
|
|
@@ -269,6 +556,21 @@ node --input-type=module -e '
|
|
|
269
556
|
|
|
270
557
|
A proposal missing any of the four parts is refused rather than filed half-formed.
|
|
271
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
|
+
|
|
272
574
|
🔴 **The loop proposes; the owner patches.** Self-applying a change to its own
|
|
273
575
|
rulebook is how an unattended run drifts irreversibly, and it collides head-on
|
|
274
576
|
with the rule that the agent authors no work for itself.
|
|
@@ -288,10 +590,10 @@ three poisons the only channel by which this project learns.
|
|
|
288
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) |
|
|
289
591
|
| Take items needing a human decision | It cannot unblock itself; those wait in the Operator queue |
|
|
290
592
|
| 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 |
|
|
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) |
|
|
292
594
|
| Merge past a blocking reviewer verdict | The reviewer gate is what replaced the human merge |
|
|
293
595
|
| Trust a `blocked` label over the links | The label is a snapshot; the links are the dependency |
|
|
294
|
-
| "Improve" on
|
|
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 |
|
|
295
597
|
| Start new work on an unhealthy runtime | The regression compounds into everything above it |
|
|
296
598
|
| Act on the "Never" tier | A hard stop, enforced by hooks |
|
|
297
599
|
|
|
@@ -302,7 +604,52 @@ three poisons the only channel by which this project learns.
|
|
|
302
604
|
while it still reads as available is invisible to the human and re-selectable by
|
|
303
605
|
the very next query.
|
|
304
606
|
- **Closing:** close it with the merged PR linked, immediately after the
|
|
305
|
-
post-merge verdict — not in a cleanup pass.
|
|
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.
|
|
306
653
|
- **Write-back:** with the close, record what it **unblocked** — the items that
|
|
307
654
|
were waiting on this one, by name. It is the journal's `unblocked` field, and
|
|
308
655
|
it is **required, not a step for when it applies**: an absent line and an
|