create-agent-rig 0.7.0 → 0.8.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 +164 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/cli/dist/policy/core/adapter.js +18 -0
- package/packages/cli/dist/policy/core/decision-record.js +201 -0
- package/packages/cli/dist/policy/core/declaration.js +86 -0
- package/packages/cli/dist/policy/core/registry.js +115 -0
- package/packages/cli/dist/policy/core/validation.js +82 -0
- package/packages/cli/dist/policy/core/vocabulary.js +56 -0
- package/packages/cli/dist/policy/harness/claude.js +39 -0
- package/packages/cli/dist/policy/harness/codex.js +40 -0
- package/packages/cli/dist/policy/harness/index.js +15 -0
- package/packages/cli/dist/policy/harness/shared-hooks.js +10 -0
- package/packages/cli/dist/policy/index.js +10 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +7 -3
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +70 -9
- package/templates/agent-os/universal/.claude/rules/autonomy.md +17 -7
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +380 -19
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +7 -3
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +70 -9
- package/templates/hash-history.json +82 -24
- package/templates/release-ledger.json +3 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small set of checks the declaration and the decision record share.
|
|
3
|
+
*
|
|
4
|
+
* Every check appends to a problem list instead of throwing, so a caller sees
|
|
5
|
+
* every defect of a record at once — `validateDeclaration` › "reports every
|
|
6
|
+
* problem at once rather than stopping at the first" in
|
|
7
|
+
* `packages/cli/test/policy-declaration.test.ts`. Each message that refuses an
|
|
8
|
+
* enumerated value quotes the value, because a refusal that names the field
|
|
9
|
+
* and not the word leaves the caller guessing which of two spellings it sent.
|
|
10
|
+
*/
|
|
11
|
+
export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
const quote = (value) => {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.stringify(value) ?? String(value);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const list = (vocabulary) => vocabulary.map(quote).join(', ');
|
|
21
|
+
/**
|
|
22
|
+
* Refuse a key the shape does not declare — the shape is closed on purpose.
|
|
23
|
+
* A nested shape passes its own field name as `prefix`, so the problem names
|
|
24
|
+
* `verdict.severity` rather than a bare `severity` the caller cannot place.
|
|
25
|
+
*/
|
|
26
|
+
export const unknownKeys = (problems, input, known, prefix = '') => {
|
|
27
|
+
for (const key of Object.keys(input)) {
|
|
28
|
+
if (!known.includes(key)) {
|
|
29
|
+
problems.push({ field: prefix === '' ? key : `${prefix}.${key}`, message: 'unknown field' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Refuse a string that is absent, not a string, or empty. */
|
|
34
|
+
export const nonEmptyString = (problems, field, value) => {
|
|
35
|
+
if (typeof value !== 'string' || value === '') {
|
|
36
|
+
problems.push({ field, message: `must be a non-empty string, got ${quote(value)}` });
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
/** Refuse a string outside a closed vocabulary, quoting the offending value. */
|
|
42
|
+
export const member = (problems, field, value, vocabulary) => {
|
|
43
|
+
if (typeof value === 'string' && vocabulary.includes(value))
|
|
44
|
+
return true;
|
|
45
|
+
problems.push({ field, message: `${quote(value)} is not one of ${list(vocabulary)}` });
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Refuse a list that is not an array, carries a value outside the vocabulary,
|
|
50
|
+
* repeats one, or — when `nonEmpty` — is empty.
|
|
51
|
+
*/
|
|
52
|
+
export const members = (problems, field, value, vocabulary, { nonEmpty }) => {
|
|
53
|
+
if (!Array.isArray(value)) {
|
|
54
|
+
problems.push({ field, message: `must be a list, got ${quote(value)}` });
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
let clean = true;
|
|
58
|
+
if (nonEmpty && value.length === 0) {
|
|
59
|
+
problems.push({ field, message: 'must not be empty' });
|
|
60
|
+
clean = false;
|
|
61
|
+
}
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const entry of value) {
|
|
64
|
+
if (!member(problems, field, entry, vocabulary)) {
|
|
65
|
+
clean = false;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (seen.has(entry)) {
|
|
69
|
+
problems.push({ field, message: `${quote(entry)} is listed twice` });
|
|
70
|
+
clean = false;
|
|
71
|
+
}
|
|
72
|
+
seen.add(entry);
|
|
73
|
+
}
|
|
74
|
+
return clean;
|
|
75
|
+
};
|
|
76
|
+
/** Refuse a string that does not match the pattern, saying what shape was expected. */
|
|
77
|
+
export const matching = (problems, field, value, pattern, expected) => {
|
|
78
|
+
if (typeof value === 'string' && pattern.test(value))
|
|
79
|
+
return true;
|
|
80
|
+
problems.push({ field, message: `must be ${expected}, got ${quote(value)}` });
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed vocabularies of the policy declaration (RP-76).
|
|
3
|
+
*
|
|
4
|
+
* Every value a declaration or a decision record may carry in an enumerated
|
|
5
|
+
* field is listed here and nowhere else. Adding a value is a schema edit — a
|
|
6
|
+
* change to this file plus the test that pins the list — never a string a
|
|
7
|
+
* caller invents at runtime. That is what makes a record auditable: an unknown
|
|
8
|
+
* word is refused rather than read as something close to a known one.
|
|
9
|
+
*
|
|
10
|
+
* Harness-neutral by construction: nothing here names a harness, a vendor, a
|
|
11
|
+
* native tool or a native path. The per-harness spellings live in
|
|
12
|
+
* `../harness/`, and `test/template/policy-declaration.test.ts` › "no file
|
|
13
|
+
* under src/policy/core mentions a harness, a vendor, a native tool or a native
|
|
14
|
+
* path" is what keeps them out of here.
|
|
15
|
+
*/
|
|
16
|
+
const closed = (values) => Object.freeze(values);
|
|
17
|
+
/**
|
|
18
|
+
* Whether a declared policy can actually be enforced on a given harness
|
|
19
|
+
* surface. `UNSUPPORTED` and `INTEGRATION-FAILED` never yield a silent pass:
|
|
20
|
+
* a decision record carrying either must qualify its verdict `UNVERIFIABLE`
|
|
21
|
+
* (`./decision-record.ts`). The four states are defined here.
|
|
22
|
+
*/
|
|
23
|
+
export const CAPABILITY_STATES = closed([
|
|
24
|
+
'SUPPORTED',
|
|
25
|
+
'DEGRADED',
|
|
26
|
+
'UNSUPPORTED',
|
|
27
|
+
'INTEGRATION-FAILED',
|
|
28
|
+
]);
|
|
29
|
+
/** The autonomy tiers of `rules/autonomy.md`; `never` is the tier the guards enforce. */
|
|
30
|
+
export const AUTONOMY_TIERS = closed(['tier-0', 'tier-1', 'tier-2', 'never']);
|
|
31
|
+
/** The operations a policy can apply to, named by what the agent does, not by a tool. */
|
|
32
|
+
export const OPERATIONS = closed(['file-edit', 'shell-command']);
|
|
33
|
+
/** When the mechanism decides, relative to the operation it judges. */
|
|
34
|
+
export const ENFORCEMENT_TIMINGS = closed(['before-operation']);
|
|
35
|
+
/** What a harness must provide for the mechanism to run at all. */
|
|
36
|
+
export const HARNESS_CAPABILITIES = closed(['pre-operation-hook']);
|
|
37
|
+
/**
|
|
38
|
+
* The three outcomes a guard can reach: allow, block, or refuse to inspect —
|
|
39
|
+
* the third being neither a match nor an error (`rules/invariants.md`,
|
|
40
|
+
* "Refusing to inspect is a third outcome").
|
|
41
|
+
*/
|
|
42
|
+
export const DECISION_OUTCOMES = closed(['allow', 'block', 'refuse-to-inspect']);
|
|
43
|
+
/** What a mechanism does when it cannot decide: let the operation through, or stop it. */
|
|
44
|
+
export const FAILURE_SEMANTICS = closed(['fail-open', 'fail-closed']);
|
|
45
|
+
/** The kinds of evidence a decision record may carry, and a policy may require. */
|
|
46
|
+
export const EVIDENCE_KINDS = closed(['exit-code', 'diagnostic-text', 'test-pointer']);
|
|
47
|
+
/** How a mechanism treats what it matched when it reports: verbatim, or omitted. */
|
|
48
|
+
export const REDACTION_RULES = closed(['none', 'omit-matched-values']);
|
|
49
|
+
/** Where a policy is in its life; a `retired` policy is no longer offered by the registry. */
|
|
50
|
+
export const LIFECYCLE_STATES = closed(['active', 'deprecated', 'retired']);
|
|
51
|
+
/**
|
|
52
|
+
* The two ways a verdict can say "this word is weaker than it looks": the
|
|
53
|
+
* question could not be put (`UNVERIFIABLE`), or nothing backs the answer
|
|
54
|
+
* (`UNMEASURED`). Either one must carry a reason.
|
|
55
|
+
*/
|
|
56
|
+
export const VERDICT_QUALIFIERS = closed(['UNVERIFIABLE', 'UNMEASURED']);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code adapter: a declaration → the native hook surface Claude Code
|
|
3
|
+
* wires for it. The authoring surface of this rulebook is Claude-shaped
|
|
4
|
+
* (`CLAUDE.md`, "One operating system, two harnesses"), so the hook files
|
|
5
|
+
* themselves live in the historical directory `./shared-hooks.ts` names and are
|
|
6
|
+
* shared by every harness.
|
|
7
|
+
*
|
|
8
|
+
* What is native here and nowhere in the core: the `PreToolUse` event, the
|
|
9
|
+
* tool names in the matchers, and the snapshot path. The matcher strings are
|
|
10
|
+
* the ones `.claude/settings.json` carries, and the correspondence test holds
|
|
11
|
+
* the two to the SAME tool set, not a subset: a tool dropped here or gained
|
|
12
|
+
* there is reported for this adapter —
|
|
13
|
+
* `test/template/policy-declaration.test.ts` › "reports the no-verify policy
|
|
14
|
+
* on %s when the shell matcher loses PowerShell (mutation: matcher)", › "reports
|
|
15
|
+
* the no-verify policy on %s when the snapshot gains a tool the adapter does
|
|
16
|
+
* not name (mutation: widened snapshot)" and › "reports a policy on %s whose
|
|
17
|
+
* adapter matcher drops a tool the snapshot still wires (mutation: narrowed
|
|
18
|
+
* adapter)". The shell matcher's tool set is owned by `shell-tools.mjs` in the
|
|
19
|
+
* shipped scripts, and `test/template/shell-tools.test.ts` holds that
|
|
20
|
+
* correspondence.
|
|
21
|
+
*/
|
|
22
|
+
import { SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
23
|
+
const EVENT_OF = {
|
|
24
|
+
'before-operation': 'PreToolUse',
|
|
25
|
+
};
|
|
26
|
+
const MATCHER_OF = {
|
|
27
|
+
'file-edit': 'Write|Edit|MultiEdit|NotebookEdit|apply_patch',
|
|
28
|
+
'shell-command': 'Bash|PowerShell',
|
|
29
|
+
};
|
|
30
|
+
export const nativeSurfaceOf = (policy) => ({
|
|
31
|
+
event: EVENT_OF[policy.timing],
|
|
32
|
+
matcher: policy.operations.map((operation) => MATCHER_OF[operation]).join('|'),
|
|
33
|
+
hookPath: `${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`,
|
|
34
|
+
});
|
|
35
|
+
export const claudeAdapter = Object.freeze({
|
|
36
|
+
harness: 'claude',
|
|
37
|
+
surfaceFile: '.claude/settings.json',
|
|
38
|
+
nativeSurfaceOf,
|
|
39
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Codex adapter: a declaration → the native hook surface Codex wires for
|
|
3
|
+
* it. Codex's hook wiring (`.codex/hooks.json`) is DERIVED from the authoring
|
|
4
|
+
* harness's snapshot by `scripts/sync-codex-adapter.mjs`
|
|
5
|
+
* (`docs/decisions/codex-adapter.md`): it keeps the authoring harness's matcher
|
|
6
|
+
* spellings, adds the canonical edit tool `apply_patch`, and runs the same hook
|
|
7
|
+
* files from the shared hooks directory. So the strings below coincide with the
|
|
8
|
+
* other adapter's today — by derivation, not by accident — and each adapter
|
|
9
|
+
* still owns its own spelling. The correspondence test holds this adapter's
|
|
10
|
+
* matcher and the derived snapshot's to the SAME tool set, so a tool the
|
|
11
|
+
* snapshot gains or loses, or one this adapter drops, is reported for this
|
|
12
|
+
* adapter alone — `test/template/policy-declaration.test.ts` › "reports the
|
|
13
|
+
* no-verify policy on %s when the shell matcher loses PowerShell (mutation:
|
|
14
|
+
* matcher)", › "reports the no-verify policy on %s when the snapshot gains a
|
|
15
|
+
* tool the adapter does not name (mutation: widened snapshot)" and › "reports
|
|
16
|
+
* a policy on %s whose adapter matcher drops a tool the snapshot still wires
|
|
17
|
+
* (mutation: narrowed adapter)".
|
|
18
|
+
*
|
|
19
|
+
* The shared hooks directory is imported from `./shared-hooks.ts` rather than
|
|
20
|
+
* restated, for the same reason the snapshot is derived rather than
|
|
21
|
+
* hand-written: one spelling of one fact.
|
|
22
|
+
*/
|
|
23
|
+
import { SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
24
|
+
const EVENT_OF = {
|
|
25
|
+
'before-operation': 'PreToolUse',
|
|
26
|
+
};
|
|
27
|
+
const MATCHER_OF = {
|
|
28
|
+
'file-edit': 'Write|Edit|MultiEdit|NotebookEdit|apply_patch',
|
|
29
|
+
'shell-command': 'Bash|PowerShell',
|
|
30
|
+
};
|
|
31
|
+
export const nativeSurfaceOf = (policy) => ({
|
|
32
|
+
event: EVENT_OF[policy.timing],
|
|
33
|
+
matcher: policy.operations.map((operation) => MATCHER_OF[operation]).join('|'),
|
|
34
|
+
hookPath: `${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`,
|
|
35
|
+
});
|
|
36
|
+
export const codexAdapter = Object.freeze({
|
|
37
|
+
harness: 'codex',
|
|
38
|
+
surfaceFile: '.codex/hooks.json',
|
|
39
|
+
nativeSurfaceOf,
|
|
40
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness adapters this generator knows. Adding a harness is one new
|
|
3
|
+
* module beside these two and one entry in the list below — nothing in
|
|
4
|
+
* `../core/` changes, which `test/template/policy-declaration.test.ts` ›
|
|
5
|
+
* "codex is named only by its own adapter and the adapter index" pins by
|
|
6
|
+
* naming exactly the files that may mention each harness.
|
|
7
|
+
*/
|
|
8
|
+
import { claudeAdapter } from './claude.js';
|
|
9
|
+
import { codexAdapter } from './codex.js';
|
|
10
|
+
export { claudeAdapter, codexAdapter };
|
|
11
|
+
export { SHARED_HOOKS_DIR } from './shared-hooks.js';
|
|
12
|
+
export const HARNESS_ADAPTERS = Object.freeze([
|
|
13
|
+
claudeAdapter,
|
|
14
|
+
codexAdapter,
|
|
15
|
+
]);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one directory every harness runs its hook files from. The rulebook's
|
|
3
|
+
* `.claude/` directory keeps its historical name but holds the shared rules,
|
|
4
|
+
* hooks, scripts and agent specifications for both harnesses (`CLAUDE.md`,
|
|
5
|
+
* "One operating system, two harnesses"), so a hook path is the same string
|
|
6
|
+
* whichever adapter names it. Stated once, here, and imported by each adapter
|
|
7
|
+
* — one spelling of one fact (`rules/invariants.md`, "One mechanism, one
|
|
8
|
+
* implementation").
|
|
9
|
+
*/
|
|
10
|
+
export const SHARED_HOOKS_DIR = '.claude/hooks';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy declaration, registry, decision-record schema and harness
|
|
3
|
+
* adapters (RP-76). Library surface only — nothing here is reached by the CLI
|
|
4
|
+
* commands yet; emitting decision records at runtime is a separate task.
|
|
5
|
+
*/
|
|
6
|
+
export * from './core/vocabulary.js';
|
|
7
|
+
export * from './core/declaration.js';
|
|
8
|
+
export * from './core/registry.js';
|
|
9
|
+
export * from './core/decision-record.js';
|
|
10
|
+
export * from './harness/index.js';
|
|
@@ -153,9 +153,13 @@ node .claude/scripts/unattended-flag.mjs on --root "$PWD" --item <item-id> --run
|
|
|
153
153
|
|
|
154
154
|
`guard-rulebook` reads it (`.claude/rules/autonomy.md`, "Never"): with the flag
|
|
155
155
|
on, a Write/Edit/MultiEdit/NotebookEdit/`apply_patch` under the generated
|
|
156
|
-
rulebook
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
rulebook is refused unless its
|
|
157
|
+
path starts with an allowed prefix. 🔴 **Which paths that covers is
|
|
158
|
+
`RULEBOOK_PREFIXES` in `.claude/scripts/unattended-flag.mjs`** — read it before
|
|
159
|
+
composing an allow-list, rather than working from a summary here. A summary is a
|
|
160
|
+
second copy, and the one that used to sit in this sentence had gone stale against
|
|
161
|
+
the set it described. One fact the set cannot carry, so it is stated: the board
|
|
162
|
+
selector is the one always-refused
|
|
159
163
|
exception and cannot be admitted by an allow-list. With no flag the guard does nothing. An
|
|
160
164
|
item that needs a rulebook path names it here — a decision made at claim
|
|
161
165
|
time, never a default — and the stop step below turns the flag off. Pinned in
|
|
@@ -57,13 +57,66 @@ blockers.
|
|
|
57
57
|
confidently-wrong reviews. Everything below is scoped to this diff.
|
|
58
58
|
|
|
59
59
|
Then, on the fetched ref, ask whether the branch is still the branch the run
|
|
60
|
-
took up
|
|
60
|
+
took up. **Two paths, and you state which one — the command infers neither.**
|
|
61
|
+
A branch that is a queue item's take-up:
|
|
61
62
|
|
|
62
63
|
```sh
|
|
63
64
|
node .claude/scripts/revalidate.mjs --point BEFORE_PR --ticket <item-id> --base origin/<default>
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
Owner-directed work or a hotfix that has **no item** — the case step 4 below
|
|
68
|
+
already tells you to declare to the reviewers:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
node .claude/scripts/revalidate.mjs --point BEFORE_PR --owner-directed --base origin/<default>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
🔴 **The second path is not a lighter checkpoint, and it is never a skip.**
|
|
75
|
+
It runs the same `main:<path>` comparison and holds on the same exit 2; what
|
|
76
|
+
it drops is the claim comparison, because work with no item has no claim to
|
|
77
|
+
compare. It resolves no queue config, so it reaches no tracker and needs no
|
|
78
|
+
credentials. Passing both flags, or neither, is exit 1.
|
|
79
|
+
|
|
80
|
+
**Four refusals** — exit 1, nothing journalled — keep it from being the way
|
|
81
|
+
around a claim or revalidation failure. It is refused when this run carries
|
|
82
|
+
an **unresolved revalidation hold** (what the ticketed path writes when it
|
|
83
|
+
holds or answers `UNVERIFIABLE`), when this run **declares a take-up**, when
|
|
84
|
+
the branch **touches a tracked `.rig/claims/*.json`** in any direction —
|
|
85
|
+
added, modified, removed or renamed — and at `BEFORE_CLOSE`. So re-running a
|
|
86
|
+
held ticketed call in this mode does not get past it: resolve the hold with
|
|
87
|
+
`outcome` instead.
|
|
88
|
+
|
|
89
|
+
⚠ **What those refusals do not cover**, because a governance mode is trusted
|
|
90
|
+
exactly as far as it is described:
|
|
91
|
+
|
|
92
|
+
- **With no `RIG_RUN_DIR` there is no run state to read**, so the hold and
|
|
93
|
+
take-up refusals cannot fire — and nothing is journalled. The command says
|
|
94
|
+
so on stdout and in `evidence.runState`; it is not evidence that neither
|
|
95
|
+
exists. An attended gate run is exactly this shape, so read that line.
|
|
96
|
+
- The claim refusal reads the **branch diff**, so a claim record already on
|
|
97
|
+
the default branch, or written and not committed, is not seen.
|
|
98
|
+
- **`--base` is the sole authority for the verdict here**, the claim
|
|
99
|
+
comparison that would otherwise survive a wrong base being absent. Pass
|
|
100
|
+
the fetched `origin/<default>`, not a local copy and not `HEAD`.
|
|
101
|
+
- Nothing can prove an item does not exist. The rest is your word, recorded
|
|
102
|
+
as such, with `ticket: null` and no invented id.
|
|
103
|
+
|
|
104
|
+
Pinned in the generator's `test/template/owner-directed-revalidation.test.ts`
|
|
105
|
+
(absent in a generated rig) › "refuses when this run carries an unresolved
|
|
106
|
+
revalidation hold", › "refuses when the declared run already carries a
|
|
107
|
+
take-up", › "refuses when the branch RENAMES a claim record — the case
|
|
108
|
+
--diff-filter=AM could not see" and › "says out loud that an undeclared run
|
|
109
|
+
checked neither the hold nor the take-up".
|
|
110
|
+
|
|
111
|
+
**Exit 2 here is a HOLD with the same shape as the ticketed one**, and the
|
|
112
|
+
same two-step remedy: re-read the default branch on each named path, then
|
|
113
|
+
record what the re-read concluded — `node .claude/scripts/revalidate.mjs
|
|
114
|
+
outcome --point BEFORE_PR --owner-directed --action-changed <true | false>
|
|
115
|
+
--note '…'` — and come back through step 0. The owner-directed detection
|
|
116
|
+
carries no ticket, so `--owner-directed` is how the outcome addresses it;
|
|
117
|
+
the ticketed `--ticket <item-id>` form below cannot match it and is refused.
|
|
118
|
+
|
|
119
|
+
The ticketed path runs the existing revalidation chain against the tracked, versioned
|
|
67
120
|
`.rig/claims/<item-id>.json`: the content-blind `scope` fingerprint set is
|
|
68
121
|
authoritative here, while `takeUps` / `updatedAt` remain evidence only. It
|
|
69
122
|
also names what the default branch changed since this branch forked on paths
|
|
@@ -71,14 +124,18 @@ blockers.
|
|
|
71
124
|
journals one `revalidation` event at `point: BEFORE_PR`; **exit code 2 is a HOLD**, with one blocker per named source: re-read the item, or the default
|
|
72
125
|
branch on that path, record what the re-read concluded —
|
|
73
126
|
`node .claude/scripts/revalidate.mjs outcome --point BEFORE_PR --ticket <item-id> --action-changed <true | false> --note '…'`
|
|
74
|
-
— and come back through step 0.
|
|
75
|
-
|
|
127
|
+
— and come back through step 0. **That `--ticket` form is this path's, not
|
|
128
|
+
both paths'** — the owner-directed detection carries no ticket for it to
|
|
129
|
+
name, and its own `--owner-directed` outcome is written out above. A hold
|
|
130
|
+
with no outcome, in either mode, is counted by the report as a re-read the
|
|
131
|
+
run skipped. A missing, untracked, unreadable or
|
|
76
132
|
unsupported claim is `UNVERIFIABLE`, exits 2, and stops automatic progress;
|
|
77
133
|
so is a tracker whose adapter the command cannot READ, which means the
|
|
78
134
|
question was never put rather than that the claim record is unreadable.
|
|
79
135
|
Neither is ever read as a pass. Exit 1 is the command refusing (unknown
|
|
80
|
-
point,
|
|
81
|
-
not resolve
|
|
136
|
+
point, neither mode or both, a base that is not a revision, or a queue config
|
|
137
|
+
that does not resolve — plus, on the owner-directed path, the four refusals
|
|
138
|
+
above): fix the call or the config — the message says which. Its limits are its own header's; the
|
|
82
139
|
cited-path set is a labelled assumption, not a recorded fact. Pinned in the
|
|
83
140
|
generator's `test/template/revalidate.test.ts` (absent in a generated rig) ›
|
|
84
141
|
"continues when only updatedAt moved and still reports the marker evidence"
|
|
@@ -182,9 +239,13 @@ blockers.
|
|
|
182
239
|
**Whatever you launch, pass it the text of the queue item this branch
|
|
183
240
|
implements.** A reviewer given only a diff cannot check the change against
|
|
184
241
|
what was asked: a cold context has no way to know, and reconstructing it from
|
|
185
|
-
the PR description would mean trusting the run under review.
|
|
186
|
-
item — owner-directed work, a hotfix —
|
|
187
|
-
|
|
242
|
+
the PR description would mean trusting the run under review. When there is
|
|
243
|
+
no item — owner-directed work, a hotfix — launch every reviewer with the
|
|
244
|
+
words **`no item — owner-directed`** instead. That skips the item-contract
|
|
245
|
+
check, openly, and **nothing else**: the checks, the routing, the security,
|
|
246
|
+
code and prose/governance reviews, the coverage check and the DoD all still
|
|
247
|
+
run. It is one check narrower than a ticketed fan-out, not a cheaper gate,
|
|
248
|
+
and the same words are what step 1's `--owner-directed` call records.
|
|
188
249
|
|
|
189
250
|
🔴 **The triggers below are lane-independent and may only ADD.** They read
|
|
190
251
|
*what the code does*; the router reads *paths*, and a path cannot say that a
|
|
@@ -99,7 +99,7 @@ own cost figures are read next to the lane they do not cover.
|
|
|
99
99
|
`MultiEdit`, `NotebookEdit`, or `apply_patch` that names a credential file or carries a credential value,
|
|
100
100
|
reading its vocabulary from `.claude/scripts/lib/secrets.mjs`. ⚠ **Only that
|
|
101
101
|
part.** The hook sees what an agent writes through those five tools and
|
|
102
|
-
nothing else — its own header states
|
|
102
|
+
nothing else — its own header states its blind spots — so whether a
|
|
103
103
|
credential typed by a human, or committed from disk, is also refused depends
|
|
104
104
|
on whether this project has a commit-time check. Look at `.husky/` and the CI
|
|
105
105
|
workflow; this file cannot tell you, and a
|
|
@@ -108,15 +108,25 @@ own cost figures are read next to the lane they do not cover.
|
|
|
108
108
|
at runtime instead of writing it out, or the check reports its own test data as
|
|
109
109
|
a leak.
|
|
110
110
|
- touch production data outside a reviewed migration
|
|
111
|
-
- edit the rulebook from an **unattended** run outside the item's allow-list — `guard-rulebook` refuses it
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
- edit the rulebook from an **unattended** run outside the item's allow-list — `guard-rulebook` refuses it,
|
|
112
|
+
and the checkout board selector is refused **even when the item's allow-list
|
|
113
|
+
names it**; that one carve-out does not follow from the set below.
|
|
114
|
+
The rulebook is both harnesses' instruction, rule, agent, skill, script and
|
|
115
|
+
hook trees, plus the settings, queue, exemption and integrity files that decide
|
|
116
|
+
what a session may do. **Which paths exactly is not restated here**: the
|
|
117
|
+
mechanism reads them from `RULEBOOK_PREFIXES` in
|
|
118
|
+
`.claude/scripts/unattended-flag.mjs`, and so should you — the guard judges an
|
|
119
|
+
edit against it, and the flag writer refuses an allow entry that *widens* it.
|
|
120
|
+
Which entries those are is `isWidening`'s answer in that same module, not a
|
|
121
|
+
paraphrase here: pinned in the generator's `unattended-flag.test.ts` — absent
|
|
122
|
+
in a generated rig — › "explains exact protected-prefix refusal separately
|
|
123
|
+
from proper-prefix widening". An entry
|
|
124
|
+
outside the set — ordinary source — is not widening and is accepted. A second
|
|
125
|
+
copy in prose is a copy that goes stale, and this one did. Mechanical:
|
|
116
126
|
the hook refuses the edit while the unattended flag the `loop` skill writes
|
|
117
127
|
at claim time is on disk (`.claude/scripts/unattended-flag.mjs`), and does
|
|
118
128
|
nothing in an attended session. ⚠ It sees edit tool calls only — a
|
|
119
|
-
shell redirect into
|
|
129
|
+
shell redirect into a protected file is not one — and the flag, not
|
|
120
130
|
the run, is what arms it; its header states the rest of its limits.
|
|
121
131
|
|
|
122
132
|
## Stop rules — by work-state, not by feelings
|