create-agent-rig 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -34
- package/README.md +12 -6
- package/package.json +1 -1
- package/packages/cli/dist/commands/init.js +6 -3
- package/packages/cli/dist/commands/upgrade.js +2 -2
- package/packages/cli/dist/index.js +46 -8
- package/packages/cli/dist/lib/manifest.js +10 -0
- package/scripts/prepare.mjs +1 -1
- package/templates/agent-os/init/AGENTS.md +11 -3
- package/templates/agent-os/init/CLAUDE.md +11 -3
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +8 -1
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +8 -1
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +8 -1
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +1 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +4 -1
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +298 -16
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +64 -6
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -1
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -1
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +8 -1
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +28 -3
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +127 -0
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +23 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +8 -0
- package/templates/agent-os/universal/.claude/rules/invariants.md +33 -3
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +19 -1
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +351 -0
- package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
- package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +28 -0
- package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +37 -8
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +27 -1
- package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
- package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +62 -2
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +479 -9
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +89 -15
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +138 -15
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +394 -46
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +68 -5
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +316 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +180 -0
- package/templates/agent-os/universal/.claude/scripts/run-state.mjs +101 -3
- package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +239 -0
- package/templates/agent-os/universal/.claude/scripts/verdict.mjs +101 -4
- package/templates/agent-os/universal/.claude/settings.json +5 -1
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +4 -1
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +298 -16
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +64 -6
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +6 -1
- package/templates/agent-os/universal/AGENTS.md +3 -1
- package/templates/agent-os/universal/CLAUDE.md +3 -1
- package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
- package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +18 -6
- package/templates/agent-os/universal/layers.json +9 -0
- package/templates/hash-history.json +309 -49
- package/templates/release-ledger.json +9 -0
|
@@ -25,7 +25,7 @@ Selection never reads a tracker directly. It goes through
|
|
|
25
25
|
```bash
|
|
26
26
|
node .claude/scripts/queue/index.mjs next # the item to take, and why the rest were skipped
|
|
27
27
|
node .claude/scripts/queue/index.mjs next --json
|
|
28
|
-
node .claude/scripts/queue/index.mjs hygiene # stale labels, link anomalies
|
|
28
|
+
node .claude/scripts/queue/index.mjs hygiene # stale labels, link anomalies, overtaken proposals
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- **`plan-md`** (default) — the Agent queue in `PLAN.md`. The only adapter that
|
|
@@ -53,13 +53,13 @@ state-vs-queue split exists to prevent.
|
|
|
53
53
|
node .claude/scripts/preflight.mjs
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
it did not check, every time**. Paste the block into the journal: a checklist that
|
|
56
|
+
Four items are scripted (kill switch absent · `RIG_RUN_DIR` not already
|
|
57
|
+
exported · local default branch matches the remote · the last deploy concluded
|
|
58
|
+
successfully) and the script **prints the ones it did not check, every time**. Paste the block into the journal: a checklist that
|
|
59
59
|
leaves no record cannot tell you it was skipped.
|
|
60
60
|
|
|
61
61
|
Verdicts: **STOP** → do not start, deal with the cause. **CAUTION** → start,
|
|
62
|
-
knowing which ground is soft. **GO** → the scripted
|
|
62
|
+
knowing which ground is soft. **GO** → the scripted four are clean; the rest are
|
|
63
63
|
still yours.
|
|
64
64
|
|
|
65
65
|
**An `unknown` never becomes a `pass`.** A probe that could not run tells you
|
|
@@ -87,6 +87,42 @@ export RIG_RUN_DIR="$PWD/.claude/runs/$(date +%Y%m%d-%H%M%S)" # one per run
|
|
|
87
87
|
mkdir -p "$RIG_RUN_DIR"
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
⚠ **That export reaches the commands THIS shell runs and nothing else.** A
|
|
91
|
+
`PreToolUse` hook is spawned by the harness with the harness's own environment,
|
|
92
|
+
never with a variable the session exported — pinned in the generator's
|
|
93
|
+
`test/template/guard-rulebook.test.ts` (absent in a generated rig) › "only a
|
|
94
|
+
flag arms it — an exported RIG_UNATTENDED=1 with no flag changes nothing" —
|
|
95
|
+
and in some harnesses the export does not even survive to the next Bash call,
|
|
96
|
+
which is why every command in this skill can also take the run directory per
|
|
97
|
+
invocation. What a hook CAN see is a file, so the unattended signal is one:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# at claim time, from the paths the item names (repo-relative prefixes, with
|
|
101
|
+
# their trailing slash); the guard refuses every other rulebook edit while it is on
|
|
102
|
+
node .claude/scripts/unattended-flag.mjs on --item <item-id> --run-dir "$RIG_RUN_DIR" --allow <prefix> [<prefix>…]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`guard-rulebook` reads it (`.claude/rules/autonomy.md`, "Never"): with the flag
|
|
106
|
+
on, a Write/Edit/MultiEdit/NotebookEdit/`apply_patch` under `.claude/hooks/`,
|
|
107
|
+
`.claude/settings.json`, `.claude/queue.json`, `.claude/scripts/queue/`, the
|
|
108
|
+
router, the gate sweep, `.claude/rules/` or `CLAUDE.md` is refused unless its
|
|
109
|
+
path starts with an allowed prefix; with no flag the guard does nothing. An
|
|
110
|
+
item that needs a rulebook path names it here — a decision made at claim
|
|
111
|
+
time, never a default — and the stop step below turns the flag off. Pinned in
|
|
112
|
+
the generator's `test/template/guard-rulebook.test.ts` — absent in a generated
|
|
113
|
+
rig — › "blocks a hook-config edit with an empty allow-list, naming path, item and the rule".
|
|
114
|
+
|
|
115
|
+
⚠ **The export outlives the run's own calls.** Everything the session spawns
|
|
116
|
+
inherits it — and a test suite that spawns the queue CLI would write fixture
|
|
117
|
+
records into this run's trace (AR-139: 38 fixture selections and 22 fixture
|
|
118
|
+
revalidation events in one session, two tests exiting 1). So preflight refuses
|
|
119
|
+
to start on a `RIG_RUN_DIR` already exported. The generator's own test harness
|
|
120
|
+
also scrubs the variable before any test file loads — its `test/setup-env.ts`,
|
|
121
|
+
pinned by its `test/template/rig-run-dir-scrub.test.ts` › "holds with the
|
|
122
|
+
variable exported around the whole vitest process" — and **neither file ships
|
|
123
|
+
into a generated rig**: here, nothing scrubs it, and a rig's own test setup is
|
|
124
|
+
the place to do the same.
|
|
125
|
+
|
|
90
126
|
🔴 **One directory per run, never shared and never reused — and the journal
|
|
91
127
|
cannot enforce this for you.** A collision or an already-ended directory is
|
|
92
128
|
refused loudly; two runs whose records merely do not collide are merged into one
|
|
@@ -127,6 +163,60 @@ not deleting the old marker, and the silent resolution goes to the **less**
|
|
|
127
163
|
restrictive gate. Until that is fixed, treat a double-marked item as
|
|
128
164
|
human-gated by hand.
|
|
129
165
|
|
|
166
|
+
🔴 **An item marked for another repository is held, never taken.** A label
|
|
167
|
+
`owner-<name>` names the repository an item belongs to; the checkout names
|
|
168
|
+
itself in `options.owner` of `.claude/queue.json`, and a mismatch — or an
|
|
169
|
+
owned item in a checkout that declares no owner — is the holding cause `owner`,
|
|
170
|
+
reported by `hygiene` as `owner-mismatch`. It clears the way `trigger-human`
|
|
171
|
+
does: a human moves the item or re-marks it. An unmarked item is unconditional.
|
|
172
|
+
Two items of another product once entered this queue as normal spacers and
|
|
173
|
+
escalated `PREMISE FALSE` back to back — a run-level stop spent on work that
|
|
174
|
+
was never this checkout's (AR-132). Pinned in the generator's
|
|
175
|
+
`test/template/queue-owner.test.ts` — absent in a generated rig — › "holds an
|
|
176
|
+
item whose owner is another repository, with the cause named".
|
|
177
|
+
|
|
178
|
+
🔴 **An item's lifecycle is a label a human wrote, and the loop infers none of
|
|
179
|
+
it** (AR-144). Four words, read by `lifecycleOf` in `core.mjs` so every adapter
|
|
180
|
+
means the same thing — three of lifecycle, one of scheduling:
|
|
181
|
+
|
|
182
|
+
- `keep-core` — the problem and the responsibility are valid and the item is
|
|
183
|
+
executable as written. A statement about the item, never a condition on taking
|
|
184
|
+
it: a bare `keep-core` item is selected like an unmarked one.
|
|
185
|
+
- `re-scope` — the problem is valid but the item is **not executable
|
|
186
|
+
literally**: a path, a mechanism, a boundary or an acceptance criterion has
|
|
187
|
+
drifted. It is the holding cause `re-scope`, reported by `hygiene` as
|
|
188
|
+
`re-scope-pending` until it clears — and it clears **only** by a human
|
|
189
|
+
re-reading the code, rewriting the item and removing the label. It is a
|
|
190
|
+
short-lived quarantine, not a backlog category, and never a synonym for
|
|
191
|
+
obsolete, low priority or parked. The loop surfaces it and never rewrites it:
|
|
192
|
+
a re-aimed item is work the agent authored (§8).
|
|
193
|
+
- `obsolete` — the responsibility is gone or fully superseded by a proven
|
|
194
|
+
mechanism. A **human verdict, terminal**: the loop never applies it and never
|
|
195
|
+
closes an item because it believes another mechanism supersedes it. Closing as
|
|
196
|
+
obsolete needs external evidence and a comment naming the replacement or the
|
|
197
|
+
reason — so an obsolete item is out of play (the cause `obsolete`), not held,
|
|
198
|
+
and the stop line says so.
|
|
199
|
+
- `parked` — valid work deliberately not active now. The **scheduling** axis,
|
|
200
|
+
orthogonal to the three above: `keep-core + parked` is the ordinary shape of a
|
|
201
|
+
deferred item and means "still needed, not now". It is the holding cause
|
|
202
|
+
**`deferred`**, freed by a human un-parking it. The cause is not spelled
|
|
203
|
+
`parked` on purpose: §3 already uses that word for the **out-of-play pile** —
|
|
204
|
+
items waiting on a human that no session will take — and a parked-labelled
|
|
205
|
+
item is the opposite, held and takeable. So the stop line reads "held by
|
|
206
|
+
deferred" for the label, and "are parked" for the pile; `obsolete` lands in
|
|
207
|
+
the pile, the `parked` label never does.
|
|
208
|
+
|
|
209
|
+
The rule under all four: **nothing infers `obsolete`** — not age, not a key
|
|
210
|
+
range, old terminology, `parked`, absence from a roadmap, or a migration marker.
|
|
211
|
+
`legacy-backlog` is that marker, retired: `hygiene` reports an open item still
|
|
212
|
+
carrying it (`stale-legacy-backlog-label`), and two lifecycle labels on one item
|
|
213
|
+
(`contradictory-lifecycle-labels`) — selection meanwhile reads the most
|
|
214
|
+
restrictive one. Reported, never corrected: which label is wrong is the human's
|
|
215
|
+
call. Pinned in the generator's `test/template/queue-lifecycle.test.ts` — absent
|
|
216
|
+
in a generated rig — › "holds a re-scope item and says a human rewrites it" and
|
|
217
|
+
› "refuses an obsolete item and says a human closes it with a comment naming the
|
|
218
|
+
evidence".
|
|
219
|
+
|
|
130
220
|
**For a `trigger-auto` item, record the declaration** — it has to outlive the
|
|
131
221
|
turn it was made in, or the next selection holds the item back again:
|
|
132
222
|
|
|
@@ -168,6 +258,60 @@ and the work turns out to touch an elevated path (`CLAUDE.md` →
|
|
|
168
258
|
`elevated-paths`), run the gate anyway, record the verdict on the PR, and treat it
|
|
169
259
|
as this run's elevated item for spacing.
|
|
170
260
|
|
|
261
|
+
**Selection also revalidates the item against its last take-up.** `next`
|
|
262
|
+
records the selected item's `updatedAt` marker in the run state (`takeUps`) and,
|
|
263
|
+
when the item is offered again, compares the two — against this run's take-up
|
|
264
|
+
when it has one, otherwise against the newest earlier run's under `.claude/runs/`
|
|
265
|
+
(AR-138: before that, an item taken up yesterday compared against nothing and
|
|
266
|
+
read as a first sight). A proposal the loop files carries its own baseline: the
|
|
267
|
+
`jira` and `github-issues` adapters record the filed item's marker as a take-up
|
|
268
|
+
in the run that filed it. The event names which it used — `baseline:
|
|
269
|
+
this-run | previous-run | null` — pinned in the generator's
|
|
270
|
+
`test/template/revalidation-baseline.test.ts` — absent in a generated rig — ›
|
|
271
|
+
"holds when the marker moved past the earlier run’s take-up, and names that
|
|
272
|
+
baseline". A marker that
|
|
273
|
+
moved prints a `revalidate:` line and the JSON carries `revalidation.changed:
|
|
274
|
+
true`: **re-read the item before acting on it**, then record what the re-read
|
|
275
|
+
concluded — whether the change altered the action is the evidence this exists to
|
|
276
|
+
collect, and the comparison alone cannot supply it:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
node .claude/scripts/revalidate.mjs outcome --point SELECT --ticket <item-id> --action-changed <true | false> --note '<what changed, or why it changes nothing>'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
It appends one `revalidation-outcome` record whose `answers` names the
|
|
283
|
+
revalidation it resolves, so the report can pair the two without guessing. The
|
|
284
|
+
note is stored verbatim from argv, so keep it in single quotes: inside double
|
|
285
|
+
quotes the shell expands a backtick or a `$` before the command sees it.
|
|
286
|
+
Nothing forces this record — a `revalidation` event with no matching outcome is
|
|
287
|
+
counted as `unresolved`, which is the honest word for a re-read the run skipped.
|
|
288
|
+
|
|
289
|
+
Under a declared run directory, every selection logs one `revalidation` event
|
|
290
|
+
`{ticket, point: SELECT, changed, source, action, task}` — the same shape the
|
|
291
|
+
BEFORE_PR and BEFORE_CLOSE points write. **No-change is always recorded**, one
|
|
292
|
+
line per selection and no sampling: the rule is explicit so the report's
|
|
293
|
+
`opportunities` is a count and not an estimate. An adapter with no marker
|
|
294
|
+
(`plan-md`) logs `changed: null`, never "unchanged". ⚠ The marker also moves on
|
|
295
|
+
the run's own claim and comments. The `jira` and `github-issues` adapters
|
|
296
|
+
re-record the take-up after each write they make — claim, comment, close,
|
|
297
|
+
escalate — so a move made through the adapter is not a hold (AR-140, from the
|
|
298
|
+
journal's RX3/RX4 entry: every BEFORE_PR catch of that run was the run's own
|
|
299
|
+
comment, counted by `revalidation-report.mjs`); a
|
|
300
|
+
comment posted by any other route — a REST call by hand, a connector — still
|
|
301
|
+
moves it like anyone else's, and a `true` can still be self-inflicted that way
|
|
302
|
+
— the re-read decides, which is why the outcome is recorded separately, and a
|
|
303
|
+
hold the re-read overturns is counted as a false hold with its source named.
|
|
304
|
+
Pinned in the generator's `test/template/self-inflicted-marker.test.ts` — absent
|
|
305
|
+
in a generated rig — › "%s leaves the take-up at the marker the write produced",
|
|
306
|
+
an `it.each` over claim, comment, close and escalate. The
|
|
307
|
+
four-week view is `node .claude/scripts/revalidation-report.mjs --since <date>`,
|
|
308
|
+
over this rig's `.claude/runs/` (or a `--runs <dir>`). The behaviour is pinned in the
|
|
309
|
+
generator's `test/template/queue-revalidation.test.ts` — absent in a generated
|
|
310
|
+
rig — › "an adapter with no marker records a blind spot, not \"unchanged\"", ›
|
|
311
|
+
"a moved marker holds on task:updatedAt, re-snapshots, and journals the change"
|
|
312
|
+
and › "the loop skill's outcome command records what the re-read concluded", and
|
|
313
|
+
in `test/template/revalidation-evidence.test.ts`.
|
|
314
|
+
|
|
171
315
|
**Then, before the Red step: `check-premises`.** The item was written by someone
|
|
172
316
|
who was not reading the code at the time, and everything downstream — the failing
|
|
173
317
|
test, the implementation, the reviewer comparing diff to item — inherits its
|
|
@@ -193,10 +337,50 @@ returns one fenced `json` verdict like every other gate, and this loop is its ca
|
|
|
193
337
|
node .claude/scripts/verdict.mjs check <report> check-premises
|
|
194
338
|
```
|
|
195
339
|
|
|
340
|
+
**`<report>` is a file you write, not one the harness leaves behind.** The
|
|
341
|
+
subagent's answer arrives as text in the conversation; its transcript on disk is
|
|
342
|
+
a JSONL file whose last fenced block does not parse, so pointing the check at it
|
|
343
|
+
exits 1 whatever the reviewer said. Save the whole answer to a file under the run
|
|
344
|
+
directory — `$RIG_RUN_DIR/check-premises.md`, one file per gate so two answers
|
|
345
|
+
never overwrite each other — and pass that path (`-` reads stdin instead). The
|
|
346
|
+
same holds for every `<report>` in this skill, as `pr-ship` already does for its
|
|
347
|
+
reviewers (AR-117). Pinned in the generator's `test/template/loop-report-file.test.ts`
|
|
348
|
+
— absent in a generated rig — › "states that the report is a file the session
|
|
349
|
+
writes from the subagent answer, before the first check".
|
|
350
|
+
|
|
196
351
|
Exit 1 means it did not answer: a stop verdict naming no premise, or no block at all.
|
|
197
352
|
That is `incomplete` — neither "the premises hold" nor a reason to escalate — so run
|
|
198
353
|
the pass again rather than reading the silence as a pass.
|
|
199
354
|
|
|
355
|
+
**Then journal the block that parsed**, exactly as `pr-ship` journals a reviewer's —
|
|
356
|
+
the paths a premise check names are what `revalidate.mjs` reads at BEFORE_PR as the
|
|
357
|
+
task's cited paths, and a verdict held only in context cites nothing after a
|
|
358
|
+
compaction:
|
|
359
|
+
|
|
360
|
+
```sh
|
|
361
|
+
node --input-type=module -e '
|
|
362
|
+
const runDir = process.env.RIG_RUN_DIR;
|
|
363
|
+
if (!runDir) process.exit(0); // an undeclared run has no trace to write
|
|
364
|
+
if (!process.argv[1]) process.exit(0); // `check` printed nothing: nothing to record
|
|
365
|
+
const journal = await import("./.claude/scripts/run-journal.mjs");
|
|
366
|
+
const v = JSON.parse(process.argv[1]);
|
|
367
|
+
try {
|
|
368
|
+
console.log(journal.recordDecision({
|
|
369
|
+
runDir,
|
|
370
|
+
gate: "check-premises",
|
|
371
|
+
verdict: v.verdict,
|
|
372
|
+
blockers: v.blockers,
|
|
373
|
+
headSha: v.headSha,
|
|
374
|
+
now: new Date().toISOString(),
|
|
375
|
+
}));
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// The same split pr-ship makes: an exhausted trace is over, the task is not.
|
|
378
|
+
if (!journal.isTraceExhausted?.(error)) throw error;
|
|
379
|
+
process.stderr.write(`run journal: ${error.message}\n the premise verdict above was NOT recorded.\n`);
|
|
380
|
+
}
|
|
381
|
+
' "$(node .claude/scripts/verdict.mjs check <report> check-premises)"
|
|
382
|
+
```
|
|
383
|
+
|
|
200
384
|
## 3. What keeps the loop running, and what stops it
|
|
201
385
|
|
|
202
386
|
Per-task stops (three strikes, attempt budget, invariant conflict, a blocking
|
|
@@ -282,10 +466,13 @@ Four of them deserve their reasons repeated:
|
|
|
282
466
|
queue**: the elevated spacing (a normal or prose-only item lands), a blocker (its item
|
|
283
467
|
closes), in-progress (the other session finishes), a trigger (a human
|
|
284
468
|
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)
|
|
469
|
+
§2, so this is the one hold that needs a command rather than only time), and
|
|
470
|
+
an owner (§2: the item is another repository's, and a human moves it or
|
|
471
|
+
re-marks it — neither time nor interleaving frees it). The
|
|
286
472
|
stop line names how many and by which, because the two endings ask the owner
|
|
287
473
|
for opposite things: an empty queue wants refilling, a held one wants
|
|
288
|
-
interleaving or
|
|
474
|
+
interleaving, time, or — for a trigger or an owner — a human act the line
|
|
475
|
+
names. 🔴 **A parked cause outranks a holding one on the
|
|
289
476
|
same item** — an escalated item is left claimed on purpose, so it arrives
|
|
290
477
|
carrying `in-progress` too. **Neither ending is an invitation to refill the
|
|
291
478
|
queue or invent work.** Why the two are split, and how the parked pile grows
|
|
@@ -351,8 +538,9 @@ perfectly good reason having produced something nobody should build on.
|
|
|
351
538
|
🔴 **What `documented-stall` requires is the STAGE and the wall, not a full
|
|
352
539
|
inventory of findings** — and this had to be settled, because the two readings
|
|
353
540
|
disagreed the first time a stop arrived without an inventory. An exhausted
|
|
354
|
-
gate-round cap names its stage (the gate) and its wall (
|
|
355
|
-
|
|
541
|
+
gate-round cap names its stage (the gate) and its wall (the branch's rounds are
|
|
542
|
+
spent — the count, not a verdict on whether the fixes were converging, which the
|
|
543
|
+
counter never measured; AR-115), while the individual blockers behind it are not persisted anywhere until
|
|
356
544
|
per-round verdicts exist. That is a `documented-stall`: the record locates the wall
|
|
357
545
|
and the next reader knows where to look.
|
|
358
546
|
|
|
@@ -525,6 +713,14 @@ If no run directory was declared, there is nothing to close and this step is
|
|
|
525
713
|
skipped — say so in the journal entry rather than leaving the reader to guess
|
|
526
714
|
which of the two happened.
|
|
527
715
|
|
|
716
|
+
**And turn the unattended flag off** — it outlives the run otherwise, and the
|
|
717
|
+
next attended session would find its rulebook edits refused in the name of an
|
|
718
|
+
item nobody is working:
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
node .claude/scripts/unattended-flag.mjs off
|
|
722
|
+
```
|
|
723
|
+
|
|
528
724
|
At every **stop** — not at a checkpoint — turn the run's findings into **at most
|
|
529
725
|
three** improvement proposals. **The cap is the mechanism, not a budget:** an
|
|
530
726
|
unbounded improvement list is another diary, and three forces a choice. Each names
|
|
@@ -550,12 +746,54 @@ node --input-type=module -e '
|
|
|
550
746
|
part: "<skill | agent | hook | rule | CLAUDE.md | workflow>",
|
|
551
747
|
change: "<concretely enough to diff>",
|
|
552
748
|
proof: "<the observation that would differ next run>",
|
|
553
|
-
|
|
749
|
+
// a pair: what the probe touched, and what is concluded from it. The
|
|
750
|
+
// mechanism accepts a proposal without them; this procedure does not.
|
|
751
|
+
measured: "<the paths the probe actually exercised>",
|
|
752
|
+
inferred: "<the conclusion, citing only surfaces named in measured>",
|
|
753
|
+
}, { project: "<KEY>" })); // jira only — the project key from .claude/queue.json;
|
|
754
|
+
// plan-md and github-issues take no second argument
|
|
554
755
|
'
|
|
555
756
|
```
|
|
556
757
|
|
|
557
758
|
A proposal missing any of the four parts is refused rather than filed half-formed.
|
|
558
759
|
|
|
760
|
+
**A finding can say what it measured and what it inferred, as two paired fields**
|
|
761
|
+
(AR-142). A proposal whose premise was never true had no check at filing, only at
|
|
762
|
+
take-up — AR-124 was filed, promoted and claimed before its platform conclusion
|
|
763
|
+
was traced to a probe that had touched one hook. So `measured` and `inferred`
|
|
764
|
+
are separate, and `validateProposal` refuses an `inferred` that cites a path
|
|
765
|
+
`measured` does not, naming both fields and the path; one field without the
|
|
766
|
+
other is refused too, and neither files as before. The surface is a cited path
|
|
767
|
+
(`citedPathsOf`), so a conclusion that names no path passes this check — it
|
|
768
|
+
catches the path-shaped overreach and nothing subtler.
|
|
769
|
+
|
|
770
|
+
⚠ **The pair is how a proposal opts into the check, and a proposal filed without
|
|
771
|
+
it is not checked at all** — `validateProposal` keeps the four-part contract, so
|
|
772
|
+
the AR-124 shape with neither field still files as it always did. That is the
|
|
773
|
+
stated limit, not an oversight: making the fields mandatory would refuse every
|
|
774
|
+
proposal the three adapters already file, and the loop is the author this rule
|
|
775
|
+
is for. So **every proposal this loop files carries both fields** — the snippet
|
|
776
|
+
above supplies them, and a stop that cannot say what it measured has nothing
|
|
777
|
+
to propose. A reviewer reading a filed proposal without the pair reads a
|
|
778
|
+
proposal that skipped this procedure. Pinned in the generator's
|
|
779
|
+
`test/template/queue.test.ts` — absent in a generated rig — › "refuses a
|
|
780
|
+
proposal whose inference names a surface its measurement did not touch", ›
|
|
781
|
+
"files a proposal whose inference stays inside what it measured", › "refuses one
|
|
782
|
+
of the two fields without the other" and › "a proposal with neither field files
|
|
783
|
+
as today".
|
|
784
|
+
|
|
785
|
+
**The filed item also records the commit it was measured against** — an `asOf:`
|
|
786
|
+
line, HEAD of this checkout unless the call passes its own `asOf` (`null` files
|
|
787
|
+
without one). It is there for `hygiene`, which lists the proposals on file and
|
|
788
|
+
reports one whose cited paths changed since its `asOf` as
|
|
789
|
+
`proposal-possibly-overtaken`, one with no `asOf` as `proposal-asof-missing`, and
|
|
790
|
+
one git cannot diff from as `proposal-asof-unanswerable` — never as clean. Two
|
|
791
|
+
proposals in a row once escalated `PREMISE FALSE` because the merge that
|
|
792
|
+
falsified each landed after it was filed, and selection hands out the oldest
|
|
793
|
+
first (AR-116). The behaviour is pinned in the generator's
|
|
794
|
+
`test/template/proposal-asof.test.ts` — absent in a generated rig — ›
|
|
795
|
+
"names the overtaken one, the unanswerable one, and stays silent on the current one".
|
|
796
|
+
|
|
559
797
|
**All three adapters write it themselves** — `jira` and `github-issues` create a
|
|
560
798
|
`triage`-labelled issue, `plan-md` appends a bullet to the **Operator queue**, and
|
|
561
799
|
each increments an existing proposal carrying the same fingerprint rather than
|
|
@@ -567,9 +805,9 @@ rather than a step in the procedure: `plan-md` returns it when the plan file has
|
|
|
567
805
|
no `## Operator queue` heading, because a proposal then has nowhere to land that
|
|
568
806
|
the selection query cannot reach. Add the heading — never the Agent queue.
|
|
569
807
|
|
|
570
|
-
One adapter needs
|
|
571
|
-
`options.project` and throws rather than filing without it
|
|
572
|
-
|
|
808
|
+
One adapter needs the second argument the snippet above carries: `jira` requires
|
|
809
|
+
`options.project` and throws rather than filing without it — loudly, so nothing
|
|
810
|
+
is lost, but a call that drops it files nothing (AR-117).
|
|
573
811
|
|
|
574
812
|
🔴 **The loop proposes; the owner patches.** Self-applying a change to its own
|
|
575
813
|
rulebook is how an unattended run drifts irreversibly, and it collides head-on
|
|
@@ -603,9 +841,53 @@ three poisons the only channel by which this project learns.
|
|
|
603
841
|
that creates the branch or worktree. Not when the PR opens. An item being worked
|
|
604
842
|
while it still reads as available is invisible to the human and re-selectable by
|
|
605
843
|
the very next query.
|
|
606
|
-
- **Closing:**
|
|
607
|
-
|
|
608
|
-
|
|
844
|
+
- **Closing:** first ask whether the item is still the item you took up — a
|
|
845
|
+
late comment or a status somebody else moved is not published as `Done`
|
|
846
|
+
underneath it (AR-135):
|
|
847
|
+
|
|
848
|
+
```bash
|
|
849
|
+
node .claude/scripts/revalidate.mjs --point BEFORE_CLOSE --ticket <item-id>
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
It compares the item's marker against the newer of this run's last
|
|
853
|
+
validation and its take-up — an adapter re-records the take-up after each
|
|
854
|
+
write of its own (§2, AR-140), so a comment posted after BEFORE_PR does not
|
|
855
|
+
hold the close; pinned in the generator's
|
|
856
|
+
`test/template/self-inflicted-marker.test.ts` › "continues when the run’s own
|
|
857
|
+
write moved the marker after the last validation" — and its
|
|
858
|
+
state against the `in-progress` a close expects, journals one `revalidation`
|
|
859
|
+
event at `point: BEFORE_CLOSE`, and lists the item's dependants with each
|
|
860
|
+
one's state re-read for the write-back below — pinned in the generator's
|
|
861
|
+
`test/template/revalidate.test.ts` (absent in a generated rig) › "appends
|
|
862
|
+
exactly one BEFORE_CLOSE revalidation event after the BEFORE_PR one, and does
|
|
863
|
+
not end the run", › "runs the BEFORE_CLOSE revalidation before the close call
|
|
864
|
+
and reads a hold as a stop" and › "re-reads each dependant's state, and names
|
|
865
|
+
one the tracker no longer offers". On a `github-issues` queue that list is
|
|
866
|
+
empty: a single `gh issue view` carries no cross-index, so `find` answers no
|
|
867
|
+
`blocks` there (`test/template/close-transitioned.test.ts` › "github asks `gh
|
|
868
|
+
issue view` with the full field list and maps CLOSED to closed"). A
|
|
869
|
+
hold (exit 2) stops the close: re-read the item, record the outcome with
|
|
870
|
+
`node .claude/scripts/revalidate.mjs outcome --point BEFORE_CLOSE --ticket
|
|
871
|
+
<item-id> --action-changed <true | false> --note '…'`, and close only if the
|
|
872
|
+
re-read leaves the action standing. Then call the
|
|
873
|
+
adapter's `close(ticket, { prUrl, transitionId })` with the merged PR linked,
|
|
874
|
+
immediately after the post-merge verdict — not in a cleanup pass — and read
|
|
875
|
+
its answer: `ok: true` says the call ran, and only `transitioned` set to
|
|
876
|
+
`true` says the close landed, because every adapter reads the item back after
|
|
877
|
+
the transition — `jira` the status category after the POST, `github-issues`
|
|
878
|
+
`gh issue view --json state`, `plan-md` the line being there and then gone
|
|
879
|
+
(the generator's `test/template/close-transitioned.test.ts` › "GETs the issue
|
|
880
|
+
status after the transition POST and reports transitioned: true when the
|
|
881
|
+
category is done", › "runs `issue view <id> --json state` after `issue close`
|
|
882
|
+
and reports transitioned: true on CLOSED", › "reports transitioned: true once
|
|
883
|
+
the item's line is gone"). A close whose result says `transitioned: false` is
|
|
884
|
+
not a close: report it, and leave the item as the adapter left it — `jira` its
|
|
885
|
+
status, `github-issues` its `in-progress` label, which comes off only after a
|
|
886
|
+
read-back that says CLOSED (› "leaves the in-progress label on an issue whose
|
|
887
|
+
close did not land"), `plan-md` nothing, because the line was never there. The
|
|
888
|
+
tier below is recorded only for a close that transitioned; a close that did
|
|
889
|
+
not is not the "something landed" the escalation streak resets on. **Record
|
|
890
|
+
the tier in the same step**, because the next selection rations on it:
|
|
609
891
|
|
|
610
892
|
```bash
|
|
611
893
|
node --input-type=module -e '
|
|
@@ -27,8 +27,17 @@ blockers.
|
|
|
27
27
|
is *gate rounds exhausted*, quoting the round count in its `note`. Do not
|
|
28
28
|
run the fan-out.
|
|
29
29
|
- **1** — the command itself failed (unreadable config, unreadable counter,
|
|
30
|
-
detached checkout)
|
|
31
|
-
|
|
30
|
+
detached checkout), **or the checkout cannot ship**: a dirty working tree,
|
|
31
|
+
a branch with no upstream, or commits the upstream has not seen. Nothing
|
|
32
|
+
was counted. This is **not** an exhausted cap: fix the cause — commit and
|
|
33
|
+
push — and run step 0 again. Treating it as exhaustion escalates a healthy
|
|
34
|
+
item. A rig with no remote can never pass this step — by design: the PR
|
|
35
|
+
flow, and this gate with it, begins when the project has a remote
|
|
36
|
+
(`workflow.md`, "PR flow"). The refusal exists because two rounds were once counted ahead of a
|
|
37
|
+
commit pre-commit then refused, so the counter and the fan-out's verdicts
|
|
38
|
+
named a head that never shipped (AR-141) — pinned in the generator's
|
|
39
|
+
`test/template/gate-rounds.test.ts` — absent in a generated rig — ›
|
|
40
|
+
"refuses to count a round on a dirty tree, and counts nothing".
|
|
32
41
|
|
|
33
42
|
The cap is **2 by default**, and no shipped `.claude/queue.json` carries the key
|
|
34
43
|
— the default lives in `core.mjs` as `DEFAULT_MAX_GATE_ROUNDS`. A project that
|
|
@@ -42,10 +51,32 @@ blockers.
|
|
|
42
51
|
removes is the honest failure mode, a run that keeps re-reviewing because no
|
|
43
52
|
check ever went red; it does not stop a session that skips it.
|
|
44
53
|
|
|
45
|
-
1. **The diff first.** Establish what is actually shipping: fetch, then diff
|
|
54
|
+
1. **The diff first — and the branch's own premises next.** Establish what is actually shipping: fetch, then diff
|
|
46
55
|
against the **remote** default branch (`origin/<default>`), not a local
|
|
47
56
|
copy that may be behind — diagnosing from stale local code produces
|
|
48
57
|
confidently-wrong reviews. Everything below is scoped to this diff.
|
|
58
|
+
|
|
59
|
+
Then, on the fetched ref, ask whether the branch is still the branch the run
|
|
60
|
+
took up (AR-134):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
node .claude/scripts/revalidate.mjs --point BEFORE_PR --ticket <item-id> --base origin/<default>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It compares two sources and names each one that moved: the item's `updatedAt`
|
|
67
|
+
against the take-up snapshot `next` recorded (`task:updatedAt`), and what the
|
|
68
|
+
default branch changed since this branch forked, on the paths the branch
|
|
69
|
+
touches or a `check-premises` record in this run cited (`main:<path>`). It
|
|
70
|
+
journals one `revalidation` event at `point: BEFORE_PR`; **exit code 2 is a HOLD**, with one blocker per named source: re-read the item, or the default
|
|
71
|
+
branch on that path, record what the re-read concluded —
|
|
72
|
+
`node .claude/scripts/revalidate.mjs outcome --point BEFORE_PR --ticket <item-id> --action-changed <true | false> --note '…'`
|
|
73
|
+
— and come back through step 0. A hold with no outcome is counted by the
|
|
74
|
+
report as a re-read the run skipped. Exit 0 with
|
|
75
|
+
`unverifiable` means the task side could not be compared — no take-up
|
|
76
|
+
snapshot in this run, or no marker — and is stated in the evidence, not read
|
|
77
|
+
as a pass. Exit 1 is the command refusing (unknown point, no ticket, a base
|
|
78
|
+
that is not a revision): fix the call. Its limits are its own header's; the
|
|
79
|
+
cited-path set is a labelled assumption, not a recorded fact.
|
|
49
80
|
2. **Route the diff before you spend on it.** This gate always ran its most
|
|
50
81
|
expensive path, so a typo fix in a README bought the same fan-out as a
|
|
51
82
|
rewrite of the storage layer. The dispatcher decides which lane the change
|
|
@@ -171,6 +202,7 @@ blockers.
|
|
|
171
202
|
🔴 **Record the set you launched, as you launch it.** The router journals the
|
|
172
203
|
set it *routed*; the triggers above may only add, so what you actually
|
|
173
204
|
launched is a different list and this is the only place that knows it:
|
|
205
|
+
**Record the fan-out even when the launched set is empty.**
|
|
174
206
|
|
|
175
207
|
```sh
|
|
176
208
|
node --input-type=module -e '
|
|
@@ -277,11 +309,33 @@ blockers.
|
|
|
277
309
|
exhausted trace must cost this round its record and nothing else. A round is
|
|
278
310
|
counted and capped, so a crash here spends one on a journal that was never
|
|
279
311
|
the thing under review.
|
|
280
|
-
5. **
|
|
312
|
+
5. 🔴 **Coverage — check your own fan-out before you believe it.** You recorded
|
|
313
|
+
what the route asked for, what you launched and what came back; this is the
|
|
314
|
+
step that compares them, and it is the only one that does — nothing else in
|
|
315
|
+
this gate would notice a reviewer that never answered:
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
node .claude/scripts/verdict.mjs coverage "$(git rev-parse HEAD)"
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
For a **declared run**, exit 0 is coverage. **Exit 1 is a `HOLD`.** A
|
|
322
|
+
reason-only unreadable-round failure always prints the evidence boundary;
|
|
323
|
+
remedies appear only when recovery is unambiguous. Reviewer lists cover the
|
|
324
|
+
four comparable cases — never launched (launch it),
|
|
325
|
+
launched and silent (go and read why), answered without naming a commit, or
|
|
326
|
+
answered for another commit (the head moved under the round). Record either
|
|
327
|
+
kind as a blocker of yours, in the same list as a failing check.
|
|
328
|
+
|
|
329
|
+
Two limits, stated because a step that looks mechanical is trusted like one.
|
|
330
|
+
It reads **this run's journal**, so with unset `RIG_RUN_DIR` the check is
|
|
331
|
+
skipped; exit 0 is then an honest nothing, not coverage. And it cannot see a
|
|
332
|
+
round that never reached this skill at all: a session that skips `pr-ship`
|
|
333
|
+
skips its coverage check with it (`docs/decisions/gate-coverage.md`).
|
|
334
|
+
6. **DoD walk.** Check the Definition of Done list in
|
|
281
335
|
`.claude/rules/workflow.md` item by item — test-first evidence, nothing
|
|
282
336
|
skipped or weakened, boundaries respected, docs updated, autonomy tier
|
|
283
337
|
honored.
|
|
284
|
-
|
|
338
|
+
7. **Named checks only.** The merge criterion is the project's *named* required
|
|
285
339
|
checks, all green. "Some checks passed" is not a criterion; an unnamed
|
|
286
340
|
green wall hides a red brick. Two traps here, both observed in the wild:
|
|
287
341
|
status watchers can exit while checks are **still unregistered** — poll the
|
|
@@ -317,7 +371,8 @@ the author, then **exactly one** fenced `json` block, and nothing after it.
|
|
|
317
371
|
}
|
|
318
372
|
],
|
|
319
373
|
"advisories": [],
|
|
320
|
-
"evidence": ["lane: model", "reviewers: code-reviewer, prose-reviewer"]
|
|
374
|
+
"evidence": ["lane: model", "reviewers: code-reviewer, prose-reviewer"],
|
|
375
|
+
"headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
|
|
321
376
|
}
|
|
322
377
|
```
|
|
323
378
|
|
|
@@ -330,6 +385,9 @@ the author, then **exactly one** fenced `json` block, and nothing after it.
|
|
|
330
385
|
pr-ship` — and fix what it refuses. Nothing downstream re-checks the gate's
|
|
331
386
|
own answer, so this call is the only thing between a malformed verdict and
|
|
332
387
|
whoever acts on it.
|
|
388
|
+
- **`headSha` is the commit you gated** — `git rev-parse HEAD`, the same one
|
|
389
|
+
step 5 asked coverage about. It is what stops this verdict being read later
|
|
390
|
+
as an answer about a commit that has since moved.
|
|
333
391
|
|
|
334
392
|
## Boundaries
|
|
335
393
|
|
|
@@ -69,7 +69,8 @@ reads; a report that never writes one is read as whatever the caller expected.
|
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
71
|
"advisories": [],
|
|
72
|
-
"evidence": ["diffed against origin/master", "queue item supplied"]
|
|
72
|
+
"evidence": ["diffed against origin/master", "queue item supplied"],
|
|
73
|
+
"headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
|
|
73
74
|
}
|
|
74
75
|
```
|
|
75
76
|
|
|
@@ -81,3 +82,9 @@ reads; a report that never writes one is read as whatever the caller expected.
|
|
|
81
82
|
what refuses them, and the shape it enforces is in
|
|
82
83
|
`.claude/scripts/lib/verdict.mjs`. The gate name is what stops your answer
|
|
83
84
|
being read as somebody else's.
|
|
85
|
+
- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the
|
|
86
|
+
checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage
|
|
87
|
+
<commit>` tell "this gate answered for the commit being merged" from "it
|
|
88
|
+
answered two pushes ago". A verdict naming no commit is counted as neither
|
|
89
|
+
covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook
|
|
90
|
+
runs that check, so a session that skips the gate skips this with it.
|
|
@@ -154,7 +154,8 @@ calling gate reads.
|
|
|
154
154
|
}
|
|
155
155
|
],
|
|
156
156
|
"advisories": [],
|
|
157
|
-
"evidence": ["opened .claude/hooks/guard-bash.mjs and quoted the line"]
|
|
157
|
+
"evidence": ["opened .claude/hooks/guard-bash.mjs and quoted the line"],
|
|
158
|
+
"headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
|
|
158
159
|
}
|
|
159
160
|
```
|
|
160
161
|
|
|
@@ -165,3 +166,9 @@ calling gate reads.
|
|
|
165
166
|
`node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses
|
|
166
167
|
them, and the gate name is what stops your answer being read as somebody
|
|
167
168
|
else's.
|
|
169
|
+
- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the
|
|
170
|
+
checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage
|
|
171
|
+
<commit>` tell "this gate answered for the commit being merged" from "it
|
|
172
|
+
answered two pushes ago". A verdict naming no commit is counted as neither
|
|
173
|
+
covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook
|
|
174
|
+
runs that check, so a session that skips the gate skips this with it.
|
|
@@ -58,7 +58,8 @@ the human who has to fix the finding.
|
|
|
58
58
|
}
|
|
59
59
|
],
|
|
60
60
|
"advisories": [],
|
|
61
|
-
"evidence": ["grepped for the pattern across services/"]
|
|
61
|
+
"evidence": ["grepped for the pattern across services/"],
|
|
62
|
+
"headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
|
|
62
63
|
}
|
|
63
64
|
```
|
|
64
65
|
|
|
@@ -71,3 +72,9 @@ the human who has to fix the finding.
|
|
|
71
72
|
`node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses
|
|
72
73
|
them, and the gate name is what stops your answer being read as somebody
|
|
73
74
|
else's.
|
|
75
|
+
- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the
|
|
76
|
+
checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage
|
|
77
|
+
<commit>` tell "this gate answered for the commit being merged" from "it
|
|
78
|
+
answered two pushes ago". A verdict naming no commit is counted as neither
|
|
79
|
+
covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook
|
|
80
|
+
runs that check, so a session that skips the gate skips this with it.
|