create-agent-rig 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +316 -11
  2. package/README.md +58 -19
  3. package/package.json +2 -1
  4. package/packages/cli/dist/commands/create.js +8 -3
  5. package/packages/cli/dist/commands/init.js +85 -34
  6. package/packages/cli/dist/commands/upgrade.js +112 -30
  7. package/packages/cli/dist/index.js +81 -19
  8. package/packages/cli/dist/lib/copy-tree.js +35 -6
  9. package/packages/cli/dist/lib/init-settings.js +12 -0
  10. package/packages/cli/dist/lib/install-set.js +6 -8
  11. package/packages/cli/dist/lib/manifest.js +31 -9
  12. package/packages/cli/dist/lib/safe-path.js +30 -0
  13. package/scripts/prepare.mjs +1 -1
  14. package/templates/agent-os/init/AGENTS.md +199 -0
  15. package/templates/agent-os/init/CLAUDE.md +69 -9
  16. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +105 -0
  17. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
  18. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +41 -3
  19. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
  20. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +41 -0
  21. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
  22. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
  23. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +236 -0
  24. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +958 -0
  25. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
  26. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
  27. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
  28. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +396 -0
  29. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
  30. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +40 -1
  31. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +71 -1
  32. package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
  33. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +301 -26
  34. package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
  35. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
  36. package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +127 -0
  37. package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
  38. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
  39. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
  40. package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +503 -0
  41. package/templates/agent-os/universal/.claude/rules/autonomy.md +69 -1
  42. package/templates/agent-os/universal/.claude/rules/invariants.md +104 -19
  43. package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
  44. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1260 -0
  45. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
  46. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +351 -0
  47. package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
  48. package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
  49. package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +28 -0
  50. package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
  51. package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +462 -0
  52. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +33 -17
  53. package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
  54. package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +149 -0
  55. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +809 -28
  56. package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
  57. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +95 -16
  58. package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +543 -15
  59. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +457 -47
  60. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +289 -25
  61. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
  62. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +316 -0
  63. package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +180 -0
  64. package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
  65. package/templates/agent-os/universal/.claude/scripts/run-state.mjs +539 -0
  66. package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
  67. package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +239 -0
  68. package/templates/agent-os/universal/.claude/scripts/verdict.mjs +198 -0
  69. package/templates/agent-os/universal/.claude/settings.json +11 -2
  70. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +118 -7
  71. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +667 -38
  72. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +359 -26
  73. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
  74. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
  75. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
  76. package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
  77. package/templates/agent-os/universal/.codex/hooks.json +70 -0
  78. package/templates/agent-os/universal/AGENTS.md +166 -0
  79. package/templates/agent-os/universal/CLAUDE.md +49 -14
  80. package/templates/agent-os/universal/PLAN.md +7 -40
  81. package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
  82. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
  83. package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
  84. package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
  85. package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
  86. package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
  87. package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
  88. package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
  89. package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +74 -0
  90. package/templates/agent-os/universal/journal/README.md +101 -0
  91. package/templates/agent-os/universal/layers.json +45 -2
  92. package/templates/hash-history.json +310 -49
  93. package/templates/release-ledger.json +9 -0
  94. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
  95. package/templates/skeleton/aws-serverless/README.md +91 -9
  96. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
  97. package/templates/skeleton/aws-serverless/gitignore +37 -0
  98. package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
  99. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
  100. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
  101. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
  102. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
  103. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
  104. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
  105. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
  106. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
  107. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
  108. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
  109. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
  110. package/templates/skeleton/node-service/README.md +11 -1
  111. package/templates/skeleton/node-service/gitignore +34 -0
  112. package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
  113. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
  114. package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
  115. package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
  116. package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
  117. package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
  118. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +28 -0
