create-agent-rig 1.0.0 → 1.0.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 +54 -0
- package/package.json +1 -1
- package/packages/cli/dist/integrations/doctor-guards.js +7 -3
- package/templates/agent-os/subagent-routing.json +7 -7
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +1 -1
- package/templates/agent-os/universal/.claude/agents/failure-diagnostician.md +1 -1
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +1 -1
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +123 -10
- package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +5 -1
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +155 -14
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +149 -10
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/failure-diagnostician.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/AGENTS.md +5 -1
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +1 -1
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +1 -1
- package/templates/hash-history.json +110 -19
- package/templates/release-ledger.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,60 @@ second recorded departure; its own entry states the direction and the reason,
|
|
|
14
14
|
and this paragraph deliberately does not restate them — a numbering rule with
|
|
15
15
|
two copies of its exceptions is the shape 0.8.0 exists to remove.
|
|
16
16
|
|
|
17
|
+
## 1.0.1
|
|
18
|
+
|
|
19
|
+
**1.0.1 is a patch on the 1.0 line.** It closes guard gaps an unattended run
|
|
20
|
+
could use to rewrite the rulebook, fixes a `doctor` side effect, and moves the
|
|
21
|
+
reviewer tier of both harnesses to newer models. The 1.0 contract is
|
|
22
|
+
unchanged: no flag, JSON shape, exit code, verdict or outcome word, and no
|
|
23
|
+
manifest key is added, removed or renamed.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **The Claude reviewer tier runs on Opus 5.5.** `code-reviewer`,
|
|
28
|
+
`security-scanner` and `failure-diagnostician` route to `claude-opus-5-5`
|
|
29
|
+
(was `claude-opus-5`), effort `high` unchanged (RP-232).
|
|
30
|
+
- **The Codex reviewer tier runs on GPT-6 Sol.** The same three roles route to
|
|
31
|
+
`gpt-6-sol` (was `gpt-5.6-sol`), reasoning effort `high` unchanged. Under
|
|
32
|
+
ChatGPT-account auth, Codex CLI 0.156.1 dispatched it and 0.154.0 rejected
|
|
33
|
+
the model id; versions in between were not tested (RP-233).
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`guard-rulebook` judges the paths an `apply_patch` removes.** A
|
|
38
|
+
`*** Delete File:` section, and the source of a `*** Move to:`, were never
|
|
39
|
+
judged, so an armed unattended run could delete or move a rulebook file —
|
|
40
|
+
including the guard itself (RP-214).
|
|
41
|
+
- **A miscased rulebook path is recognised.** `.Codex/config.toml` or
|
|
42
|
+
`.AGENTS/…` passed where the lowercase spelling was refused, and on a
|
|
43
|
+
case-insensitive filesystem the write landed on the real file. A miscased
|
|
44
|
+
allow-list entry is refused like its canonical spelling (RP-215).
|
|
45
|
+
- **A Win32 trailing dot or space in a path component no longer hides a
|
|
46
|
+
rulebook path.** Windows strips them, so `.claude./settings.json` is
|
|
47
|
+
`.claude/settings.json` (RP-243).
|
|
48
|
+
- **A Win32 verbatim or device path no longer hides a rulebook path.**
|
|
49
|
+
`\\?\C:\…`, `\\.\C:\…` and a `..` run above the drive root are judged as the
|
|
50
|
+
plain path Windows writes to. A UNC or other device-namespace path that does
|
|
51
|
+
not resolve under the repository root is refused while unattended rather
|
|
52
|
+
than judged as outside the rulebook (RP-244). One case stays open: when the
|
|
53
|
+
repository root is itself spelled as a UNC admin share, the local drive
|
|
54
|
+
spelling of the same file is not caught (RP-246).
|
|
55
|
+
- **`doctor` no longer leaves an unattended flag in your home directory.** Its
|
|
56
|
+
guard fixture armed the flag in the invoking user's real `~/.claude` as well
|
|
57
|
+
as in its temporary home, and never cleared it (RP-238).
|
|
58
|
+
- **The workflow layer's seed declares the revalidation contract elevated.**
|
|
59
|
+
`.rig/revalidation.json` was already protected from an unattended edit but
|
|
60
|
+
was missing from the `elevated-paths` seed, so the gate sweep could not see a
|
|
61
|
+
merge that rewrote it (RP-217).
|
|
62
|
+
|
|
63
|
+
### Generator repository (not a rig-facing change)
|
|
64
|
+
|
|
65
|
+
- The evidence-pointer check resolves a directory-qualified citation against
|
|
66
|
+
that exact path instead of any file with the same name (RP-216).
|
|
67
|
+
- The package-manager CLI-start test bounds its child process and reports a
|
|
68
|
+
timeout as one (RP-212); a credential-masking test no longer serialises 41
|
|
69
|
+
fixture rewrites, which made it time out under sibling disk load (RP-218).
|
|
70
|
+
|
|
17
71
|
## 1.0.0
|
|
18
72
|
|
|
19
73
|
**The harness-configuration contract is frozen.** Nothing here changes what a
|
package/package.json
CHANGED
|
@@ -48,12 +48,13 @@ const root=fs.mkdtempSync(path.join(os.tmpdir(),'rig-guard-fixtures-'));
|
|
|
48
48
|
const parent=path.resolve(os.tmpdir()),resolved=path.resolve(root);
|
|
49
49
|
if(!resolved.startsWith(parent+path.sep)||!path.basename(resolved).startsWith('rig-guard-fixtures-'))throw new Error('unsafe fixture root');
|
|
50
50
|
let code=1;
|
|
51
|
+
let flag,env;
|
|
51
52
|
try {
|
|
52
53
|
const home=path.join(root,'home'); fs.mkdirSync(path.join(root,'.claude','agents'),{recursive:true});
|
|
53
54
|
fs.writeFileSync(path.join(root,'.claude','agents','code-reviewer.md'),'---\nmodel: gpt-5.6-sol\n---\n');
|
|
54
|
-
|
|
55
|
+
env={...process.env,HOME:home,APPDATA:home,CLAUDE_PROJECT_DIR:root};
|
|
55
56
|
const unattended=path.join(source,'.claude','scripts','unattended-flag.mjs');
|
|
56
|
-
|
|
57
|
+
flag=await import(url.pathToFileURL(unattended).href); flag.writeUnattended({item:'fixture',runDir:root,allow:[]},env);
|
|
57
58
|
const run=(rel,input)=>cp.spawnSync(process.execPath,[path.join(source,...rel.split('/'))],{cwd:root,env,input:JSON.stringify(input),encoding:'utf8',timeout:5000,maxBuffer:8192}).status;
|
|
58
59
|
const pre=(tool_input)=>({hook_event_name:'PreToolUse',tool_name:tool_input.command?'Bash':'Write',tool_input});
|
|
59
60
|
const cases=[
|
|
@@ -69,7 +70,10 @@ try {
|
|
|
69
70
|
['.claude/hooks/guard-subagent-model.mjs',{hook_event_name:'PreToolUse',tool_name:'Agent',tool_input:{subagent_type:'code-reviewer'}},true]
|
|
70
71
|
];
|
|
71
72
|
for(const [rel,input,allowed] of cases){const status=run(rel,input);if(allowed ? status!==0 : status!==2){code=1;break;} code=0;}
|
|
72
|
-
} finally {
|
|
73
|
+
} finally {
|
|
74
|
+
try { if(flag&&typeof flag.clearUnattended==='function') flag.clearUnattended(env); } catch {}
|
|
75
|
+
fs.rmSync(resolved,{recursive:true,force:true});
|
|
76
|
+
}
|
|
73
77
|
process.exitCode=code;
|
|
74
78
|
})().catch(()=>{process.exitCode=1;});
|
|
75
79
|
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"claudeModels": {
|
|
3
|
-
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
3
|
+
"claude-opus-5-5": ["low", "medium", "high", "xhigh", "max"],
|
|
4
4
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"]
|
|
5
5
|
},
|
|
6
6
|
"unnamed": {
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
"codex": { "model": "gpt-5.6-terra", "effort": "high" }
|
|
22
22
|
},
|
|
23
23
|
"code-reviewer": {
|
|
24
|
-
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
25
|
-
"codex": { "model": "gpt-
|
|
24
|
+
"claude": { "model": "claude-opus-5-5", "effort": "high" },
|
|
25
|
+
"codex": { "model": "gpt-6-sol", "effort": "high" }
|
|
26
26
|
},
|
|
27
27
|
"security-scanner": {
|
|
28
|
-
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
29
|
-
"codex": { "model": "gpt-
|
|
28
|
+
"claude": { "model": "claude-opus-5-5", "effort": "high" },
|
|
29
|
+
"codex": { "model": "gpt-6-sol", "effort": "high" }
|
|
30
30
|
},
|
|
31
31
|
"failure-diagnostician": {
|
|
32
|
-
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
33
|
-
"codex": { "model": "gpt-
|
|
32
|
+
"claude": { "model": "claude-opus-5-5", "effort": "high" },
|
|
33
|
+
"codex": { "model": "gpt-6-sol", "effort": "high" }
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: code-reviewer
|
|
3
3
|
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 that reaches the `model` lane — everything the two cheap lanes do not claim (code, a rulebook document, an unclassifiable path, a derived artifact git does not report as drift, or anything a risk flag escalated), decided by `decision-router.mjs` where the opt-in workflow layer is installed, or by the same rule applied by hand otherwise. Blocking findings must be resolved, not argued with.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
-
model: claude-opus-5
|
|
5
|
+
model: claude-opus-5-5
|
|
6
6
|
effort: high
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: failure-diagnostician
|
|
3
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
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
-
model: claude-opus-5
|
|
5
|
+
model: claude-opus-5-5
|
|
6
6
|
effort: high
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: security-scanner
|
|
3
3
|
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.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
-
model: claude-opus-5
|
|
5
|
+
model: claude-opus-5-5
|
|
6
6
|
effort: high
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
// names the flag; an edit outside the rulebook still passes. Refusing to
|
|
29
29
|
// inspect is not allowing (`.claude/rules/invariants.md`).
|
|
30
30
|
//
|
|
31
|
-
// Limits — each stated here and
|
|
31
|
+
// Limits — each stated here and, unless it is marked untested, measured in
|
|
32
|
+
// the generator's
|
|
32
33
|
// `test/template/guard-rulebook.test.ts` (absent in a generated rig), by the
|
|
33
34
|
// test named beside it:
|
|
34
35
|
// - it sees one edit at a time, as text, before it lands — a rulebook file
|
|
@@ -62,17 +63,46 @@
|
|
|
62
63
|
// prefix (`.`, `.claude/`, `.claude/scripts/`) makes the flag unreadable
|
|
63
64
|
// and the guard refuses — › "a flag whose allow-list widens the rulebook is
|
|
64
65
|
// unreadable, so `--allow .` cannot disarm it";
|
|
66
|
+
// - a miscased payload path is judged against the rulebook's CANONICAL
|
|
67
|
+
// spelling (`unattended-flag.mjs`'s `canonicalRulebookPath`), but an
|
|
68
|
+
// `allow` entry never is — a miscased entry authorizes nothing, including
|
|
69
|
+
// the one prefix deliberately withheld as an allow root
|
|
70
|
+
// (`.claude/scripts/`) and the board selector, refused regardless of case
|
|
71
|
+
// — › "guard-rulebook: an allow-list entry is judged by its literal
|
|
72
|
+
// spelling, not the one the payload folds to (RP-215 round 2)";
|
|
65
73
|
// - fail-open on its own errors and on a payload it cannot parse — › "allows
|
|
66
74
|
// an empty payload object" and › "allows non-JSON stdin" — and fail-closed
|
|
67
75
|
// on a flag it cannot read — › "blocks a rulebook edit when the flag exists
|
|
68
76
|
// but cannot be read, and names the file": the guard targets drift, not an
|
|
69
|
-
// adversary
|
|
77
|
+
// adversary;
|
|
78
|
+
// - a `//`-prefixed normalised path is refused rather than resolved while
|
|
79
|
+
// armed exactly when it does not relativise under any comparison root —
|
|
80
|
+
// a repository root spelled as a plain path can never strip a UNC admin
|
|
81
|
+
// share or a device path with no drive letter (`\\?\Volume{GUID}\…`), but
|
|
82
|
+
// a root that is itself UNC-spelled (`CLAUDE_PROJECT_DIR` as
|
|
83
|
+
// `\\server\share\repo`) strips a payload path genuinely under it, and
|
|
84
|
+
// such a path is judged normally instead — › "guard-rulebook: an
|
|
85
|
+
// unjudgeable UNC/device-namespace path is refused, not silently allowed
|
|
86
|
+
// (RP-244 round 2)" and › "guard-rulebook: a `//`-prefixed path is
|
|
87
|
+
// refused only when it resolves under no repository root (RP-244
|
|
88
|
+
// round 3)". The same refusal covers a MultiEdit past the fragment cap
|
|
89
|
+
// (the `appliesToAll` global refusal) aimed at such a path, not only the
|
|
90
|
+
// ordinary per-fragment case — › "guard-rulebook: a MultiEdit global
|
|
91
|
+
// refusal is not exempt from the `//`-prefix refusal (RP-244 round 3)";
|
|
92
|
+
// - the reverse direction is untested and left as a design limit: when the
|
|
93
|
+
// repository root is itself spelled as a UNC admin share
|
|
94
|
+
// (`\\host\X$\…`), the LOCAL DRIVE spelling of the same file (`X:\…`) is
|
|
95
|
+
// never placed against it — `relativeTo` strips a `//`-prefixed payload
|
|
96
|
+
// path from a `//`-rooted comparison root, not a drive-letter one from
|
|
97
|
+
// it — so such a payload path is judged normally rather than refused as
|
|
98
|
+
// unjudgeable, and a rulebook edit reaching the guard that way is not
|
|
99
|
+
// caught. Tracked as RP-246.
|
|
70
100
|
//
|
|
71
101
|
// The rule it enforces is stated in `.claude/rules/autonomy.md`, "Never".
|
|
72
102
|
import { realpathSync } from 'node:fs';
|
|
73
103
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
74
104
|
import { editFragments } from './lib/edit-input.mjs';
|
|
75
|
-
import { RULEBOOK_PREFIXES, isRulebookPath, readUnattended } from '../scripts/unattended-flag.mjs';
|
|
105
|
+
import { RULEBOOK_PREFIXES, canonicalRulebookPath, isRulebookPath, readUnattended } from '../scripts/unattended-flag.mjs';
|
|
76
106
|
import { readHookInput } from './lib/hook-input.mjs';
|
|
77
107
|
|
|
78
108
|
export { RULEBOOK_PREFIXES, isRulebookPath };
|
|
@@ -134,10 +164,38 @@ export const isAllowed = (rel, allow) =>
|
|
|
134
164
|
// otherwise, the same way `canonicalRoot`/`comparisonRoots` above seed both
|
|
135
165
|
// spellings of the checkout root. Every other edit surface never sets it, so
|
|
136
166
|
// this is a no-op for them.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
//
|
|
168
|
+
// RP-215 round 2: the return value is the CANONICAL rulebook spelling
|
|
169
|
+
// (`canonicalRulebookPath`), not the candidate that matched it. `isAllowed`
|
|
170
|
+
// and the `.claude/queue.board` carve-out below both compare this result
|
|
171
|
+
// against literal allow-list entries, so a miscased entry (`.Claude/`,
|
|
172
|
+
// `.claude/Scripts/`) never matches — only the path is canonicalised, never
|
|
173
|
+
// the allow-list. See the generator's `test/template/guard-rulebook.test.ts` (absent in a generated rig) ›
|
|
174
|
+
// "guard-rulebook: an allow-list entry is judged by its literal spelling,
|
|
175
|
+
// not the one the payload folds to (RP-215 round 2)".
|
|
176
|
+
const protectedRelative = (roots, filePath, rawFilePath) => {
|
|
177
|
+
const candidates = [...new Set([filePath, rawFilePath, canonicalPath(filePath)].filter((spelling) => typeof spelling === 'string' && spelling !== ''))]
|
|
178
|
+
.flatMap((spelling) => roots.map((root) => relativeTo(root, spelling)));
|
|
179
|
+
for (const candidate of candidates) {
|
|
180
|
+
const canonical = canonicalRulebookPath(candidate);
|
|
181
|
+
if (canonical !== undefined) return canonical;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// RP-244 round 3: a `//`-prefixed normalised path is undecidable ONLY when it
|
|
187
|
+
// does not relativise under any comparison root — reusing `relativeTo`, the
|
|
188
|
+
// same comparison `protectedRelative` above already makes, rather than a
|
|
189
|
+
// second implementation. A UNC-spelled repository root (`CLAUDE_PROJECT_DIR`
|
|
190
|
+
// itself a `\\server\share\repo` spelling) maps to the same `//`-prefixed
|
|
191
|
+
// shape a payload path under it normalises to, so `relativeTo` CAN strip it —
|
|
192
|
+
// and a path that strips cleanly is judged normally, not refused as
|
|
193
|
+
// unjudgeable, whether or not it lands inside the rulebook.
|
|
194
|
+
const isUnjudgeablePath = (roots, filePath) => {
|
|
195
|
+
if (typeof filePath !== 'string' || !filePath.startsWith('//')) return false;
|
|
196
|
+
const file = toPosix(filePath);
|
|
197
|
+
return !roots.some((root) => relativeTo(root, filePath) !== file);
|
|
198
|
+
};
|
|
141
199
|
|
|
142
200
|
function main() {
|
|
143
201
|
const input = readHookInput();
|
|
@@ -154,8 +212,28 @@ function main() {
|
|
|
154
212
|
);
|
|
155
213
|
if (globalRefusal) {
|
|
156
214
|
if (globalRefusal.filePath) {
|
|
215
|
+
// No `rawFilePath` argument here: the only `appliesToAll` refusal that
|
|
216
|
+
// carries a non-empty `filePath` at all is the MultiEdit fragment-cap
|
|
217
|
+
// refusal (`edit-input.mjs`), and it never sets `rawFilePath` — so
|
|
218
|
+
// there is nothing a third argument would add for this call site.
|
|
157
219
|
const rel = protectedRelative(comparisonRoots, globalRefusal.filePath);
|
|
158
|
-
if (rel === undefined)
|
|
220
|
+
if (rel === undefined) {
|
|
221
|
+
// RP-244 round 3: `rel === undefined` used to read as "outside the
|
|
222
|
+
// rulebook, never judged" unconditionally — exactly the reading the
|
|
223
|
+
// per-fragment block below already proved wrong for a `//`-prefixed
|
|
224
|
+
// path that resolves under no comparison root. A global refusal
|
|
225
|
+
// carrying such a path is refused the same way, instead of exiting 0
|
|
226
|
+
// before `isUnjudgeablePath` is ever consulted.
|
|
227
|
+
if (!isUnjudgeablePath(comparisonRoots, globalRefusal.filePath)) return 0;
|
|
228
|
+
const mode = readUnattended(unattendedEnv);
|
|
229
|
+
if (!mode.on) return 0;
|
|
230
|
+
process.stderr.write(
|
|
231
|
+
`BLOCKED — "${globalRefusal.filePath}" could not be resolved against the repository root: ` +
|
|
232
|
+
'a UNC or device-namespace path is refused rather than judged while unattended. ' +
|
|
233
|
+
'Write through the repository path instead.\n',
|
|
234
|
+
);
|
|
235
|
+
return 2;
|
|
236
|
+
}
|
|
159
237
|
}
|
|
160
238
|
const mode = readUnattended(unattendedEnv);
|
|
161
239
|
if (!mode.on) return 0;
|
|
@@ -165,25 +243,60 @@ function main() {
|
|
|
165
243
|
);
|
|
166
244
|
return 2;
|
|
167
245
|
}
|
|
246
|
+
// RP-244 round 2: `normalisePath` (`edit-input.mjs`) maps a UNC or other
|
|
247
|
+
// device-namespace spelling to a `//`-prefixed result — `relativeTo` can
|
|
248
|
+
// only strip a path that starts with the literal repository root, and a
|
|
249
|
+
// `//`-prefixed path never does, on any platform or root. That is not
|
|
250
|
+
// "outside the rulebook": it is undecidable, and undecidable is refused,
|
|
251
|
+
// not allowed, while armed (`.claude/rules/invariants.md`, "The remedy
|
|
252
|
+
// belongs to the refusal"). RP-244 round 3: "does not relativise under any
|
|
253
|
+
// comparison root" replaces the plain `startsWith('//')` check — a
|
|
254
|
+
// UNC-spelled repository root makes a payload path under it decidable, so
|
|
255
|
+
// it is judged normally rather than refused as unjudgeable.
|
|
256
|
+
const unjudgeable = fragments.find(({ filePath }) => isUnjudgeablePath(comparisonRoots, filePath));
|
|
257
|
+
|
|
168
258
|
const paths = [];
|
|
169
259
|
for (const { filePath, rawFilePath } of fragments) {
|
|
170
260
|
if (typeof filePath !== 'string' || filePath === '') continue;
|
|
171
261
|
const rel = protectedRelative(comparisonRoots, filePath, rawFilePath);
|
|
172
262
|
if (rel !== undefined && !paths.includes(rel)) paths.push(rel);
|
|
173
263
|
}
|
|
174
|
-
if (paths.length === 0) return 0; // nothing under the rulebook: never judged
|
|
264
|
+
if (paths.length === 0 && !unjudgeable) return 0; // nothing under the rulebook: never judged
|
|
175
265
|
|
|
176
266
|
const mode = readUnattended(unattendedEnv);
|
|
177
267
|
if (!mode.on) return 0; // attended session
|
|
178
268
|
|
|
179
269
|
if (mode.unreadable) {
|
|
270
|
+
// RP-244 round 3: `paths[0] ?? unjudgeable.filePath` used to print "is
|
|
271
|
+
// part of the rulebook" even when `paths` was empty and only an
|
|
272
|
+
// unjudgeable `//`-prefixed path put this branch on the table — a claim
|
|
273
|
+
// this guard never established. An unjudgeable path gets the same
|
|
274
|
+
// "could not be resolved" reason it gets everywhere else in this file.
|
|
275
|
+
const target = paths[0];
|
|
276
|
+
if (target === undefined) {
|
|
277
|
+
process.stderr.write(
|
|
278
|
+
`BLOCKED — "${unjudgeable.filePath}" could not be resolved against the repository root, ` +
|
|
279
|
+
`and the unattended flag at ${mode.path} is unreadable (${mode.why}). ` +
|
|
280
|
+
'Refusing to inspect is not allowing: fix it, or clear this checkout with `node .claude/scripts/unattended-flag.mjs off --root "$PWD"`, then retry.\n',
|
|
281
|
+
);
|
|
282
|
+
return 2;
|
|
283
|
+
}
|
|
180
284
|
process.stderr.write(
|
|
181
|
-
`BLOCKED — "${
|
|
285
|
+
`BLOCKED — "${target}" is part of the rulebook and the unattended flag at ${mode.path} is unreadable (${mode.why}). ` +
|
|
182
286
|
'Refusing to inspect is not allowing: fix it, or clear this checkout with `node .claude/scripts/unattended-flag.mjs off --root "$PWD"`, then retry.\n',
|
|
183
287
|
);
|
|
184
288
|
return 2;
|
|
185
289
|
}
|
|
186
290
|
|
|
291
|
+
if (unjudgeable) {
|
|
292
|
+
process.stderr.write(
|
|
293
|
+
`BLOCKED — "${unjudgeable.filePath}" could not be resolved against the repository root: ` +
|
|
294
|
+
'a UNC or device-namespace path is refused rather than judged while unattended. ' +
|
|
295
|
+
'Write through the repository path instead.\n',
|
|
296
|
+
);
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
|
|
187
300
|
const refused = paths.filter(
|
|
188
301
|
(rel) => rel === '.claude/queue.board' || !isAllowed(rel, mode.allow),
|
|
189
302
|
);
|
|
@@ -99,7 +99,11 @@ function main() {
|
|
|
99
99
|
const editTools = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'apply_patch']);
|
|
100
100
|
if (!editTools.has(input?.tool_name)) return 0;
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// RP-214: `editFragments` also reports a removal (a Delete File section, or
|
|
103
|
+
// the source half of a Move) as its own fragment, `removes: true` — deleting
|
|
104
|
+
// a credential file is not writing one, so this guard never judges those,
|
|
105
|
+
// including an inspection refusal attached to one.
|
|
106
|
+
const fragments = editFragments(input).filter(({ removes }) => !removes);
|
|
103
107
|
const globalRefusal = fragments.find(
|
|
104
108
|
({ inspectionRefusal, appliesToAll }) => appliesToAll && inspectionRefusal,
|
|
105
109
|
);
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* Claude sends Write/Edit fields directly. Codex sends an apply_patch command,
|
|
5
5
|
* so added lines are returned for ordinary edits. A move is different: the
|
|
6
6
|
* destination receives the existing file too, so guards inspect the resulting
|
|
7
|
-
* content instead of only the patch additions when inspection succeeds.
|
|
7
|
+
* content instead of only the patch additions when inspection succeeds. A
|
|
8
|
+
* removal — a `*** Delete File:` section, or the source half of a
|
|
9
|
+
* `*** Move to:` — also becomes its own fragment, `removes: true`, `fragment:
|
|
10
|
+
* ''` (RP-214): the path stops existing, which a guard judging paths still
|
|
11
|
+
* needs to see even though there is no text to inspect.
|
|
8
12
|
*
|
|
9
13
|
* Inspection is bounded globally per patch: sources, hunks, output, splices,
|
|
10
14
|
* comparisons, sections and path components.
|
|
@@ -240,6 +244,21 @@ function patchFragments(command, payloadCwd) {
|
|
|
240
244
|
let current = null;
|
|
241
245
|
let patchRefusal = null;
|
|
242
246
|
|
|
247
|
+
// RP-214: one counter, spent on every path this section resolves — the
|
|
248
|
+
// destination (or the removed path, for a Delete File section) and, for a
|
|
249
|
+
// Move, the source too (below). Still one forward pass, still the same
|
|
250
|
+
// bound.
|
|
251
|
+
const overPathComponentBudget = (value) => {
|
|
252
|
+
budget.pathComponents += String(value ?? '').replaceAll('\\', '/').split('/').length;
|
|
253
|
+
return budget.pathComponents > MAX_PATCH_PATH_COMPONENTS;
|
|
254
|
+
};
|
|
255
|
+
const pathComponentRefusal = () => ({
|
|
256
|
+
filePath: '',
|
|
257
|
+
fragment: '',
|
|
258
|
+
inspectionRefusal: `apply_patch destination path component count exceeds the ${MAX_PATCH_PATH_COMPONENTS}-component inspection limit`,
|
|
259
|
+
appliesToAll: true,
|
|
260
|
+
});
|
|
261
|
+
|
|
243
262
|
const flush = () => {
|
|
244
263
|
if (current !== null) {
|
|
245
264
|
budget.sections += 1;
|
|
@@ -254,16 +273,8 @@ function patchFragments(command, payloadCwd) {
|
|
|
254
273
|
return false;
|
|
255
274
|
}
|
|
256
275
|
const destinationPath = current.moveTo ?? current.sourcePath;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.split('/').length;
|
|
260
|
-
if (budget.pathComponents > MAX_PATCH_PATH_COMPONENTS) {
|
|
261
|
-
patchRefusal = {
|
|
262
|
-
filePath: '',
|
|
263
|
-
fragment: '',
|
|
264
|
-
inspectionRefusal: `apply_patch destination path component count exceeds the ${MAX_PATCH_PATH_COMPONENTS}-component inspection limit`,
|
|
265
|
-
appliesToAll: true,
|
|
266
|
-
};
|
|
276
|
+
if (overPathComponentBudget(destinationPath)) {
|
|
277
|
+
patchRefusal = pathComponentRefusal();
|
|
267
278
|
current = null;
|
|
268
279
|
return false;
|
|
269
280
|
}
|
|
@@ -274,6 +285,22 @@ function patchFragments(command, payloadCwd) {
|
|
|
274
285
|
fragment: '',
|
|
275
286
|
inspectionRefusal: 'patch destination is outside the repository or cannot be resolved safely',
|
|
276
287
|
appliesToAll: true,
|
|
288
|
+
// RP-214: a Delete File section has no destination — only the
|
|
289
|
+
// removed path itself — so this refusal is about a removal too;
|
|
290
|
+
// carry the flag so a guard that must not treat a removal as a
|
|
291
|
+
// write (guard-secret-file) can still tell the two apart.
|
|
292
|
+
...(current.removes ? { removes: true } : {}),
|
|
293
|
+
});
|
|
294
|
+
} else if (current.removes) {
|
|
295
|
+
// RP-214: `*** Delete File: <path>` used to flush only the SECTION
|
|
296
|
+
// BEFORE it and never turn the removed path itself into a fragment —
|
|
297
|
+
// resolved the same way every other verb resolves its path, through
|
|
298
|
+
// `repositoryPatchPath` and the same budgets above, in this same pass.
|
|
299
|
+
fragments.push({
|
|
300
|
+
filePath: destination.resolved,
|
|
301
|
+
rawFilePath: destination.raw,
|
|
302
|
+
fragment: '',
|
|
303
|
+
removes: true,
|
|
277
304
|
});
|
|
278
305
|
} else {
|
|
279
306
|
const moved = current.moveTo
|
|
@@ -285,6 +312,31 @@ function patchFragments(command, payloadCwd) {
|
|
|
285
312
|
// through a guarded prefix that is itself a symlink/junction to
|
|
286
313
|
// somewhere else inside the checkout.
|
|
287
314
|
fragments.push({ filePath: destination.resolved, rawFilePath: destination.raw, ...moved });
|
|
315
|
+
|
|
316
|
+
if (current.moveTo) {
|
|
317
|
+
// RP-214: a Move's SOURCE stops existing too — it is a removal the
|
|
318
|
+
// line above never surfaced, because `current.moveTo ?? …`
|
|
319
|
+
// resolves only the destination. One more path through the same
|
|
320
|
+
// resolver and the same budgets, still inside this one flush.
|
|
321
|
+
if (overPathComponentBudget(current.sourcePath)) {
|
|
322
|
+
patchRefusal = pathComponentRefusal();
|
|
323
|
+
current = null;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const source = repositoryPatchPath(current.sourcePath, budget);
|
|
327
|
+
fragments.push(
|
|
328
|
+
source === null
|
|
329
|
+
? {
|
|
330
|
+
filePath: '',
|
|
331
|
+
fragment: '',
|
|
332
|
+
inspectionRefusal:
|
|
333
|
+
'patch destination is outside the repository or cannot be resolved safely',
|
|
334
|
+
appliesToAll: true,
|
|
335
|
+
removes: true,
|
|
336
|
+
}
|
|
337
|
+
: { filePath: source.resolved, rawFilePath: source.raw, fragment: '', removes: true },
|
|
338
|
+
);
|
|
339
|
+
}
|
|
288
340
|
}
|
|
289
341
|
current = null;
|
|
290
342
|
}
|
|
@@ -303,7 +355,17 @@ function patchFragments(command, payloadCwd) {
|
|
|
303
355
|
current.moveTo = move[1];
|
|
304
356
|
continue;
|
|
305
357
|
}
|
|
306
|
-
|
|
358
|
+
// RP-214: a Delete File section becomes its own fragment — `removes:
|
|
359
|
+
// true`, resolved through the same path the other verbs use — instead of
|
|
360
|
+
// only flushing whatever section came before it.
|
|
361
|
+
const del = /^\*\*\* Delete File: (.+)$/.exec(line);
|
|
362
|
+
if (del) {
|
|
363
|
+
if (!flush()) break;
|
|
364
|
+
current = { sourcePath: del[1], moveTo: null, additions: [], hunks: [], activeHunk: null, removes: true };
|
|
365
|
+
if (!flush()) break;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (/^\*\*\* End Patch/.test(line)) {
|
|
307
369
|
if (!flush()) break;
|
|
308
370
|
continue;
|
|
309
371
|
}
|
|
@@ -486,9 +548,88 @@ function inspectionRefusal(current, reason) {
|
|
|
486
548
|
return { fragment: current.additions.join('\n'), inspectionRefusal: reason };
|
|
487
549
|
}
|
|
488
550
|
|
|
551
|
+
// RP-244: a Win32 verbatim (`\\?\`) or device-namespace (`\\.\`) prefix, in
|
|
552
|
+
// any slash mix, names the plain drive (or UNC) path underneath it. The
|
|
553
|
+
// backslash-to-slash conversion below would collapse the leading `//` these
|
|
554
|
+
// prefixes depend on, so they are stripped first. Anchored at the very start:
|
|
555
|
+
// a prefix check, not a scan, which keeps this inside the fail-open
|
|
556
|
+
// bounded-work rule.
|
|
557
|
+
//
|
|
558
|
+
// RP-244 round 2: `[\\/]+` (not `[\\/]`) after the `?`/`.` marker, so a
|
|
559
|
+
// doubled separator still strips. A DRIVE match (`\\?\C:\…`) is the only case
|
|
560
|
+
// that yields a plain, prefix-free spelling — every other two-separator input
|
|
561
|
+
// (verbatim UNC, plain UNC, a device path with no drive letter such as
|
|
562
|
+
// `\\?\Volume{GUID}\…`) is judged as UNDECIDABLE by a repository root spelled
|
|
563
|
+
// as a plain path, so it keeps a leading `//` instead, with its remainder
|
|
564
|
+
// normalised as ABSOLUTE (clamped at root) rather than relative — a relative
|
|
565
|
+
// normalisation let a crafted run of `../` segments do unbounded work instead
|
|
566
|
+
// of collapsing at the root.
|
|
567
|
+
const WIN32_VERBATIM_UNC_PREFIX = /^[\\/]{2}[?.][\\/]+UNC(?:[\\/]+|$)/i;
|
|
568
|
+
const WIN32_VERBATIM_DRIVE_PREFIX = /^[\\/]{2}[?.][\\/]+([A-Za-z]:)/;
|
|
569
|
+
// RP-244 round 3: a drive letter followed immediately by a separator — the
|
|
570
|
+
// drive ROOT, as opposed to the drive-RELATIVE `C:foo\bar` spelling below,
|
|
571
|
+
// which carries no separator there. `slashed` has already had every
|
|
572
|
+
// backslash converted to `/` by the time this is applied, so only the
|
|
573
|
+
// forward-slash form is checked.
|
|
574
|
+
const DRIVE_ROOT_PREFIX = /^[A-Za-z]:\//;
|
|
575
|
+
// RP-244 round 4: a drive letter with NO separator immediately following —
|
|
576
|
+
// the DRIVE-RELATIVE spelling, which Win32 resolves against the current
|
|
577
|
+
// directory of that drive rather than the drive root, so (unlike
|
|
578
|
+
// DRIVE_ROOT_PREFIX above) it has no root of its own to clamp at. Mutually
|
|
579
|
+
// exclusive with DRIVE_ROOT_PREFIX by construction: one requires a `/`
|
|
580
|
+
// immediately after the colon, this one requires there is none (including
|
|
581
|
+
// end of string, for a bare `C:`).
|
|
582
|
+
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?!\/)/;
|
|
583
|
+
|
|
584
|
+
// RP-244 round 3: the DRIVE branch below and the plain fallback both used to
|
|
585
|
+
// run `path.posix.normalize` over the drive letter and its remainder
|
|
586
|
+
// TOGETHER, relatively — so a leading `..` walked straight past the drive
|
|
587
|
+
// letter (`C:/../Users/…` normalised to the relative `Users/…`, not clamped
|
|
588
|
+
// at `C:/`) instead of stopping at the root the way Win32 does, and a long
|
|
589
|
+
// run of `../` segments did unbounded relative work instead of the linear
|
|
590
|
+
// work an absolute normalisation does. Splitting the drive off first and
|
|
591
|
+
// normalising only the remainder, ABSOLUTE, fixes both: `path.posix.normalize`
|
|
592
|
+
// clamps an absolute `..` run at `/` instead of carrying it past the drive.
|
|
593
|
+
//
|
|
594
|
+
// RP-244 round 4: the drive-RELATIVE case (no separator after the colon) got
|
|
595
|
+
// the same together-normalisation treatment via the plain `else` branch below
|
|
596
|
+
// — `path.posix.normalize` reads a leading `C:..` segment as an ordinary
|
|
597
|
+
// filename, not the literal `..`, so a second `..` cancelled it and the
|
|
598
|
+
// drive marker vanished from the result (`C:../../a/b` → `a/b`). Splitting
|
|
599
|
+
// the drive off here too and normalising the remainder alone — RELATIVELY,
|
|
600
|
+
// never absolutely, since a drive-relative spelling has no root to clamp
|
|
601
|
+
// at — keeps a `..` in the remainder from ever reaching back far enough to
|
|
602
|
+
// cancel the marker itself; an empty remainder (a bare `C:`) is left as-is
|
|
603
|
+
// rather than turned into `C:.`, unchanged from before this round.
|
|
604
|
+
function clampAtDriveRoot(slashed) {
|
|
605
|
+
if (DRIVE_ROOT_PREFIX.test(slashed)) {
|
|
606
|
+
return slashed.slice(0, 2) + path.posix.normalize(slashed.slice(2));
|
|
607
|
+
}
|
|
608
|
+
if (DRIVE_RELATIVE_PREFIX.test(slashed)) {
|
|
609
|
+
const remainder = slashed.slice(2);
|
|
610
|
+
return remainder === '' ? slashed : slashed.slice(0, 2) + path.posix.normalize(remainder);
|
|
611
|
+
}
|
|
612
|
+
return path.posix.normalize(slashed);
|
|
613
|
+
}
|
|
614
|
+
|
|
489
615
|
function normalisePath(value) {
|
|
490
|
-
const
|
|
491
|
-
|
|
616
|
+
const raw = String(value ?? '').trim();
|
|
617
|
+
if (raw === '') return '';
|
|
618
|
+
const driveMatch = WIN32_VERBATIM_DRIVE_PREFIX.exec(raw);
|
|
619
|
+
if (driveMatch) {
|
|
620
|
+
const slashed = raw.replace(WIN32_VERBATIM_DRIVE_PREFIX, '$1').replaceAll('\\', '/');
|
|
621
|
+
return slashed === '' ? '' : clampAtDriveRoot(slashed);
|
|
622
|
+
}
|
|
623
|
+
const uncMatch = WIN32_VERBATIM_UNC_PREFIX.exec(raw);
|
|
624
|
+
if (uncMatch || /^[\\/]{2}/.test(raw)) {
|
|
625
|
+
const rest = (uncMatch ? raw.slice(uncMatch[0].length) : raw.slice(2)).replaceAll('\\', '/');
|
|
626
|
+
// Absolute normalisation, not relative: clamps a leading `../` run at the
|
|
627
|
+
// root instead of carrying it through — the outer `'/' +` restores the
|
|
628
|
+
// `//` marker that `path.posix.normalize` collapses to one.
|
|
629
|
+
return '/' + path.posix.normalize('/' + rest);
|
|
630
|
+
}
|
|
631
|
+
const slashed = raw.replaceAll('\\', '/');
|
|
632
|
+
return slashed === '' ? '' : clampAtDriveRoot(slashed);
|
|
492
633
|
}
|
|
493
634
|
|
|
494
635
|
function canonicalPatchPath(value) {
|
|
@@ -104,9 +104,137 @@ export const RULEBOOK_PREFIXES = Object.freeze([
|
|
|
104
104
|
'.rig/revalidation.json',
|
|
105
105
|
]);
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
// Case-folded once at module load, for the same comparison every caller shares
|
|
108
|
+
// — never re-derived per call, and never used anywhere but inside
|
|
109
|
+
// `isWidening` below (`canonicalRulebookPath` folds its own comparison from
|
|
110
|
+
// `RULEBOOK_PREFIX_SEGMENTS` instead).
|
|
111
|
+
const FOLDED_RULEBOOK_PREFIXES = RULEBOOK_PREFIXES.map((prefix) => prefix.toLowerCase());
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The canonical rulebook spelling of a repo-relative path, or `undefined`
|
|
115
|
+
* when it names no rulebook prefix at all. Case-insensitive on the PREFIX
|
|
116
|
+
* match only — NTFS and default APFS resolve a miscased spelling
|
|
117
|
+
* (`.Codex/config.toml`) to the same file as the canonical one, and this
|
|
118
|
+
* comparison is what a directory this checkout has never created yet (or
|
|
119
|
+
* any path on a case-sensitive filesystem) reaches, exactly as spelled by
|
|
120
|
+
* the caller — see the generator's `test/template/guard-rulebook.test.ts`
|
|
121
|
+
* (absent in a generated rig) › "guard-rulebook: blocks a miscased
|
|
122
|
+
* rulebook path even when the guarded directory does not exist on disk yet
|
|
123
|
+
* (RP-215)" › "blocks a Write to .Codex/config.toml when .codex/ is absent
|
|
124
|
+
* from the checkout".
|
|
125
|
+
*
|
|
126
|
+
* RP-215 round 2: the returned spelling replaces only the matched prefix
|
|
127
|
+
* characters with `RULEBOOK_PREFIXES`' own spelling — the remainder of the
|
|
128
|
+
* path (and any allow-list entry) keeps whatever case it was written in.
|
|
129
|
+
* This is the one place a path is re-cased; every comparison downstream
|
|
130
|
+
* (`isAllowed`, the `.claude/queue.board` carve-out in `guard-rulebook.mjs`)
|
|
131
|
+
* takes this canonical value and stays a literal, case-sensitive match —
|
|
132
|
+
* canonicalising the PATH, never folding the ALLOW-LIST, is what keeps a
|
|
133
|
+
* miscased allow entry (`.Claude/`, `.claude/Scripts/`) from ever matching
|
|
134
|
+
* anything, including the one prefix deliberately withheld as an allow root
|
|
135
|
+
* (`.claude/scripts/`, `isWidening`).
|
|
136
|
+
*
|
|
137
|
+
* RP-243: before the fold above, a trailing run of `.`/` ` is stripped from
|
|
138
|
+
* a path component — `.codex./config.toml` folds the same as
|
|
139
|
+
* `.codex/config.toml` — except a component made entirely of dots (`.`,
|
|
140
|
+
* `..`), left untouched so a traversal segment is never collapsed into an
|
|
141
|
+
* empty name. Only the PATH is normalised this way, never an allow-list
|
|
142
|
+
* entry — the same asymmetry as the case fold above.
|
|
143
|
+
*
|
|
144
|
+
* RP-243 round 2: only the components the matched prefix itself spans are
|
|
145
|
+
* normalised — bounded by the longest `RULEBOOK_PREFIXES` entry's own
|
|
146
|
+
* segment count, computed once below — so a component beyond that span
|
|
147
|
+
* keeps its literal spelling and the canonical path returns it raw. See the
|
|
148
|
+
* generator's `test/template/unattended-flag.test.ts` (absent in a
|
|
149
|
+
* generated rig) › "isRulebookPath: a trailing dot or space on a path
|
|
150
|
+
* component is judged the same as the component with it stripped (RP-243)".
|
|
151
|
+
*/
|
|
152
|
+
const stripTrailingDotsAndSpaces = (component) => {
|
|
153
|
+
if (/^\.+$/.test(component)) return component;
|
|
154
|
+
let end = component.length;
|
|
155
|
+
while (end > 0) {
|
|
156
|
+
const code = component.charCodeAt(end - 1);
|
|
157
|
+
if (code !== 46 /* '.' */ && code !== 32 /* ' ' */) break;
|
|
158
|
+
end -= 1;
|
|
159
|
+
}
|
|
160
|
+
return end === component.length ? component : component.slice(0, end);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `RULEBOOK_PREFIXES` split into segments once, at module load — a directory
|
|
165
|
+
* entry (`.claude/hooks/`) drops its trailing slash first, so both forms
|
|
166
|
+
* yield the segment names a path's own `/`-split components are compared
|
|
167
|
+
* against. `directory` records whether the entry itself ended in `/`: a
|
|
168
|
+
* directory entry needs an exact fold-match on every one of its segments
|
|
169
|
+
* plus something after them; a file entry (`CLAUDE.md`, `.rig/revalidation.json`)
|
|
170
|
+
* is matched with a `startsWith` on its last segment — see the generator's
|
|
171
|
+
* `test/template/unattended-flag.test.ts` (absent in a generated rig) ›
|
|
172
|
+
* "isRulebookPath: a path continuing past a matched FILE entry is still a rulebook path (round 3 regression)".
|
|
173
|
+
*/
|
|
174
|
+
const RULEBOOK_PREFIX_SEGMENTS = RULEBOOK_PREFIXES.map((prefix) => {
|
|
175
|
+
const directory = prefix.endsWith('/');
|
|
176
|
+
const segments = (directory ? prefix.slice(0, -1) : prefix).split('/');
|
|
177
|
+
const foldedSegments = segments.map((segment) => segment.toLowerCase());
|
|
178
|
+
return { prefix, directory, foldedSegments, length: segments.length };
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// The most components any single entry's match can ever need to inspect —
|
|
182
|
+
// never re-derived per call, and the only thing that bounds how many of a
|
|
183
|
+
// path's own components `canonicalRulebookPath` normalises.
|
|
184
|
+
const MAX_PREFIX_SEGMENTS = RULEBOOK_PREFIX_SEGMENTS.reduce(
|
|
185
|
+
(max, entry) => Math.max(max, entry.length),
|
|
186
|
+
0,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export const canonicalRulebookPath = (rel) => {
|
|
190
|
+
const components = rel.split('/');
|
|
191
|
+
const headCount = Math.min(components.length, MAX_PREFIX_SEGMENTS);
|
|
192
|
+
const normalisedHead = new Array(headCount);
|
|
193
|
+
const foldedHead = new Array(headCount);
|
|
194
|
+
for (let i = 0; i < headCount; i += 1) {
|
|
195
|
+
normalisedHead[i] = stripTrailingDotsAndSpaces(components[i]);
|
|
196
|
+
foldedHead[i] = normalisedHead[i].toLowerCase();
|
|
197
|
+
}
|
|
198
|
+
for (const entry of RULEBOOK_PREFIX_SEGMENTS) {
|
|
199
|
+
const { directory, foldedSegments, length: segmentCount, prefix } = entry;
|
|
200
|
+
if (components.length < segmentCount) continue;
|
|
201
|
+
let leadingMatches = true;
|
|
202
|
+
for (let i = 0; i < segmentCount - 1; i += 1) {
|
|
203
|
+
if (foldedHead[i] !== foldedSegments[i]) {
|
|
204
|
+
leadingMatches = false;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!leadingMatches) continue;
|
|
209
|
+
const lastFolded = foldedHead[segmentCount - 1];
|
|
210
|
+
const lastSegment = foldedSegments[segmentCount - 1];
|
|
211
|
+
if (directory) {
|
|
212
|
+
if (lastFolded !== lastSegment) continue;
|
|
213
|
+
if (components.length === segmentCount) continue; // no trailing slash, nothing after
|
|
214
|
+
if (components.length === segmentCount + 1 && components[segmentCount] === '') {
|
|
215
|
+
return prefix; // the path itself ends with the literal trailing slash
|
|
216
|
+
}
|
|
217
|
+
return prefix + components.slice(segmentCount).join('/');
|
|
218
|
+
}
|
|
219
|
+
// File entry: the last spanned component is compared folded+normalised,
|
|
220
|
+
// same as every other component this function inspects; anything past
|
|
221
|
+
// it — the rest of that component, and every component after it — is
|
|
222
|
+
// returned exactly as written.
|
|
223
|
+
if (lastFolded === lastSegment) {
|
|
224
|
+
if (components.length === segmentCount) return prefix;
|
|
225
|
+
return `${prefix}/${components.slice(segmentCount).join('/')}`;
|
|
226
|
+
}
|
|
227
|
+
if (lastFolded.startsWith(lastSegment)) {
|
|
228
|
+
const extra = normalisedHead[segmentCount - 1].slice(lastSegment.length);
|
|
229
|
+
if (components.length === segmentCount) return prefix + extra;
|
|
230
|
+
return `${prefix}${extra}/${components.slice(segmentCount).join('/')}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Is this repo-relative path part of the rulebook? See `canonicalRulebookPath`. */
|
|
237
|
+
export const isRulebookPath = (rel) => canonicalRulebookPath(rel) !== undefined;
|
|
110
238
|
|
|
111
239
|
/**
|
|
112
240
|
* Does this allow entry widen the rulebook? It is unsafe when it is an
|
|
@@ -116,14 +244,25 @@ export const isRulebookPath = (rel) =>
|
|
|
116
244
|
* protected script paths sit under `.claude/scripts/`; a narrower path such as
|
|
117
245
|
* `.claude/scripts/queue/` is an ordinary allow entry and does not widen it.
|
|
118
246
|
* `src/` also does not widen it because the guard judges nothing there.
|
|
247
|
+
*
|
|
248
|
+
* RP-215 round 2: the entry is case-folded once before either comparison —
|
|
249
|
+
* the allow side now folds like the path side (`canonicalRulebookPath`) — so
|
|
250
|
+
* a miscased entry (`.Claude/`, `.claude/Scripts/`) is judged exactly like its
|
|
251
|
+
* canonical spelling instead of slipping through as ordinary, harmless text;
|
|
252
|
+
* see the generator's `test/template/unattended-flag.test.ts` (absent in a
|
|
253
|
+
* generated rig) › "isWidening: a miscased allow entry must be refused
|
|
254
|
+
* exactly like its canonical spelling (RP-215 round 2)".
|
|
119
255
|
*/
|
|
120
|
-
export const isWidening = (entry) =>
|
|
121
|
-
typeof entry !== 'string' ||
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
256
|
+
export const isWidening = (entry) => {
|
|
257
|
+
if (typeof entry !== 'string' || entry === '') return true;
|
|
258
|
+
const folded = entry.toLowerCase();
|
|
259
|
+
return (
|
|
260
|
+
folded === '.agents/' ||
|
|
261
|
+
folded === '.claude/scripts/' ||
|
|
262
|
+
folded === '.codex/' ||
|
|
263
|
+
FOLDED_RULEBOOK_PREFIXES.some((prefix) => prefix !== folded && prefix.startsWith(folded))
|
|
264
|
+
);
|
|
265
|
+
};
|
|
127
266
|
|
|
128
267
|
/**
|
|
129
268
|
* One spelling for one directory — the single canonicaliser this file compares
|
|
@@ -1,6 +1,6 @@
|
|
|
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 that reaches the `model` lane — everything the two cheap lanes do not claim (code, a rulebook document, an unclassifiable path, a derived artifact git does not report as drift, or anything a risk flag escalated), decided by `decision-router.mjs` where the opt-in workflow layer is installed, or by the same rule applied by hand otherwise. Blocking findings must be resolved, not argued with."
|
|
3
|
-
model = "gpt-
|
|
3
|
+
model = "gpt-6-sol"
|
|
4
4
|
model_reasoning_effort = "high"
|
|
5
5
|
sandbox_mode = "read-only"
|
|
6
6
|
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** — changes that bypass module ownership or layer\n boundaries the repository itself declares. See any applicable architecture\n 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.\n7. **Independent oracle** — a test of a security, ownership or governance\n mechanism whose expected result is derived from the same production\n mechanism it checks. See the independent-oracle invariant in\n `.claude/rules/invariants.md`.\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 whoever runs that check holds on it —\n `pr-ship` where the opt-in workflow layer is installed, the session itself\n running `node .claude/scripts/verdict.mjs coverage` by hand otherwise: no\n hook runs it either way, so skipping the gate skips this with it."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name = "failure-diagnostician"
|
|
2
2
|
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."
|
|
3
|
-
model = "gpt-
|
|
3
|
+
model = "gpt-6-sol"
|
|
4
4
|
model_reasoning_effort = "high"
|
|
5
5
|
sandbox_mode = "read-only"
|
|
6
6
|
developer_instructions = "You diagnose. You take one of two input kinds — a red check or a crashed run,\nor a claimed defect / historical finding to reproduce on the current default\nbranch — and answer with evidence, not a guess dressed as one.\n\n## Hard limits\n\n- **You make no repository edits.** A throwaway reproduction file goes\n outside the repository, never inside it — you are not the Green step, and a\n fix is not your answer. You do not commit, push, merge or open a pull\n request.\n- **Never re-run a check until it goes green.** A flaky-looking result is a\n defect to report, not a thing to retry — the same stop rule that applies\n everywhere else in this rulebook (`.claude/rules/autonomy.md`, \"Stop rules —\n by work-state, not by feelings\").\n- **On Claude Code, no hook enforces the no-edit limit above.** `tools: Read,\n Grep, Glob, Bash` carries no Write or Edit, which stops the ordinary path,\n but nothing refuses a shell redirect the way a guard would. On Codex the\n equivalent profile is `sandbox_mode = \"read-only\"`, enforced by the runtime\n itself. Either way, a sandbox that blocks the reproduction you need is not\n something to work around: answer `INCONCLUSIVE` or `INSUFFICIENT_EVIDENCE`\n and name the sandbox as the missing evidence.\n\n## Method\n\n1. **Reproduce.** A failure input (red check, crashed run) reproduces at the\n commit or branch where it failed — the PR head, or the commit the caller\n names. A claimed defect or historical finding reproduces on the current\n default branch. No reproduction, no diagnosis.\n2. **Isolate.** Narrow to the smallest change (input, config, code path) that\n flips the result.\n3. **Hypothesize.** State the mechanism you think is responsible, in one or\n two sentences.\n4. **Confirm with evidence.** Show the command and its output, or the\n file:line the mechanism lives at. A hypothesis nothing confirms is\n `INCONCLUSIVE`, not `ROOT_CAUSE`.\n\nFor a failure input that reaches `ROOT_CAUSE`, classify it: `product` (the\ncode is wrong), `test` (the test's premise or fixture is wrong),\n`infrastructure` (CI, network, environment — not the code under test), or\n`upstream` (a dependency or external service).\n\n## Optional evidence (opt-in workflow layer)\n\nWhere this repository has installed the opt-in workflow layer, the run\njournal and `run-state.mjs`'s recorded verdict may already carry evidence\nworth reading before you reproduce anything by hand — a prior `REGRESSION`,\nor an earlier run's own trace. Their absence is the normal Core path, not a\ngap: read them when present, reproduce directly when not.\n\n## The answer\n\nEnd your report with **exactly one** fenced `json` block of the shared shape\n(`.claude/scripts/lib/verdict.mjs`), and nothing after it.\n\n- **A failure input** (red check, crash) answers `ROOT_CAUSE` or\n `INCONCLUSIVE`.\n- **A claim or historical finding** answers `STILL_LIVE`, `ALREADY_FIXED`,\n `OBSOLETE` or `INSUFFICIENT_EVIDENCE`.\n- `ROOT_CAUSE`, `INCONCLUSIVE`, `STILL_LIVE` and `INSUFFICIENT_EVIDENCE` are\n blocking and must name at least one blocker: for `ROOT_CAUSE` and\n `STILL_LIVE` the blocker is the cause, with `file`/`line` where there is\n one; for `INCONCLUSIVE` and `INSUFFICIENT_EVIDENCE` it is what evidence\n would decide the question.\n- `ALREADY_FIXED` and `OBSOLETE` carry no blockers — the fixing commit or the\n superseding mechanism goes in `evidence` instead.\n- `classification` is required on `ROOT_CAUSE`, allowed but optional on\n `STILL_LIVE`, and refused on every other word.\n- `node .claude/scripts/verdict.mjs check <report> failure-diagnostician` is\n what refuses a malformed answer before anyone reads it as one.\n\n```json\n{\n \"gate\": \"failure-diagnostician\",\n \"verdict\": \"ROOT_CAUSE\",\n \"blockers\": [\n {\n \"file\": \"src/example.ts\",\n \"line\": 42,\n \"rule\": \"reproduced failure\",\n \"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\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"reproduced with the failing test on the PR head\", \"the stack trace from that run names the file:line above\"],\n \"classification\": \"product\"\n}\n```\n\n```json\n{\n \"gate\": \"failure-diagnostician\",\n \"verdict\": \"ALREADY_FIXED\",\n \"blockers\": [],\n \"advisories\": [],\n \"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\"]\n}\n```\n\n## Not a merge gate\n\nYou are never a routed reviewer: no `decision-router` lane names you and no\n`pr-ship` coverage check (opt-in workflow layer) expects your answer. You\ndiagnose on request; you never implement the fix, and a report with no\nunbacked behaviour claim or invented figure is the only kind you write."
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
model = "gpt-
|
|
3
|
+
model = "gpt-6-sol"
|
|
4
4
|
model_reasoning_effort = "high"
|
|
5
5
|
sandbox_mode = "read-only"
|
|
6
6
|
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 whoever runs that check holds on it —\n `pr-ship` where the opt-in workflow layer is installed, the session itself\n running `node .claude/scripts/verdict.mjs coverage` by hand otherwise: no\n hook runs it either way, so skipping the gate skips this with it."
|
|
@@ -171,7 +171,9 @@ All four are one-liners, and all four are inert until you do them.
|
|
|
171
171
|
— a JSON array like `["npm test", "npm run lint"]` — the stop gate is a
|
|
172
172
|
no-op, and the Definition of Done is back to being a wish.
|
|
173
173
|
2. **The elevated-path list below is a seed, not a survey.** It names only what
|
|
174
|
-
every repo has
|
|
174
|
+
every repo has, plus `.rig/revalidation.json`, which only the opt-in
|
|
175
|
+
workflow layer installs (`init --layer workflow`). Everything else is yours
|
|
176
|
+
to add.
|
|
175
177
|
3. **One runtime path needs a `.gitignore` line always; four more only when
|
|
176
178
|
the opt-in workflow layer is installed** (`init --layer workflow`), and
|
|
177
179
|
`init`/`init --layer workflow` cannot add any of them — they install into
|
|
@@ -236,6 +238,8 @@ rule does not change with or without the script.
|
|
|
236
238
|
AGENTS.md
|
|
237
239
|
CLAUDE.md
|
|
238
240
|
.github/workflows/
|
|
241
|
+
# workflow layer only (init --layer workflow)
|
|
242
|
+
.rig/revalidation.json
|
|
239
243
|
```
|
|
240
244
|
|
|
241
245
|
They are there because they are what *disarms* the rest: a merge that rewrites
|
|
@@ -32,7 +32,7 @@ refuses duplicate source-agent names across layers, since one profile name
|
|
|
32
32
|
cannot route two definitions.
|
|
33
33
|
|
|
34
34
|
Frequent bounded work (`test-writer`, `implementation-agent`, `prose-reviewer`) uses `gpt-5.6-terra`;
|
|
35
|
-
correctness, security, and infrastructure gates use `gpt-
|
|
35
|
+
correctness, security, and infrastructure gates use `gpt-6-sol`. Every named
|
|
36
36
|
gate uses `high` reasoning effort. Unnamed subagents inherit repository defaults
|
|
37
37
|
of `gpt-5.6-terra` and `medium` from `.codex/config.toml`. `xhigh` is not a
|
|
38
38
|
continuous-loop default; using it for a named role requires an intentional
|
|
@@ -10,7 +10,7 @@ Each named subagent this rig ships pins the model and the effort it reads with:
|
|
|
10
10
|
|
|
11
11
|
| Role | Claude Code | Codex |
|
|
12
12
|
| --- | --- | --- |
|
|
13
|
-
| `code-reviewer`, `security-scanner`, `failure-diagnostician`, a stack's infrastructure reviewer | `claude-opus-5`, `high` | `gpt-
|
|
13
|
+
| `code-reviewer`, `security-scanner`, `failure-diagnostician`, a stack's infrastructure reviewer | `claude-opus-5-5`, `high` | `gpt-6-sol`, `high` |
|
|
14
14
|
| `test-writer`, `implementation-agent`, `prose-reviewer` | `claude-sonnet-5`, `high` | `gpt-5.6-terra`, `high` |
|
|
15
15
|
| a subagent with no definition | `claude-sonnet-5`; effort follows the session | `gpt-5.6-terra`, `medium` |
|
|
16
16
|
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"0.9.0",
|
|
16
16
|
"0.9.1",
|
|
17
17
|
"0.10.0",
|
|
18
|
-
"0.10.1"
|
|
18
|
+
"0.10.1",
|
|
19
|
+
"1.0.0"
|
|
19
20
|
],
|
|
20
21
|
"files": {
|
|
21
22
|
".agents/skills/check-premises/SKILL.md": {
|
|
@@ -27,6 +28,12 @@
|
|
|
27
28
|
"ee3ad33f4269cc197d3b0d953802158cee6bc8a9b30c07892448ba6fb1d385e7"
|
|
28
29
|
]
|
|
29
30
|
},
|
|
31
|
+
".agents/skills/diagnose/SKILL.md": {
|
|
32
|
+
"since": "1.0.0",
|
|
33
|
+
"hashes": [
|
|
34
|
+
"c1499c7ce9c9e5fc61c7a323246d9c3005acdeec82d3ec74c074cc3e89b5ad2a"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
30
37
|
".agents/skills/loop/SKILL.md": {
|
|
31
38
|
"since": "0.5.0",
|
|
32
39
|
"hashes": [
|
|
@@ -37,7 +44,8 @@
|
|
|
37
44
|
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d",
|
|
38
45
|
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546",
|
|
39
46
|
"b47bdf0d7a2f6c5b2034b8f6d683ad2b3c83bd65d15baf7de9b58cc4689d0ab1",
|
|
40
|
-
"e48766714f4bceb5113c7036c380dfdbe07229feaf9b57fe4168660c5c90feb2"
|
|
47
|
+
"e48766714f4bceb5113c7036c380dfdbe07229feaf9b57fe4168660c5c90feb2",
|
|
48
|
+
"b588ea0edfc953405bcad24ecbecd174da85dbc81b930aeeeb42b06a0307b261"
|
|
41
49
|
]
|
|
42
50
|
},
|
|
43
51
|
".agents/skills/new-invariant/SKILL.md": {
|
|
@@ -59,6 +67,12 @@
|
|
|
59
67
|
"7bef9179c5b56d280d462cf21fb2c38dc4cb759b671b114b3995aac93720caf8"
|
|
60
68
|
]
|
|
61
69
|
},
|
|
70
|
+
".agents/skills/plan-slices/SKILL.md": {
|
|
71
|
+
"since": "1.0.0",
|
|
72
|
+
"hashes": [
|
|
73
|
+
"42d944f08cc71895c786cd0afe7a643a6af8e9b30b6269fe2b91a93dc4f6a77f"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
62
76
|
".agents/skills/post-deploy-verify/SKILL.md": {
|
|
63
77
|
"since": "0.5.0",
|
|
64
78
|
"hashes": [
|
|
@@ -73,7 +87,14 @@
|
|
|
73
87
|
"9616e4767942b2021e34ab7bb76eec9a5c280cc608a0756abb02743741d5fc8a",
|
|
74
88
|
"a6340b95a03000b89fb27f5357cdd9c7ec12b714054b5334b61a30da600405e8",
|
|
75
89
|
"dc1d9c02879e383448f2336ae6433f960f86e8f8efb4ab8ccf9a17a5f7f524c3",
|
|
76
|
-
"75a5bc3e1aa3fdef96b59edc90a3cb4b996708710fe16f65038e0e21853df4e8"
|
|
90
|
+
"75a5bc3e1aa3fdef96b59edc90a3cb4b996708710fe16f65038e0e21853df4e8",
|
|
91
|
+
"2c2545d327be19d685dd0b758fe72806369a383b90df56164224bbb3b6f61cd3"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
".agents/skills/release-propose/SKILL.md": {
|
|
95
|
+
"since": "1.0.0",
|
|
96
|
+
"hashes": [
|
|
97
|
+
"11d641f4d2004da55aff82454fb541284ff4476790eca4d1c74dfd7f9e7c7a2f"
|
|
77
98
|
]
|
|
78
99
|
},
|
|
79
100
|
".agents/skills/ro-debug/SKILL.md": {
|
|
@@ -82,6 +103,12 @@
|
|
|
82
103
|
"4a391a0a42b613683d897e2b4fa46db4eb0e778cbfc1e4d4340bb855073de528"
|
|
83
104
|
]
|
|
84
105
|
},
|
|
106
|
+
".agents/skills/skill-authoring/SKILL.md": {
|
|
107
|
+
"since": "1.0.0",
|
|
108
|
+
"hashes": [
|
|
109
|
+
"f85f58d4f2a4037af73a497db258653a4634acd7f851e795d26db3e0b3bc242d"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
85
112
|
".agents/skills/worktree-task/SKILL.md": {
|
|
86
113
|
"since": "0.5.0",
|
|
87
114
|
"hashes": [
|
|
@@ -107,7 +134,14 @@
|
|
|
107
134
|
"f125cfbce848cdaa91fd81e961808b4dbde452287959ec2cf10b905ee3ede789",
|
|
108
135
|
"8ede9297480c889ec6d6caf8d076e7d69111fb472933dcf5a48b9a8425fe50c2",
|
|
109
136
|
"f12cb04871578eca3c9c36a1c2d60f5ae15a0e9094ecfc7c704c341756de4e3e",
|
|
110
|
-
"f260b1f2d60c57b96b2d735dbc7eb29890638ca9dedec467ff4784f6d8d8fab5"
|
|
137
|
+
"f260b1f2d60c57b96b2d735dbc7eb29890638ca9dedec467ff4784f6d8d8fab5",
|
|
138
|
+
"492fcd965f14194495721cb88eae5791cfd1f213c96fbc2307da8ddfb676c490"
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
".claude/agents/failure-diagnostician.md": {
|
|
142
|
+
"since": "1.0.0",
|
|
143
|
+
"hashes": [
|
|
144
|
+
"c3daf330246eb47beee8dae804dc0f84a7bfc626b749d0759f3491fe48627ec5"
|
|
111
145
|
]
|
|
112
146
|
},
|
|
113
147
|
".claude/agents/implementation-agent.md": {
|
|
@@ -169,7 +203,8 @@
|
|
|
169
203
|
"33edb11904e5c4f88626b41c7e649b85d20957a11ff7e1062538917647c3e7f0",
|
|
170
204
|
"19202a28355dac1d7a95d29411668cf970006b3044d89a772768ce21793931aa",
|
|
171
205
|
"d2af03ed141fadd0b1e69b230b9277e8f03169757a8e2a679767653ab7828d9a",
|
|
172
|
-
"4a367adc0b4d0e940e907624c56bf94456df7fd8438028a1afc7e718ad7bb318"
|
|
206
|
+
"4a367adc0b4d0e940e907624c56bf94456df7fd8438028a1afc7e718ad7bb318",
|
|
207
|
+
"bd469db21c352d6a97bdd372a47831516aa00c17d1a16d943084400c9b824524"
|
|
173
208
|
]
|
|
174
209
|
},
|
|
175
210
|
".claude/hooks/guard-bash.mjs": {
|
|
@@ -196,7 +231,8 @@
|
|
|
196
231
|
"hashes": [
|
|
197
232
|
"4524d715f2e41f4cfdd3d0f9c858817f913aaca5192bda97dff876b4d8c51f19",
|
|
198
233
|
"f9e3e4b7c22c4a64f521402419707ff382b05c8deb6310f62e5545db87f7cb4f",
|
|
199
|
-
"493ae58e54b055bd13ba992ee0dbfa2cb9ce9a3637ec345ab8d7cfff95831c60"
|
|
234
|
+
"493ae58e54b055bd13ba992ee0dbfa2cb9ce9a3637ec345ab8d7cfff95831c60",
|
|
235
|
+
"e3a03aa6d3077b7c397f84bfecfb6e8df0f18a5849045c51150300b4ce9f48b2"
|
|
200
236
|
]
|
|
201
237
|
},
|
|
202
238
|
".claude/hooks/guard-secret-file.mjs": {
|
|
@@ -237,7 +273,8 @@
|
|
|
237
273
|
"4f836bc3ff75a868ffe035b179ac9e7dc1ad7715d520677ff9f6aa5bfdb200eb",
|
|
238
274
|
"0c191d326a7bb0bbfc110e2210f44b4aa39276e8db3779aaa4545cffd4e11863",
|
|
239
275
|
"572c671f8f2cad190fe2f144071954ac4d3b1357cab80a19bad584dd3b3768c8",
|
|
240
|
-
"6695698d2b38c79a12292601048dd0082a0cd18bf9e69c047d47679c2e6ecf67"
|
|
276
|
+
"6695698d2b38c79a12292601048dd0082a0cd18bf9e69c047d47679c2e6ecf67",
|
|
277
|
+
"79f400a6eccd1aa99fed2e5966f857c030fe8ba7a69df3f6819605e48277b202"
|
|
241
278
|
]
|
|
242
279
|
},
|
|
243
280
|
".claude/hooks/lib/hook-input.mjs": {
|
|
@@ -294,7 +331,8 @@
|
|
|
294
331
|
"a2e10499a0bcd2cdc32f3986d27b59077c86e524d40323faabb2e5d78afc002c",
|
|
295
332
|
"c94c33d7ccbd46f191948325dfc7f68ce13b0d3a9470215de1de3f06b1499111",
|
|
296
333
|
"ce90c1e697fdd444ca3fe2203d7991645f2c284c413ae187e4d6d6c618af52c9",
|
|
297
|
-
"aa0afa471fe7e3d06a42c61e9e2fcb14b2791ecf805cc3638cb13d2ee8aa9827"
|
|
334
|
+
"aa0afa471fe7e3d06a42c61e9e2fcb14b2791ecf805cc3638cb13d2ee8aa9827",
|
|
335
|
+
"355083bae7c6e37008af7b739d7b79652458059aa1cec2eae13c7320707b6199"
|
|
298
336
|
]
|
|
299
337
|
},
|
|
300
338
|
".claude/rules/node-ts.md": {
|
|
@@ -392,7 +430,8 @@
|
|
|
392
430
|
"since": "0.5.0",
|
|
393
431
|
"hashes": [
|
|
394
432
|
"4d33a07a53a502277fcc6e5d7f9a89738b75e1d672dd1e6985c058bb4c35c0de",
|
|
395
|
-
"51ef5185da289cfd883422e7ac51500e527a19c7780721af9cecf4f845a368f3"
|
|
433
|
+
"51ef5185da289cfd883422e7ac51500e527a19c7780721af9cecf4f845a368f3",
|
|
434
|
+
"ed5e63260071968333b867312aff7e5ac80385d9b19b32a4b1acc9a7b801dadf"
|
|
396
435
|
]
|
|
397
436
|
},
|
|
398
437
|
".claude/scripts/preflight.mjs": {
|
|
@@ -427,7 +466,8 @@
|
|
|
427
466
|
"1804c2351a4f3042dc6a3fb4203f96e98ae12bec32569b99bda77d002d6ba5f2",
|
|
428
467
|
"b81ae3181e02b9c1c3f0e954688490ab3db3961381c2dc397c47c651c9d07f88",
|
|
429
468
|
"c9902da15d06de3e952352115db0288433520eae2caac1f70ee6e06753d1f444",
|
|
430
|
-
"e31f9899d6edad127db7a2f43dafdfff2671c5c780209cccd3107e56b09c6042"
|
|
469
|
+
"e31f9899d6edad127db7a2f43dafdfff2671c5c780209cccd3107e56b09c6042",
|
|
470
|
+
"915f0e73fe0842196227c2bd56ba813ed0b7e358670eeabab456cc27fbc4bc9d"
|
|
431
471
|
]
|
|
432
472
|
},
|
|
433
473
|
".claude/scripts/queue/gate-rounds.mjs": {
|
|
@@ -444,7 +484,8 @@
|
|
|
444
484
|
"86489ac7fcd06e43e31fb3d0ac9ab23174ef9c95196b08f127f1ae74da620c0c",
|
|
445
485
|
"f84148fee47109a388a248c51d3eb40919ce341785b561dd4135b69c6fedabf3",
|
|
446
486
|
"5ab8f741f17efb935b11caf245edf2b562cb887020c2a43d45464158342c933a",
|
|
447
|
-
"b5349837d16de0966139f5b65e2800486592f1b24d7e87087c2b7212cead479d"
|
|
487
|
+
"b5349837d16de0966139f5b65e2800486592f1b24d7e87087c2b7212cead479d",
|
|
488
|
+
"b14d15e30dd74585a3470913f6a6bb0250010a8dd5561dfa2033c211ac8288cb"
|
|
448
489
|
]
|
|
449
490
|
},
|
|
450
491
|
".claude/scripts/queue/index.mjs": {
|
|
@@ -481,6 +522,12 @@
|
|
|
481
522
|
"816aec4cb84492370972f71813a9fec7c0309e5f501951a93620a37cb318ac39"
|
|
482
523
|
]
|
|
483
524
|
},
|
|
525
|
+
".claude/scripts/queue/propose.mjs": {
|
|
526
|
+
"since": "1.0.0",
|
|
527
|
+
"hashes": [
|
|
528
|
+
"13115b1cd4d40f8b2a618a5dbabd5c973118a71789db17d704ac8d0c478cb024"
|
|
529
|
+
]
|
|
530
|
+
},
|
|
484
531
|
".claude/scripts/queue/state.mjs": {
|
|
485
532
|
"since": "0.5.0",
|
|
486
533
|
"hashes": [
|
|
@@ -496,6 +543,12 @@
|
|
|
496
543
|
"354c713747e16ce7df51e8230c73f38bf7164ad7abd18ec1caca3f8d8546c224"
|
|
497
544
|
]
|
|
498
545
|
},
|
|
546
|
+
".claude/scripts/release-evidence.mjs": {
|
|
547
|
+
"since": "1.0.0",
|
|
548
|
+
"hashes": [
|
|
549
|
+
"729ed9e049d755c94b2de9e470d96079673ad36f65f79198600c202d7f0b1865"
|
|
550
|
+
]
|
|
551
|
+
},
|
|
499
552
|
".claude/scripts/revalidate.mjs": {
|
|
500
553
|
"since": "0.6.0",
|
|
501
554
|
"hashes": [
|
|
@@ -511,7 +564,8 @@
|
|
|
511
564
|
"hashes": [
|
|
512
565
|
"b99a3537158c2851698bfac027f7061811d7d428c3eea57f3ce0f58ea75dc249",
|
|
513
566
|
"78adac5c8f696f2c9cc86f404abf1d55604ccb10f11dd612501c14d4cf5935b1",
|
|
514
|
-
"fed627d2c8f0f5e6a4524b7015ac7a1cb5051a8c316ff4544af18be9c61eb163"
|
|
567
|
+
"fed627d2c8f0f5e6a4524b7015ac7a1cb5051a8c316ff4544af18be9c61eb163",
|
|
568
|
+
"9baffc9133f1cec35332cb105c70dab9c8ea8250aab2bd8daa71511e4c53bb54"
|
|
515
569
|
]
|
|
516
570
|
},
|
|
517
571
|
".claude/scripts/run-journal.mjs": {
|
|
@@ -541,7 +595,8 @@
|
|
|
541
595
|
"060b001927d8b471f1c1a8c8851fbb4266d3b2edd674cccbd6c16c44b57b91dd",
|
|
542
596
|
"7721ee19fd1bcf6902be3d359def40742d79b88f5d83e7e3ca22ba45ca3211aa",
|
|
543
597
|
"250292c9959916240bfc0fdf5987a864f6d54799de3d932dc6b85b776e62eaa9",
|
|
544
|
-
"ad58b25f9ce30a1549b5047daa1e5b43c30352306aeceebacd638db86087e01f"
|
|
598
|
+
"ad58b25f9ce30a1549b5047daa1e5b43c30352306aeceebacd638db86087e01f",
|
|
599
|
+
"f49142e6a64d5e8a8b4c4d239247f09255d65dd9d7de2cff0267b16421509e12"
|
|
545
600
|
]
|
|
546
601
|
},
|
|
547
602
|
".claude/scripts/verdict.mjs": {
|
|
@@ -573,6 +628,12 @@
|
|
|
573
628
|
"ee3ad33f4269cc197d3b0d953802158cee6bc8a9b30c07892448ba6fb1d385e7"
|
|
574
629
|
]
|
|
575
630
|
},
|
|
631
|
+
".claude/skills/diagnose/SKILL.md": {
|
|
632
|
+
"since": "1.0.0",
|
|
633
|
+
"hashes": [
|
|
634
|
+
"c1499c7ce9c9e5fc61c7a323246d9c3005acdeec82d3ec74c074cc3e89b5ad2a"
|
|
635
|
+
]
|
|
636
|
+
},
|
|
576
637
|
".claude/skills/loop/SKILL.md": {
|
|
577
638
|
"since": "0.2.0",
|
|
578
639
|
"hashes": [
|
|
@@ -587,7 +648,8 @@
|
|
|
587
648
|
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d",
|
|
588
649
|
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546",
|
|
589
650
|
"b47bdf0d7a2f6c5b2034b8f6d683ad2b3c83bd65d15baf7de9b58cc4689d0ab1",
|
|
590
|
-
"e48766714f4bceb5113c7036c380dfdbe07229feaf9b57fe4168660c5c90feb2"
|
|
651
|
+
"e48766714f4bceb5113c7036c380dfdbe07229feaf9b57fe4168660c5c90feb2",
|
|
652
|
+
"b588ea0edfc953405bcad24ecbecd174da85dbc81b930aeeeb42b06a0307b261"
|
|
591
653
|
]
|
|
592
654
|
},
|
|
593
655
|
".claude/skills/new-invariant/SKILL.md": {
|
|
@@ -609,6 +671,12 @@
|
|
|
609
671
|
"7bef9179c5b56d280d462cf21fb2c38dc4cb759b671b114b3995aac93720caf8"
|
|
610
672
|
]
|
|
611
673
|
},
|
|
674
|
+
".claude/skills/plan-slices/SKILL.md": {
|
|
675
|
+
"since": "1.0.0",
|
|
676
|
+
"hashes": [
|
|
677
|
+
"42d944f08cc71895c786cd0afe7a643a6af8e9b30b6269fe2b91a93dc4f6a77f"
|
|
678
|
+
]
|
|
679
|
+
},
|
|
612
680
|
".claude/skills/post-deploy-verify/SKILL.md": {
|
|
613
681
|
"since": "0.2.0",
|
|
614
682
|
"hashes": [
|
|
@@ -626,7 +694,14 @@
|
|
|
626
694
|
"9616e4767942b2021e34ab7bb76eec9a5c280cc608a0756abb02743741d5fc8a",
|
|
627
695
|
"a6340b95a03000b89fb27f5357cdd9c7ec12b714054b5334b61a30da600405e8",
|
|
628
696
|
"dc1d9c02879e383448f2336ae6433f960f86e8f8efb4ab8ccf9a17a5f7f524c3",
|
|
629
|
-
"75a5bc3e1aa3fdef96b59edc90a3cb4b996708710fe16f65038e0e21853df4e8"
|
|
697
|
+
"75a5bc3e1aa3fdef96b59edc90a3cb4b996708710fe16f65038e0e21853df4e8",
|
|
698
|
+
"2c2545d327be19d685dd0b758fe72806369a383b90df56164224bbb3b6f61cd3"
|
|
699
|
+
]
|
|
700
|
+
},
|
|
701
|
+
".claude/skills/release-propose/SKILL.md": {
|
|
702
|
+
"since": "1.0.0",
|
|
703
|
+
"hashes": [
|
|
704
|
+
"11d641f4d2004da55aff82454fb541284ff4476790eca4d1c74dfd7f9e7c7a2f"
|
|
630
705
|
]
|
|
631
706
|
},
|
|
632
707
|
".claude/skills/ro-debug/SKILL.md": {
|
|
@@ -635,6 +710,12 @@
|
|
|
635
710
|
"4a391a0a42b613683d897e2b4fa46db4eb0e778cbfc1e4d4340bb855073de528"
|
|
636
711
|
]
|
|
637
712
|
},
|
|
713
|
+
".claude/skills/skill-authoring/SKILL.md": {
|
|
714
|
+
"since": "1.0.0",
|
|
715
|
+
"hashes": [
|
|
716
|
+
"f85f58d4f2a4037af73a497db258653a4634acd7f851e795d26db3e0b3bc242d"
|
|
717
|
+
]
|
|
718
|
+
},
|
|
638
719
|
".claude/skills/worktree-task/SKILL.md": {
|
|
639
720
|
"since": "0.3.0",
|
|
640
721
|
"hashes": [
|
|
@@ -656,7 +737,14 @@
|
|
|
656
737
|
"e0e3475404c95402f33d77ebb14001e289d26bb429205a0a73bdcef99b4a62c7",
|
|
657
738
|
"9ec92775791907f068d85ba58489236bd2bee69a8d769554ac530e8eadd5e374",
|
|
658
739
|
"180c182cc909cc0b6c06cfa286f528f22b77189eee49cac067fdaaf2920f8bae",
|
|
659
|
-
"29b4de4273097fc9e6c1c3e41c650fa20d95fa34b406365aabab62a6ae89b686"
|
|
740
|
+
"29b4de4273097fc9e6c1c3e41c650fa20d95fa34b406365aabab62a6ae89b686",
|
|
741
|
+
"32e6cdf32a21da712889c50e6d74bceda8cdac79fce941426c8bc8769651023e"
|
|
742
|
+
]
|
|
743
|
+
},
|
|
744
|
+
".codex/agents/failure-diagnostician.toml": {
|
|
745
|
+
"since": "1.0.0",
|
|
746
|
+
"hashes": [
|
|
747
|
+
"4d1209540407249c96cd6e3a5b874cf1c6c29bf382d3b88fdeb6f3eaafc5e2b6"
|
|
660
748
|
]
|
|
661
749
|
},
|
|
662
750
|
".codex/agents/implementation-agent.toml": {
|
|
@@ -719,7 +807,8 @@
|
|
|
719
807
|
"hashes": [
|
|
720
808
|
"616ab573d3cf4b603bb4bfbc5e7631d17ebaa696af16474845fdaf35f41d0a97",
|
|
721
809
|
"406c9a0a0e58e78788a10186b9e10b48f4ed215b6d68504a4fbb71e5ee506f85",
|
|
722
|
-
"93f9b4ed7a49df9c83b74fd728fb3e0f83ff7c7e84f71f51d26c89b84cd091bd"
|
|
810
|
+
"93f9b4ed7a49df9c83b74fd728fb3e0f83ff7c7e84f71f51d26c89b84cd091bd",
|
|
811
|
+
"615e15fed813dc1fae10c3ff498aab28096382f9e9bd5a89c72d1a6e5adb1219"
|
|
723
812
|
]
|
|
724
813
|
},
|
|
725
814
|
"CLAUDE.md": {
|
|
@@ -818,7 +907,8 @@
|
|
|
818
907
|
"since": "0.9.0",
|
|
819
908
|
"hashes": [
|
|
820
909
|
"97bb882dd066a65a740780a836199ecef8887392c1ece02a92670ecf153988d3",
|
|
821
|
-
"f4a458a8938f8517b997cd6f0a0036d4eba640cd1ff213683804e7c0e2d02d7a"
|
|
910
|
+
"f4a458a8938f8517b997cd6f0a0036d4eba640cd1ff213683804e7c0e2d02d7a",
|
|
911
|
+
"f7f13dc4c19c31bc950c58aa6276e67f42d41c7331510d5a5e6f60e4bfd0c38c"
|
|
822
912
|
]
|
|
823
913
|
},
|
|
824
914
|
"docs/decisions/two-empty-endings.md": {
|
|
@@ -831,7 +921,8 @@
|
|
|
831
921
|
"docs/decisions/workflow-layer-split.md": {
|
|
832
922
|
"since": "0.10.0",
|
|
833
923
|
"hashes": [
|
|
834
|
-
"1006275e907e7083d5cfa331fa4844ac9ed7f738519f788af130e6c82e5d9465"
|
|
924
|
+
"1006275e907e7083d5cfa331fa4844ac9ed7f738519f788af130e6c82e5d9465",
|
|
925
|
+
"be410febe87e660d0be6ad022c205ad151e16e8702eb19c44e6992a477749f92"
|
|
835
926
|
]
|
|
836
927
|
},
|
|
837
928
|
"journal/README.md": {
|
|
@@ -15,5 +15,6 @@
|
|
|
15
15
|
"0.9.0": "c27f391e7395accaf09354f255a07e1b9e4710c1",
|
|
16
16
|
"0.9.1": "872f7f67f11761c17aad3cbbe26540862581795b",
|
|
17
17
|
"0.10.0": "279fbf928b811b8ebc7ba2d1c4700ee943b7dab1",
|
|
18
|
-
"0.10.1": "738b494806b435f0718f290dc3befb40829e5ebf"
|
|
18
|
+
"0.10.1": "738b494806b435f0718f290dc3befb40829e5ebf",
|
|
19
|
+
"1.0.0": "8876147ce93d54ba8321d2bcdafab7f5dd1f8994"
|
|
19
20
|
}
|