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.
- package/CHANGELOG.md +155 -0
- package/README.md +8 -8
- package/package.json +2 -2
- package/packages/cli/dist/commands/upgrade.js +69 -31
- package/packages/cli/dist/index.js +15 -2
- package/templates/agent-os/subagent-routing.json +4 -0
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +32 -3
- package/templates/agent-os/universal/.agents/skills/diagnose/SKILL.md +43 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +65 -22
- package/templates/agent-os/universal/.agents/skills/plan-slices/SKILL.md +30 -0
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/release-propose/SKILL.md +74 -0
- package/templates/agent-os/universal/.agents/skills/skill-authoring/SKILL.md +39 -0
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +4 -0
- package/templates/agent-os/universal/.claude/agents/failure-diagnostician.md +112 -0
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +18 -5
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +21 -6
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +13 -3
- package/templates/agent-os/universal/.claude/rules/invariants.md +33 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +14 -2
- package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +63 -0
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +5 -6
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +71 -14
- package/templates/agent-os/universal/.claude/scripts/queue/propose.mjs +139 -0
- package/templates/agent-os/universal/.claude/scripts/release-evidence.mjs +188 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +4 -2
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +8 -0
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +32 -3
- package/templates/agent-os/universal/.claude/skills/diagnose/SKILL.md +43 -0
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +65 -22
- package/templates/agent-os/universal/.claude/skills/plan-slices/SKILL.md +30 -0
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/release-propose/SKILL.md +74 -0
- package/templates/agent-os/universal/.claude/skills/skill-authoring/SKILL.md +39 -0
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/failure-diagnostician.toml +6 -0
- package/templates/agent-os/universal/AGENTS.md +6 -4
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +5 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +15 -3
- package/templates/agent-os/universal/layers.json +12 -0
- package/templates/hash-history.json +112 -39
- 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 **
|
|
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
|
|
197
|
-
// below run with the environment as given,
|
|
198
|
-
// own commands and their environment is
|
|
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
|
-
|
|
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 -
|
|
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`,
|
|
9
|
-
//
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
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 =
|
|
641
|
+
export const DEFAULT_MAX_GATE_ROUNDS = 3;
|
|
643
642
|
|
|
644
643
|
/**
|
|
645
644
|
* Is this round allowed, and if not, what stops?
|