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
|
@@ -9,7 +9,54 @@
|
|
|
9
9
|
* and not the word leaves the caller guessing which of two spellings it sent.
|
|
10
10
|
*/
|
|
11
11
|
export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether an outside record CARRIES a field — own and enumerable, which is
|
|
14
|
+
* exactly the set `Object.keys` walks, and which `JSON.stringify` writes out
|
|
15
|
+
* whenever the value is serialisable.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ The second half is a "whenever", not an equivalence, and the earlier
|
|
18
|
+
* wording claimed the equivalence. `{ downgradeReason: undefined }` is own and
|
|
19
|
+
* enumerable, so this returns `true`, while `JSON.stringify` drops it — which
|
|
20
|
+
* is why a `SUPPORTED` row written that way is refused for carrying a reason
|
|
21
|
+
* even though its serialisation carries none. The refusal is the conservative
|
|
22
|
+
* direction, but a reader who took "the rule is what `JSON.stringify` sees"
|
|
23
|
+
* literally would predict acceptance. `Object.keys` is the set this actually
|
|
24
|
+
* implements; that is the half to reason from.
|
|
25
|
+
*
|
|
26
|
+
* 🔴 The two halves are one rule, and each was a real false pass. A field found
|
|
27
|
+
* on the PROTOTYPE let a hook serialising as `{}` be read as running the
|
|
28
|
+
* generated command, and let an evidence row own nothing and still validate; a
|
|
29
|
+
* field that is own but NOT ENUMERABLE let a row validate and then serialise
|
|
30
|
+
* without the pointer that made it pass. Both are the same defect stated twice:
|
|
31
|
+
* something was accepted as evidence that no serialisation of the value
|
|
32
|
+
* carries. `unknownKeys` already judges a record by `Object.keys`, so reading
|
|
33
|
+
* by any wider notion made the closed-shape check and the field reads disagree
|
|
34
|
+
* about what the record even contains — and the reads were the wider of the
|
|
35
|
+
* two.
|
|
36
|
+
*
|
|
37
|
+
* Held over both readers at once: `packages/cli/test/policy-coverage.test.ts`
|
|
38
|
+
* (absent in a generated rig) › "refuses a hook entry whose command is only
|
|
39
|
+
* inherited, because the entry itself carries no command" and › "refuses a row
|
|
40
|
+
* whose evidence pointer is own but not enumerable, because the rule is what
|
|
41
|
+
* JSON.stringify sees".
|
|
42
|
+
*/
|
|
43
|
+
export const carriesField = (input, field) => Object.prototype.propertyIsEnumerable.call(input, field);
|
|
44
|
+
/**
|
|
45
|
+
* Read one field the way the record's own serialisation would carry it, or
|
|
46
|
+
* `undefined` when the record does not carry it at all.
|
|
47
|
+
*
|
|
48
|
+
* Presence and value travel through the same predicate on purpose: a caller
|
|
49
|
+
* that tested presence one way and read the value another is how the two
|
|
50
|
+
* came apart the first time.
|
|
51
|
+
*/
|
|
52
|
+
export const ownField = (input, field) => carriesField(input, field) ? input[field] : undefined;
|
|
53
|
+
/**
|
|
54
|
+
* A value as it appeared, escaped, for a message a person reads. Exported
|
|
55
|
+
* because every module here that puts OUTSIDE data into a diagnostic must put
|
|
56
|
+
* it through the same escaping — a raw newline or ANSI sequence in a matcher
|
|
57
|
+
* can otherwise forge a line of the report it lands in (`./probe.ts`).
|
|
58
|
+
*/
|
|
59
|
+
export const quote = (value) => {
|
|
13
60
|
try {
|
|
14
61
|
return JSON.stringify(value) ?? String(value);
|
|
15
62
|
}
|
|
@@ -38,6 +85,67 @@ export const nonEmptyString = (problems, field, value) => {
|
|
|
38
85
|
}
|
|
39
86
|
return true;
|
|
40
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* Refuse a string that is absent, not a string, or has no non-space character.
|
|
90
|
+
*
|
|
91
|
+
* Stricter than `nonEmptyString` in exactly one place — a value of whitespace
|
|
92
|
+
* only — and a separate helper rather than a tightening of that one, because
|
|
93
|
+
* the shapes already validated by it are not in this change's scope. Where a
|
|
94
|
+
* field is a fact a later reader has to act on (an exact version, a pointer to
|
|
95
|
+
* evidence), a blank is the same defect as an absence and is refused as one.
|
|
96
|
+
*/
|
|
97
|
+
export const nonBlankString = (problems, field, value) => {
|
|
98
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
99
|
+
problems.push({ field, message: `must be a non-blank string, got ${quote(value)}` });
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* A real calendar date, `T`, time to the second (fractions allowed), and an
|
|
106
|
+
* explicit zone.
|
|
107
|
+
*
|
|
108
|
+
* One spelling of one fact (`rules/invariants.md`, "One mechanism, one
|
|
109
|
+
* implementation"). Its three readers are `./decision-record.ts`
|
|
110
|
+
* (`recordedAt`), `./evidence-matrix.ts` (`observedAt`) and `./coverage.ts`
|
|
111
|
+
* (`verifiedAt`, through `requireTimestamp`), so a bare date is refused the
|
|
112
|
+
* same way whichever of them is validating — including lexically shaped but
|
|
113
|
+
* impossible dates — `packages/cli/test/policy-coverage.test.ts`
|
|
114
|
+
* › "refuses the probe timestamp %j, which is exactly what the shared ISO-8601
|
|
115
|
+
* pattern refuses" imports this pattern rather than restating it, so the two
|
|
116
|
+
* sides cannot drift apart. A timestamp is always supplied by the caller —
|
|
117
|
+
* nothing under this directory reads a clock.
|
|
118
|
+
*/
|
|
119
|
+
const ISO_8601_SHAPE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/;
|
|
120
|
+
/**
|
|
121
|
+
* A date-time shape whose `test` also proves the named calendar instant exists.
|
|
122
|
+
* Keeping the semantic check behind the same exported predicate prevents the
|
|
123
|
+
* coverage, decision-record and evidence-row validators from drifting apart.
|
|
124
|
+
*/
|
|
125
|
+
export const ISO_8601 = {
|
|
126
|
+
test(value) {
|
|
127
|
+
const match = ISO_8601_SHAPE.exec(value);
|
|
128
|
+
if (match === null)
|
|
129
|
+
return false;
|
|
130
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, zoneHour, zoneMinute] = match;
|
|
131
|
+
const year = Number(yearText);
|
|
132
|
+
const month = Number(monthText);
|
|
133
|
+
const day = Number(dayText);
|
|
134
|
+
const hour = Number(hourText);
|
|
135
|
+
const minute = Number(minuteText);
|
|
136
|
+
const second = Number(secondText);
|
|
137
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
138
|
+
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
139
|
+
return (month >= 1 &&
|
|
140
|
+
month <= 12 &&
|
|
141
|
+
day >= 1 &&
|
|
142
|
+
day <= daysInMonth[month - 1] &&
|
|
143
|
+
hour <= 23 &&
|
|
144
|
+
minute <= 59 &&
|
|
145
|
+
second <= 59 &&
|
|
146
|
+
(zoneHour === undefined || (Number(zoneHour) <= 23 && Number(zoneMinute) <= 59)));
|
|
147
|
+
},
|
|
148
|
+
};
|
|
41
149
|
/** Refuse a string outside a closed vocabulary, quoting the offending value. */
|
|
42
150
|
export const member = (problems, field, value, vocabulary) => {
|
|
43
151
|
if (typeof value === 'string' && vocabulary.includes(value))
|
|
@@ -80,3 +188,88 @@ export const matching = (problems, field, value, pattern, expected) => {
|
|
|
80
188
|
problems.push({ field, message: `must be ${expected}, got ${quote(value)}` });
|
|
81
189
|
return false;
|
|
82
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* A version that names one build rather than a set of them.
|
|
193
|
+
*
|
|
194
|
+
* One spelling of one fact: `./evidence-matrix.ts` refuses a matrix row on it
|
|
195
|
+
* and `./coverage.ts` refuses a surface identity on it, so "the exact version
|
|
196
|
+
* observed" means the same thing wherever it is written.
|
|
197
|
+
*
|
|
198
|
+
* 🔴 This is an ALLOWLIST, and it replaced a denylist that could not be
|
|
199
|
+
* finished. The denylist refused five vague words, the range operator
|
|
200
|
+
* characters, a wildcard component and two npm range spellings — and accepted
|
|
201
|
+
* `main`, `master`, `stable`, `next`, `nightly`, `dev`, `edge`, `canary` and
|
|
202
|
+
* `1.2.3 or 2.0.0`, because none of them is any of those things. A moving label
|
|
203
|
+
* is not a shape you can enumerate: every branch name a harness ever publishes
|
|
204
|
+
* from is a new entry, added by whoever notices, which is nobody. So the check
|
|
205
|
+
* asks what a build identifier LOOKS LIKE instead of what a moving target is
|
|
206
|
+
* called, and a word the grammar does not describe is refused whether or not
|
|
207
|
+
* anyone anticipated it.
|
|
208
|
+
*
|
|
209
|
+
* Two shapes are accepted, and they are the two this rig actually reads:
|
|
210
|
+
*
|
|
211
|
+
* - a build NUMBER — a dotted numeric version, optionally `v`-prefixed, with an
|
|
212
|
+
* optional pre-release or build-metadata suffix after `-` or `+`. That covers
|
|
213
|
+
* `2.0.14`, `v2.0.14`, `1.104.2`, a date build id like `2026-09-05`, a plain
|
|
214
|
+
* build id like `20260904.3`, and a suffix carrying any letter at all,
|
|
215
|
+
* including `1.0.0-X` and `0.0.0-fixture`;
|
|
216
|
+
* - a build ID — 7 to 64 hex characters, which is a git object id at every
|
|
217
|
+
* length git itself abbreviates to.
|
|
218
|
+
*
|
|
219
|
+
* The distinction that costs the most to get wrong is the one between a bare
|
|
220
|
+
* channel word and a suffix: `beta` names whatever is on that channel today and
|
|
221
|
+
* is refused, while `1.0.0-beta.2` names one build and is accepted. The grammar
|
|
222
|
+
* draws that line by requiring the number first — a suffix cannot stand alone.
|
|
223
|
+
*
|
|
224
|
+
* The value is matched AS GIVEN, with no trim. An earlier version validated
|
|
225
|
+
* `value.trim()` while both callers stored the value verbatim, so `" 2.0.14 "`,
|
|
226
|
+
* `"2.0.14\r\n"` and a BOM-prefixed form were accepted and then persisted with
|
|
227
|
+
* their padding: two rows for one build that compare unequal, and a version
|
|
228
|
+
* carrying a newline sitting in a field a report will one day render. What is
|
|
229
|
+
* checked and what is stored are now the same string: ›
|
|
230
|
+
* "refuses an evidence row whose harness version carries %s, because the row
|
|
231
|
+
* would store what it was not validated on" and › "refuses to probe against a
|
|
232
|
+
* harness version carrying %s, so two maps of one build cannot compare
|
|
233
|
+
* unequal", with › "still accepts the same build once %s is gone, because it is
|
|
234
|
+
* the padding that is refused and not the version" holding the other side.
|
|
235
|
+
*
|
|
236
|
+
* Refused, and now by construction rather than by enumeration: the vague words,
|
|
237
|
+
* every moving branch label, range OPERATORS, wildcard components, both npm
|
|
238
|
+
* range spellings, and any text carrying whitespace or a comma — which is what
|
|
239
|
+
* `1.2.3 or 2.0.0` and `1.2.3, 2.0.0` are. Both readers are pinned in
|
|
240
|
+
* `packages/cli/test/policy-coverage.test.ts` (absent in a generated rig) ›
|
|
241
|
+
* "refuses the harness version %j, because it names a moving label or more than
|
|
242
|
+
* one build" and › "refuses to probe against the harness version %j, because it
|
|
243
|
+
* names a moving label or more than one build", with the other direction held
|
|
244
|
+
* so the grammar cannot swallow a real build id: › "still accepts the harness
|
|
245
|
+
* version %j, because it names one build" and › "still probes against the
|
|
246
|
+
* harness version %j, because it names one build".
|
|
247
|
+
*/
|
|
248
|
+
const BUILD_NUMBER = /^v?\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?$/;
|
|
249
|
+
const BUILD_ID = /^[0-9a-fA-F]{7,64}$/;
|
|
250
|
+
/**
|
|
251
|
+
* What a refusal says is expected — one spelling, read by this module and by
|
|
252
|
+
* `./coverage.ts`, so the two cannot come to describe different grammars.
|
|
253
|
+
*/
|
|
254
|
+
export const EXACT_VERSION_EXPECTED = 'must name one immutable build: a version number like 2.0.14, ' +
|
|
255
|
+
'optionally v-prefixed and optionally carrying a -pre-release or +build suffix, ' +
|
|
256
|
+
'or a 7-to-64-character hex build id';
|
|
257
|
+
export const isExactVersion = (value) => BUILD_NUMBER.test(value) || BUILD_ID.test(value);
|
|
258
|
+
/**
|
|
259
|
+
* Refuse a version the grammar does not describe, quoting the value and naming
|
|
260
|
+
* the two shapes that are accepted.
|
|
261
|
+
*
|
|
262
|
+
* The message says what would be accepted rather than what was wrong, because
|
|
263
|
+
* the check is an allowlist: it also refuses `1.0.0.beta` and `2026_09_05`,
|
|
264
|
+
* which are neither a range nor a moving target, and the earlier message told
|
|
265
|
+
* their author they had written one. `./probe.ts` states the principle this
|
|
266
|
+
* trips over — a refusal naming a cause that did not occur sends an operator
|
|
267
|
+
* looking for something that is not there.
|
|
268
|
+
*/
|
|
269
|
+
export const exactVersion = (problems, field, value) => {
|
|
270
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
271
|
+
return;
|
|
272
|
+
if (!isExactVersion(value)) {
|
|
273
|
+
problems.push({ field, message: `${EXACT_VERSION_EXPECTED}; got ${quote(value)}` });
|
|
274
|
+
}
|
|
275
|
+
};
|
|
@@ -16,9 +16,13 @@
|
|
|
16
16
|
const closed = (values) => Object.freeze(values);
|
|
17
17
|
/**
|
|
18
18
|
* Whether a declared policy can actually be enforced on a given harness
|
|
19
|
-
* surface. `UNSUPPORTED` and `INTEGRATION-FAILED` never yield a silent
|
|
20
|
-
* a decision record carrying either
|
|
21
|
-
* (`./decision-record.ts`). The four states are defined here.
|
|
19
|
+
* surface. `UNSUPPORTED` and `INTEGRATION-FAILED` must never yield a silent
|
|
20
|
+
* pass: a decision record carrying either has to qualify its verdict
|
|
21
|
+
* `UNVERIFIABLE` (`./decision-record.ts`). The four states are defined here.
|
|
22
|
+
*
|
|
23
|
+
* ⚠ That is the rule, and the enforcement behind it is narrower than the rule
|
|
24
|
+
* — see `UNENFORCEABLE_STATES` below, which states the gap once for every
|
|
25
|
+
* reader of this file.
|
|
22
26
|
*/
|
|
23
27
|
export const CAPABILITY_STATES = closed([
|
|
24
28
|
'SUPPORTED',
|
|
@@ -26,6 +30,69 @@ export const CAPABILITY_STATES = closed([
|
|
|
26
30
|
'UNSUPPORTED',
|
|
27
31
|
'INTEGRATION-FAILED',
|
|
28
32
|
]);
|
|
33
|
+
/**
|
|
34
|
+
* The states under which no question was actually put to a working mechanism.
|
|
35
|
+
*
|
|
36
|
+
* One spelling of one fact (`rules/invariants.md`, "One mechanism, one
|
|
37
|
+
* implementation"): `./decision-record.ts` refuses an unqualified verdict
|
|
38
|
+
* carrying one of these, and `./coverage.ts` › `qualifierFor` returns
|
|
39
|
+
* `UNVERIFIABLE` for exactly the same set. Two copies would disagree, and the
|
|
40
|
+
* one nobody is looking at would be the one that let a silent pass through.
|
|
41
|
+
*
|
|
42
|
+
* ⚠ **"Refuses" now holds against a hand-built prototype too** — this note used
|
|
43
|
+
* to say the opposite, and this is the file a reader auditing that question
|
|
44
|
+
* lands on first, so it is corrected here rather than only where the fix landed.
|
|
45
|
+
* `./decision-record.ts` used to decide whether a verdict carries a qualifier
|
|
46
|
+
* with the `in` operator, so a verdict INHERITING one satisfied the check and
|
|
47
|
+
* then serialised without it. RP-153 closed that: every field of both
|
|
48
|
+
* `./decision-record.ts` and `./declaration.ts` is read through
|
|
49
|
+
* `carriesField`/`ownField`, the way `./probe.ts` and `./evidence-matrix.ts`
|
|
50
|
+
* already read theirs, and both uncarried shapes are pinned in
|
|
51
|
+
* `packages/cli/test/policy-declaration.test.ts` › "refuses an UNSUPPORTED
|
|
52
|
+
* record whose verdict qualifier is %s, because what it writes out is a silent
|
|
53
|
+
* pass" — `%s` as the `it.each` case declares it, so one grep lands on it.
|
|
54
|
+
*
|
|
55
|
+
* What remains is narrower and is NOT this sentence's subject: a value carrying
|
|
56
|
+
* a live accessor is validated on one read and serialised from another (RP-157),
|
|
57
|
+
* and an array HOLE serialises as `null` while the two `forEach` loops in
|
|
58
|
+
* `./decision-record.ts` skip it (RP-161) — those two only, since `members` in
|
|
59
|
+
* `./validation.ts` and `./probe.ts` iterate with `for…of`, which sees a hole as
|
|
60
|
+
* `undefined` and refuses it. `docs/decisions/capability-coverage.md`, "What
|
|
61
|
+
* this does NOT do", carries the current limits at length.
|
|
62
|
+
*
|
|
63
|
+
* It is deliberately NOT derived from a rank or an ordering. `coverage.ts`
|
|
64
|
+
* carries an enforcement ordering for deciding what counts as a downgrade;
|
|
65
|
+
* keying verdict qualification off that would mean a future re-rank silently
|
|
66
|
+
* changed which verdicts are unverifiable.
|
|
67
|
+
*/
|
|
68
|
+
export const UNENFORCEABLE_STATES = closed(['UNSUPPORTED', 'INTEGRATION-FAILED']);
|
|
69
|
+
/**
|
|
70
|
+
* The two ways a capability status is established, and the only two.
|
|
71
|
+
*
|
|
72
|
+
* `probe` is one active read of the surface's own wiring, taken when the
|
|
73
|
+
* surface changes (`PROBE_TRIGGERS`). `traffic` is passive: an operation that
|
|
74
|
+
* was expected to produce an observable signal, and what it actually produced.
|
|
75
|
+
* There is deliberately no third source meaning "time passed" — silence is
|
|
76
|
+
* absence of evidence, not evidence of absence.
|
|
77
|
+
*/
|
|
78
|
+
export const VERIFICATION_SOURCES = closed(['probe', 'traffic']);
|
|
79
|
+
/**
|
|
80
|
+
* The occasions on which a surface is probed. Every member is an event on the
|
|
81
|
+
* surface; none of them is an interval.
|
|
82
|
+
*
|
|
83
|
+
* What this vocabulary does, exactly: `./coverage.ts` › `coverageFromProbe`
|
|
84
|
+
* takes a trigger as a required argument and refuses a word outside this list,
|
|
85
|
+
* so a probe must NAME its occasion and cannot name a schedule — ›
|
|
86
|
+
* "refuses the trigger %j, because the coverage contract accepts only a
|
|
87
|
+
* declared surface-change trigger".
|
|
88
|
+
*
|
|
89
|
+
* ⚠ What it does NOT do, stated because an earlier draft of this comment
|
|
90
|
+
* claimed it: nothing here stops a caller passing `'upgrade'` on a timer. The
|
|
91
|
+
* check refuses a LABEL outside the vocabulary, not the practice of probing
|
|
92
|
+
* periodically. `docs/decisions/capability-coverage.md` §1 says the same, and
|
|
93
|
+
* this file used to contradict it.
|
|
94
|
+
*/
|
|
95
|
+
export const PROBE_TRIGGERS = closed(['install', 'upgrade', 'registration', 'reconnect']);
|
|
29
96
|
/** The autonomy tiers of `rules/autonomy.md`; `never` is the tier the guards enforce. */
|
|
30
97
|
export const AUTONOMY_TIERS = closed(['tier-0', 'tier-1', 'tier-2', 'never']);
|
|
31
98
|
/** The operations a policy can apply to, named by what the agent does, not by a tool. */
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* shipped scripts, and `test/template/shell-tools.test.ts` holds that
|
|
20
20
|
* correspondence.
|
|
21
21
|
*/
|
|
22
|
-
import { SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
22
|
+
import { SHARED_HOOK_ROOT_ENV, SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
23
23
|
const EVENT_OF = {
|
|
24
24
|
'before-operation': 'PreToolUse',
|
|
25
25
|
};
|
|
@@ -31,6 +31,14 @@ export const nativeSurfaceOf = (policy) => ({
|
|
|
31
31
|
event: EVENT_OF[policy.timing],
|
|
32
32
|
matcher: policy.operations.map((operation) => MATCHER_OF[operation]).join('|'),
|
|
33
33
|
hookPath: `${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`,
|
|
34
|
+
// The exact command this harness generates for a hook. The probe compares
|
|
35
|
+
// against this rather than parsing what it finds, so this string and the one
|
|
36
|
+
// in the shipped snapshot must agree — pinned in both directions by
|
|
37
|
+
// `test/template/policy-coverage.test.ts` (absent in a generated rig) ›
|
|
38
|
+
// "the %s snapshot wires %s with exactly the command that adapter generates".
|
|
39
|
+
commands: {
|
|
40
|
+
command: [`node "$${SHARED_HOOK_ROOT_ENV}/${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs"`],
|
|
41
|
+
},
|
|
34
42
|
});
|
|
35
43
|
export const claudeAdapter = Object.freeze({
|
|
36
44
|
harness: 'claude',
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* restated, for the same reason the snapshot is derived rather than
|
|
21
21
|
* hand-written: one spelling of one fact.
|
|
22
22
|
*/
|
|
23
|
-
import { SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
23
|
+
import { SHARED_HOOK_ROOT_ENV, SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
24
24
|
const EVENT_OF = {
|
|
25
25
|
'before-operation': 'PreToolUse',
|
|
26
26
|
};
|
|
@@ -28,10 +28,57 @@ const MATCHER_OF = {
|
|
|
28
28
|
'file-edit': 'Write|Edit|MultiEdit|NotebookEdit|apply_patch',
|
|
29
29
|
'shell-command': 'Bash|PowerShell',
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* The Windows spelling this harness generates for a hook.
|
|
33
|
+
*
|
|
34
|
+
* ⚠ A SECOND implementation of the wrapper that `scripts/sync-codex-adapter.mjs`
|
|
35
|
+
* writes into the shipped file, and deliberately so: that script is a build tool
|
|
36
|
+
* outside the published package, and this module may not import it. The two are
|
|
37
|
+
* held equal by a correspondence check that goes red in BOTH directions —
|
|
38
|
+
* `test/template/policy-coverage.test.ts` (absent in a generated rig) › "the %s
|
|
39
|
+
* snapshot wires %s with exactly the spelling that adapter generates, in every
|
|
40
|
+
* field it generates one for" — which is what `rules/invariants.md` ("One
|
|
41
|
+
* mechanism, one implementation") requires of a copy that has to stay.
|
|
42
|
+
*
|
|
43
|
+
* The wrapper exists because PowerShell owns its stdin, so the hook would
|
|
44
|
+
* receive an empty stream; it copies the original bytes into the child instead
|
|
45
|
+
* of re-encoding them.
|
|
46
|
+
*/
|
|
47
|
+
const windowsCommand = (hookPath) => {
|
|
48
|
+
const script = [
|
|
49
|
+
"$ErrorActionPreference = 'Stop'",
|
|
50
|
+
'$repoRoot = git rev-parse --show-toplevel',
|
|
51
|
+
'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }',
|
|
52
|
+
`$env:${SHARED_HOOK_ROOT_ENV} = $repoRoot`,
|
|
53
|
+
`$hookPath = Join-Path $repoRoot '${hookPath}'`,
|
|
54
|
+
'$startInfo = New-Object System.Diagnostics.ProcessStartInfo',
|
|
55
|
+
"$startInfo.FileName = 'node'",
|
|
56
|
+
"$startInfo.Arguments = '\"' + $hookPath + '\"'",
|
|
57
|
+
'$startInfo.UseShellExecute = $false',
|
|
58
|
+
'$startInfo.RedirectStandardInput = $true',
|
|
59
|
+
'$child = [System.Diagnostics.Process]::Start($startInfo)',
|
|
60
|
+
'[Console]::OpenStandardInput().CopyTo($child.StandardInput.BaseStream)',
|
|
61
|
+
'$child.StandardInput.Close()',
|
|
62
|
+
'$child.WaitForExit()',
|
|
63
|
+
'exit $child.ExitCode',
|
|
64
|
+
].join('; ');
|
|
65
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
66
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
|
|
67
|
+
};
|
|
31
68
|
export const nativeSurfaceOf = (policy) => ({
|
|
32
69
|
event: EVENT_OF[policy.timing],
|
|
33
70
|
matcher: policy.operations.map((operation) => MATCHER_OF[operation]).join('|'),
|
|
34
71
|
hookPath: `${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`,
|
|
72
|
+
// Both commands this harness generates. It runs the first on POSIX and the
|
|
73
|
+
// second on Windows, so the probe has to know both: replacing only the
|
|
74
|
+
// Windows spelling in the shipped file used to leave every policy reading
|
|
75
|
+
// SUPPORTED while the guard no longer ran there.
|
|
76
|
+
commands: {
|
|
77
|
+
command: [
|
|
78
|
+
`repoRoot="$(git rev-parse --show-toplevel)" && ${SHARED_HOOK_ROOT_ENV}="$repoRoot" node "$repoRoot/${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs"`,
|
|
79
|
+
],
|
|
80
|
+
commandWindows: [windowsCommand(`${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`)],
|
|
81
|
+
},
|
|
35
82
|
});
|
|
36
83
|
export const codexAdapter = Object.freeze({
|
|
37
84
|
harness: 'codex',
|
|
@@ -8,3 +8,21 @@
|
|
|
8
8
|
* implementation").
|
|
9
9
|
*/
|
|
10
10
|
export const SHARED_HOOKS_DIR = '.claude/hooks';
|
|
11
|
+
/**
|
|
12
|
+
* The environment variable a shared hook reads when it needs the repository
|
|
13
|
+
* root, and therefore the one each harness sets when it runs one.
|
|
14
|
+
*
|
|
15
|
+
* Not every hook needs it: of the eight this rig ships, `guard-rulebook` and
|
|
16
|
+
* `guard-secret-file` read it, both falling back to the working directory.
|
|
17
|
+
* The variable is still part of the wiring contract, because the harness sets
|
|
18
|
+
* it for whichever hook it runs.
|
|
19
|
+
*
|
|
20
|
+
* It carries a harness's name for the same historical reason `.claude/hooks`
|
|
21
|
+
* does — the hooks are shared, so both harnesses speak this one variable — and
|
|
22
|
+
* this module is the one adapter-side file allowed to spell that name for both
|
|
23
|
+
* (`test/template/policy-declaration.test.ts` › "claude is named only by its
|
|
24
|
+
* own adapter, the shared hooks directory and the adapter index"). Stating it
|
|
25
|
+
* here is what lets the other harness's adapter build its generated command
|
|
26
|
+
* without naming a harness that is not its own.
|
|
27
|
+
*/
|
|
28
|
+
export const SHARED_HOOK_ROOT_ENV = 'CLAUDE_PROJECT_DIR';
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The policy declaration, registry, decision-record schema and harness
|
|
3
|
-
* adapters (RP-76)
|
|
4
|
-
*
|
|
3
|
+
* adapters (RP-76), plus the capability & degradation contract that says what
|
|
4
|
+
* each of them is worth on a given surface (RP-36).
|
|
5
|
+
*
|
|
6
|
+
* Library surface only — nothing here is reached by the CLI commands yet;
|
|
7
|
+
* emitting decision records at runtime, and rendering the coverage report in
|
|
8
|
+
* `doctor`, are separate tasks.
|
|
5
9
|
*/
|
|
6
10
|
export * from './core/vocabulary.js';
|
|
7
11
|
export * from './core/declaration.js';
|
|
8
12
|
export * from './core/registry.js';
|
|
9
13
|
export * from './core/decision-record.js';
|
|
14
|
+
export * from './core/probe.js';
|
|
15
|
+
export * from './core/coverage.js';
|
|
16
|
+
export * from './core/evidence-matrix.js';
|
|
10
17
|
export * from './harness/index.js';
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
name: cdk-diff-reviewer
|
|
3
3
|
description: Reviews an infrastructure change via `cdk diff` BEFORE any deploy. MUST run on every change under infra/ — a deploy without this review is a Never-tier action. Read-only; findings gate the deploy.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: claude-opus-5
|
|
6
|
+
effort: high
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
You review what a deploy would actually do to running infrastructure. Your
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
name = "cdk-diff-reviewer"
|
|
2
2
|
description = "Reviews an infrastructure change via `cdk diff` BEFORE any deploy. MUST run on every change under infra/ — a deploy without this review is a Never-tier action. Read-only; findings gate the deploy."
|
|
3
|
+
model = "gpt-5.6-sol"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
3
5
|
sandbox_mode = "read-only"
|
|
4
6
|
developer_instructions = "You review what a deploy would actually do to running infrastructure. Your\ninput is the change under `infra/` **and** the synthesized diff (`cdk diff`,\nrun it yourself); your output is a verdict. You never fix and never deploy.\n\n## How you work\n\n1. Run `cdk diff` (and read the changed `infra/` sources for intent). The diff\n is the truth: review what CloudFormation will do, not what the TypeScript\n looks like it does.\n2. Walk every resource change and flag it **by named rule** (below). Findings\n come as **BLOCKERS first, then nits** — one list each, with the resource\n and the rule it violates.\n3. Your message IS the review, not a summary of it: every finding carries the\n resource, the change, the rule, and the smallest fix. End with the verdict\n block below — `SHIP` where `DEPLOY: OK` used to be, `HOLD` where\n `DEPLOY: BLOCKED` did.\n\n## Named rules — blockers\n\n- **IAM broadening.** Any policy gaining actions, resources widening to `*`,\n or a grant that outruns what a usecase does today. Least privilege is added\n in the same PR as the need, never \"for later\".\n- **Data loss paths.** A stateful resource (table, bucket, queue) being\n replaced, deleted, or flipping its RemovalPolicy toward DESTROY.\n Logical-id renames on stateful resources are replacements in disguise.\n- **Safety-net removal.** A DLQ detached, an alarm deleted or loosened, a\n retry budget widened to infinity, a dead-letter retention shortened.\n- **Blast-radius growth.** New public surface (endpoints, permissions to\n external principals), broadened network access, cross-stack exports that\n make future changes harder to reverse.\n- **Cost-relevant flips.** On-demand → provisioned capacity, log retention to\n \"forever\", memory/timeout jumps with no stated reason.\n\n## Nits (report, do not block)\n\nNaming drift, missing descriptions, constructs that could use the narrower\ngrant helper, duplication between stacks.\n\n## Boundaries\n\n- Read-only: you run `cdk diff` and read code; you never run `cdk deploy`,\n never edit files, never mutate AWS state.\n- An empty diff is a real finding too — say \"no infrastructure change\" and\n return `SHIP`, so the gate leaves a trace either way.\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.\n\n```json\n{\n \"gate\": \"cdk-diff-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"infra/lib/api-stack.ts\",\n \"line\": 88,\n \"rule\": \"data loss\",\n \"note\": \"the table's RemovalPolicy went to DESTROY — replacement drops it\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"cdk diff against the deployed stage\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP` (nothing blocking, including an empty diff), `HOLD`, or\n `NOT_APPLICABLE` when the change touches no infrastructure at all.\n- Every blocker names the `rule` it violates, with `file` and `line` when it has\n 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> cdk-diff-reviewer` is what\n refuses them.\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."
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"claudeModels": {
|
|
3
|
+
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
4
|
+
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"]
|
|
5
|
+
},
|
|
6
|
+
"unnamed": {
|
|
7
|
+
"claude": { "model": "claude-sonnet-5" },
|
|
8
|
+
"codex": { "model": "gpt-5.6-terra", "effort": "medium" }
|
|
9
|
+
},
|
|
10
|
+
"roles": {
|
|
11
|
+
"test-writer": {
|
|
12
|
+
"claude": { "model": "claude-sonnet-5", "effort": "high" },
|
|
13
|
+
"codex": { "model": "gpt-5.6-terra", "effort": "high" }
|
|
14
|
+
},
|
|
15
|
+
"prose-reviewer": {
|
|
16
|
+
"claude": { "model": "claude-sonnet-5", "effort": "high" },
|
|
17
|
+
"codex": { "model": "gpt-5.6-terra", "effort": "high" }
|
|
18
|
+
},
|
|
19
|
+
"code-reviewer": {
|
|
20
|
+
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
21
|
+
"codex": { "model": "gpt-5.6-sol", "effort": "high" }
|
|
22
|
+
},
|
|
23
|
+
"security-scanner": {
|
|
24
|
+
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
25
|
+
"codex": { "model": "gpt-5.6-sol", "effort": "high" }
|
|
26
|
+
},
|
|
27
|
+
"cdk-diff-reviewer": {
|
|
28
|
+
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
29
|
+
"codex": { "model": "gpt-5.6-sol", "effort": "high" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -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
|
|
@@ -2,6 +2,8 @@
|
|
|
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 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.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: claude-opus-5
|
|
6
|
+
effort: high
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
You review changes. You do not fix them — you report, with file:line
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
name: prose-reviewer
|
|
3
3
|
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.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: claude-sonnet-5
|
|
6
|
+
effort: high
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
In this project the prose **is** the implementation. A rule file is what an agent
|
|
@@ -2,6 +2,8 @@
|
|
|
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
|
|
6
|
+
effort: high
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
You are the security gate. You run on changes in sensitive territory and your
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
name: test-writer
|
|
3
3
|
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.
|
|
4
4
|
tools: Read, Grep, Glob, Write, Edit, Bash
|
|
5
|
+
model: claude-sonnet-5
|
|
6
|
+
effort: high
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
You write tests that define behavior which does not exist yet. You are the Red
|