@@ -0,0 +1,83 @@
1
+ # Why the fan-out is checked in `pr-ship`, and never in a hook
2
+
3
+ The check lives in the `pr-ship` skill, step 5 ("Coverage"), and its rule is one
4
+ sentence: *a merge is not gated by a fan-out whose reviewers did not all answer
5
+ for the commit being merged.* This file explains why that check sits in a skill
6
+ rather than in `guard-bash`. The hook version is the one that keeps being
7
+ proposed, and a refusal nobody wrote down gets proposed again. It is not loaded
8
+ into any session.
9
+
10
+ ## The shape that was refused
11
+
12
+ The proposal: `guard-bash` denies a merge command unless the run journal holds a
13
+ `SHIP` from every reviewer the router named, for the PR's head commit. It is the
14
+ attractive version — mechanical, at the tool layer, impossible for a hurried
15
+ session to skip.
16
+
17
+ It was refused on four costs, each of them a rule this project already paid for.
18
+
19
+ **1. It needs to recognise a merge command while the brake is off.** The kill
20
+ switch can be coarse — it denies the network clients *as a class of binaries*,
21
+ reading no merge semantics at all, while still allowing the wind-down it asks
22
+ for — because a false block is cheap when the session is already stopped. `invariants.md` states that
23
+ directly: match a rule's precision to the cost of a false positive. A merge gate
24
+ runs during ordinary work, where a false block lands on the last step of finished
25
+ work. The one attempt at enumerating merge routes was reverted, and what it cost
26
+ is recorded where the attempt was made rather than restated here:
27
+ `.claude/hooks/guard-bash.mjs`, the comment above `NETWORK_CLIENTS`.
28
+
29
+ **2. It needs the head commit, inside a hook that must stay pure.** For
30
+ `gh pr merge <number>` the head is only knowable over the network, and
31
+ `guard-bash` records what an unbounded hook costs: killed by its own timeout, and
32
+ a killed hook does not block — so **every** rule silently switched off, not just
33
+ the new one.
34
+
35
+ **3. It needs to parse the journal in a guard that fails open.** The journal
36
+ reader refuses a malformed line by throwing, which is right for a reader and
37
+ fatal for a fail-open guard: the throw resolves to *allow*, again for every rule
38
+ at once. `stop-flag.mjs` is not the precedent it resembles — that is `existsSync`
39
+ over a capped list of fixed paths, not a parse.
40
+
41
+ **4. Its sweep half cannot exist.** The natural companion — audit merged PRs from
42
+ outside — has nothing to read: the run directory is gitignored, and the sweep
43
+ sees labels and PR bodies only.
44
+
45
+ ## And the reason the four costs were not worth paying
46
+
47
+ Even fully built, the guard would read a file **the audited run wrote itself**.
48
+ For the threat model a hook actually has — drift, not an adversary — that is
49
+ tolerable, and it is why the skill-level check below is worth having at all. What
50
+ it is not is the *guarantee* the hook version promised, and the four costs above
51
+ were being paid for that word.
52
+
53
+ ## What was built instead
54
+
55
+ `.claude/scripts/lib/gate-coverage.mjs` compares the three sets the journal already holds —
56
+ routed, launched, answered — and `pr-ship` runs it through
57
+ `verdict.mjs coverage <commit>` before its own verdict. It sits in the layer that
58
+ already knows the head commit, is allowed to run git, and is allowed to throw.
59
+
60
+ Two limits, and both are stated in the skill beside the step rather than only
61
+ here:
62
+
63
+ - with no `RIG_RUN_DIR` there is no journal, so the check says it was skipped and
64
+ exits 0 — an honest nothing, never a pass;
65
+ - a session that skips `pr-ship` entirely skips this with it. That is the gap the
66
+ hook version was reaching for, and it stays open.
67
+
68
+ So `CLAUDE.md` keeps its sentence unchanged: *no hook launches the gates, so "the
69
+ gate ran" is a claim, not a guarantee.* What changed is narrower and worth
70
+ having on its own — the driver can no longer accept a fan-out that did not
71
+ answer, or one that answered about a commit two pushes ago.
72
+
73
+ ## The variant that removes self-report, and why it is not the default
74
+
75
+ Running the reviewers as CI check runs is the only shape where the evidence is
76
+ authored by something other than the audited run, and where the driver cannot
77
+ skip the gate at all. It is not refused on its merits.
78
+
79
+ It is refused **as a default for this layer**: everything under `.claude/` ships
80
+ into every project this rulebook generates, so making it the shape would require
81
+ a model credential in every generated project's CI and a per-PR cost, for
82
+ projects that never asked for either. A project that wants that guarantee should
83
+ adopt it deliberately; the check above is what every project gets.
@@ -0,0 +1,72 @@
1
+ # Why the router's prose set and the gate sweep's inert set differ
2
+
3
+ The rule lives in `.claude/rules/workflow.md`, under "PR flow". This file
4
+ explains the one part of it that reads like an inconsistency and is not, so that
5
+ nobody "tidies" the two sets into agreement. It is not loaded into any session.
6
+
7
+ ## The sets
8
+
9
+ Three different mechanisms each carry their own notion of "this file is only
10
+ words":
11
+
12
+ **The first two carve out the rulebook**, and only then differ; the third runs
13
+ *after* the sweep has already answered, so it sees only what survived:
14
+
15
+ | mechanism | rulebook exemption | inert / prose after it | consequence |
16
+ | ------------------------ | ------------------------------------------ | ------------------------------------------ | --------------------------------------------------- |
17
+ | the Tier-2 gate sweep | `isRulebook` in `detect-missed-gate.mjs` | `.md`, `.mdx`, non-provisioning test paths | does not escalate on the elevated-path ground alone |
18
+ | `decision-router.mjs` | `isRulebookPath`, checked before extension | `.md`, `.txt` | may route to the `fast-path` prose lane |
19
+ | the queue's elevated ration (`queue/state.mjs`) | none of its own — it classifies what `elevatedPathsIn` already returned, so only **rulebook** markdown ever reaches it | `.md` (`executesNothing`) | records `elevated-prose`, which does **not** space the next item |
20
+
21
+ The third row is the one added last, and its question is different again: not
22
+ "does this need the gate" nor "can a prose reviewer judge it alone", but **"can
23
+ a merge of this compound into a broken runtime overnight"**. That is why it
24
+ excludes `.mdx` where the sweep includes it — same reasoning as the router's,
25
+ one row below — and why it is `.md` only rather than the sweep's set. Its two
26
+ predicates live in `detect-missed-gate.mjs` side by side so *those two* cannot
27
+ drift; that is a statement about one file, **not** a rule that every markdown
28
+ test in the rig belongs there. The router's sets stay where they are, for the
29
+ reason this whole record exists.
30
+
31
+ **Neither row applies to a decision record, including this one.** Both
32
+ predicates recognise `CLAUDE.md` anywhere, everything under `.claude/`, and —
33
+ since these records were extracted — everything under `docs/decisions/`, through
34
+ the one shared `isDecisionRecord`. A change here escalates the sweep and takes
35
+ the `model` lane, exactly as the rule it explains would. The router's call is
36
+ not a redundant copy of the sweep's; deleting it silently drops this file to the
37
+ prose lane.
38
+
39
+ Without that exemption, declaring a path elevated was a no-op for every `.md`
40
+ under it — the defect that put it there — and it is why moving this rationale
41
+ out of `.claude/` needed a code change rather than only a declaration:
42
+ `isInert` is consulted first, so the declaration alone would have reported clean
43
+ over every record.
44
+
45
+ The **remaining** difference is the inert/prose column, and no set contains
46
+ another. That is deliberate, and every difference earns its keep. One more
47
+ asymmetry belongs to the router alone: it **folds case** before its rulebook
48
+ check, because there folding can only escalate, while a sweep that folded would
49
+ name a path the repository does not have.
50
+
51
+ ## Why the sweep does not also take `.txt`
52
+
53
+ Widening the sweep to `.txt` would stop `requirements.txt` inside an elevated
54
+ directory from escalating. A dependency manifest is the opposite of inert — it
55
+ is one of the risk flags — and its extension happens to be `.txt`.
56
+
57
+ ## Why neither the router nor the ration takes `.mdx`
58
+
59
+ Copying the sweep's `.mdx` into the router's prose set would put executable MDX
60
+ back on the prose lane. MDX carries components and imports; it is a program that
61
+ renders, not a document that is read. The ration reads it the same way and for
62
+ the same reason (`spacing-rations-mechanisms.md`): calling it prose there would
63
+ clear the spacing hold on a file this rig treats as a program, on the permissive
64
+ side.
65
+
66
+ ## The shape of the mistake this prevents
67
+
68
+ Both edits look like clean-ups. Each removes an apparent inconsistency between
69
+ two lists that "obviously" mean the same thing, and each one silently downgrades
70
+ a real review. The lists mean different things because the questions differ:
71
+ "does this change need the elevated gate?" is not "can a prose reviewer judge
72
+ this alone?"
@@ -0,0 +1,44 @@
1
+ # Why one run directory per run, and why nothing can enforce it
2
+
3
+ The rule lives in the `loop` skill, section 1 ("Declare the run directory here").
4
+ This file explains what the enforcement does and does not reach, so nobody
5
+ mistakes the warning for a guarantee. It is not loaded into any session.
6
+
7
+ ## An undeclared run is not a run with a missing journal
8
+
9
+ `RIG_RUN_DIR` holds two unrelated things: the machine trace, and the run's
10
+ **stop conditions** — the escalation streak, the deploy verdict, the budget flag.
11
+
12
+ With `RIG_RUN_DIR` unset, the two verdicts a session writes by hand
13
+ (`run-state.mjs deploy …`, `run-state.mjs budget …`) refuse loudly, so it finds
14
+ out. **The escalation count does not.** It is recorded nowhere, silently, and
15
+ selection then hands out work after two walls in a row with nothing on stderr to
16
+ say why.
17
+
18
+ So an undeclared run is a run whose main brake is off, and which looks exactly
19
+ like a healthy one from the outside.
20
+
21
+ The trace's first call site is **selection**, which runs before every task.
22
+ Declared later, it misses everything that already happened — which is why the
23
+ declaration belongs in preflight or nowhere.
24
+
25
+ ## What the reuse check catches
26
+
27
+ - Two writers landing on the same sequence number.
28
+ - A directory that already carries its run-end marker.
29
+
30
+ Both are refused. Selection still works and says so on stderr, and that run's
31
+ trace ends there — **loudly**, which is the good case.
32
+
33
+ ## What it cannot catch
34
+
35
+ Two runs whose records happen not to collide.
36
+
37
+ A run that died before writing its end marker — and dying unexpectedly is
38
+ exactly the case the checkpoint discipline exists for — leaves an intact
39
+ sequence behind it. The next run pointed at that directory **continues it in
40
+ silence**: one seamless trace of two runs, with nothing in the file able to say
41
+ so.
42
+
43
+ Nothing detects this after the fact. A fresh directory per run is the only thing
44
+ that prevents it, and it is the session's to do.
@@ -0,0 +1,125 @@
1
+ # Why the elevated ration counts mechanisms and not documents
2
+
3
+ The rule lives in the `loop` skill, section 2; the code is
4
+ `.claude/scripts/queue/state.mjs` (which tier a close records) and
5
+ `.claude/scripts/queue/core.mjs` (which tiers clear the ration). This file
6
+ explains why the elevated tier splits in two, why only one half spaces the next
7
+ item, and why an unrecognised tier holds. It is not loaded into any session.
8
+
9
+ ## What the ration was bought for
10
+
11
+ The rule's own sentence names it: _one **unreviewed** schema or permissions
12
+ change is recoverable; a chain of them compounding overnight is not._ Two words
13
+ in it do the work. **Unreviewed** — the risk is an unattended run stacking
14
+ changes nobody read. **Compounding** — the second change lands on top of what the
15
+ first one did, so the damage is not additive but multiplied.
16
+
17
+ A rulebook document satisfies neither. It is reviewed on the model lane by two
18
+ cold readers, and it compounds into nothing overnight because no runtime
19
+ executes it. Yet in any repository whose rulebook lives under a declared
20
+ elevated path — every rig, by construction — a `.md` close recorded the same
21
+ `elevated` as a migration and held the next item exactly as hard.
22
+
23
+ Two measurements on this repository forced the change, and they are different
24
+ facts — the second is the mechanism's own report, the first is not:
25
+
26
+ - The `loop` run of 2026-08-17 hit `nothing-selectable` with **56 takeable items
27
+ held, 53 of them by spacing** — the count `queue/index.mjs next` printed, so
28
+ this one is the ration speaking. It holds on the item's **marker**, which is
29
+ what those 53 carried. ⚠ Not reproducible from this repository: the queue is
30
+ tracker-backed and the run's own record is under the gitignored
31
+ `.claude/runs/`, so this figure is a report, not a check anyone can re-run.
32
+ - Earlier, the AR-63 run recorded (journal, 2026-08) that of 58 selectable items
33
+ **56 were elevated in fact — by declared path** — 31 of them marked so and 25
34
+ marked `normal` while their own bodies named an elevated path. That is a
35
+ measurement of the queue, not of the ration: it says the pressure is not going
36
+ to ease, because a rig's rulebook lives under a declared path by construction.
37
+
38
+ Together: the ration was not pacing the project, it was halting it, and the halt
39
+ was indistinguishable from the rule working.
40
+
41
+ ## The split, and where it does NOT apply
42
+
43
+ `recordCompletedTier` classifies the elevated paths a close actually crossed:
44
+
45
+ | what the close crossed | tier recorded | effect on the next elevated item |
46
+ | --- | --- | --- |
47
+ | every elevated path is a document | `elevated-prose` | clears the ration |
48
+ | any elevated path is not | `elevated-mechanism` | spaces it |
49
+ | no elevated path at all | `normal` | clears the ration |
50
+
51
+ ⚠ **"Elevated path" is the sweep's answer, not the diff's.** `elevatedPathsIn`
52
+ drops inert paths *before* this classification runs, and a non-rulebook `.md` is
53
+ inert — so `scripts/notes.md` under a declared directory records `normal`, not
54
+ `elevated-prose`. In practice the only markdown that ever reaches the split is
55
+ **rulebook** markdown: `CLAUDE.md` anywhere, everything under `.claude/`, and the
56
+ decision records. That is pre-existing sweep behaviour, restated here because the
57
+ table above reads more broadly than the code behaves.
58
+
59
+ The predicate is `executesNothing` — **`.md` only** — and it lives in
60
+ `detect-missed-gate.mjs` beside the sweep's own markdown test so the two cannot
61
+ drift apart.
62
+
63
+ **They are different tests on purpose, and `.mdx` is where they part.** The
64
+ sweep asks *does this need a reviewer* and calls both flavours inert. The ration
65
+ asks *can a merge of this compound overnight*, and MDX carries components and
66
+ imports — it is a program that renders, not a document that is read, which is why
67
+ `decision-router.mjs` already sends it down the code lane
68
+ (`review-lanes.md`). Calling it prose here would clear the spacing hold on a file
69
+ this same rig treats as a program, on the permissive side.
70
+
71
+ Two questions, two predicates, **one file** — that pairing is why they cannot
72
+ drift. It is not a rule that every markdown test in the rig belongs there:
73
+ `decision-router.mjs` keeps its own sets on purpose, and `review-lanes.md` — now
74
+ with a row for this ration — exists to stop anyone consolidating them.
75
+
76
+ **The known soft spot: a skill's `SKILL.md` is prose by this test**, and some
77
+ skills carry shell snippets an agent copies and runs. The ruling is that skills
78
+ stay prose for rationing — they are reviewed like the rules they are, and
79
+ rewriting a procedure is not the chain of unreviewed compounding changes the
80
+ ration was bought to stop. It is still the weakest ground the "no runtime
81
+ executes it" justification stands on, and the first place to look if the ration
82
+ turns out too loose.
83
+
84
+ **A mixed diff is a mechanism close.** The half that runs decides. Reading the
85
+ tier off the first path, or off "most of them are documents", would ship a ration
86
+ any diff can opt out of by also touching a `.md`.
87
+
88
+ The split is read by **exactly one** consumer: the ration. `elevated-prose` is
89
+ still an elevated change wherever it is *reviewed* — the model lane, the cold
90
+ readers, the `human-review` label, the gate sweep — and `recordCompletedTier`
91
+ still reports every elevated path it found, unchanged. Narrowing the ration must
92
+ not narrow the report: a prose merge that stopped listing its rulebook files
93
+ would look clean to the sweep that exists to catch exactly those merges.
94
+
95
+ ## Unknown holds; absent does not
96
+
97
+ `core.mjs` releases on `normal`, on `elevated-prose`, and on an **absent** tier —
98
+ `null`/`undefined`, which is the honest statement that nothing has closed yet and
99
+ what a fresh checkout says. Everything else holds: the legacy `'elevated'` an
100
+ older state file carries, a word in the wrong case, a value that is not a string.
101
+
102
+ The tempting implementation is `tier === 'elevated-mechanism'`. It is wrong in
103
+ the one direction that matters: every unrecognised value would read as "nothing
104
+ elevated closed" and hand out the next elevated item — un-rationing the queue
105
+ silently, on state files this project itself wrote last week. The whole seam
106
+ exists because a filter whose input nobody supplies is indistinguishable from a
107
+ filter that agrees with you.
108
+
109
+ The legacy word stays *readable* for the same reason it must not be *permissive*:
110
+ a checkout that upgrades mid-run still has one on disk, and refusing it outright
111
+ would exit 1 on the next selection. `loadState` accepts four words and refuses
112
+ the rest — **for the state file only**. Selection falls back to a tier left in
113
+ `queue.json`, and `loadConfig` validates nothing but JSON syntax, so a word like
114
+ `banana` in the config reaches `selectNext` unrefused — measured: the CLI exits 0
115
+ with no refusal, and then **holds** the elevated item with `causes: ["spacing"]`.
116
+ It holds because `core.mjs` is the layer that reads every unrecognised value
117
+ restrictively — the same layer that holds when `selectNext` is called directly.
118
+
119
+ ## What this does not fix
120
+
121
+ The livelock where *every* remaining item is elevated-by-mechanism still exists —
122
+ narrowed, not closed. Its operator-only remedy is filed and **not built**: no
123
+ such command exists in this repository today, so a run that hits the livelock
124
+ stops and says so rather than clearing its own tier. Spacing is also still per
125
+ checkout, so a second worktree does not get its own allowance.
@@ -0,0 +1,46 @@
1
+ # Why the run's stop conditions are a file, not a memory
2
+
3
+ The rule lives in the `loop` skill, section 3 ("What keeps the loop running,
4
+ and what stops it"). This file records why the state was moved out of the
5
+ session, and why only one of the two written verdicts can be taken back. It is
6
+ not loaded into any session.
7
+
8
+ ## The defect the file fixed
9
+
10
+ `escalations` and `lastDeployVerdict` now live in `<RIG_RUN_DIR>/state.json`,
11
+ written by `run-state.mjs`.
12
+
13
+ Before that existed, the CLI called `stopConditionOf` with the queue counts
14
+ alone. Every state-dependent branch therefore held its **default** on every real
15
+ selection — the runtime-regression stop, the two-escalations stop, the budget
16
+ stop. None of them could fire. The rules were enforced only by whichever session
17
+ happened to remember them.
18
+
19
+ Compaction is exactly the moment a long unattended run stops remembering, and a
20
+ long unattended run is the only situation these stops exist for. So the stops
21
+ were reliably absent precisely where they were needed.
22
+
23
+ ## Why the escalation count has two writers, and both must be the documented one
24
+
25
+ - It **rises** through the adapter's `escalate()`. Hand-labelling an item
26
+ escalated marks the item and counts nothing, so the run grinds past the wall.
27
+ - It **resets** through the close step's `recordCompletedTier`, and only when
28
+ that call is passed `runDir` — see `docs/decisions/closing-a-task.md`.
29
+
30
+ Both halves fail quietly when done the undocumented way, and they fail in
31
+ opposite directions: one never stops a broken run, the other stops a healthy one.
32
+
33
+ ## Why only the deploy verdict can be taken back
34
+
35
+ The asymmetry is deliberate.
36
+
37
+ `HEALTHY` names a **real later event** — the revert landed and the runtime was
38
+ re-verified. Without a way to record it, one bad deploy would end every later
39
+ selection in the run, however thoroughly it was fixed.
40
+
41
+ Spend only accumulates. Un-exhausting a budget would name no event at all — only
42
+ a decision to keep going, taken by the very run that declared the stop. That is
43
+ the shape of a stop condition a run can talk itself out of, which is not a stop
44
+ condition.
45
+
46
+ A new run starts from a clean state. That is the way back from both.
@@ -0,0 +1,74 @@
1
+ # Why "queue empty" and "nothing selectable" are different endings
2
+
3
+ The rule lives in the `loop` skill, section 3. This file explains why the two
4
+ stops are reported separately, how the parked pile grows under each adapter, and
5
+ why a parked cause outranks a holding one. It is not loaded into any session.
6
+
7
+ ## The two endings ask the owner for opposite things
8
+
9
+ - **Queue empty after the filters** — there is no work. Refilling it is the
10
+ owner's job. Expect this to be the most common ending: the queue is finite and
11
+ the loop drains it.
12
+ - **Nothing selectable** — the work is there and every piece of it is held back
13
+ by a condition that clears when _something else happens_: the elevated spacing
14
+ (a normal item lands — or an elevated one whose paths were all documents, see
15
+ `spacing-rations-mechanisms.md`), a blocker (its item closes), `in-progress`
16
+ (the other session finishes), a trigger (a human declares it), an owner (the
17
+ item is marked for another repository and a human moves or re-marks it —
18
+ AR-132), a `re-scope` item (a human rewrites it against the current code and
19
+ removes the label — AR-144), and a `deferred` item (it carries the `parked`
20
+ label and a human un-parks it — AR-144).
21
+
22
+ An empty queue wants refilling. A held one wants interleaving, time, or — for a
23
+ trigger or an owner — the human act the stop line names.
24
+ Reporting the second as the first sends the owner to write tickets that already
25
+ exist; reporting the first as the second tells them to wait for nothing.
26
+
27
+ ## The parked pile is reported next to the verdict, never inside it
28
+
29
+ Parked items are out of play and waiting on a human. They are not work this run
30
+ can take, and they are not why the queue is empty — so they are named separately,
31
+ by cause and count. An `obsolete` item (AR-144) is one of them: it waits on a
32
+ human close with a comment naming the evidence, which the loop never writes.
33
+
34
+ ⚠ "Parked" here is the pile, not the `parked` **label**. The label (AR-144) means
35
+ "valid work, deliberately not now" — takeable, held, freed by an un-park — so its
36
+ cause is spelled `deferred`, and it is never in this pile. The two were named
37
+ before each other existed; this note is the reconciliation rather than a rename,
38
+ because the pile word runs through the stop line, the tests and this record.
39
+
40
+ How that pile grows is the adapter's business, and the stop line does not guess:
41
+
42
+ | adapter | what lands in the parked pile |
43
+ | ---------------------------------------- | ----------------------------------------------------- |
44
+ | tracker-backed (`github-issues`, `jira`) | an escalated item, a filed proposal (both stay open), and an item labelled `obsolete` (AR-144) |
45
+ | `plan-md` | a `[triage]` or an `[obsolete]` line sitting in the Agent queue — nothing else, because a flat list has no per-item state |
46
+
47
+ Under `plan-md` a filed proposal goes to the Operator queue, where selection
48
+ never looks, and an escalation leaves **no mark on the queue at all** — a flat
49
+ list has no per-item state.
50
+
51
+ ## That absence is not a safety
52
+
53
+ `plan-md`'s `escalate` writes nothing to the queue and returns `ok: false`,
54
+ handing back the instruction with it: move the item to the Operator queue in the
55
+ same edit. That move is the session's, and skipping it means the next run takes
56
+ the stuck item straight back.
57
+
58
+ It does still record the escalation into the run state, like every other
59
+ adapter. The two are different facts, which is why escalating must go through
60
+ the adapter rather than by hand-labelling: the count is what ends a run that has
61
+ hit the same wall twice.
62
+
63
+ ## Why a parked cause outranks a holding one
64
+
65
+ An escalated item is left **claimed** on purpose, so it arrives carrying
66
+ `in-progress` as well as `escalated`.
67
+
68
+ Reading that as _held_ would report "another session will finish it" about an
69
+ item no session is on — and, worse, would make the empty verdict unreachable
70
+ from the first escalation onward. Reporting a working queue as empty is the
71
+ defect this distinction exists to prevent; reporting a parked one as working is
72
+ the same defect reversed.
73
+
74
+ Neither ending is an invitation to refill the queue or invent work.
@@ -0,0 +1,101 @@
1
+ # The journal
2
+
3
+ The human record of what sessions did here. **One file per month**, named
4
+ `journal/YYYY-MM.md` — the month is the filename's job, so entries themselves
5
+ stay date-free and their order carries the sequence.
6
+
7
+ **Newest-on-top inside each file.** A new entry goes directly under the month's
8
+ heading, so the file reads as a stack. Prune freely: this is operational memory,
9
+ not an archive.
10
+
11
+ **The shape of a month file**, stated because the first session of a new month
12
+ creates it and a shape nobody wrote down is one that has to be guessed:
13
+
14
+ - it **opens with a single `#` heading** naming the month;
15
+ - **every entry is a `###`** under that heading, and there is nothing at `##`
16
+ or any other level in between;
17
+ - a new entry is inserted directly beneath the `#` heading, above the previous
18
+ newest.
19
+
20
+ **What is mechanically checked, precisely — because this file ships into a
21
+ project and its checks do not.** The generator that produced this rig has tests
22
+ for the first two bullets and for the `YYYY-MM.md` filename; **this project has
23
+ none of them** unless you write them. That is the same arrangement
24
+ `.claude/rules/invariants.md` describes for the hooks, and the same caveat
25
+ applies: what is not tested here is convention, however firmly it is written.
26
+
27
+ The third bullet — *where* a new entry is inserted — is convention even in the
28
+ generator. Nothing checks entry order anywhere yet; making "newest" decidable
29
+ for date-free entries is its own piece of work. Do not read the presence of a
30
+ shape rule as a guarantee that a misordered file would be caught.
31
+
32
+ `README.md` is this document, not a month file. Other names: a file that does
33
+ not match `YYYY-MM.md` is never **read** as a month file — but do not read that
34
+ as permission, because in the generator any other `*.md` in this directory
35
+ **fails** the filename check. Keep archives and side notes out of this
36
+ directory, or somewhere with a non-`.md` extension.
37
+
38
+ An unattended run writes an entry at every stop **and** at checkpoints along the
39
+ way, because a run that dies unexpectedly must not take its history with it.
40
+
41
+ 🔴 **This is not the run trace.** `.claude/runs/<run>/events.jsonl` and
42
+ `decisions.jsonl` are machine-readable, append-only and **oldest-first**, written
43
+ by `run-journal.mjs`. This file is the human record and is newest-on-top. They
44
+ answer different questions — the trace says what the run decided and on what
45
+ basis, this says what a reader needs to know afterwards — and reading one as the
46
+ other is how a reader concludes a run did nothing.
47
+
48
+ ## The entry
49
+
50
+ The fields exist so an entry can be visibly **incomplete**. A journal with no
51
+ stated shape decays into a diary that reads fine and proves nothing.
52
+
53
+ Copy the block and drop the fields that do not apply — `unblocked` is the
54
+ exception, and it is stated even when the answer is "nothing".
55
+
56
+ ```markdown
57
+ ### <one-line summary of the session>
58
+
59
+ - **done** — what landed, one line each, with the PR reference
60
+ - **escalated** — what stopped, and the diagnosis: what failed, what was tried,
61
+ the current hypothesis, and the one question whose answer unblocks it
62
+ - **reviewed** — changes that went through a reviewer gate, and what it returned
63
+ - **stopped at** — which stop condition ended the session (or "checkpoint,
64
+ still running")
65
+ - **unblocked** — what the session's closes released
66
+ - **queue hygiene** — queue state the session found unreliable and reported
67
+ - **cost** — the counts the session actually observed
68
+ ```
69
+
70
+ ## The two fields that are most often got wrong
71
+
72
+ **`unblocked` is never dropped**, and it has **three** answers that do not
73
+ substitute for each other:
74
+
75
+ 1. the items that were waiting, by name;
76
+ 2. "nothing was waiting" — the queue carries dependency links and none pointed
77
+ here;
78
+ 3. "this queue has no dependency links" — it cannot answer at all.
79
+
80
+ A flat-list queue is **absent, not satisfied**, and writing "nothing was waiting"
81
+ there claims a look that no query could perform. A missing line and an unpaid
82
+ debt read identically from outside, which is why the empty case has to be
83
+ written to mean anything.
84
+
85
+ **`cost` carries only what the session observed** — reviewer subagents run, CI
86
+ runs consumed (re-runs included, the cheapest signal that a task fought its
87
+ tests), deploys triggered.
88
+
89
+ ## Never estimated — and this governs every field, not just `cost`
90
+
91
+ **Any** field the session cannot observe stays **visibly empty, never
92
+ estimated**. A plausible number will be believed, by the next reader and by the
93
+ next run reasoning about its own budget — and the same is true of a plausible
94
+ sentence: a guessed `reviewed` or an inferred `unblocked` is the identical
95
+ failure without the digits. Leave the gap; it is information.
96
+
97
+ ## Queue hygiene is reported, never corrected in passing
98
+
99
+ A stale marker, a dependency already satisfied, an item describing work already
100
+ done — record it here. Quietly fixing the metadata destroys the evidence that the
101
+ metadata is unreliable.
@@ -14,21 +14,62 @@
14
14
  ".claude/hooks/guard-bash.mjs",
15
15
  ".claude/hooks/gate-stop-dod.mjs",
16
16
  ".claude/hooks/inject-rules.mjs",
17
+ ".claude/hooks/guard-secret-file.mjs",
18
+ ".claude/hooks/guard-rulebook.mjs",
19
+ ".claude/hooks/lib/edit-input.mjs",
17
20
  ".claude/skills/pr-ship/SKILL.md",
18
21
  ".claude/skills/loop/SKILL.md",
19
22
  ".claude/skills/check-premises/SKILL.md",
20
23
  ".claude/skills/worktree-task/SKILL.md",
24
+ ".agents/skills/new-invariant/SKILL.md",
25
+ ".agents/skills/new-invariant/guard-invariant.example.mjs",
26
+ ".agents/skills/new-invariant/guard-invariant.example.test.mjs",
27
+ ".agents/skills/pr-ship/SKILL.md",
28
+ ".agents/skills/loop/SKILL.md",
29
+ ".agents/skills/check-premises/SKILL.md",
30
+ ".agents/skills/worktree-task/SKILL.md",
31
+ ".codex/agents/test-writer.toml",
32
+ ".codex/agents/code-reviewer.toml",
33
+ ".codex/agents/security-scanner.toml",
34
+ ".codex/agents/prose-reviewer.toml",
21
35
  ".claude/scripts/detect-missed-gate.mjs",
36
+ ".claude/scripts/decision-router.mjs",
22
37
  ".claude/scripts/reconcile-external-prs.mjs",
23
38
  ".claude/scripts/stop-flag.mjs",
39
+ ".claude/scripts/unattended-flag.mjs",
40
+ ".claude/scripts/git-env.mjs",
24
41
  ".claude/scripts/preflight.mjs",
42
+ ".claude/scripts/doctor.mjs",
43
+ ".claude/scripts/run-journal.mjs",
44
+ ".claude/scripts/run-state.mjs",
45
+ ".claude/scripts/verdict.mjs",
46
+ ".claude/scripts/revalidate.mjs",
47
+ ".claude/scripts/revalidation-report.mjs",
48
+ ".claude/scripts/lib/gate-coverage.mjs",
49
+ ".claude/scripts/lib/revalidation-points.mjs",
50
+ ".claude/scripts/lib/verdict.mjs",
51
+ ".claude/scripts/lib/secrets.mjs",
25
52
  ".claude/scripts/queue/core.mjs",
26
53
  ".claude/scripts/queue/plan-md.mjs",
27
54
  ".claude/scripts/queue/github-issues.mjs",
28
55
  ".claude/scripts/queue/jira.mjs",
29
56
  ".claude/scripts/queue/index.mjs",
57
+ ".claude/scripts/queue/as-of.mjs",
58
+ ".claude/scripts/queue/checkout.mjs",
59
+ ".claude/scripts/queue/state.mjs",
60
+ ".claude/scripts/queue/gate-rounds.mjs",
30
61
  ".claude/queue.json",
31
- "PLAN.md"
62
+ "PLAN.md",
63
+ "journal/README.md",
64
+ "docs/decisions/gate-coverage.md",
65
+ "docs/decisions/fail-open-guards.md",
66
+ "docs/decisions/codex-adapter.md",
67
+ "docs/decisions/closing-a-task.md",
68
+ "docs/decisions/review-lanes.md",
69
+ "docs/decisions/run-directory.md",
70
+ "docs/decisions/spacing-rations-mechanisms.md",
71
+ "docs/decisions/stop-conditions-in-a-file.md",
72
+ "docs/decisions/two-empty-endings.md"
32
73
  ],
33
74
  "architecture": [
34
75
  ".claude/rules/architecture.md",
@@ -37,6 +78,8 @@
37
78
  ],
38
79
  "meta": [
39
80
  ".claude/settings.json",
40
- "CLAUDE.md"
81
+ ".codex/hooks.json",
82
+ "CLAUDE.md",
83
+ "AGENTS.md"
41
84
  ]
42
85
  }