create-agent-rig 0.8.0 → 0.9.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 +105 -1
- package/README.md +92 -3
- package/package.json +4 -3
- package/packages/cli/dist/commands/memory.js +123 -0
- package/packages/cli/dist/commands/setup.js +45 -0
- package/packages/cli/dist/index.js +107 -3
- package/packages/cli/dist/lib/subsystems.js +269 -0
- package/packages/cli/dist/lib/version.js +15 -0
- package/packages/cli/dist/policy/benchmark/corpus.js +165 -0
- package/packages/cli/dist/policy/core/coverage.js +253 -0
- package/packages/cli/dist/policy/core/decision-record.js +130 -44
- package/packages/cli/dist/policy/core/declaration.js +58 -17
- package/packages/cli/dist/policy/core/evidence-matrix.js +94 -0
- package/packages/cli/dist/policy/core/probe.js +442 -0
- package/packages/cli/dist/policy/core/validation.js +194 -1
- package/packages/cli/dist/policy/core/vocabulary.js +70 -3
- package/packages/cli/dist/policy/harness/claude.js +9 -1
- package/packages/cli/dist/policy/harness/codex.js +48 -1
- package/packages/cli/dist/policy/harness/shared-hooks.js +18 -0
- package/packages/cli/dist/policy/index.js +9 -2
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +2 -0
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +2 -0
- package/templates/agent-os/subagent-routing.json +32 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +41 -4
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +2 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +2 -0
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +2 -0
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -0
- package/templates/agent-os/universal/.claude/hooks/guard-subagent-model.mjs +234 -0
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +75 -32
- package/templates/agent-os/universal/.claude/hooks/warn-subagent-routing.mjs +120 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +5 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +27 -3
- package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +70 -2
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +12 -4
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +64 -1
- package/templates/agent-os/universal/.claude/settings.json +16 -0
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +41 -4
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +2 -0
- package/templates/agent-os/universal/.codex/config.toml +3 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +31 -5
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +142 -0
- package/templates/agent-os/universal/layers.json +4 -0
- package/templates/hash-history.json +8 -4
- package/templates/release-ledger.json +2 -1
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +3 -4
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +40 -0
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +51 -0
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +9 -8
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// defaults to `plan-md`, which is the only adapter that works in a freshly
|
|
13
13
|
// generated project. An unknown adapter is a hard error, never a fallback: a loop
|
|
14
14
|
// that silently reads the wrong queue is worse than one that refuses to start.
|
|
15
|
-
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { basename, dirname, join } from 'node:path';
|
|
18
18
|
import {
|
|
@@ -57,11 +57,19 @@ export const COMMANDS = ['next', 'list', 'hygiene', 'gate-round', 'board'];
|
|
|
57
57
|
* trailing comma in `queue.json` made the loop read a different queue than the one
|
|
58
58
|
* configured, which is the exact failure this file's header refuses for adapters.
|
|
59
59
|
*/
|
|
60
|
-
export const loadConfig = (configPath) => {
|
|
60
|
+
export const loadConfig = (configPath, { strictRead = false } = {}) => {
|
|
61
61
|
let raw;
|
|
62
62
|
try {
|
|
63
63
|
raw = readFileSync(configPath, 'utf8');
|
|
64
|
-
} catch {
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (
|
|
66
|
+
strictRead &&
|
|
67
|
+
(error?.code !== 'ENOENT' || lstatSync(configPath, { throwIfNoEntry: false }) !== undefined)
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(`${configPath} could not be read (${error?.code ?? 'unknown error'})`, {
|
|
70
|
+
cause: error,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
65
73
|
return {};
|
|
66
74
|
}
|
|
67
75
|
let parsed;
|
|
@@ -70,7 +78,7 @@ export const loadConfig = (configPath) => {
|
|
|
70
78
|
} catch (error) {
|
|
71
79
|
throw new Error(
|
|
72
80
|
`${configPath} exists but is not valid JSON, so the configured queue cannot be ` +
|
|
73
|
-
|
|
81
|
+
'read. Fix the file — ' +
|
|
74
82
|
'silently reading a different queue is worse than refusing to start.',
|
|
75
83
|
{ cause: error },
|
|
76
84
|
);
|
|
@@ -445,6 +445,69 @@ if (invokedDirectly()) {
|
|
|
445
445
|
process.stdout.write(removed.length === 0 ? 'no unattended flag was set\n' : `${removed.join('\n')}\n`);
|
|
446
446
|
process.exit(0);
|
|
447
447
|
}
|
|
448
|
-
|
|
448
|
+
// 🔴 **The read-back `on` did not have, and the reason it is a subcommand
|
|
449
|
+
// rather than a line in the skill (RP-103).**
|
|
450
|
+
//
|
|
451
|
+
// `on` refuses a widening allow entry — correctly — by throwing before it
|
|
452
|
+
// writes anything, so the refusal leaves NO flag on disk. Pinned in the
|
|
453
|
+
// generator's `test/template/unattended-flag.test.ts` (absent in a generated
|
|
454
|
+
// rig) › "does not change `on`: a widening --allow still exits 1 and still
|
|
455
|
+
// writes no flag" — the claim is checkable, so it carries a pointer rather
|
|
456
|
+
// than standing on its own. And `guard-rulebook`
|
|
457
|
+
// reads "no flag" as "attended session" and does nothing. So the run that was
|
|
458
|
+
// meant to be the most constrained became the LEAST: every rulebook path
|
|
459
|
+
// editable, including the hook wiring that enforces the rule. Loud at arming
|
|
460
|
+
// (exit 1), and completely silent for the rest of the run.
|
|
461
|
+
//
|
|
462
|
+
// The asymmetry is what made it a defect rather than a rough edge: the `off`
|
|
463
|
+
// branch above ALREADY reads back, and refuses while naming the record it
|
|
464
|
+
// found. One direction of the same operation verified itself and the other
|
|
465
|
+
// did not.
|
|
466
|
+
//
|
|
467
|
+
// ⚠ **Stating the limit, because this whole file is about a mechanism being
|
|
468
|
+
// trusted further than it goes.** `verify` is mechanical where it runs; that
|
|
469
|
+
// it runs is still the `loop` skill's prose. This closes "the refusal was
|
|
470
|
+
// silent" — the run is told, in a command whose exit status is its whole
|
|
471
|
+
// output — and does NOT close "a run that ignores exit statuses ignores this
|
|
472
|
+
// one too". The hook-enforced version would need `guard-rulebook` to tell
|
|
473
|
+
// "attended" from "unattended but unarmed", and it cannot: absence of a flag
|
|
474
|
+
// is all it sees.
|
|
475
|
+
if (word === 'verify') {
|
|
476
|
+
const item = valueOf('--item');
|
|
477
|
+
if (!item || item.startsWith('--')) {
|
|
478
|
+
process.stderr.write(
|
|
479
|
+
'unattended-flag verify: --item <id> is required — verifying "some flag is armed" would pass on a stale one from a previous run\n',
|
|
480
|
+
);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
const state = readUnattended(cliEnv);
|
|
484
|
+
// ⚠ `unreadable` carries `on: true` — it means "a flag is THERE and cannot be
|
|
485
|
+
// trusted", which is what `off` needs in order to refuse to clear it blindly.
|
|
486
|
+
// For a read-back it is a FAILURE, not an arming: an unreadable record
|
|
487
|
+
// authorizes nothing, and its `item` is absent, so testing only `!state.on`
|
|
488
|
+
// would fall through to the item-mismatch branch and print the wrong reason
|
|
489
|
+
// for the right refusal.
|
|
490
|
+
if (!state.on || state.unreadable) {
|
|
491
|
+
const why = state.why ? ` (${state.why})` : '';
|
|
492
|
+
process.stderr.write(
|
|
493
|
+
`unattended-flag verify: NO usable unattended flag is armed for ${item}${why}. ` +
|
|
494
|
+
'guard-rulebook reads an absent flag as an attended session and refuses nothing, so this run is ' +
|
|
495
|
+
'UNGUARDED against the rulebook — every rule, hook, skill and settings path is editable. ' +
|
|
496
|
+
'Arm it with a narrower --allow (an allow-list narrows the rulebook, never widens it) and verify again, or stop the run.\n',
|
|
497
|
+
);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
if (state.item !== item) {
|
|
501
|
+
process.stderr.write(
|
|
502
|
+
`unattended-flag verify: the armed flag names ${JSON.stringify(state.item)}, not ${JSON.stringify(item)} — ` +
|
|
503
|
+
`it is a leftover from another run and does not authorize this one. This run is UNGUARDED against the rulebook. ` +
|
|
504
|
+
`Clear it with \`off\` and arm it for ${item}, or stop the run.\n`,
|
|
505
|
+
);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
process.stdout.write(`${state.path ?? 'armed'}\n`);
|
|
509
|
+
process.exit(0);
|
|
510
|
+
}
|
|
511
|
+
process.stderr.write(`unknown word: ${word ?? '(none)'}. This CLI has three: on, verify, off.\n`);
|
|
449
512
|
process.exit(1);
|
|
450
513
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
+
"env": {
|
|
3
|
+
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-5"
|
|
4
|
+
},
|
|
2
5
|
"hooks": {
|
|
3
6
|
"PreToolUse": [
|
|
4
7
|
{
|
|
@@ -34,6 +37,15 @@
|
|
|
34
37
|
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.mjs\""
|
|
35
38
|
}
|
|
36
39
|
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"matcher": "Agent",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-subagent-model.mjs\""
|
|
47
|
+
}
|
|
48
|
+
]
|
|
37
49
|
}
|
|
38
50
|
],
|
|
39
51
|
"Stop": [
|
|
@@ -53,6 +65,10 @@
|
|
|
53
65
|
{
|
|
54
66
|
"type": "command",
|
|
55
67
|
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-rules.mjs\""
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/warn-subagent-routing.mjs\""
|
|
56
72
|
}
|
|
57
73
|
]
|
|
58
74
|
}
|
|
@@ -79,16 +79,23 @@ state-vs-queue split exists to prevent.
|
|
|
79
79
|
node .claude/scripts/preflight.mjs
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
default
|
|
82
|
+
Scripted checks cover the kill switch, inherited `RIG_RUN_DIR`, the versioned
|
|
83
|
+
revalidation detection contract, queue readability through its configured adapter,
|
|
84
|
+
default-branch freshness, and the last deploy result,
|
|
85
85
|
and the script **prints the ones it did not check, every time**. Paste the block into the journal: a checklist that
|
|
86
86
|
leaves no record cannot tell you it was skipped.
|
|
87
87
|
|
|
88
88
|
Verdicts: **STOP** → do not start, deal with the cause. **CAUTION** → start,
|
|
89
|
-
knowing which ground is soft. **GO** → the scripted
|
|
89
|
+
knowing which ground is soft. **GO** → the scripted checks are clean; the rest are
|
|
90
90
|
still yours.
|
|
91
91
|
|
|
92
|
+
The queue probe reads one adapter listing without selecting or claiming an item.
|
|
93
|
+
A readable empty queue passes this probe; a configuration, adapter, or queue-read
|
|
94
|
+
failure produces **STOP**. See the generator's `test/template/preflight-queue.test.ts`
|
|
95
|
+
(absent in a generated rig) › "reads exactly one adapter listing without selecting, claiming, or writing queue and run files",
|
|
96
|
+
› "passes a readable empty queue without changing the other preflight verdict or queue state",
|
|
97
|
+
and › "stops when %s cannot be read".
|
|
98
|
+
|
|
92
99
|
**An `unknown` never becomes a `pass`.** A probe that could not run tells you
|
|
93
100
|
nothing.
|
|
94
101
|
|
|
@@ -149,8 +156,38 @@ What a hook CAN see is a file, so the unattended signal is one:
|
|
|
149
156
|
# at claim time, from the paths the item names (repo-relative prefixes, with
|
|
150
157
|
# their trailing slash); the guard refuses every other rulebook edit while it is on
|
|
151
158
|
node .claude/scripts/unattended-flag.mjs on --root "$PWD" --item <item-id> --run-dir "$RIG_RUN_DIR" --allow <prefix> [<prefix>…]
|
|
159
|
+
|
|
160
|
+
# 🔴 THEN READ IT BACK, and stop the run if it is not armed. Not optional.
|
|
161
|
+
node .claude/scripts/unattended-flag.mjs verify --root "$PWD" --item <item-id>
|
|
152
162
|
```
|
|
153
163
|
|
|
164
|
+
🔴 **The second command is the one that makes the first one's failure
|
|
165
|
+
visible, and skipping it inverts the whole mechanism.** `on` refuses an allow
|
|
166
|
+
entry that *widens* the rulebook — and the refusal leaves **no flag on disk**
|
|
167
|
+
(pinned in the generator's `test/template/unattended-flag.test.ts`, absent in a
|
|
168
|
+
generated rig, › "does not change `on`: a widening --allow still exits 1 and
|
|
169
|
+
still writes no flag"), while `guard-rulebook` reads an absent flag as an
|
|
170
|
+
attended session and refuses nothing. So a run that armed with a widening entry and did not check is the
|
|
171
|
+
**least** constrained run this project can produce: every rule, hook, skill and
|
|
172
|
+
settings path editable, with nothing downstream saying so. The failure is loud
|
|
173
|
+
for one line at claim time and silent for the rest of the session.
|
|
174
|
+
|
|
175
|
+
`verify` exits non-zero when no usable flag is armed for this item — absent,
|
|
176
|
+
unreadable, or naming a different item — and its message says the run is
|
|
177
|
+
unguarded rather than merely that a file is missing. **A non-zero exit here ends
|
|
178
|
+
the run**; it does not get retried with a wider allow-list. The natural way to
|
|
179
|
+
hit this is not exotic: an item touching the queue adapter invites `--allow
|
|
180
|
+
.claude/scripts/`, and that entry is refused outright. Pinned in the
|
|
181
|
+
generator's `test/template/unattended-flag.test.ts` (absent in a generated rig)
|
|
182
|
+
› "refuses when no flag is armed, naming the item and the unguarded rulebook"
|
|
183
|
+
and › "refuses when the armed flag names a different item, naming both".
|
|
184
|
+
|
|
185
|
+
⚠ **What this does not close.** `verify` is mechanical where it runs; that it
|
|
186
|
+
runs is this sentence. It removes the silence, not the possibility that a run
|
|
187
|
+
ignores an exit status — and the hook-enforced version is not available, because
|
|
188
|
+
`guard-rulebook` cannot tell "attended" from "unattended but unarmed": absence of
|
|
189
|
+
a flag is all it sees.
|
|
190
|
+
|
|
154
191
|
`guard-rulebook` reads it (`.claude/rules/autonomy.md`, "Never"): with the flag
|
|
155
192
|
on, a Write/Edit/MultiEdit/NotebookEdit/`apply_patch` under the generated
|
|
156
193
|
rulebook is refused unless its
|
|
@@ -1,4 +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 the decision-router puts on its `model` lane, which is everything its two cheap lanes did not claim — code, a rulebook document, an unclassifiable path, a derived artifact git does not report as drift, or anything a risk flag escalated. Blocking findings must be resolved, not argued with."
|
|
3
|
+
model = "gpt-5.6-sol"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
3
5
|
sandbox_mode = "read-only"
|
|
4
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** — imports that cross layers the wrong way; storage\n or SDK access outside its owning module; handlers reaching past the usecase\n layer. See the architecture rules in `.claude/rules/`.\n2. **Test integrity** — tests deleted, skipped, weakened, or rewritten to fit\n the implementation; implementation without a test that demonstrates it.\n3. **Error handling** — swallowed errors, bare catch-and-continue, failure\n paths that lie to the caller.\n4. **Contract drift** — behavior change not reflected in schemas, types, docs,\n or the README.\n5. **Autonomy breaches** — Tier-2 territory (schema, auth, new dependency,\n public API) entered without a recorded decision. See\n `.claude/rules/autonomy.md`.\n6. **Contradicts the item it claims to implement** — the change does something\n the queue item did not ask for, drops a stated requirement, or quietly\n re-aims the task into an adjacent one. Read the item first, then the diff.\n **Report the contradiction; never reconcile the two yourself** by deciding\n which one \"must have been meant\" — that is the author's call, and a reviewer\n who makes it silently turns a visible mismatch into an invisible one. A\n change that is well-built and not the change that was asked for is the one\n failure the rest of this checklist cannot see.\n\n **If the item was not handed to you, say so and stop there.** Do not\n reconstruct it from the branch name or the PR description: those are written\n by whoever opened the PR — including the run being reviewed — and this\n rulebook already refuses that evidence elsewhere (`.claude/rules/autonomy.md`).\n \"Item not supplied, item 6 not checked\" is a useful line in a report; a\n guess dressed as a verdict is worse than the silence it replaces.\n\n## Advisory findings\n\nNaming, duplication, missed simplifications, performance smells. Report them;\ndo not block on them.\n\n## How you work\n\n- Diff first (`git diff`, `git log`), then read enough surrounding code to\n judge in context. Review what changed, not the whole repo.\n- Quote the checklist item a blocking finding violates. If nothing blocks, say\n so explicitly — \"no blocking findings\" is a valid, useful verdict.\n- Do not request rewrites of working, tested code for style alone.\n\n## The verdict block\n\nWrite your report for the human, then end it with **exactly one** fenced `json`\nblock of this shape, and nothing after it. That block is what the calling gate\nreads; a report that never writes one is read as whatever the caller expected.\n\n```json\n{\n \"gate\": \"code-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"packages/core/src/note.ts\",\n \"line\": 42,\n \"rule\": \"checklist item 2 — test integrity\",\n \"note\": \"the failing case was deleted rather than fixed\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"diffed against origin/master\", \"queue item supplied\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates. `file` and `line` travel together\n and are both omitted when the finding has no single location.\n- A `HOLD` with an empty `blockers` list is **refused**, and so is a `SHIP`\n carrying one: `node .claude/scripts/verdict.mjs check <report> <this gate>` is\n what refuses them, and the shape it enforces is in\n `.claude/scripts/lib/verdict.mjs`. The gate name is what stops your answer\n being read as somebody else's.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
name = "prose-reviewer"
|
|
2
2
|
description = "Reviews the documents that instruct agents — rule files, skills, agent specs, CLAUDE.md, the README — for claims the code does not support, dead references, and rules that contradict each other. Use when a change touches any of them, before the PR."
|
|
3
|
+
model = "gpt-5.6-terra"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
3
5
|
sandbox_mode = "read-only"
|
|
4
6
|
developer_instructions = "In this project the prose **is** the implementation. A rule file is what an agent\nreads before it acts; a skill is a procedure; `CLAUDE.md` is the map. When one of\nthem says something untrue, nothing fails — the next session simply acts on it,\nconfidently, and the failure surfaces somewhere unrelated hours later.\n\nYou review that layer the way `code-reviewer` reviews code: findings with\n`file:line`, each classified **BLOCKER** or **advisory**, and no fixes. You do\nnot edit anything.\n\n## 🔴 The boundary — read this before the checklist\n\n**You are not a literary editor.** Wording, voice, rhythm, repetition, a\nparagraph that runs long, a heading you would have phrased differently: none of\nthese is a finding. Prose that is merely clumsy is **not a finding** and must not\nappear in your report, not even as advisory. Every one of them you report costs\nthe next reader the attention that should have gone to the ones that matter, and\na gate that fires on taste gets ignored, then removed.\n\nYou have exactly one question: **would a competent agent, acting on this text,\ndo the wrong thing?** If no, it is not yours.\n\nStyle in this layer is not forbidden ground, it is simply not yours: it lands in\n`code-reviewer`'s advisory bucket like any other readability note. Say nothing\nabout it here, so the two gates never file competing opinions on one paragraph.\n\n## Checklist (blocking findings)\n\n1. **An overstated claim of enforcement.** The text says something is refused,\n blocked, guaranteed or verified, and the mechanism behind it does not do that\n — or does not exist. Read the hook, the script, the CI job, and quote what it\n actually does. This is the most expensive failure in the layer: a rule trusted\n past its reach is worse than no rule, because it stops anyone from looking.\n2. **A dead reference.** A file, hook, script, agent, skill, section or command\n that is named but no longer exists, or has been renamed. Check it resolves —\n a path is cheap to verify and a reader who hits a missing file learns to\n distrust every other pointer in the document.\n3. **Two rules that contradict each other.** Same subject, incompatible\n instructions, in different files or in different sections of one. Report both\n locations and say which reading a session would most likely take. Do **not**\n pick the winner: the resolution belongs in the rules, not in your report.\n4. **A stated limit that has gone stale — in either direction.** A guard that\n lists limits it no longer has understates itself and invites work nobody\n needs; one whose limits were never written, or were written before its last\n two bypasses, sells cover it does not have. Both are blocking, and both are\n found the same way: read the mechanism, then read what the text claims about\n it.\n5. **An unbacked behaviour claim.** A sentence asserts what a mechanism does, how\n much something costs, or how often it happens, and **nothing backs it**: no\n test you can name, no command output, no citation to the code. Per\n `.claude/rules/invariants.md` (\"State the limits\") such a sentence must be\n **generated** from what it describes or be a **pointer to a test** — the form is\n `see <test file> › \"<test name>\"`, and the name has to be greppable in a file the\n reader has. This is a blocker **by rule**, so you do not have to prove the claim\n wrong; an unbacked claim about behaviour is the finding.\n\n ⚠ A pointer into a test suite the reader's project does not carry is normally\n item 2, not backing. There is one narrow inherited-snapshot exception from\n `invariants.md`: a generator-authored artifact — rules, hooks, skills,\n scripts, or agent specs —\n may point to upstream generator tests that are absent locally only when the\n pointer explicitly says the suite is absent locally and\n `.claude/.rig-manifest.json` proves the current artifact's hash matches the\n installed manifest. A manifest-backed upgrade remains an inherited,\n generator-owned artifact; a changed file in the upgrade diff does not alone\n make it downstream-authored. The exception applies **only while the manifest\n hash matches**. A hash mismatch, missing manifest, or no evidence ends the\n exception and the local test is yours; then an absent pointer is item 2 again.\n\n 🔴 Three things this is not. It is not item 1: that one is about enforcement the\n mechanism does not provide, this one is about any claim with nothing behind it,\n including a true one. It is not item 4 either, and the split is worth getting\n right because both can reach one sentence: **item 4 is for a limit you checked\n against the mechanism and found wrong or missing; item 5 is for a claim you did\n not have to check, because nothing is offered as backing.** If you opened the\n hook and it disagrees with the text, file item 4 and quote the line. If there was\n nothing offered to open, file item 5. If you opened it and the claim was right,\n there is no finding. One sentence, one item. And it is not an attack on rationale — \"we chose X\n because Y\" needs no test. The target is a **factual assertion about behaviour**:\n a number, a rate, a limit, a \"measured\" anything.\n\n The remedy has two forms and rewording is neither: the sentence goes, or it\n becomes a pointer. Say which you would expect, and where the test lives if one\n exists.\n6. **Domain that must not travel.** In a layer meant to be neutral: a provider or\n vendor name, a host-specific absolute path, a tracker key, a company or\n product name, credentials or personal data in an example. State which layer\n the file belongs to and why the mention breaks it.\n\n 🔴 **A seam built to name a vendor is not a leak.** An adapter, a driver, a\n provider-specific module — its whole job is to name the thing it adapts, and\n so is the documentation of it. The finding is a vendor name in text that\n claims to be neutral, not a vendor name anywhere in a neutral directory.\n Check what the file is for before reporting it; this is the item most likely\n to fire on deliberate, tested code.\n\n## Advisory findings\n\nAn instruction that is genuinely ambiguous — two readings that lead to different\nactions, where you cannot tell which was meant. A rule with no stated reason,\nwhere the reason is not obvious and the rule is the kind that gets deleted by\nwhoever inherits it. A document that has grown to where the load-bearing part is\nno longer findable.\n\nThat is the whole advisory list, on purpose. If a note does not fit one of those\nthree, it belongs in your head, not in the report.\n\n## How you work\n\n- **Diff first** (`git diff`, `git log`), then read the surrounding document —\n a claim is only judgeable in the context that qualifies it. Review what\n changed, not the whole rulebook.\n- **Verify against the mechanism, never against your memory of it.** Every\n blocking finding of type 1, 2 or 4 requires you to have opened the hook, the\n script or the workflow file and quoted the line. A finding you could not check\n is reported as unverified, or not at all.\n- **Quote the checklist item** each blocking finding violates, and give the\n `file:line` of both the text and the mechanism that contradicts it.\n- **\"No blocking findings\" is a valid and useful verdict.** Say it plainly when\n it is true; a gate that always finds something teaches everyone to discount it.\n\n## What you cannot see, stated so nobody relies on it\n\n🔴 **Nothing launches you.** No hook fires this review; a session reads a rule\nand decides to. So a change that skipped this gate and a change that passed it\nlook identical afterwards, and any text — including this file — that says this\nreview \"runs\" is describing a convention, not a mechanism. Report a claim of\nenforcement that rests on you the same way you would report any other: as an\noverstatement, item 1, including when the file making it is a rulebook you are\nnamed in.\n\nYou read text and the mechanisms it names. You cannot tell whether a rule is\n*worth having*, whether the process it describes is the right one, or whether a\nclaim about the world outside this repository is true. Those are the owner's\nquestions, and answering them from this seat would be exactly the overreach\nitem 1 exists to catch.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. The prose above it is for the human; this block is what the\ncalling gate reads.\n\n```json\n{\n \"gate\": \"prose-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \".claude/rules/invariants.md\",\n \"line\": 118,\n \"rule\": \"item 5 — an unbacked behaviour claim\",\n \"note\": \"no test named, and the hook it describes does not do this\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"opened .claude/hooks/guard-bash.mjs and quoted the line\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates; give the `file` and `line` of the\n text, and cite the contradicting mechanism in the `note`.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> <this gate>` is what refuses\n them, and the gate name is what stops your answer being read as somebody\n else's.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."
|
|
@@ -1,4 +1,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-5.6-sol"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
3
5
|
sandbox_mode = "read-only"
|
|
4
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 `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
name = "test-writer"
|
|
2
2
|
description = "Writes the failing test BEFORE any implementation exists. Use at the start of every feature, bug fix, or behavior change — the Red step of TDD. Also use to reproduce a reported bug as a test."
|
|
3
|
+
model = "gpt-5.6-terra"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
3
5
|
sandbox_mode = "workspace-write"
|
|
4
6
|
developer_instructions = "You write tests that define behavior which does not exist yet. You are the Red\nstep of TDD, and only the Red step.\n\n## Scope — hard boundaries\n\n- You create and modify **test files only**. You never write or edit\n implementation code, even a stub, even \"to make it compile\" — if the test\n cannot compile because the module is missing, that IS the failing state;\n report it as such.\n- You never mark tests as skipped or todo to avoid a failure. A failing test is\n your deliverable.\n\n## How you work\n\n1. Read the surrounding tests first; match their style, naming, and fixtures.\n2. Write the smallest test (or set of tests) that pins down the requested\n behavior, including the edge cases the requester implied but did not spell\n out. Name tests after behavior (\"refuses an empty title\"), not after methods.\n3. Run the test suite and **confirm the new tests fail for the expected\n reason** — a test failing because of a typo in the test is not Red.\n4. Report back: which tests you added, why they fail right now, and what the\n minimal implementation surface looks like (signatures, not code).\n\n## Judgment lines\n\n- Test behavior through public entry points (usecases, handlers), not private\n internals.\n- One behavior per test; shared setup in fixtures, not copy-paste.\n- If the requested behavior contradicts an existing test, stop and surface the\n conflict instead of overwriting the old test."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Codex adapter: derived parity
|
|
2
2
|
|
|
3
|
-
Status: accepted for AR-113.
|
|
3
|
+
Status: accepted for AR-113; subagent routing extended for RP-166.
|
|
4
4
|
|
|
5
5
|
## Decision
|
|
6
6
|
|
|
@@ -18,7 +18,22 @@ generated release. There is no downstream automatic drift check.
|
|
|
18
18
|
|
|
19
19
|
Two hand-maintained rulebooks inevitably diverge. Derivation keeps the generated
|
|
20
20
|
snapshots aligned while preserving native Codex formats. The adapter translates
|
|
21
|
-
the fields for which Codex has native controls
|
|
21
|
+
the fields for which Codex has native controls. Subagent routing — each role's
|
|
22
|
+
model and effort, for both harnesses — is declared once in the generator's
|
|
23
|
+
`templates/agent-os/subagent-routing.json` (rationale:
|
|
24
|
+
`docs/decisions/subagent-routing.md`). The Codex profiles are derived from that
|
|
25
|
+
table, and the projector refuses a missing or orphaned agent profile. It also
|
|
26
|
+
refuses duplicate source-agent names across layers, since one profile name
|
|
27
|
+
cannot route two definitions.
|
|
28
|
+
|
|
29
|
+
Frequent bounded work (`test-writer`, `prose-reviewer`) uses `gpt-5.6-terra`;
|
|
30
|
+
correctness, security, and infrastructure gates use `gpt-5.6-sol`. Every named
|
|
31
|
+
gate uses `high` reasoning effort. Unnamed subagents inherit repository defaults
|
|
32
|
+
of `gpt-5.6-terra` and `medium` from `.codex/config.toml`. `xhigh` is not a
|
|
33
|
+
continuous-loop default; using it for a named role requires an intentional
|
|
34
|
+
profile-policy change (or a separate escalation profile), because named-profile
|
|
35
|
+
fields take precedence over spawn and repository defaults. Concurrency remains
|
|
36
|
+
a machine/session decision rather than repository policy.
|
|
22
37
|
|
|
23
38
|
Claude agent `tools` allowlists have no equivalent custom-agent allowlist in the
|
|
24
39
|
documented Codex TOML schema. The adapter therefore uses those fields only to
|
|
@@ -28,7 +43,15 @@ not carried over. This is a known parity limit, not an implicit restriction.
|
|
|
28
43
|
## Risk and rollback
|
|
29
44
|
|
|
30
45
|
The main risks are generated-file drift, downstream edits to only one snapshot,
|
|
31
|
-
|
|
46
|
+
unsupported Claude shapes, and a pinned model being unavailable in a user's
|
|
47
|
+
workspace. In the generator, recovery is to update the central routing table to
|
|
48
|
+
an available model or use a separate supported escalation profile — the Codex
|
|
49
|
+
profiles are regenerated from it, while the Claude agent definitions are checked
|
|
50
|
+
against it and are edited to match in the same change. A generated
|
|
51
|
+
project has no copy of that table: there, recovery is editing the role's
|
|
52
|
+
definitions on both harnesses in one reviewed change, and `upgrade` then reports
|
|
53
|
+
those files as the project's own instead of replacing them
|
|
54
|
+
(`docs/decisions/subagent-routing.md`). In the generator, the adapter fails loudly when
|
|
32
55
|
it cannot derive a portable hook command and its drift check catches stale
|
|
33
56
|
output. Generated projects rely on review for subsequent local parity. Rollback
|
|
34
57
|
is deleting the derived Codex files from the generated project and reverting the
|
|
@@ -80,8 +103,11 @@ Codex path.
|
|
|
80
103
|
|
|
81
104
|
## Schema and executable contracts
|
|
82
105
|
|
|
83
|
-
The emitted agent fields are `name`, `description`, `
|
|
84
|
-
`
|
|
106
|
+
The emitted agent fields are `name`, `description`, `model`,
|
|
107
|
+
`model_reasoning_effort`, `sandbox_mode`, and `developer_instructions`, matching
|
|
108
|
+
the documented Codex custom-agent TOML. Named-agent fields take precedence over
|
|
109
|
+
the repository defaults in `.codex/config.toml`; the defaults cover only agents
|
|
110
|
+
without their own model or effort assignment.
|
|
85
111
|
For hooks, the [official Codex hooks documentation](https://learn.chatgpt.com/docs/hooks)
|
|
86
112
|
documents `tool_input.command` for both `Bash` and `apply_patch` and requires a
|
|
87
113
|
string `command` when a hook replaces that input. The same document is what makes
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Subagent routing: every gate pins its model and effort
|
|
2
|
+
|
|
3
|
+
Status: accepted for RP-173 (Claude Code). The Codex half, RP-166, is recorded in
|
|
4
|
+
`codex-adapter.md`. Both harnesses' routing comes from one role table: the Codex
|
|
5
|
+
profiles are generated from it, and the Claude agent definitions are checked against it.
|
|
6
|
+
|
|
7
|
+
## Decision
|
|
8
|
+
|
|
9
|
+
Each named subagent this rig ships pins the model and the effort it reads with:
|
|
10
|
+
|
|
11
|
+
| Role | Claude Code | Codex |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `code-reviewer`, `security-scanner`, a stack's infrastructure reviewer | `claude-opus-5`, `high` | `gpt-5.6-sol`, `high` |
|
|
14
|
+
| `test-writer`, `prose-reviewer` | `claude-sonnet-5`, `high` | `gpt-5.6-terra`, `high` |
|
|
15
|
+
| a subagent with no definition | `claude-sonnet-5`; effort follows the session | `gpt-5.6-terra`, `medium` |
|
|
16
|
+
|
|
17
|
+
In this project the pins are the files themselves: `model:` and `effort:` in
|
|
18
|
+
`.claude/agents/<role>.md`, `model` and `model_reasoning_effort` in
|
|
19
|
+
`.codex/agents/<role>.toml`, `env.CLAUDE_CODE_SUBAGENT_MODEL` in
|
|
20
|
+
`.claude/settings.json`, and the `[agents]` defaults in `.codex/config.toml`.
|
|
21
|
+
|
|
22
|
+
The driver session's own model and effort are not pinned. They belong to whoever
|
|
23
|
+
starts the session.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
A gate that inherits the driver's model returns a verdict whose meaning changes
|
|
28
|
+
whenever the driver does. Pinning makes the reader behind a SHIP a property of the
|
|
29
|
+
rulebook rather than of how a session happened to be launched.
|
|
30
|
+
|
|
31
|
+
- **Full model IDs, not aliases.** An alias moves when a new model ships, and every
|
|
32
|
+
verdict would change meaning without a diff. Moving a role to another model is a
|
|
33
|
+
policy change.
|
|
34
|
+
- **The larger model reads correctness, security and infrastructure; the smaller one
|
|
35
|
+
does bounded, frequent work** — the same split as the Codex tiers.
|
|
36
|
+
- **`high` for every gate, and escalation is a policy change.** A dispatch has no way
|
|
37
|
+
to set a subagent's effort — the `per-dispatch-effort` row below — so raising a
|
|
38
|
+
gate's effort is an edit to its definition, and nothing here invents a per-call knob.
|
|
39
|
+
|
|
40
|
+
## How Claude Code resolves a pin, and what voids one
|
|
41
|
+
|
|
42
|
+
Claude Code's changelog states two of the rules:
|
|
43
|
+
|
|
44
|
+
- A subagent's model comes from the per-dispatch `model` first, then the
|
|
45
|
+
definition's `model:`, then `CLAUDE_CODE_SUBAGENT_MODEL`, then the parent session —
|
|
46
|
+
and that order holds from **2.1.251**. Before it the environment variable overrode
|
|
47
|
+
definitions, so the unnamed default this rig sets would replace every gate's pin.
|
|
48
|
+
That is why 2.1.251 is the version the session-start check warns below; it is
|
|
49
|
+
the release that makes the MODEL pins outrank the shipped default, and nothing more
|
|
50
|
+
is claimed for it — the `effort:` field and the `Agent` tool name the guard matches
|
|
51
|
+
were observed working on 2.1.269 and 2.1.270 only.
|
|
52
|
+
- `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`, added in 2.1.257, applies one model to every
|
|
53
|
+
subagent and ignores each definition's `model:`.
|
|
54
|
+
|
|
55
|
+
The rest is measured. Each row records a live Claude Code run, read by a script from
|
|
56
|
+
what Claude Code itself wrote — a subagent transcript, a hook's payload or a hook's
|
|
57
|
+
environment — never from a subagent's own report. The rows are the generator's
|
|
58
|
+
`docs/capability-evidence.json` (absent in a generated rig), keyed by `mechanism`
|
|
59
|
+
and `surface`:
|
|
60
|
+
|
|
61
|
+
- `subagent-model-pin` and `subagent-effort-pin` hold under a driver on another model
|
|
62
|
+
and effort (surface `.claude/agents/test-writer.md`);
|
|
63
|
+
- `subagent-model-pin` does not hold with `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1`, and
|
|
64
|
+
`subagent-effort-pin` does not hold with `CLAUDE_CODE_EFFORT_LEVEL=low` — each
|
|
65
|
+
variable replaced only the pin it names;
|
|
66
|
+
- `subagent-model-pin` does not hold against a call-site `model` when no guard
|
|
67
|
+
refuses the dispatch;
|
|
68
|
+
- `unnamed-subagent-model` holds and `unnamed-subagent-effort` is unsupported: a
|
|
69
|
+
subagent with no definition ran on the shipped default at the session's effort.
|
|
70
|
+
|
|
71
|
+
Two mechanisms follow, both in `.claude/hooks/`. `guard-subagent-model` refuses a
|
|
72
|
+
per-dispatch `model` for an agent whose definition pins one (`call-site-model-guard`).
|
|
73
|
+
`warn-subagent-routing` warns at session start when either variable is set, when
|
|
74
|
+
Claude Code is older than 2.1.251, or when its version cannot be read from `AI_AGENT`
|
|
75
|
+
(`claude-code-version-signal`); it never blocks, because the variables are the
|
|
76
|
+
operator's to set and the warning makes their cost visible instead of taking the
|
|
77
|
+
decision. Pinned in `subagent-routing-hooks.test.ts` (absent in a generated rig)
|
|
78
|
+
› "blocks a call-site model on a project agent that pins one, and says to re-dispatch without it"
|
|
79
|
+
and › "warns when %s is set, and never blocks the session".
|
|
80
|
+
|
|
81
|
+
Neither hook is wired for Codex, which has no dispatch hook and no Claude
|
|
82
|
+
environment: `subagent-routing.test.ts` (absent in a generated rig) ›
|
|
83
|
+
"keeps both routing hooks out of the Codex projection, which has no Agent tool and no Claude environment".
|
|
84
|
+
|
|
85
|
+
## What this does not do
|
|
86
|
+
|
|
87
|
+
- **The effort of a subagent with no definition is not pinned.** It runs at the
|
|
88
|
+
session's effort (`unnamed-subagent-effort`). That is recorded as unsupported, not
|
|
89
|
+
worked around.
|
|
90
|
+
- **A definition says what was asked for; only the transcript says what ran.** A
|
|
91
|
+
verdict does not yet carry the model and effort it was produced on.
|
|
92
|
+
- **The guard's notion of a role is "a project agent whose definition pins a model".**
|
|
93
|
+
In a generated project the definitions are the policy (next section), so an agent
|
|
94
|
+
a project adds with a pin is a role, and a role whose definition is changed to
|
|
95
|
+
`model: inherit` has stopped being one — by that project's reviewed decision.
|
|
96
|
+
- **The built-in agents get no definitions here.** A general-purpose subagent follows
|
|
97
|
+
the unnamed default like any subagent without a definition.
|
|
98
|
+
- **The warning is a warning.** A session started with `CLAUDE_CODE_EFFORT_LEVEL` set
|
|
99
|
+
still runs every gate — at that level.
|
|
100
|
+
|
|
101
|
+
## Changing a role in this project
|
|
102
|
+
|
|
103
|
+
The item this record implements asked that a project lower a default by editing
|
|
104
|
+
"its policy", with "the test following". Here that is realised as follows, and the
|
|
105
|
+
difference from a literal reading is deliberate:
|
|
106
|
+
|
|
107
|
+
- **The agent definitions are this project's policy.** It receives no copy of the
|
|
108
|
+
generator's role table and no routing check: a second copy beside the definitions,
|
|
109
|
+
with no projector here to keep the two aligned, is the drift `rules/invariants.md`
|
|
110
|
+
forbids ("one mechanism, one implementation"), and the Codex half already shipped
|
|
111
|
+
that way.
|
|
112
|
+
- **To move a role**, edit `model:` / `effort:` in `.claude/agents/<role>.md` and the
|
|
113
|
+
matching `.codex/agents/<role>.toml` in one reviewed change; both trees are
|
|
114
|
+
elevated paths.
|
|
115
|
+
- **The test that follows the table lives in the generator**, where the expected
|
|
116
|
+
values are read from the table rather than restated.
|
|
117
|
+
- **`upgrade` reports the edited agent file as yours and leaves it alone**, while an
|
|
118
|
+
agent file nobody edited is replaced by the release's pinned one —
|
|
119
|
+
`subagent-routing-install.test.ts` (absent in a generated rig) ›
|
|
120
|
+
"replaces an untouched pre-pin agent with the pinned one" and ›
|
|
121
|
+
"reports a pre-pin agent the user edited as a conflict and leaves its bytes alone".
|
|
122
|
+
|
|
123
|
+
## In the generator
|
|
124
|
+
|
|
125
|
+
`templates/agent-os/subagent-routing.json` is the one table.
|
|
126
|
+
`scripts/sync-codex-adapter.mjs --check` loads it through `scripts/subagent-routing.mjs`,
|
|
127
|
+
derives the Codex profiles from it, and refuses a Claude agent without `model` or
|
|
128
|
+
`effort`, a value that differs from its role, a role without an agent or an agent
|
|
129
|
+
without a role, an effort the pinned model does not support, an unknown field in
|
|
130
|
+
either harness's mapping, and shipped settings that miss the unnamed default or set
|
|
131
|
+
either voiding variable —
|
|
132
|
+
`subagent-routing.test.ts` (absent in a generated rig) › "refuses a routing policy with %s"
|
|
133
|
+
and › "refuses a Claude agent template whose model disagrees with the routing policy".
|
|
134
|
+
|
|
135
|
+
## Risk and rollback
|
|
136
|
+
|
|
137
|
+
A pinned model can be unavailable to an account. In the generator the recovery is
|
|
138
|
+
changing the table and, in the same change, the Claude agent definitions checked
|
|
139
|
+
against it; in a project it is changing the definitions as above — never a
|
|
140
|
+
per-call override. Rollback in a project is deleting the `model:` / `effort:` lines,
|
|
141
|
+
the `env` entry, and the two hooks with their wiring; every gate then inherits the
|
|
142
|
+
session again.
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
".claude/hooks/inject-rules.mjs",
|
|
17
17
|
".claude/hooks/guard-secret-file.mjs",
|
|
18
18
|
".claude/hooks/guard-rulebook.mjs",
|
|
19
|
+
".claude/hooks/guard-subagent-model.mjs",
|
|
20
|
+
".claude/hooks/warn-subagent-routing.mjs",
|
|
19
21
|
".claude/hooks/lib/edit-input.mjs",
|
|
20
22
|
".claude/hooks/lib/hook-input.mjs",
|
|
21
23
|
".claude/skills/pr-ship/SKILL.md",
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
"docs/decisions/gate-coverage.md",
|
|
70
72
|
"docs/decisions/fail-open-guards.md",
|
|
71
73
|
"docs/decisions/codex-adapter.md",
|
|
74
|
+
"docs/decisions/subagent-routing.md",
|
|
72
75
|
"docs/decisions/closing-a-task.md",
|
|
73
76
|
"docs/decisions/content-blind-revalidation.md",
|
|
74
77
|
"docs/decisions/review-lanes.md",
|
|
@@ -84,6 +87,7 @@
|
|
|
84
87
|
],
|
|
85
88
|
"meta": [
|
|
86
89
|
".claude/settings.json",
|
|
90
|
+
".codex/config.toml",
|
|
87
91
|
".codex/hooks.json",
|
|
88
92
|
"CLAUDE.md",
|
|
89
93
|
"AGENTS.md"
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"0.6.1",
|
|
11
11
|
"0.6.2",
|
|
12
12
|
"0.7.0",
|
|
13
|
-
"0.7.1"
|
|
13
|
+
"0.7.1",
|
|
14
|
+
"0.8.0"
|
|
14
15
|
],
|
|
15
16
|
"files": {
|
|
16
17
|
".agents/skills/check-premises/SKILL.md": {
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
"47922b182915cb92696f851174f2e88c4ae8c67dc119ef39a3a597ef1300fab5",
|
|
28
29
|
"57f6add0504db45e7577474f97705fe474310e4cd3310cdb348a2c64eabe88cd",
|
|
29
30
|
"18f4300de4c4c7ea380d726fb109fa224da950ef0a4cea5015ab1d408b823be5",
|
|
30
|
-
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d"
|
|
31
|
+
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d",
|
|
32
|
+
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546"
|
|
31
33
|
]
|
|
32
34
|
},
|
|
33
35
|
".agents/skills/new-invariant/SKILL.md": {
|
|
@@ -229,7 +231,8 @@
|
|
|
229
231
|
"ce2104ce697d3e5ed9210e85fe0daeb66b432a36868493f06fec54073a5fff31",
|
|
230
232
|
"530f63a4b29a5cc67867fa7b23575db81d42609ae401bfc4089b05194c586c21",
|
|
231
233
|
"a7359408d56e6bdbf268019ad9193d5fbf4a872385a65ba08564204e6a8f5b5f",
|
|
232
|
-
"619999500386b3d2867bbd4799f86a800fe7e168083c6e35fb2e629a2c926c5f"
|
|
234
|
+
"619999500386b3d2867bbd4799f86a800fe7e168083c6e35fb2e629a2c926c5f",
|
|
235
|
+
"532dc5cdffb1464fb68889628664b1e7e345272c806cf2ccb7a063c6a3a7f1bf"
|
|
233
236
|
]
|
|
234
237
|
},
|
|
235
238
|
".claude/rules/aws-cdk.md": {
|
|
@@ -517,7 +520,8 @@
|
|
|
517
520
|
"47922b182915cb92696f851174f2e88c4ae8c67dc119ef39a3a597ef1300fab5",
|
|
518
521
|
"57f6add0504db45e7577474f97705fe474310e4cd3310cdb348a2c64eabe88cd",
|
|
519
522
|
"18f4300de4c4c7ea380d726fb109fa224da950ef0a4cea5015ab1d408b823be5",
|
|
520
|
-
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d"
|
|
523
|
+
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d",
|
|
524
|
+
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546"
|
|
521
525
|
]
|
|
522
526
|
},
|
|
523
527
|
".claude/skills/new-invariant/SKILL.md": {
|
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
"0.6.1": "f1d1e3dbd2161545d77ee5e7f90fa8e74b8a9f3e",
|
|
11
11
|
"0.6.2": "2a1fc8e10cd2b65deb5c95d937fd7de39f2c92a2",
|
|
12
12
|
"0.7.0": "6589db36e1daa63a99ec595191db1cccf7373196",
|
|
13
|
-
"0.7.1": "52e879b6c103f6ba70493007b6a6466c57ea9824"
|
|
13
|
+
"0.7.1": "52e879b6c103f6ba70493007b6a6466c57ea9824",
|
|
14
|
+
"0.8.0": "870f9a3ecae2881908ece8ec3e2ac13f84f505f5"
|
|
14
15
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
// CD brief §2: node-service's deployable artifact must actually build and run.
|
|
2
2
|
// This bundles the server, boots the bundle over a real socket, and closes the
|
|
3
3
|
// path — proving the artifact is genuine, not a stub.
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
5
|
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import type { AddressInfo } from 'node:net';
|
|
8
8
|
import { createServer } from 'node:net';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { promisify } from 'node:util';
|
|
12
11
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
12
|
+
import { runPackageManager } from './package-manager.js';
|
|
13
13
|
|
|
14
|
-
const exec = promisify(execFile);
|
|
15
14
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
16
15
|
const dist = path.join(projectRoot, 'dist');
|
|
17
16
|
|
|
@@ -38,7 +37,7 @@ describe('deployable artifact (dist/)', () => {
|
|
|
38
37
|
() => true,
|
|
39
38
|
() => false,
|
|
40
39
|
);
|
|
41
|
-
if (!built) await
|
|
40
|
+
if (!built) await runPackageManager(['build:artifact'], { cwd: projectRoot });
|
|
42
41
|
}, 180_000);
|
|
43
42
|
|
|
44
43
|
afterAll(async () => {
|