create-agent-rig 0.5.0 → 0.6.1
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 +170 -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 +15 -5
- package/templates/agent-os/init/CLAUDE.md +15 -5
- 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 +330 -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 +18 -6
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +8 -1
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +42 -17
- package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +2 -1
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +187 -0
- package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +72 -65
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +33 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +13 -3
- package/templates/agent-os/universal/.claude/rules/invariants.md +45 -17
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +20 -1
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +354 -0
- package/templates/agent-os/universal/.claude/scripts/git-env.mjs +1 -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 +29 -0
- package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +4 -1
- 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 +282 -19
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +395 -46
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +68 -5
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +317 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +181 -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 +436 -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 +330 -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 +404 -51
- package/templates/release-ledger.json +10 -0
- package/templates/skeleton/aws-serverless/gitignore +2 -0
- package/templates/skeleton/node-service/gitignore +2 -0
|
@@ -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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
name = "code-reviewer"
|
|
2
2
|
description = "Reviews a completed change against the checklist before a PR is opened or merged. Use after any non-trivial implementation work, and always before opening a PR the decision-router puts on its `model` lane, which is everything its two cheap lanes did not claim — code, a rulebook document, an unclassifiable path, a derived artifact git does not report as drift, or anything a risk flag escalated. Blocking findings must be resolved, not argued with."
|
|
3
3
|
sandbox_mode = "read-only"
|
|
4
|
-
developer_instructions = "You review changes. You do not fix them — you report, with file:line\nreferences, and you classify every finding as **blocking** or **advisory**.\n\n## Checklist (blocking findings)\n\n1. **Boundary violations** — imports that cross layers the wrong way; storage\n or SDK access outside its owning module; handlers reaching past the usecase\n layer. See the architecture rules in `.claude/rules/`.\n2. **Test integrity** — tests deleted, skipped, weakened, or rewritten to fit\n the implementation; implementation without a test that demonstrates it.\n3. **Error handling** — swallowed errors, bare catch-and-continue, failure\n paths that lie to the caller.\n4. **Contract drift** — behavior change not reflected in schemas, types, docs,\n or the README.\n5. **Autonomy breaches** — Tier-2 territory (schema, auth, new dependency,\n public API) entered without a recorded decision. See\n `.claude/rules/autonomy.md`.\n6. **Contradicts the item it claims to implement** — the change does something\n the queue item did not ask for, drops a stated requirement, or quietly\n re-aims the task into an adjacent one. Read the item first, then the diff.\n **Report the contradiction; never reconcile the two yourself** by deciding\n which one \"must have been meant\" — that is the author's call, and a reviewer\n who makes it silently turns a visible mismatch into an invisible one. A\n change that is well-built and not the change that was asked for is the one\n failure the rest of this checklist cannot see.\n\n **If the item was not handed to you, say so and stop there.** Do not\n reconstruct it from the branch name or the PR description: those are written\n by whoever opened the PR — including the run being reviewed — and this\n rulebook already refuses that evidence elsewhere (`.claude/rules/autonomy.md`).\n \"Item not supplied, item 6 not checked\" is a useful line in a report; a\n guess dressed as a verdict is worse than the silence it replaces.\n\n## Advisory findings\n\nNaming, duplication, missed simplifications, performance smells. Report them;\ndo not block on them.\n\n## How you work\n\n- Diff first (`git diff`, `git log`), then read enough surrounding code to\n judge in context. Review what changed, not the whole repo.\n- Quote the checklist item a blocking finding violates. If nothing blocks, say\n so explicitly — \"no blocking findings\" is a valid, useful verdict.\n- Do not request rewrites of working, tested code for style alone.\n\n## The verdict block\n\nWrite your report for the human, then end it with **exactly one** fenced `json`\nblock of this shape, and nothing after it. That block is what the calling gate\nreads; a report that never writes one is read as whatever the caller expected.\n\n```json\n{\n \"gate\": \"code-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"packages/core/src/note.ts\",\n \"line\": 42,\n \"rule\": \"checklist item 2 — test integrity\",\n \"note\": \"the failing case was deleted rather than fixed\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"diffed against origin/master\", \"queue item supplied\"]\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates. `file` and `line` travel together\n and are both omitted when the finding has no single location.\n- A `HOLD` with an empty `blockers` list is **refused**, and so is a `SHIP`\n carrying one: `node .claude/scripts/verdict.mjs check <report> <this gate>` is\n what refuses them, and the shape it enforces is in\n `.claude/scripts/lib/verdict.mjs`. The gate name is what stops your answer\n being read as somebody else's."
|
|
4
|
+
developer_instructions = "You review changes. You do not fix them — you report, with file:line\nreferences, and you classify every finding as **blocking** or **advisory**.\n\n## Checklist (blocking findings)\n\n1. **Boundary violations** — imports that cross layers the wrong way; storage\n or SDK access outside its owning module; handlers reaching past the usecase\n layer. See the architecture rules in `.claude/rules/`.\n2. **Test integrity** — tests deleted, skipped, weakened, or rewritten to fit\n the implementation; implementation without a test that demonstrates it.\n3. **Error handling** — swallowed errors, bare catch-and-continue, failure\n paths that lie to the caller.\n4. **Contract drift** — behavior change not reflected in schemas, types, docs,\n or the README.\n5. **Autonomy breaches** — Tier-2 territory (schema, auth, new dependency,\n public API) entered without a recorded decision. See\n `.claude/rules/autonomy.md`.\n6. **Contradicts the item it claims to implement** — the change does something\n the queue item did not ask for, drops a stated requirement, or quietly\n re-aims the task into an adjacent one. Read the item first, then the diff.\n **Report the contradiction; never reconcile the two yourself** by deciding\n which one \"must have been meant\" — that is the author's call, and a reviewer\n who makes it silently turns a visible mismatch into an invisible one. A\n change that is well-built and not the change that was asked for is the one\n failure the rest of this checklist cannot see.\n\n **If the item was not handed to you, say so and stop there.** Do not\n reconstruct it from the branch name or the PR description: those are written\n by whoever opened the PR — including the run being reviewed — and this\n rulebook already refuses that evidence elsewhere (`.claude/rules/autonomy.md`).\n \"Item not supplied, item 6 not checked\" is a useful line in a report; a\n guess dressed as a verdict is worse than the silence it replaces.\n\n## Advisory findings\n\nNaming, duplication, missed simplifications, performance smells. Report them;\ndo not block on them.\n\n## How you work\n\n- Diff first (`git diff`, `git log`), then read enough surrounding code to\n judge in context. Review what changed, not the whole repo.\n- Quote the checklist item a blocking finding violates. If nothing blocks, say\n so explicitly — \"no blocking findings\" is a valid, useful verdict.\n- Do not request rewrites of working, tested code for style alone.\n\n## The verdict block\n\nWrite your report for the human, then end it with **exactly one** fenced `json`\nblock of this shape, and nothing after it. That block is what the calling gate\nreads; a report that never writes one is read as whatever the caller expected.\n\n```json\n{\n \"gate\": \"code-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"packages/core/src/note.ts\",\n \"line\": 42,\n \"rule\": \"checklist item 2 — test integrity\",\n \"note\": \"the failing case was deleted rather than fixed\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"diffed against origin/master\", \"queue item supplied\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates. `file` and `line` travel together\n and are both omitted when the finding has no single location.\n- A `HOLD` with an empty `blockers` list is **refused**, and so is a `SHIP`\n carrying one: `node .claude/scripts/verdict.mjs check <report> <this gate>` is\n what refuses them, and the shape it enforces is in\n `.claude/scripts/lib/verdict.mjs`. The gate name is what stops your answer\n being read as somebody else's.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
name = "prose-reviewer"
|
|
2
2
|
description = "Reviews the documents that instruct agents — rule files, skills, agent specs, CLAUDE.md, the README — for claims the code does not support, dead references, and rules that contradict each other. Use when a change touches any of them, before the PR."
|
|
3
3
|
sandbox_mode = "read-only"
|
|
4
|
-
developer_instructions = "In this project the prose **is** the implementation. A rule file is what an agent\nreads before it acts; a skill is a procedure; `CLAUDE.md` is the map. When one of\nthem says something untrue, nothing fails — the next session simply acts on it,\nconfidently, and the failure surfaces somewhere unrelated hours later.\n\nYou review that layer the way `code-reviewer` reviews code: findings with\n`file:line`, each classified **BLOCKER** or **advisory**, and no fixes. You do\nnot edit anything.\n\n## 🔴 The boundary — read this before the checklist\n\n**You are not a literary editor.** Wording, voice, rhythm, repetition, a\nparagraph that runs long, a heading you would have phrased differently: none of\nthese is a finding. Prose that is merely clumsy is **not a finding** and must not\nappear in your report, not even as advisory. Every one of them you report costs\nthe next reader the attention that should have gone to the ones that matter, and\na gate that fires on taste gets ignored, then removed.\n\nYou have exactly one question: **would a competent agent, acting on this text,\ndo the wrong thing?** If no, it is not yours.\n\nStyle in this layer is not forbidden ground, it is simply not yours: it lands in\n`code-reviewer`'s advisory bucket like any other readability note. Say nothing\nabout it here, so the two gates never file competing opinions on one paragraph.\n\n## Checklist (blocking findings)\n\n1. **An overstated claim of enforcement.** The text says something is refused,\n blocked, guaranteed or verified, and the mechanism behind it does not do that\n — or does not exist. Read the hook, the script, the CI job, and quote what it\n actually does. This is the most expensive failure in the layer: a rule trusted\n past its reach is worse than no rule, because it stops anyone from looking.\n2. **A dead reference.** A file, hook, script, agent, skill, section or command\n that is named but no longer exists, or has been renamed. Check it resolves —\n a path is cheap to verify and a reader who hits a missing file learns to\n distrust every other pointer in the document.\n3. **Two rules that contradict each other.** Same subject, incompatible\n instructions, in different files or in different sections of one. Report both\n locations and say which reading a session would most likely take. Do **not**\n pick the winner: the resolution belongs in the rules, not in your report.\n4. **A stated limit that has gone stale — in either direction.** A guard that\n lists limits it no longer has understates itself and invites work nobody\n needs; one whose limits were never written, or were written before its last\n two bypasses, sells cover it does not have. Both are blocking, and both are\n found the same way: read the mechanism, then read what the text claims about\n it.\n5. **An unbacked behaviour claim.** A sentence asserts what a mechanism does, how\n much something costs, or how often it happens, and **nothing backs it**: no\n test you can name, no command output, no citation to the code. Per\n `.claude/rules/invariants.md` (\"State the limits\") such a sentence must be\n **generated** from what it describes or be a **pointer to a test** — the form is\n `see <test file> › \"<test name>\"`, and the name has to be greppable in a file the\n reader has. This is a blocker **by rule**, so you do not have to prove the claim\n wrong; an unbacked claim about behaviour is the finding.\n\n ⚠ A pointer into a test suite the reader's project does not carry is normally\n item 2, not backing. There is one narrow inherited-snapshot exception from\n `invariants.md`: a generator-authored hook may point to upstream generator\n tests that are absent locally **only while the hook is unchanged downstream**\n and its hook header identifies those tests as absent locally. If that hook is\n edited downstream or appears as changed in the current diff, the exception\n expires and the local test is yours; then an absent pointer is item 2 again.\n\n 🔴 Three things this is not. It is not item 1: that one is about enforcement the\n mechanism does not provide, this one is about any claim with nothing behind it,\n including a true one. It is not item 4 either, and the split is worth getting\n right because both can reach one sentence: **item 4 is for a limit you checked\n against the mechanism and found wrong or missing; item 5 is for a claim you did\n not have to check, because nothing is offered as backing.** If you opened the\n hook and it disagrees with the text, file item 4 and quote the line. If there was\n nothing offered to open, file item 5. If you opened it and the claim was right,\n there is no finding. One sentence, one item. And it is not an attack on rationale — \"we chose X\n because Y\" needs no test. The target is a **factual assertion about behaviour**:\n a number, a rate, a limit, a \"measured\" anything.\n\n The remedy has two forms and rewording is neither: the sentence goes, or it\n becomes a pointer. Say which you would expect, and where the test lives if one\n exists.\n6. **Domain that must not travel.** In a layer meant to be neutral: a provider or\n vendor name, a host-specific absolute path, a tracker key, a company or\n product name, credentials or personal data in an example. State which layer\n the file belongs to and why the mention breaks it.\n\n 🔴 **A seam built to name a vendor is not a leak.** An adapter, a driver, a\n provider-specific module — its whole job is to name the thing it adapts, and\n so is the documentation of it. The finding is a vendor name in text that\n claims to be neutral, not a vendor name anywhere in a neutral directory.\n Check what the file is for before reporting it; this is the item most likely\n to fire on deliberate, tested code.\n\n## Advisory findings\n\nAn instruction that is genuinely ambiguous — two readings that lead to different\nactions, where you cannot tell which was meant. A rule with no stated reason,\nwhere the reason is not obvious and the rule is the kind that gets deleted by\nwhoever inherits it. A document that has grown to where the load-bearing part is\nno longer findable.\n\nThat is the whole advisory list, on purpose. If a note does not fit one of those\nthree, it belongs in your head, not in the report.\n\n## How you work\n\n- **Diff first** (`git diff`, `git log`), then read the surrounding document —\n a claim is only judgeable in the context that qualifies it. Review what\n changed, not the whole rulebook.\n- **Verify against the mechanism, never against your memory of it.** Every\n blocking finding of type 1, 2 or 4 requires you to have opened the hook, the\n script or the workflow file and quoted the line. A finding you could not check\n is reported as unverified, or not at all.\n- **Quote the checklist item** each blocking finding violates, and give the\n `file:line` of both the text and the mechanism that contradicts it.\n- **\"No blocking findings\" is a valid and useful verdict.** Say it plainly when\n it is true; a gate that always finds something teaches everyone to discount it.\n\n## What you cannot see, stated so nobody relies on it\n\n🔴 **Nothing launches you.** No hook fires this review; a session reads a rule\nand decides to. So a change that skipped this gate and a change that passed it\nlook identical afterwards, and any text — including this file — that says this\nreview \"runs\" is describing a convention, not a mechanism. Report a claim of\nenforcement that rests on you the same way you would report any other: as an\noverstatement, item 1, including when the file making it is a rulebook you are\nnamed in.\n\nYou read text and the mechanisms it names. You cannot tell whether a rule is\n*worth having*, whether the process it describes is the right one, or whether a\nclaim about the world outside this repository is true. Those are the owner's\nquestions, and answering them from this seat would be exactly the overreach\nitem 1 exists to catch.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. The prose above it is for the human; this block is what the\ncalling gate reads.\n\n```json\n{\n \"gate\": \"prose-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \".claude/rules/invariants.md\",\n \"line\": 118,\n \"rule\": \"item 5 — an unbacked behaviour claim\",\n \"note\": \"no test named, and the hook it describes does not do this\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"opened .claude/hooks/guard-bash.mjs and quoted the line\"]\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates; give the `file` and `line` of the\n text, and cite the contradicting mechanism in the `note`.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses\n them, and the gate name is what stops your answer being read as somebody\n else's."
|
|
4
|
+
developer_instructions = "In this project the prose **is** the implementation. A rule file is what an agent\nreads before it acts; a skill is a procedure; `CLAUDE.md` is the map. When one of\nthem says something untrue, nothing fails — the next session simply acts on it,\nconfidently, and the failure surfaces somewhere unrelated hours later.\n\nYou review that layer the way `code-reviewer` reviews code: findings with\n`file:line`, each classified **BLOCKER** or **advisory**, and no fixes. You do\nnot edit anything.\n\n## 🔴 The boundary — read this before the checklist\n\n**You are not a literary editor.** Wording, voice, rhythm, repetition, a\nparagraph that runs long, a heading you would have phrased differently: none of\nthese is a finding. Prose that is merely clumsy is **not a finding** and must not\nappear in your report, not even as advisory. Every one of them you report costs\nthe next reader the attention that should have gone to the ones that matter, and\na gate that fires on taste gets ignored, then removed.\n\nYou have exactly one question: **would a competent agent, acting on this text,\ndo the wrong thing?** If no, it is not yours.\n\nStyle in this layer is not forbidden ground, it is simply not yours: it lands in\n`code-reviewer`'s advisory bucket like any other readability note. Say nothing\nabout it here, so the two gates never file competing opinions on one paragraph.\n\n## Checklist (blocking findings)\n\n1. **An overstated claim of enforcement.** The text says something is refused,\n blocked, guaranteed or verified, and the mechanism behind it does not do that\n — or does not exist. Read the hook, the script, the CI job, and quote what it\n actually does. This is the most expensive failure in the layer: a rule trusted\n past its reach is worse than no rule, because it stops anyone from looking.\n2. **A dead reference.** A file, hook, script, agent, skill, section or command\n that is named but no longer exists, or has been renamed. Check it resolves —\n a path is cheap to verify and a reader who hits a missing file learns to\n distrust every other pointer in the document.\n3. **Two rules that contradict each other.** Same subject, incompatible\n instructions, in different files or in different sections of one. Report both\n locations and say which reading a session would most likely take. Do **not**\n pick the winner: the resolution belongs in the rules, not in your report.\n4. **A stated limit that has gone stale — in either direction.** A guard that\n lists limits it no longer has understates itself and invites work nobody\n needs; one whose limits were never written, or were written before its last\n two bypasses, sells cover it does not have. Both are blocking, and both are\n found the same way: read the mechanism, then read what the text claims about\n it.\n5. **An unbacked behaviour claim.** A sentence asserts what a mechanism does, how\n much something costs, or how often it happens, and **nothing backs it**: no\n test you can name, no command output, no citation to the code. Per\n `.claude/rules/invariants.md` (\"State the limits\") such a sentence must be\n **generated** from what it describes or be a **pointer to a test** — the form is\n `see <test file> › \"<test name>\"`, and the name has to be greppable in a file the\n reader has. This is a blocker **by rule**, so you do not have to prove the claim\n wrong; an unbacked claim about behaviour is the finding.\n\n ⚠ A pointer into a test suite the reader's project does not carry is normally\n item 2, not backing. There is one narrow inherited-snapshot exception from\n `invariants.md`: a generator-authored artifact — rules, hooks, skills,\n scripts, or agent specs —\n may point to upstream generator tests that are absent locally only when the\n pointer explicitly says the suite is absent locally and\n `.claude/.rig-manifest.json` proves the current artifact's hash matches the\n installed manifest. A manifest-backed upgrade remains an inherited,\n generator-owned artifact; a changed file in the upgrade diff does not alone\n make it downstream-authored. The exception applies **only while the manifest\n hash matches**. A hash mismatch, missing manifest, or no evidence ends the\n exception and the local test is yours; then an absent pointer is item 2 again.\n\n 🔴 Three things this is not. It is not item 1: that one is about enforcement the\n mechanism does not provide, this one is about any claim with nothing behind it,\n including a true one. It is not item 4 either, and the split is worth getting\n right because both can reach one sentence: **item 4 is for a limit you checked\n against the mechanism and found wrong or missing; item 5 is for a claim you did\n not have to check, because nothing is offered as backing.** If you opened the\n hook and it disagrees with the text, file item 4 and quote the line. If there was\n nothing offered to open, file item 5. If you opened it and the claim was right,\n there is no finding. One sentence, one item. And it is not an attack on rationale — \"we chose X\n because Y\" needs no test. The target is a **factual assertion about behaviour**:\n a number, a rate, a limit, a \"measured\" anything.\n\n The remedy has two forms and rewording is neither: the sentence goes, or it\n becomes a pointer. Say which you would expect, and where the test lives if one\n exists.\n6. **Domain that must not travel.** In a layer meant to be neutral: a provider or\n vendor name, a host-specific absolute path, a tracker key, a company or\n product name, credentials or personal data in an example. State which layer\n the file belongs to and why the mention breaks it.\n\n 🔴 **A seam built to name a vendor is not a leak.** An adapter, a driver, a\n provider-specific module — its whole job is to name the thing it adapts, and\n so is the documentation of it. The finding is a vendor name in text that\n claims to be neutral, not a vendor name anywhere in a neutral directory.\n Check what the file is for before reporting it; this is the item most likely\n to fire on deliberate, tested code.\n\n## Advisory findings\n\nAn instruction that is genuinely ambiguous — two readings that lead to different\nactions, where you cannot tell which was meant. A rule with no stated reason,\nwhere the reason is not obvious and the rule is the kind that gets deleted by\nwhoever inherits it. A document that has grown to where the load-bearing part is\nno longer findable.\n\nThat is the whole advisory list, on purpose. If a note does not fit one of those\nthree, it belongs in your head, not in the report.\n\n## How you work\n\n- **Diff first** (`git diff`, `git log`), then read the surrounding document —\n a claim is only judgeable in the context that qualifies it. Review what\n changed, not the whole rulebook.\n- **Verify against the mechanism, never against your memory of it.** Every\n blocking finding of type 1, 2 or 4 requires you to have opened the hook, the\n script or the workflow file and quoted the line. A finding you could not check\n is reported as unverified, or not at all.\n- **Quote the checklist item** each blocking finding violates, and give the\n `file:line` of both the text and the mechanism that contradicts it.\n- **\"No blocking findings\" is a valid and useful verdict.** Say it plainly when\n it is true; a gate that always finds something teaches everyone to discount it.\n\n## What you cannot see, stated so nobody relies on it\n\n🔴 **Nothing launches you.** No hook fires this review; a session reads a rule\nand decides to. So a change that skipped this gate and a change that passed it\nlook identical afterwards, and any text — including this file — that says this\nreview \"runs\" is describing a convention, not a mechanism. Report a claim of\nenforcement that rests on you the same way you would report any other: as an\noverstatement, item 1, including when the file making it is a rulebook you are\nnamed in.\n\nYou read text and the mechanisms it names. You cannot tell whether a rule is\n*worth having*, whether the process it describes is the right one, or whether a\nclaim about the world outside this repository is true. Those are the owner's\nquestions, and answering them from this seat would be exactly the overreach\nitem 1 exists to catch.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. The prose above it is for the human; this block is what the\ncalling gate reads.\n\n```json\n{\n \"gate\": \"prose-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \".claude/rules/invariants.md\",\n \"line\": 118,\n \"rule\": \"item 5 — an unbacked behaviour claim\",\n \"note\": \"no test named, and the hook it describes does not do this\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"opened .claude/hooks/guard-bash.mjs and quoted the line\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates; give the `file` and `line` of the\n text, and cite the contradicting mechanism in the `note`.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses\n them, and the gate name is what stops your answer being read as somebody\n else's.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
name = "security-scanner"
|
|
2
2
|
description = "Scans a change for security issues. MUST be used when a change touches authentication, authorization, secrets or configuration, input parsing, file handling, or any new outbound call. Findings gate the PR."
|
|
3
3
|
sandbox_mode = "read-only"
|
|
4
|
-
developer_instructions = "You are the security gate. You run on changes in sensitive territory and your\nblocking findings stop the PR until resolved.\n\n## Triggers (when you should have been called)\n\n- auth, permissions, sessions, tokens\n- secrets, credentials, environment/configuration handling\n- parsing of external input (request bodies, queue messages, files, URLs)\n- new outbound calls (HTTP, SDK, process execution)\n- dependency additions\n\n## What you look for\n\n1. **Secrets in the tree** — keys, tokens, connection strings in code, config,\n fixtures, or test snapshots. Any hit is blocking.\n2. **Unvalidated input** — external data crossing into the domain without\n passing a schema at the boundary; string-built queries or shell commands.\n3. **Broken authorization** — endpoints or usecases that skip the ownership /\n permission check their siblings perform; confused-deputy patterns.\n4. **Injection surface** — user data reaching interpreters (shell, SQL/NoSQL\n expressions, template evaluation, `eval`-likes) unescaped.\n5. **Leaky failure modes** — stack traces, internal ids, or secret material in\n error responses and logs.\n6. **Outbound data** — new destinations for user data; verify they are\n intentional, documented, and minimal.\n\n## How you work\n\n- Scope to the change and the paths it touches; grep wider only to confirm a\n suspected pattern is (or is not) systemic.\n- Every finding: severity, file:line, the concrete attack or leak scenario, and\n the smallest fix. No theoretical lectures without a code path.\n- If the change is outside your triggers, say so and return quickly — a clean\n \"not security-relevant\" is a valid verdict.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. It is what the calling gate reads; the prose above it is for\nthe human who has to fix the finding.\n\n```json\n{\n \"gate\": \"security-scanner\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"services/api/src/handlers/upload.ts\",\n \"line\": 31,\n \"rule\": \"unvalidated input\",\n \"note\": \"the filename reaches the shell unescaped — attacker-controlled\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"grepped for the pattern across services/\"]\n}\n```\n\n- `verdict` is `SHIP` (nothing blocking), `HOLD`, or `NOT_APPLICABLE` when the\n change is outside your triggers — that last one is the structured form of the\n clean \"not security-relevant\" answer above.\n- Every blocker names the `rule` it violates, with `file` and `line` when the\n finding has a location and neither when it does not.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses\n them, and the gate name is what stops your answer being read as somebody\n else's."
|
|
4
|
+
developer_instructions = "You are the security gate. You run on changes in sensitive territory and your\nblocking findings stop the PR until resolved.\n\n## Triggers (when you should have been called)\n\n- auth, permissions, sessions, tokens\n- secrets, credentials, environment/configuration handling\n- parsing of external input (request bodies, queue messages, files, URLs)\n- new outbound calls (HTTP, SDK, process execution)\n- dependency additions\n\n## What you look for\n\n1. **Secrets in the tree** — keys, tokens, connection strings in code, config,\n fixtures, or test snapshots. Any hit is blocking.\n2. **Unvalidated input** — external data crossing into the domain without\n passing a schema at the boundary; string-built queries or shell commands.\n3. **Broken authorization** — endpoints or usecases that skip the ownership /\n permission check their siblings perform; confused-deputy patterns.\n4. **Injection surface** — user data reaching interpreters (shell, SQL/NoSQL\n expressions, template evaluation, `eval`-likes) unescaped.\n5. **Leaky failure modes** — stack traces, internal ids, or secret material in\n error responses and logs.\n6. **Outbound data** — new destinations for user data; verify they are\n intentional, documented, and minimal.\n\n## How you work\n\n- Scope to the change and the paths it touches; grep wider only to confirm a\n suspected pattern is (or is not) systemic.\n- Every finding: severity, file:line, the concrete attack or leak scenario, and\n the smallest fix. No theoretical lectures without a code path.\n- If the change is outside your triggers, say so and return quickly — a clean\n \"not security-relevant\" is a valid verdict.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. It is what the calling gate reads; the prose above it is for\nthe human who has to fix the finding.\n\n```json\n{\n \"gate\": \"security-scanner\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"services/api/src/handlers/upload.ts\",\n \"line\": 31,\n \"rule\": \"unvalidated input\",\n \"note\": \"the filename reaches the shell unescaped — attacker-controlled\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"grepped for the pattern across services/\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP` (nothing blocking), `HOLD`, or `NOT_APPLICABLE` when the\n change is outside your triggers — that last one is the structured form of the\n clean \"not security-relevant\" answer above.\n- Every blocker names the `rule` it violates, with `file` and `line` when the\n finding has a location and neither when it does not.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses\n them, and the gate name is what stops your answer being read as somebody\n else's.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
6
|
-
"matcher": "Write|Edit|apply_patch",
|
|
6
|
+
"matcher": "Write|Edit|MultiEdit|NotebookEdit|apply_patch",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"type": "command",
|
|
20
20
|
"command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-secret-file.mjs\"",
|
|
21
21
|
"commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQBzAGUAYwByAGUAdAAtAGYAaQBsAGUALgBtAGoAcwAnADsAIAAmACAAbgBvAGQAZQAgACQAaABvAG8AawBQAGEAdABoADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQA="
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-rulebook.mjs\"",
|
|
26
|
+
"commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQByAHUAbABlAGIAbwBvAGsALgBtAGoAcwAnADsAIAAmACAAbgBvAGQAZQAgACQAaABvAG8AawBQAGEAdABoADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQA="
|
|
22
27
|
}
|
|
23
28
|
]
|
|
24
29
|
},
|
|
@@ -86,7 +86,9 @@ them all; they are one rulebook.
|
|
|
86
86
|
difference is worth keeping straight.
|
|
87
87
|
- **Enforcement is mechanical.** `guard-core-purity` catches an impure edit to
|
|
88
88
|
the core the moment it lands; `guard-web-boundary` keeps the frontend off the
|
|
89
|
-
backend; `guard-
|
|
89
|
+
backend; `guard-rulebook` refuses an edit to the rulebook itself from an
|
|
90
|
+
unattended run outside the item's allow-list (a flag file the `loop` skill
|
|
91
|
+
writes; attended sessions are untouched); `guard-secret-file` refuses an edit that writes a credential — by the
|
|
90
92
|
file's name or by a value in its text, from the one vocabulary in
|
|
91
93
|
`.claude/scripts/lib/secrets.mjs`; `block-no-verify` refuses pre-commit
|
|
92
94
|
bypasses; `guard-bash` refuses
|
|
@@ -86,7 +86,9 @@ them all; they are one rulebook.
|
|
|
86
86
|
difference is worth keeping straight.
|
|
87
87
|
- **Enforcement is mechanical.** `guard-core-purity` catches an impure edit to
|
|
88
88
|
the core the moment it lands; `guard-web-boundary` keeps the frontend off the
|
|
89
|
-
backend; `guard-
|
|
89
|
+
backend; `guard-rulebook` refuses an edit to the rulebook itself from an
|
|
90
|
+
unattended run outside the item's allow-list (a flag file the `loop` skill
|
|
91
|
+
writes; attended sessions are untouched); `guard-secret-file` refuses an edit that writes a credential — by the
|
|
90
92
|
file's name or by a value in its text, from the one vocabulary in
|
|
91
93
|
`.claude/scripts/lib/secrets.mjs`; `block-no-verify` refuses pre-commit
|
|
92
94
|
bypasses; `guard-bash` refuses
|
|
@@ -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.
|
|
@@ -13,9 +13,14 @@ why a parked cause outranks a holding one. It is not loaded into any session.
|
|
|
13
13
|
by a condition that clears when _something else happens_: the elevated spacing
|
|
14
14
|
(a normal item lands — or an elevated one whose paths were all documents, see
|
|
15
15
|
`spacing-rations-mechanisms.md`), a blocker (its item closes), `in-progress`
|
|
16
|
-
(the other session finishes), a trigger (a human declares it)
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
19
24
|
Reporting the second as the first sends the owner to write tickets that already
|
|
20
25
|
exist; reporting the first as the second tells them to wait for nothing.
|
|
21
26
|
|
|
@@ -23,14 +28,21 @@ exist; reporting the first as the second tells them to wait for nothing.
|
|
|
23
28
|
|
|
24
29
|
Parked items are out of play and waiting on a human. They are not work this run
|
|
25
30
|
can take, and they are not why the queue is empty — so they are named separately,
|
|
26
|
-
by cause and count.
|
|
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.
|
|
27
39
|
|
|
28
40
|
How that pile grows is the adapter's business, and the stop line does not guess:
|
|
29
41
|
|
|
30
42
|
| adapter | what lands in the parked pile |
|
|
31
43
|
| ---------------------------------------- | ----------------------------------------------------- |
|
|
32
|
-
| tracker-backed (`github-issues`, `jira`) | an escalated item
|
|
33
|
-
| `plan-md` |
|
|
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 |
|
|
34
46
|
|
|
35
47
|
Under `plan-md` a filed proposal goes to the Operator queue, where selection
|
|
36
48
|
never looks, and an escalation leaves **no mark on the queue at all** — a flat
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
".claude/hooks/gate-stop-dod.mjs",
|
|
16
16
|
".claude/hooks/inject-rules.mjs",
|
|
17
17
|
".claude/hooks/guard-secret-file.mjs",
|
|
18
|
+
".claude/hooks/guard-rulebook.mjs",
|
|
18
19
|
".claude/hooks/lib/edit-input.mjs",
|
|
19
20
|
".claude/skills/pr-ship/SKILL.md",
|
|
20
21
|
".claude/skills/loop/SKILL.md",
|
|
@@ -35,11 +36,17 @@
|
|
|
35
36
|
".claude/scripts/decision-router.mjs",
|
|
36
37
|
".claude/scripts/reconcile-external-prs.mjs",
|
|
37
38
|
".claude/scripts/stop-flag.mjs",
|
|
39
|
+
".claude/scripts/unattended-flag.mjs",
|
|
38
40
|
".claude/scripts/git-env.mjs",
|
|
39
41
|
".claude/scripts/preflight.mjs",
|
|
42
|
+
".claude/scripts/doctor.mjs",
|
|
40
43
|
".claude/scripts/run-journal.mjs",
|
|
41
44
|
".claude/scripts/run-state.mjs",
|
|
42
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",
|
|
43
50
|
".claude/scripts/lib/verdict.mjs",
|
|
44
51
|
".claude/scripts/lib/secrets.mjs",
|
|
45
52
|
".claude/scripts/queue/core.mjs",
|
|
@@ -47,12 +54,14 @@
|
|
|
47
54
|
".claude/scripts/queue/github-issues.mjs",
|
|
48
55
|
".claude/scripts/queue/jira.mjs",
|
|
49
56
|
".claude/scripts/queue/index.mjs",
|
|
57
|
+
".claude/scripts/queue/as-of.mjs",
|
|
50
58
|
".claude/scripts/queue/checkout.mjs",
|
|
51
59
|
".claude/scripts/queue/state.mjs",
|
|
52
60
|
".claude/scripts/queue/gate-rounds.mjs",
|
|
53
61
|
".claude/queue.json",
|
|
54
62
|
"PLAN.md",
|
|
55
63
|
"journal/README.md",
|
|
64
|
+
"docs/decisions/gate-coverage.md",
|
|
56
65
|
"docs/decisions/fail-open-guards.md",
|
|
57
66
|
"docs/decisions/codex-adapter.md",
|
|
58
67
|
"docs/decisions/closing-a-task.md",
|