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.
@@ -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 — both harnesses' rules, skills, agents and hook wiring, plus their
157
- scripts, queue config and integrity manifest — is refused unless its
158
- path starts with an allowed prefix; the board selector is the one always-refused
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
- It runs the existing revalidation chain against the tracked, versioned
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. A hold with no outcome is counted by the
75
- report as a re-read the run skipped. A missing, untracked, unreadable or
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, no ticket, a base that is not a revision, or a queue config that does
81
- not resolve): fix the call or the config — the message says which. Its limits are its own header's; the
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. If there is no
186
- item — owner-directed work, a hotfix — say so when launching, and the
187
- reviewer skips that check openly instead of guessing at it.
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 the four blind spots — so whether a
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
- The rulebook is both harnesses' instruction, agent, skill, script and hook
113
- trees (`CLAUDE.md`, `AGENTS.md`, `.claude/{agents,hooks,rules,scripts,skills}`,
114
- `.agents/`, `.codex/`), plus `.claude/settings.json`, the queue config and its
115
- always-refused board selector, and the integrity manifest. Mechanical:
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 `.claude/settings.json` is not one — and the flag, not
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