create-agent-rig 0.10.0 → 1.0.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 (42) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +8 -8
  3. package/package.json +2 -2
  4. package/packages/cli/dist/commands/upgrade.js +69 -31
  5. package/packages/cli/dist/index.js +15 -2
  6. package/templates/agent-os/subagent-routing.json +4 -0
  7. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +32 -3
  8. package/templates/agent-os/universal/.agents/skills/diagnose/SKILL.md +43 -0
  9. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +65 -22
  10. package/templates/agent-os/universal/.agents/skills/plan-slices/SKILL.md +30 -0
  11. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  12. package/templates/agent-os/universal/.agents/skills/release-propose/SKILL.md +74 -0
  13. package/templates/agent-os/universal/.agents/skills/skill-authoring/SKILL.md +39 -0
  14. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +4 -0
  15. package/templates/agent-os/universal/.claude/agents/failure-diagnostician.md +112 -0
  16. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +18 -5
  17. package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +21 -6
  18. package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +13 -3
  19. package/templates/agent-os/universal/.claude/rules/invariants.md +33 -0
  20. package/templates/agent-os/universal/.claude/rules/workflow.md +14 -2
  21. package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +63 -0
  22. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +5 -6
  23. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +71 -14
  24. package/templates/agent-os/universal/.claude/scripts/queue/propose.mjs +139 -0
  25. package/templates/agent-os/universal/.claude/scripts/release-evidence.mjs +188 -0
  26. package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +4 -2
  27. package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +8 -0
  28. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +32 -3
  29. package/templates/agent-os/universal/.claude/skills/diagnose/SKILL.md +43 -0
  30. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +65 -22
  31. package/templates/agent-os/universal/.claude/skills/plan-slices/SKILL.md +30 -0
  32. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  33. package/templates/agent-os/universal/.claude/skills/release-propose/SKILL.md +74 -0
  34. package/templates/agent-os/universal/.claude/skills/skill-authoring/SKILL.md +39 -0
  35. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +1 -1
  36. package/templates/agent-os/universal/.codex/agents/failure-diagnostician.toml +6 -0
  37. package/templates/agent-os/universal/AGENTS.md +6 -4
  38. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +5 -3
  39. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +15 -3
  40. package/templates/agent-os/universal/layers.json +12 -0
  41. package/templates/hash-history.json +112 -39
  42. package/templates/release-ledger.json +3 -1
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: plan-slices
3
+ description: Use when a task cannot be verified as one reviewable PR and splits into slices that are each independently verifiable on their own. Ships only with the opt-in workflow layer.
4
+ allowed-tools: Read, Grep, Glob, Write, Edit
5
+ ---
6
+
7
+ # Splitting a task into slices
8
+
9
+ This applies when a task's change cannot be reviewed and verified as one PR,
10
+ and decomposes into slices that are each independently verifiable — each
11
+ slice stands on its own claim about behaviour, checked by its own test.
12
+
13
+ Do not trigger it mechanically based on file or module count: a change
14
+ that touches many files but makes one verifiable claim stays one PR, and a
15
+ change touching few files but making several independent claims still
16
+ splits.
17
+
18
+ There is no planner role dispatched for this — the session writes the slice
19
+ plan directly. For each slice, record:
20
+
21
+ - the outcome the slice delivers, stated as a claim a test can check;
22
+ - its own failing test, written by `test-writer` in the ordinary Red step;
23
+ - the elevated paths it touches, if any (`AGENTS.md`'s elevated-paths block);
24
+ - where it sits in the slice order, and what it depends on.
25
+
26
+ Each slice then ships as its own PR through the ordinary flow —
27
+ `.claude/rules/workflow.md` has the TDD cycle and the PR flow in full; this
28
+ skill does not restate them.
29
+
30
+ This skill ships only with the opt-in workflow layer.
@@ -39,7 +39,7 @@ blockers.
39
39
  `test/template/gate-rounds.test.ts` — absent in a generated rig — ›
40
40
  "refuses to count a round on a dirty tree, and counts nothing".
41
41
 
42
- The cap is **2 by default**, and no shipped `.claude/queue.json` carries the key
42
+ The cap is **3 by default**, and no shipped `.claude/queue.json` carries the key
43
43
  — the default lives in `core.mjs` as `DEFAULT_MAX_GATE_ROUNDS`. A project that
44
44
  wants a different cap sets `options.maxGateRounds` there, which in a rig whose
45
45
  `queue.json` is composed means changing what composes it, not editing the file.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: release-propose
3
+ description: Use to turn repeated evidence from `release-evidence.mjs` into a bounded candidate-release proposal for the owner to decide. Ships only with the opt-in workflow layer.
4
+ allowed-tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ # Proposing a release from repeated pain
8
+
9
+ ## 1. What this is not
10
+
11
+ The loop's own §7 improvement proposals are per-run fixes, filed and read one
12
+ run at a time. This skill is different: a release-level proposal, built from
13
+ evidence that recurred across more than one run, addressed to the owner —
14
+ never to the queue, and never approved by the skill itself.
15
+
16
+ ## 2. Gather
17
+
18
+ Run exactly:
19
+
20
+ ```sh
21
+ node "$(git rev-parse --show-toplevel)/.claude/scripts/release-evidence.mjs" --since <date> --json
22
+ ```
23
+
24
+ Read its `verdict`, `groups` and `why`. Optionally read
25
+ `revalidation-report.mjs --json` the same way, the triage proposals already
26
+ on file, and the tracker — cite every one of these by pointer (run id, file,
27
+ seq, or ticket id), never from memory.
28
+
29
+ ## 3. Measured vs. inferred
30
+
31
+ A number in the proposal is `measured` only when it came straight out of
32
+ `release-evidence.mjs`'s JSON or a cited line of a run/ticket. Every other
33
+ number or claim is labelled `inferred`, or `UNVERIFIED` when nothing backs it
34
+ at all — never stated as if it were measured.
35
+
36
+ ## 4. Routing
37
+
38
+ - `GATHER_MORE_EVIDENCE` — gather-more-evidence: name what evidence would decide it, file nothing, and stop — never build a candidate release out of anecdotes.
39
+ - `REPEATED_PAIN` — write the proposal (§5) and hand it off (§6).
40
+
41
+ ## 5. Proposal template
42
+
43
+ Write these headings, in order, into `$RIG_RUN_DIR/release-proposal.md`:
44
+
45
+ - **Observed repeated pain** — the repeated groups, each with its pointers
46
+ - **Candidate release** — the bounded scope this pain justifies
47
+ - **Why now**
48
+ - **Why not the alternatives** — including a required "Do nothing" row
49
+ - **Dependencies** — proven only; nothing inferred here
50
+ - **Scope / non-goals**
51
+ - **Complexity** — small, medium or large, plus the maintenance burden it adds
52
+ - **Evidence gaps** — what is still `inferred` or `UNVERIFIED`
53
+ - **Upstream capability check** — could a native plugin, connector, MCP
54
+ server, CLI or provider feature do this instead: sufficient, insufficient
55
+ or rejected, and why
56
+ - **Owner decision** — approve, reject or gather-more-evidence; left blank
57
+ for the owner to fill in, never pre-filled by this skill
58
+
59
+ ## 6. Hand-off
60
+
61
+ - Write the proposal to `$RIG_RUN_DIR/release-proposal.md`.
62
+ - File exactly ONE triage item, pointing at it:
63
+ `node "$(git rev-parse --show-toplevel)/.claude/scripts/queue/propose.mjs" --file <proposal.json>`
64
+ — finding = the repeated-pain groups by pointer; part = `"release"`;
65
+ change = `"candidate release: <one line>"`; proof = what the owner would
66
+ observe if the release lands.
67
+ - A triage item filed this way is unselectable by the queue on its own;
68
+ promotion out of triage into selectable work is the owner's act, never
69
+ this skill's.
70
+ - This skill never files a ticket in the selectable queue, never opens a
71
+ GitHub issue directly, and never edits PLAN.md's Agent queue: the Agent
72
+ queue is not something this skill touches, under any verdict.
73
+
74
+ This skill ships only with the opt-in workflow layer.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: skill-authoring
3
+ description: Use when authoring or editing a skill in this rig — a new SKILL.md under .claude/skills/, or a change to an existing one.
4
+ allowed-tools: Read, Grep, Glob
5
+ ---
6
+
7
+ # Authoring a skill
8
+
9
+ No check in this rig runs on a skill you write here — the rules below are
10
+ applied by the author, and by a reviewer reading this file, never by a
11
+ mechanism. On the skills this rig itself ships, the generator's own
12
+ `test/template/skill-authoring.test.ts` (absent in a generated rig) checks
13
+ only the first rule, that a `description` is present, and that the skills and
14
+ roles a skill names by name exist.
15
+
16
+ - **The frontmatter `name` equals the directory name.** A skill loaded from
17
+ `.claude/skills/foo/` is named `foo`, not something else.
18
+ - **The `description` says when to use the skill**, not just what it does.
19
+ - **Keep it short, and point at existing rules or scripts rather than
20
+ restating them.** A skill that copies a rule's wording is a second copy
21
+ that goes stale the day the rule changes; link to `.claude/rules/` or a
22
+ script instead.
23
+ - **Every sentence describing how a mechanism behaves either points at the
24
+ test that proves it, or is deleted.** `.claude/rules/invariants.md`,
25
+ "State the limits — and test them", is the norm; the form is `see <file>
26
+ (absent in a generated rig) › "<exact test name>"`.
27
+ - **A mention of a workflow-layer skill or script in a Core document is
28
+ qualified "opt-in workflow layer"** — `loop` and `pr-ship` are examples,
29
+ not this skill's own concern.
30
+ - **Both copies ship and must stay identical**: the Claude skill under
31
+ `.claude/skills/` and the Codex repository skill under `.agents/skills/`.
32
+ That mirror is checked in `test/template/codex.test.ts` (absent in a
33
+ generated rig) › "publishes every shared skill through the Codex
34
+ repository skill location" — not repeated here.
35
+
36
+ ## Out of scope
37
+
38
+ This is guidance to apply by reading, not an evaluator: the skill itself has
39
+ no procedure and checks nothing.
@@ -38,6 +38,10 @@ references, and you classify every finding as **blocking** or **advisory**.
38
38
  rulebook already refuses that evidence elsewhere (`.claude/rules/autonomy.md`).
39
39
  "Item not supplied, item 6 not checked" is a useful line in a report; a
40
40
  guess dressed as a verdict is worse than the silence it replaces.
41
+ 7. **Independent oracle** — a test of a security, ownership or governance
42
+ mechanism whose expected result is derived from the same production
43
+ mechanism it checks. See the independent-oracle invariant in
44
+ `.claude/rules/invariants.md`.
41
45
 
42
46
  ## Advisory findings
43
47
 
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: failure-diagnostician
3
+ description: Use when a check is red or a run crashed and the cause is not obvious, or to reproduce a claimed defect/historical finding on the current default branch before work is planned on it.
4
+ tools: Read, Grep, Glob, Bash
5
+ model: claude-opus-5
6
+ effort: high
7
+ ---
8
+
9
+ You diagnose. You take one of two input kinds — a red check or a crashed run,
10
+ or a claimed defect / historical finding to reproduce on the current default
11
+ branch — and answer with evidence, not a guess dressed as one.
12
+
13
+ ## Hard limits
14
+
15
+ - **You make no repository edits.** A throwaway reproduction file goes
16
+ outside the repository, never inside it — you are not the Green step, and a
17
+ fix is not your answer. You do not commit, push, merge or open a pull
18
+ request.
19
+ - **Never re-run a check until it goes green.** A flaky-looking result is a
20
+ defect to report, not a thing to retry — the same stop rule that applies
21
+ everywhere else in this rulebook (`.claude/rules/autonomy.md`, "Stop rules —
22
+ by work-state, not by feelings").
23
+ - **On Claude Code, no hook enforces the no-edit limit above.** `tools: Read,
24
+ Grep, Glob, Bash` carries no Write or Edit, which stops the ordinary path,
25
+ but nothing refuses a shell redirect the way a guard would. On Codex the
26
+ equivalent profile is `sandbox_mode = "read-only"`, enforced by the runtime
27
+ itself. Either way, a sandbox that blocks the reproduction you need is not
28
+ something to work around: answer `INCONCLUSIVE` or `INSUFFICIENT_EVIDENCE`
29
+ and name the sandbox as the missing evidence.
30
+
31
+ ## Method
32
+
33
+ 1. **Reproduce.** A failure input (red check, crashed run) reproduces at the
34
+ commit or branch where it failed — the PR head, or the commit the caller
35
+ names. A claimed defect or historical finding reproduces on the current
36
+ default branch. No reproduction, no diagnosis.
37
+ 2. **Isolate.** Narrow to the smallest change (input, config, code path) that
38
+ flips the result.
39
+ 3. **Hypothesize.** State the mechanism you think is responsible, in one or
40
+ two sentences.
41
+ 4. **Confirm with evidence.** Show the command and its output, or the
42
+ file:line the mechanism lives at. A hypothesis nothing confirms is
43
+ `INCONCLUSIVE`, not `ROOT_CAUSE`.
44
+
45
+ For a failure input that reaches `ROOT_CAUSE`, classify it: `product` (the
46
+ code is wrong), `test` (the test's premise or fixture is wrong),
47
+ `infrastructure` (CI, network, environment — not the code under test), or
48
+ `upstream` (a dependency or external service).
49
+
50
+ ## Optional evidence (opt-in workflow layer)
51
+
52
+ Where this repository has installed the opt-in workflow layer, the run
53
+ journal and `run-state.mjs`'s recorded verdict may already carry evidence
54
+ worth reading before you reproduce anything by hand — a prior `REGRESSION`,
55
+ or an earlier run's own trace. Their absence is the normal Core path, not a
56
+ gap: read them when present, reproduce directly when not.
57
+
58
+ ## The answer
59
+
60
+ End your report with **exactly one** fenced `json` block of the shared shape
61
+ (`.claude/scripts/lib/verdict.mjs`), and nothing after it.
62
+
63
+ - **A failure input** (red check, crash) answers `ROOT_CAUSE` or
64
+ `INCONCLUSIVE`.
65
+ - **A claim or historical finding** answers `STILL_LIVE`, `ALREADY_FIXED`,
66
+ `OBSOLETE` or `INSUFFICIENT_EVIDENCE`.
67
+ - `ROOT_CAUSE`, `INCONCLUSIVE`, `STILL_LIVE` and `INSUFFICIENT_EVIDENCE` are
68
+ blocking and must name at least one blocker: for `ROOT_CAUSE` and
69
+ `STILL_LIVE` the blocker is the cause, with `file`/`line` where there is
70
+ one; for `INCONCLUSIVE` and `INSUFFICIENT_EVIDENCE` it is what evidence
71
+ would decide the question.
72
+ - `ALREADY_FIXED` and `OBSOLETE` carry no blockers — the fixing commit or the
73
+ superseding mechanism goes in `evidence` instead.
74
+ - `classification` is required on `ROOT_CAUSE`, allowed but optional on
75
+ `STILL_LIVE`, and refused on every other word.
76
+ - `node .claude/scripts/verdict.mjs check <report> failure-diagnostician` is
77
+ what refuses a malformed answer before anyone reads it as one.
78
+
79
+ ```json
80
+ {
81
+ "gate": "failure-diagnostician",
82
+ "verdict": "ROOT_CAUSE",
83
+ "blockers": [
84
+ {
85
+ "file": "src/example.ts",
86
+ "line": 42,
87
+ "rule": "reproduced failure",
88
+ "note": "the function reads the value before the guard that handles the missing case — reproduced on the PR head with the fixture the failing test supplies"
89
+ }
90
+ ],
91
+ "advisories": [],
92
+ "evidence": ["reproduced with the failing test on the PR head", "the stack trace from that run names the file:line above"],
93
+ "classification": "product"
94
+ }
95
+ ```
96
+
97
+ ```json
98
+ {
99
+ "gate": "failure-diagnostician",
100
+ "verdict": "ALREADY_FIXED",
101
+ "blockers": [],
102
+ "advisories": [],
103
+ "evidence": ["the commit that added the missing guard fixes exactly this report", "re-ran the original repro on the current default branch; it now passes"]
104
+ }
105
+ ```
106
+
107
+ ## Not a merge gate
108
+
109
+ You are never a routed reviewer: no `decision-router` lane names you and no
110
+ `pr-ship` coverage check (opt-in workflow layer) expects your answer. You
111
+ diagnose on request; you never implement the fix, and a report with no
112
+ unbacked behaviour claim or invented figure is the only kind you write.
@@ -193,9 +193,13 @@ function main() {
193
193
  // repo" and waves through, running the whole Definition-of-Done suite
194
194
  // against a tree it never managed to read.
195
195
  //
196
- // 🔴 Limit: only THIS command is sanitised. The Definition-of-Done checks
197
- // below run with the environment as given, because they are the project's
198
- // own commands and their environment is the project's business.
196
+ // 🔴 Limit: only THIS command is sanitised beyond `RIG_RUN_DIR`. The
197
+ // Definition-of-Done checks below run with the environment as given,
198
+ // because they are the project's own commands and their environment is
199
+ // the project's business — except `RIG_RUN_DIR`, which names the
200
+ // directory of the workflow run executing this very hook and belongs to
201
+ // the run, not to the project's own commands.
202
+ // see hooks.test.ts (absent in a generated rig) › "never lets a Definition-of-Done check see RIG_RUN_DIR, even though the hook itself is given one"
199
203
  // `timeout` below is the preamble's own leash: this is the one call that
200
204
  // runs before the budget's clock starts. A repository slow enough to exceed
201
205
  // it throws into the catch and the checks run anyway — the safe direction,
@@ -264,7 +268,15 @@ function main() {
264
268
 
265
269
  const budget = budgetMs(process.env);
266
270
  if (budget.notice) process.stderr.write(`gate-stop-dod: ${budget.notice}\n`);
267
- const deadline = Date.now() + budget.ms;
271
+ // Elapsed time, so the monotonic clock: a step of the wall clock must neither
272
+ // lengthen nor shorten the budget.
273
+ const deadline = performance.now() + budget.ms;
274
+
275
+ // Computed once, not per check: `RIG_RUN_DIR` names the workflow run
276
+ // directory of the run executing this hook, and it is the one variable the
277
+ // project's own commands never get — see the 🔴 limit above.
278
+ const envForChecks = { ...process.env };
279
+ delete envForChecks.RIG_RUN_DIR;
268
280
 
269
281
  for (const command of usable) {
270
282
  // A 1 ms floor rather than a branch for "the budget is already gone": the
@@ -274,9 +286,10 @@ function main() {
274
286
  const result = spawnSync(command, {
275
287
  cwd: PROJECT_ROOT,
276
288
  shell: true,
289
+ env: envForChecks,
277
290
  encoding: 'utf8',
278
291
  stdio: ['ignore', 'pipe', 'pipe'],
279
- timeout: Math.max(1, deadline - Date.now()),
292
+ timeout: Math.max(1, Math.ceil(deadline - performance.now())),
280
293
  // Output volume is not a verdict — below this bound. Past it the child is
281
294
  // killed and its result is unknown, which the error branch treats as
282
295
  // unmeasured rather than as a failure or a pass. The old 1 MB default
@@ -5,8 +5,10 @@
5
5
  // The rulebook is the set of files that decide what this session may do — hooks
6
6
  // and settings wiring, the queue config and selector, all scripts, rules, skills
7
7
  // and agents, the `.codex/` configuration, the integrity manifest,
8
- // `.claude/doctor-exemptions.json`, `AGENTS.md` and `CLAUDE.md`. Every one of
9
- // them was editable by the run it governs: a
8
+ // `.claude/doctor-exemptions.json`, the revalidation detection contract
9
+ // `.rig/revalidation.json` — but not the claim records beside it under
10
+ // `.rig/claims/`, which a SELECT has to write — `AGENTS.md` and `CLAUDE.md`.
11
+ // Every one of them was editable by the run it governs: a
10
12
  // Write to `.claude/hooks/dod-checks.json` with `[]` switched the stop gate
11
13
  // off, and nothing refused it until the merge sweep, long after.
12
14
  //
@@ -49,6 +51,12 @@
49
51
  // checkout root before guarding a canonical payload path", › "blocks when
50
52
  // the checkout root and payload use the same symlink spelling", and
51
53
  // › "blocks an existing rulebook file when only the payload path uses a symlink spelling";
54
+ // - an `apply_patch` destination keeps the lexical spelling it named even when
55
+ // a guarded prefix (`.claude/hooks`, say) is itself a symlink/junction to
56
+ // somewhere else inside the checkout — `edit-input.mjs`'s `repositoryPatchPath`
57
+ // carries that spelling alongside the realpath-resolved one, RP-60 — ›
58
+ // "refuses an apply_patch through a guarded prefix junctioned to a target
59
+ // inside the checkout";
52
60
  // - an `allow` prefix is a string prefix of the repo-relative path and may
53
61
  // not widen the rulebook — an entry that is itself a prefix of a rulebook
54
62
  // prefix (`.`, `.claude/`, `.claude/scripts/`) makes the flag unreadable
@@ -119,8 +127,15 @@ export const relativeTo = (root, filePath) => {
119
127
  export const isAllowed = (rel, allow) =>
120
128
  (Array.isArray(allow) ? allow : []).some((prefix) => prefix !== '' && (rel === prefix || rel.startsWith(prefix)));
121
129
 
122
- const protectedRelative = (roots, filePath) =>
123
- [...new Set([filePath, canonicalPath(filePath)])]
130
+ // RP-60: `rawFilePath` is the lexical spelling `apply_patch` fragments carry
131
+ // alongside the realpath-resolved `filePath` (`edit-input.mjs`,
132
+ // `repositoryPatchPath`) — a guarded prefix that is itself a symlink/junction
133
+ // to somewhere else inside the checkout resolves away the rulebook spelling
134
+ // otherwise, the same way `canonicalRoot`/`comparisonRoots` above seed both
135
+ // spellings of the checkout root. Every other edit surface never sets it, so
136
+ // this is a no-op for them.
137
+ const protectedRelative = (roots, filePath, rawFilePath) =>
138
+ [...new Set([filePath, rawFilePath, canonicalPath(filePath)].filter((spelling) => typeof spelling === 'string' && spelling !== ''))]
124
139
  .flatMap((spelling) => roots.map((root) => relativeTo(root, spelling)))
125
140
  .find(isRulebookPath);
126
141
 
@@ -151,9 +166,9 @@ function main() {
151
166
  return 2;
152
167
  }
153
168
  const paths = [];
154
- for (const { filePath } of fragments) {
169
+ for (const { filePath, rawFilePath } of fragments) {
155
170
  if (typeof filePath !== 'string' || filePath === '') continue;
156
- const rel = protectedRelative(comparisonRoots, filePath);
171
+ const rel = protectedRelative(comparisonRoots, filePath, rawFilePath);
157
172
  if (rel !== undefined && !paths.includes(rel)) paths.push(rel);
158
173
  }
159
174
  if (paths.length === 0) return 0; // nothing under the rulebook: never judged
@@ -279,7 +279,12 @@ function patchFragments(command, payloadCwd) {
279
279
  const moved = current.moveTo
280
280
  ? movedFragment(current, budget)
281
281
  : { fragment: current.additions.join('\n') };
282
- fragments.push({ filePath: destination, ...moved });
282
+ // RP-60: `rawFilePath` is the lexical repo-relative spelling, taken
283
+ // before symlink resolution — carried alongside the resolved
284
+ // `filePath` so a guard can still see a patch destination named
285
+ // through a guarded prefix that is itself a symlink/junction to
286
+ // somewhere else inside the checkout.
287
+ fragments.push({ filePath: destination.resolved, rawFilePath: destination.raw, ...moved });
283
288
  }
284
289
  current = null;
285
290
  }
@@ -510,6 +515,11 @@ function repositoryPatchPath(value, budget) {
510
515
  const candidate = path.resolve(budget.patchCwd, patchPath);
511
516
  if (!isWithin(budget.repoRoot, candidate)) return null;
512
517
 
518
+ // RP-60: the lexical repo-relative spelling, fixed BEFORE any symlink in the
519
+ // path (a guarded prefix junctioned elsewhere inside the checkout, say) gets
520
+ // resolved away below. One extra string, computed once — not a new loop.
521
+ const raw = path.relative(budget.repoRoot, candidate).split(path.sep).join('/');
522
+
513
523
  let existing = candidate;
514
524
  const suffix = [];
515
525
  while (true) {
@@ -517,7 +527,7 @@ function repositoryPatchPath(value, budget) {
517
527
  const resolved = budget.resolvedDirectories.get(existing);
518
528
  const resolvedCandidate = path.resolve(resolved, ...suffix);
519
529
  if (!isWithin(budget.repoRoot, resolvedCandidate)) return null;
520
- return path.relative(budget.repoRoot, resolvedCandidate).split(path.sep).join('/');
530
+ return { raw, resolved: path.relative(budget.repoRoot, resolvedCandidate).split(path.sep).join('/') };
521
531
  }
522
532
  try {
523
533
  const resolved = realpathSync(existing);
@@ -533,7 +543,7 @@ function repositoryPatchPath(value, budget) {
533
543
  }
534
544
  const resolvedCandidate = path.resolve(resolved, ...suffix);
535
545
  if (!isWithin(budget.repoRoot, resolvedCandidate)) return null;
536
- return path.relative(budget.repoRoot, resolvedCandidate).split(path.sep).join('/');
546
+ return { raw, resolved: path.relative(budget.repoRoot, resolvedCandidate).split(path.sep).join('/') };
537
547
  } catch (error) {
538
548
  if (error?.code !== 'ENOENT') return null;
539
549
  try {
@@ -215,6 +215,39 @@ The invariants worth your slots are the ones you can finish this sentence about:
215
215
  *"the last time this went wrong, it cost us ___."* If you cannot finish it, you
216
216
  are guessing, and a guessed invariant is the one that will fire on honest work.
217
217
 
218
+ ## The independent-oracle invariant
219
+
220
+ A test of a security, ownership or governance mechanism must not derive its expected result from the same production mechanism it checks.
221
+ Check it against an independent oracle instead: an alternative
222
+ implementation of the check, a mutation proof, or externally observable behaviour.
223
+
224
+ ⚠ **The independent-oracle invariant has parts 1 and 3 of the pattern above, and not part 2.** No hook enforces it: "is this expectation derived from the same production
225
+ mechanism" is not decidable from a single diff fragment — it takes reading
226
+ both the test and the code path it claims to verify, and judging which one
227
+ stands in as the oracle. `code-reviewer` is where it is enforced, as a
228
+ checklist item, never a hook — see the generator's
229
+ `test/template/correspondence.test.ts` (absent in a generated rig) ›
230
+ "the rule states the invariant and code-reviewer.md carries a matching checklist item".
231
+
232
+ Why this earned its own name: a test that asks production's own logic what the
233
+ right answer is cannot detect an under-approximation in that logic. Test and
234
+ code are the same computation run twice, agreeing by construction — so the
235
+ test passes, the reviewer sees a test that genuinely exercises the code, and
236
+ CI is green, while the defect the test was written for goes straight through.
237
+
238
+ The fix that came out of it is the worked example:
239
+ `packages/cli/test/uninstall.test.ts` (absent in a generated rig),
240
+ whose `expectImports` re-derives the import edges with a deliberately
241
+ duplicated regex rather than importing production's own — its comment says
242
+ "deliberately a second copy rather than an import of the private constant" —
243
+ so the test can never be satisfied merely by production checking its own
244
+ work.
245
+
246
+ Scope: this applies going forward, to tests of security, ownership and
247
+ governance mechanisms. The existing suite is not retrofitted wholesale — an
248
+ existing test is corrected only where doing so is cheap and the derivation
249
+ is demonstrably vacuous.
250
+
218
251
  ## About the hooks you were given
219
252
 
220
253
  Generator-authored rulebook artifacts — rules, hooks, skills, scripts and agent
@@ -50,6 +50,18 @@ A reviewer whose definition pins its model is never dispatched with a call-site
50
50
  dispatching session's, and `guard-subagent-model` refuses the override
51
51
  (`docs/decisions/subagent-routing.md`).
52
52
 
53
+ ## Review findings
54
+
55
+ A blocking finding — from `code-reviewer`, `security-scanner`,
56
+ `prose-reviewer`, or a review a repository rule names — is **resolved with
57
+ evidence, not argued away**. It closes one of two ways: the change is fixed
58
+ and the reviewer reads the fixed head, or the finding's premise is shown
59
+ false with something the reviewer can re-check — a `file:line`, a test name,
60
+ a command and its output. A reply that disagrees with neither is not a
61
+ resolution, and neither is softening the flagged sentence or dropping the
62
+ reviewer from the fan-out. This holds with or without the opt-in workflow
63
+ layer, whoever drives the fan-out.
64
+
53
65
  ## PR flow
54
66
 
55
67
  This applies **once the project has a remote and CI checks** — a freshly
@@ -122,8 +134,8 @@ travels one path to merge, in this order:
122
134
  the session itself drives the same fan-out** — dispatch each reviewer the
123
135
  table and the triggers above name, read every verdict, and resolve every
124
136
  blocking finding the same way `pr-ship` would; nothing about the review
125
- floor changes with or without the skill. Blocking findings are resolved,
126
- not argued with, either way.
137
+ floor changes with or without the skill. Blocking findings are resolved
138
+ as "Review findings" above says, either way.
127
139
 
128
140
  **A verdict is a block, not a sentence.** Every gate ends its report with one
129
141
  fenced `json` block of the shape `.claude/scripts/lib/verdict.mjs` defines
@@ -73,6 +73,10 @@
73
73
  * absence itself — `lib/gate-coverage.mjs` is the one that does, and it puts
74
74
  * such a verdict in its own list rather than counting it either way. When
75
75
  * present the value is a commit SHAPE, not free text: see `isCommitId`.
76
+ * 7. **`failure-diagnostician` answers in this shape and is not a merge gate.**
77
+ * No `decision-router` lane names it and `pr-ship` coverage never expects an
78
+ * answer from it — see `test/template/verdict.test.ts`
79
+ * (absent in a generated rig) › "the diagnostician is never a routed reviewer".
76
80
  */
77
81
 
78
82
  /** Every word any gate in this rulebook may return. */
@@ -86,6 +90,14 @@ export const VERDICT_WORDS = Object.freeze([
86
90
  'UNVERIFIABLE',
87
91
  'UNMEASURED',
88
92
  'NOT_APPLICABLE',
93
+ // RP-195 slice 1: failure-diagnostician's own words, split by what it was
94
+ // asked to look at — a failure, or a claimed/historical finding.
95
+ 'ROOT_CAUSE',
96
+ 'INCONCLUSIVE',
97
+ 'STILL_LIVE',
98
+ 'ALREADY_FIXED',
99
+ 'OBSOLETE',
100
+ 'INSUFFICIENT_EVIDENCE',
89
101
  ]);
90
102
 
91
103
  /**
@@ -111,6 +123,15 @@ export const GATE_VOCABULARY = Object.freeze({
111
123
  'UNMEASURED',
112
124
  ]),
113
125
  'post-deploy-verify': Object.freeze(['HEALTHY', 'REGRESSION']),
126
+ // RP-195 slice 1 (design decision 1): the diagnostician's own words.
127
+ 'failure-diagnostician': Object.freeze([
128
+ 'ROOT_CAUSE',
129
+ 'INCONCLUSIVE',
130
+ 'STILL_LIVE',
131
+ 'ALREADY_FIXED',
132
+ 'OBSOLETE',
133
+ 'INSUFFICIENT_EVIDENCE',
134
+ ]),
114
135
  });
115
136
 
116
137
  /**
@@ -126,6 +147,13 @@ export const BLOCKING_VERDICTS = Object.freeze([
126
147
  'PREMISE_FALSE',
127
148
  'UNVERIFIABLE',
128
149
  'UNMEASURED',
150
+ // RP-195 slice 1 (design decision 1): the cause, for ROOT_CAUSE and
151
+ // STILL_LIVE; the missing evidence, for INCONCLUSIVE and
152
+ // INSUFFICIENT_EVIDENCE. ALREADY_FIXED and OBSOLETE carry no blockers.
153
+ 'ROOT_CAUSE',
154
+ 'INCONCLUSIVE',
155
+ 'STILL_LIVE',
156
+ 'INSUFFICIENT_EVIDENCE',
129
157
  ]);
130
158
 
131
159
  /** The only keys a block may carry. */
@@ -136,8 +164,16 @@ const SHAPE_KEYS = Object.freeze([
136
164
  'advisories',
137
165
  'evidence',
138
166
  'headSha',
167
+ 'classification',
139
168
  ]);
140
169
 
170
+ /**
171
+ * The one optional key `failure-diagnostician` alone may carry (RP-195 slice
172
+ * 1, design decision 2): required on ROOT_CAUSE, optional on STILL_LIVE,
173
+ * refused on every other word and on every other gate.
174
+ */
175
+ const CLASSIFICATIONS = Object.freeze(['product', 'test', 'infrastructure', 'upstream']);
176
+
141
177
  const FENCE = '```json';
142
178
 
143
179
  /** How much of one reviewer-written value a diagnosis will carry. */
@@ -443,6 +479,32 @@ export function parseVerdict(text) {
443
479
  }
444
480
  }
445
481
 
482
+ const classification = parsed.classification;
483
+ const hasClassification = classification !== undefined;
484
+ if (hasClassification) {
485
+ if (!isText(gate) || gate !== 'failure-diagnostician') {
486
+ problems.push(
487
+ '`classification` is refused here: only failure-diagnostician may carry it, and ' +
488
+ `this block names \`gate\` as ${safeForDiagnosis(gate)}.`,
489
+ );
490
+ } else if (verdict !== 'ROOT_CAUSE' && verdict !== 'STILL_LIVE') {
491
+ problems.push(
492
+ `\`classification\` is refused on ${safeForDiagnosis(verdict)} — only ROOT_CAUSE ` +
493
+ '(required) and STILL_LIVE (optional) may carry one.',
494
+ );
495
+ } else if (!CLASSIFICATIONS.includes(classification)) {
496
+ problems.push(
497
+ `\`classification\` is \`${safeForDiagnosis(classification)}\`, which is not one of: ` +
498
+ `${CLASSIFICATIONS.join(', ')}.`,
499
+ );
500
+ }
501
+ } else if (isText(gate) && gate === 'failure-diagnostician' && verdict === 'ROOT_CAUSE') {
502
+ problems.push(
503
+ 'ROOT_CAUSE names no `classification`: it is required on this word — one of ' +
504
+ `${CLASSIFICATIONS.join(', ')}.`,
505
+ );
506
+ }
507
+
446
508
  if (problems.length > 0) return { ok: false, problems };
447
509
 
448
510
  return {
@@ -457,6 +519,7 @@ export function parseVerdict(text) {
457
519
  // back without the key at all, so a caller can tell "answered for this
458
520
  // commit" from "said nothing about which commit".
459
521
  ...(headSha === undefined ? {} : { headSha }),
522
+ ...(hasClassification ? { classification } : {}),
460
523
  },
461
524
  };
462
525
  }
@@ -633,13 +633,12 @@ const clearsSpacing = (lastCompletedTier) =>
633
633
  * items with the whole suite green throughout, and its `budget` stop arriving "later
634
634
  * than it should have".
635
635
  *
636
- * Two is the cap because the second round is what verifies the first round's fixes.
637
- * A third is a decision for a human rather than another pass to buy — and the
638
- * refusal says only that the count is spent, never that the fixes "are not
639
- * converging": on one branch a granted third round found that round 2's fix had
640
- * opened the mirror of the bug it closed (AR-115).
636
+ * Three is the cap: the second round verifies the first round's fixes, and the
637
+ * third lets a round-2 fix be read once more before the item needs a human. A
638
+ * project that wants a different cap sets `options.maxGateRounds` instead of
639
+ * changing this default.
641
640
  */
642
- export const DEFAULT_MAX_GATE_ROUNDS = 2;
641
+ export const DEFAULT_MAX_GATE_ROUNDS = 3;
643
642
 
644
643
  /**
645
644
  * Is this round allowed, and if not, what stops?