create-agent-rig 0.6.1 → 0.6.2

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 CHANGED
@@ -11,6 +11,54 @@ Numbering is ordinary semver — **additive is a minor, a fix is a patch** — s
11
11
  that "I only take minors" remains a usable policy; 0.3.2 shipped additive
12
12
  content as a patch by the owner's call and stays recorded as one.
13
13
 
14
+ ## 0.6.2
15
+
16
+ **Patch hardening for the Agent OS shipped by 0.6.1.** This release closes six
17
+ downstream-found governance and transport defects without changing the public
18
+ CLI or adding a dependency.
19
+
20
+ ### Fixed
21
+
22
+ - **A UTF-8 BOM on a hook's stdin no longer disarms it.** PowerShell prepends
23
+ one on some Windows hosts, `JSON.parse` throws on a leading U+FEFF, and every
24
+ hook resolved that to its documented fail-open — so a well-formed refusal
25
+ became an allow. On such a host all eight hooks failed open together,
26
+ `guard-bash` among them, which carries the Never tier and the kill switch.
27
+ They now read through one shared `.claude/hooks/lib/hook-input.mjs`. Pinned in
28
+ `test/template/hook-stdin.test.ts` › "blocks the same command when PowerShell
29
+ prepends a UTF-8 BOM" and › "reads stdin through the one shared reader, in
30
+ every hook that reads it".
31
+ - **Windows 8.3 short paths no longer hide a rulebook edit from the guard.**
32
+ `realpathSync` normalises separators but leaves a short name (`RUNNER~1`,
33
+ `SERHII~1`) unexpanded, so a checkout reached by two spellings hashed to two
34
+ unattended-flag names and compared as two directories. Both
35
+ `guard-rulebook` and `unattended-flag` now canonicalise with
36
+ `realpathSync.native`. Pinned in `test/template/unattended-flag.test.ts` ›
37
+ "scopes the flag by the checkout, so two spellings of one directory arm one
38
+ file".
39
+ - **Codex hooks carry the canonical repository root in `CLAUDE_PROJECT_DIR` on
40
+ POSIX and Windows.** A session started in a nested directory therefore judges
41
+ a rulebook edit against the checkout the hook came from. Pinned in
42
+ `test/template/codex.test.ts` › "anchors a nested-cwd Codex rulebook edit to
43
+ the canonical repository root"; the same test file decodes and checks the
44
+ Windows command.
45
+ - **Jira retry is limited to safe reads and the semantically read-only search
46
+ POST.** Comment, transition, issue-create and issue-update mutations return
47
+ the first ambiguous transient failure instead of replaying the write. Pinned
48
+ in `test/template/queue-jira.test.ts` › "does not retry %s" and › "retries a
49
+ semantically read-only search POST after a 429".
50
+ - **`.claude/doctor-exemptions.json` is protected as rulebook input.** An
51
+ unattended edit is refused unless the current item's allow-list names that
52
+ exact file. Pinned in `test/template/guard-rulebook.test.ts` › "allows doctor
53
+ exemptions only when the item names that exact rulebook file" and the guarded
54
+ path table in the same suite.
55
+ - **`lastCompletedTier` is explicitly repository-global across board switches.**
56
+ A selector change cannot reset the spacing brake and admit a second elevated
57
+ mechanism change in the same checkout. The ruling is in
58
+ `docs/decisions/spacing-rations-mechanisms.md`, pinned by
59
+ `test/template/queue-board.test.ts` › "keeps completed-tier spacing
60
+ repository-global when the active board switches".
61
+
14
62
  ## 0.6.1
15
63
 
16
64
  **Security and upgrade hardening for the Agent OS shipped by 0.6.0.** This patch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agent-rig",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Scaffold a new project with an agent operating system (rules, gates, hooks) and a runnable code skeleton",
5
5
  "keywords": [
6
6
  "create",
@@ -51,6 +51,8 @@
51
51
  "devDependencies": {
52
52
  "@eslint/js": "^10.0.1",
53
53
  "@types/node": "^26.1.1",
54
+ "ajv": "^8.20.0",
55
+ "ajv-formats": "^3.0.1",
54
56
  "eslint": "^10.7.0",
55
57
  "eslint-config-prettier": "^10.1.8",
56
58
  "globals": "^17.7.0",
@@ -59,7 +59,11 @@ and the `board` command itself refuses a switch while the checkout is unattended
59
59
  does not prevent an arbitrary direct shell write to the selector — edit-tool
60
60
  hooks cannot see one. `.claude/queue.state.json` stays per config, not per board:
61
61
  the tier the last close recorded rations the next selection whichever board it
62
- lands on.
62
+ lands on. This is repository-risk state, not tracker metadata: switching from
63
+ one independent queue to another must not turn a previous mechanism close into
64
+ permission for a second one in the same checkout. Pinned in the generator's
65
+ `test/template/queue-board.test.ts` (absent in a generated rig) › "keeps
66
+ completed-tier spacing repository-global when the active board switches".
63
67
 
64
68
  Adding a fourth is an adapter, not a rewrite: `core.mjs` holds every selection
65
69
  decision and each adapter only maps its tracker's records onto the neutral shape.
@@ -12,15 +12,11 @@
12
12
  // to tokenise: it owns exactly one invariant and stays readable because of it
13
13
  // (see .claude/rules/invariants.md, "One invariant per hook"). Use a file rather
14
14
  // than a heredoc, or quote the example.
15
- import { readFileSync } from 'node:fs';
15
+ import { readHookInput } from './lib/hook-input.mjs';
16
16
 
17
17
  function main() {
18
- let input;
19
- try {
20
- input = JSON.parse(readFileSync(0, 'utf8'));
21
- } catch {
22
- return 0;
23
- }
18
+ const input = readHookInput();
19
+ if (input === null) return 0;
24
20
  if (input.tool_name !== 'Bash') return 0;
25
21
  const raw = String(input.tool_input?.command ?? '');
26
22
 
@@ -66,6 +66,7 @@ import { readFileSync, realpathSync } from 'node:fs';
66
66
  import { fileURLToPath } from 'node:url';
67
67
 
68
68
  import { withoutGitLocation } from '../scripts/git-env.mjs';
69
+ import { readHookInput } from './lib/hook-input.mjs';
69
70
 
70
71
  // 🔴 The tree this gate measures is the project the hook BELONGS to — the
71
72
  // directory above `.claude/hooks/` — never the directory the session happens
@@ -166,12 +167,8 @@ function budgetMs(env) {
166
167
  }
167
168
 
168
169
  function main() {
169
- let input;
170
- try {
171
- input = JSON.parse(readFileSync(0, 'utf8'));
172
- } catch (error) {
173
- return failOpen(`the Stop payload could not be read: ${error.message}`);
174
- }
170
+ const input = readHookInput();
171
+ if (input === null) return failOpen('the Stop payload could not be read');
175
172
  if (input.hook_event_name !== 'Stop' && input.hook_event_name !== 'SubagentStop') return 0;
176
173
  if (input.stop_hook_active) return 0;
177
174
 
@@ -77,9 +77,10 @@
77
77
  // Contract (Claude Code): JSON on stdin; exit 0 = allow, exit 2 = block, and
78
78
  // stderr is shown to the agent as the reason. Fails open on anything it cannot
79
79
  // parse — a crashed guard must never make the session unusable.
80
- import { readFileSync, realpathSync } from 'node:fs';
80
+ import { realpathSync } from 'node:fs';
81
81
  import { fileURLToPath } from 'node:url';
82
82
  import { brakeIsOn } from '../scripts/stop-flag.mjs';
83
+ import { readHookInput } from './lib/hook-input.mjs';
83
84
 
84
85
  /** Branches that are shared by definition. */
85
86
  const PROTECTED_BRANCH = /^(main|master|develop|development|trunk)$/;
@@ -804,12 +805,8 @@ export const inspect = (raw, brake, depth = 0) => {
804
805
  };
805
806
 
806
807
  function main() {
807
- let input;
808
- try {
809
- input = JSON.parse(readFileSync(0, 'utf8'));
810
- } catch {
811
- return 0;
812
- }
808
+ const input = readHookInput();
809
+ if (input === null) return 0;
813
810
  if (input.tool_name !== 'Bash') return 0;
814
811
  const commandValue = input.tool_input?.command;
815
812
  if (typeof commandValue !== 'string') return 0;
@@ -8,8 +8,8 @@
8
8
  // Generator-owned coverage for the neutral bounded-inspection refusal lives upstream in
9
9
  // codex.test.ts › "$guard blocks with a neutral, actionable size-limit refusal"; generated
10
10
  // projects do not carry that suite, and a downstream edit requires a local replacement test.
11
- import { readFileSync } from 'node:fs';
12
11
  import { editFragments } from './lib/edit-input.mjs';
12
+ import { readHookInput } from './lib/hook-input.mjs';
13
13
 
14
14
  /** The only non-relative import the core may use: its schema/validation library. */
15
15
  const ALLOWED_PACKAGES = ['zod'];
@@ -29,12 +29,8 @@ const BANNED = [
29
29
  ];
30
30
 
31
31
  function main() {
32
- let input;
33
- try {
34
- input = JSON.parse(readFileSync(0, 'utf8'));
35
- } catch {
36
- return 0; // unparseable payload: not ours to judge
37
- }
32
+ const input = readHookInput();
33
+ if (input === null) return 0; // unparseable payload: not ours to judge
38
34
  const fragments = editFragments(input);
39
35
  const blocked = fragments.find(
40
36
  ({ inspectionRefusal, appliesToAll }) => appliesToAll && inspectionRefusal,
@@ -4,8 +4,9 @@
4
4
  //
5
5
  // The rulebook is the set of files that decide what this session may do — hooks
6
6
  // and settings wiring, the queue config and selector, all scripts, rules, skills
7
- // and agents, the `.codex/` configuration, the integrity manifest, `AGENTS.md`
8
- // and `CLAUDE.md`. Every one of them was editable by the run it governs: a
7
+ // and agents, the `.codex/` configuration, the integrity manifest,
8
+ // `.claude/doctor-exemptions.json`, `AGENTS.md` and `CLAUDE.md`. Every one of
9
+ // them was editable by the run it governs: a
9
10
  // Write to `.claude/hooks/dod-checks.json` with `[]` switched the stop gate
10
11
  // off, and nothing refused it until the merge sweep, long after.
11
12
  //
@@ -60,10 +61,11 @@
60
61
  // adversary.
61
62
  //
62
63
  // The rule it enforces is stated in `.claude/rules/autonomy.md`, "Never".
63
- import { readFileSync, realpathSync } from 'node:fs';
64
+ import { realpathSync } from 'node:fs';
64
65
  import { basename, dirname, join, resolve } from 'node:path';
65
66
  import { editFragments } from './lib/edit-input.mjs';
66
67
  import { RULEBOOK_PREFIXES, isRulebookPath, readUnattended } from '../scripts/unattended-flag.mjs';
68
+ import { readHookInput } from './lib/hook-input.mjs';
67
69
 
68
70
  export { RULEBOOK_PREFIXES, isRulebookPath };
69
71
 
@@ -71,9 +73,20 @@ const EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'apply
71
73
 
72
74
  const toPosix = (value) => String(value ?? '').replaceAll('\\', '/');
73
75
 
76
+ // RP-54: `.native`, never plain `realpathSync`. Both resolve symlinks and
77
+ // normalise separators; only the native one expands a Windows 8.3 short name,
78
+ // so `C:\Users\RUNNER~1\…` and `C:\Users\runneradmin\…` otherwise survive as
79
+ // two spellings of one directory. The root arrives from
80
+ // `git rev-parse --show-toplevel` (long) while a payload path arrives however
81
+ // the tool spelled it (short under an 8.3 temp or home), and a root that
82
+ // matches no spelling of the payload makes this fail-open guard allow the
83
+ // rulebook edit it exists to refuse. Both sites take the same canonicaliser or
84
+ // the comparison is between two different normalisations. Measured on Windows
85
+ // by codex.test.ts (absent in a generated rig) › "anchors a nested-cwd Windows
86
+ // Codex rulebook edit to the canonical repository root".
74
87
  const canonicalRoot = (root) => {
75
88
  try {
76
- return realpathSync(root);
89
+ return realpathSync.native(root);
77
90
  } catch {
78
91
  return root;
79
92
  }
@@ -85,7 +98,7 @@ const canonicalPath = (filePath) => {
85
98
  const tail = [];
86
99
  for (;;) {
87
100
  try {
88
- return join(realpathSync(cursor), ...tail);
101
+ return join(realpathSync.native(cursor), ...tail);
89
102
  } catch {
90
103
  const parent = dirname(cursor);
91
104
  if (parent === cursor) return filePath;
@@ -112,12 +125,8 @@ const protectedRelative = (roots, filePath) =>
112
125
  .find(isRulebookPath);
113
126
 
114
127
  function main() {
115
- let input;
116
- try {
117
- input = JSON.parse(readFileSync(0, 'utf8'));
118
- } catch {
119
- return 0; // unparseable payload: not ours to judge
120
- }
128
+ const input = readHookInput();
129
+ if (input === null) return 0; // unparseable payload: not ours to judge
121
130
  if (!EDIT_TOOLS.has(input?.tool_name)) return 0;
122
131
 
123
132
  const selectedRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -82,10 +82,10 @@
82
82
  // by an explicit per-line candidate cap rather than running to exhaustion. Any
83
83
  // unbounded work in a fail-open guard is a total bypass of every rule at once,
84
84
  // not just of this one.
85
- import { readFileSync } from 'node:fs';
86
85
 
87
86
  import { findSecretValues, isCredentialPath } from '../scripts/lib/secrets.mjs';
88
87
  import { editFragments } from './lib/edit-input.mjs';
88
+ import { readHookInput } from './lib/hook-input.mjs';
89
89
 
90
90
  /** Where a refusal points the agent, so the block is actionable rather than a wall. */
91
91
  const WHERE_CREDENTIALS_BELONG =
@@ -93,12 +93,8 @@ const WHERE_CREDENTIALS_BELONG =
93
93
  'and reach the process through the environment — see .claude/rules/autonomy.md, "Never".';
94
94
 
95
95
  function main() {
96
- let input;
97
- try {
98
- input = JSON.parse(readFileSync(0, 'utf8'));
99
- } catch {
100
- return 0; // unparseable payload: not ours to judge
101
- }
96
+ const input = readHookInput();
97
+ if (input === null) return 0; // unparseable payload: not ours to judge
102
98
 
103
99
  const editTools = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'apply_patch']);
104
100
  if (!editTools.has(input?.tool_name)) return 0;
@@ -8,8 +8,8 @@
8
8
  // Generator-owned coverage for the neutral bounded-inspection refusal lives upstream in
9
9
  // codex.test.ts › "$guard blocks with a neutral, actionable size-limit refusal"; generated
10
10
  // projects do not carry that suite, and a downstream edit requires a local replacement test.
11
- import { readFileSync } from 'node:fs';
12
11
  import { editFragments } from './lib/edit-input.mjs';
12
+ import { readHookInput } from './lib/hook-input.mjs';
13
13
 
14
14
  const WEB_PATH = /(^|\/)apps\/web\//;
15
15
  const CODE_FILE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
@@ -20,12 +20,8 @@ const FORBIDDEN_WORKSPACE = /^@[^/]+\/(db|api|worker)$/;
20
20
  const FORBIDDEN_RELATIVE = /(^|\/)(packages\/db|services)(\/|$)/;
21
21
 
22
22
  function main() {
23
- let input;
24
- try {
25
- input = JSON.parse(readFileSync(0, 'utf8'));
26
- } catch {
27
- return 0; // unparseable payload: not ours to judge
28
- }
23
+ const input = readHookInput();
24
+ if (input === null) return 0; // unparseable payload: not ours to judge
29
25
  const violations = [];
30
26
  const importRe =
31
27
  /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)['"]([^'"]+)['"]/gm;
@@ -30,6 +30,7 @@
30
30
  // is why every ambiguity resolves toward injecting more.
31
31
  import { readFileSync, realpathSync } from 'node:fs';
32
32
  import { fileURLToPath } from 'node:url';
33
+ import { readHookInput } from './lib/hook-input.mjs';
33
34
 
34
35
  // Regions the rule file marks as not worth injecting. The marker is explicit
35
36
  // and lives in the rule file itself, where the person editing it can see it —
@@ -150,12 +151,8 @@ function invokedDirectly() {
150
151
  }
151
152
 
152
153
  function main() {
153
- let input;
154
- try {
155
- input = JSON.parse(readFileSync(0, 'utf8'));
156
- } catch {
157
- return 0;
158
- }
154
+ const input = readHookInput();
155
+ if (input === null) return 0;
159
156
  if (input.hook_event_name !== 'SessionStart') return 0;
160
157
 
161
158
  let rules;
@@ -0,0 +1,55 @@
1
+ // The one place a hook reads its payload — `invariants.md`, "One mechanism,
2
+ // one implementation". Eight hooks each parsing stdin is eight chances for one
3
+ // of them to keep a defect the others fixed, and the one nobody looks at is the
4
+ // one that will.
5
+ //
6
+ // 🔴 Why this module exists at all (RP-54). A hook that cannot parse its
7
+ // payload allows the tool call — that is the documented fail-open, and it is
8
+ // correct: a crashed guard must not make the session unusable. But it means an
9
+ // unreadable payload silently disarms EVERY rule the hook carries, so what
10
+ // counts as unreadable has to be as narrow as the format honestly allows.
11
+ //
12
+ // Measured on the hosted `windows-unit` runner, through the generated Codex
13
+ // `commandWindows` wrapper (CI run 33281160544): the wrapper delivers stdin and
14
+ // propagates the child's exit code, but the guard received **290 bytes for a
15
+ // 287-byte payload**. PowerShell prepends a UTF-8 BOM on that host. `JSON.parse`
16
+ // throws on a leading U+FEFF, so every guard resolved a well-formed payload to
17
+ // "not ours to judge" and allowed the edit — including `guard-bash`, which
18
+ // carries the Never tier and the kill switch. A Windows host whose PowerShell
19
+ // does not add the BOM parses the same payload fine, which is why this looked
20
+ // like a runner-only mystery for two days.
21
+ //
22
+ // A BOM is a byte-order mark, not content: stripping one leading U+FEFF is what
23
+ // the format means, not a tolerance added to get a test green.
24
+ //
25
+ // Bounded work, because a fail-open reader is a total bypass if it can throw or
26
+ // spin: one read, one character comparison, one slice, one parse. No loop, no
27
+ // recursion, no rescanning.
28
+ //
29
+ // Limits, stated rather than implied:
30
+ // - Only a BOM at position 0 is stripped. A payload with other leading bytes is
31
+ // still unreadable, and still resolves to `null`.
32
+ // - `null` means "no payload this hook can judge" and every caller keeps its own
33
+ // fail-open on it. This module does not decide policy; it only removes the
34
+ // spelling difference between two hosts.
35
+ // Pinned in hook-stdin.test.ts (absent in a generated rig) ›
36
+ // "blocks the same command when PowerShell prepends a UTF-8 BOM" and ›
37
+ // "reads stdin through the one shared reader, in every hook that reads it".
38
+
39
+ import { readFileSync } from 'node:fs';
40
+
41
+ /** The hook payload as an object, or `null` when there is none this hook can read. */
42
+ export const readHookInput = () => {
43
+ let raw;
44
+ try {
45
+ raw = readFileSync(0, 'utf8');
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
50
+ try {
51
+ return JSON.parse(raw);
52
+ } catch {
53
+ return null;
54
+ }
55
+ };
@@ -286,22 +286,32 @@ const retryDelayMs = (response, attempt) => {
286
286
  * - a timeout (`timeoutMs`, default 20 s) through an AbortController — a stalled
287
287
  * connection used to block selection forever, and a loop that cannot read its
288
288
  * queue must stop, not hang;
289
- * - a retry on 429/502/503/504, at most `MAX_ATTEMPTS`, honouring `Retry-After`;
290
- * 401/403/404 and every other status fail at once a bad credential is not
291
- * transient, and retrying it only delays the diagnosis;
289
+ * - safe reads retry 429/502/503/504 at most `MAX_ATTEMPTS`, honouring
290
+ * `Retry-After`; mutating calls do not retry an ambiguous response, because
291
+ * Jira may have applied the write before a proxy returned the error. The
292
+ * search endpoint opts in explicitly: it is a POST at the transport layer and
293
+ * a read at the operation layer;
292
294
  * - `retry.sleep` injectable, so a test measures the delay it would have waited
293
295
  * instead of waiting it.
294
296
  *
295
297
  * Pinned in the generator's `test/template/queue-jira.test.ts` (absent in a
296
298
  * generated rig) › "hands fetch an AbortSignal", › "rejects naming the timeout
297
- * and the route when fetch never resolves", › "a 429 followed by a 200 yields
298
- * the 200 body", › "sleeps for the Retry-After the 429 carried, in
299
+ * and the route when fetch never resolves", › "retries a semantically read-only
300
+ * search POST after a 429", › "keeps bounded retry for a safe issue GET",
301
+ * "does not retry %s", › "sleeps for the Retry-After the 429 carried, in
299
302
  * milliseconds", › "gives up after four consecutive 503s, naming the status and
300
303
  * the attempts" and › "does not retry a 401 — a bad credential is not transient".
301
304
  */
302
305
  const request = async (
303
306
  route,
304
- { method = 'GET', body = null, env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, retry = {} } = {},
307
+ {
308
+ method = 'GET',
309
+ body = null,
310
+ env = process.env,
311
+ timeoutMs = DEFAULT_TIMEOUT_MS,
312
+ retry = {},
313
+ retryTransient = method === 'GET',
314
+ } = {},
305
315
  ) => {
306
316
  const { baseUrl, email, token } = requireCredentials(env);
307
317
  const sleep = retry.sleep ?? defaultSleep;
@@ -334,13 +344,14 @@ const request = async (
334
344
  clearTimeout(timer);
335
345
  }
336
346
  if (response.ok) return payload;
337
- if (TRANSIENT.has(response.status) && attempt < MAX_ATTEMPTS) {
347
+ const retryable = retryTransient && TRANSIENT.has(response.status);
348
+ if (retryable && attempt < MAX_ATTEMPTS) {
338
349
  await sleep(retryDelayMs(response, attempt));
339
350
  continue;
340
351
  }
341
352
  // The status alone; never echo the response body, which can carry the token
342
353
  // back in an error envelope.
343
- const attempts = TRANSIENT.has(response.status) ? ` after ${attempt} attempts` : '';
354
+ const attempts = retryable ? ` after ${attempt} attempts` : '';
344
355
  throw new Error(`jira ${method} ${route} failed: ${response.status} ${response.statusText}${attempts}`);
345
356
  }
346
357
  };
@@ -443,6 +454,7 @@ export const search = async ({
443
454
  pages += 1;
444
455
  const page = await request('/rest/api/3/search/jql', {
445
456
  method: 'POST',
457
+ retryTransient: true,
446
458
  body: {
447
459
  jql: query,
448
460
  maxResults: limit,
@@ -80,6 +80,7 @@ export const MAX_ALLOW_ENTRIES = 64;
80
80
  export const RULEBOOK_PREFIXES = Object.freeze([
81
81
  '.agents/',
82
82
  '.claude/.rig-manifest.json',
83
+ '.claude/doctor-exemptions.json',
83
84
  '.claude/agents/',
84
85
  '.claude/hooks/',
85
86
  '.claude/settings.json',
@@ -116,16 +117,38 @@ export const isWidening = (entry) =>
116
117
  entry === '.codex/' ||
117
118
  RULEBOOK_PREFIXES.some((prefix) => prefix !== entry && prefix.startsWith(entry));
118
119
 
119
- const canonicalCheckout = (env) => {
120
- const declared = typeof env.CLAUDE_PROJECT_DIR === 'string' ? env.CLAUDE_PROJECT_DIR.trim() : '';
121
- if (declared === '') return null;
120
+ /**
121
+ * One spelling for one directory the single canonicaliser this file compares
122
+ * and hashes with (`invariants.md`, "One mechanism, one implementation").
123
+ *
124
+ * RP-54: it is `realpathSync.native`, not `realpathSync`, and on Windows those
125
+ * differ. Both normalise separators; only the native one expands an 8.3 short
126
+ * name, so `C:\Users\RUNNER~1\…` and `C:\Users\runneradmin\…` survive
127
+ * `realpathSync` as two strings for one directory. The flag is named by a hash
128
+ * of this value while the generated Codex Windows hook supplies
129
+ * `git rev-parse --show-toplevel` — a different spelling of the same checkout —
130
+ * so the guard looked for a file nobody wrote and, being fail-open, allowed the
131
+ * rulebook edit it exists to refuse. Proven by
132
+ * unattended-flag.test.ts › "scopes the flag by the checkout, so two spellings
133
+ * of one directory arm one file".
134
+ *
135
+ * A path that does not exist has no real path; `resolve` is the fallback, and
136
+ * it is the same one on both sides of every comparison below.
137
+ */
138
+ const canonicalPath = (p) => {
122
139
  try {
123
- return realpathSync(declared);
140
+ return realpathSync.native(p);
124
141
  } catch {
125
- return resolve(declared);
142
+ return resolve(p);
126
143
  }
127
144
  };
128
145
 
146
+ const canonicalCheckout = (env) => {
147
+ const declared = typeof env.CLAUDE_PROJECT_DIR === 'string' ? env.CLAUDE_PROJECT_DIR.trim() : '';
148
+ if (declared === '') return null;
149
+ return canonicalPath(declared);
150
+ };
151
+
129
152
  const checkoutId = (env) => {
130
153
  const canonical = canonicalCheckout(env);
131
154
  if (canonical === null) return null;
@@ -290,12 +313,10 @@ export const writeUnattended = ({ item, runDir = null, allow = [] } = {}, env =
290
313
  };
291
314
 
292
315
  const pathBelongsToCheckout = (candidate, checkout) => {
293
- let resolved;
294
- try {
295
- resolved = realpathSync(candidate);
296
- } catch {
297
- resolved = resolve(candidate);
298
- }
316
+ // `checkout` is `canonicalCheckout`'s output, so the candidate takes the same
317
+ // canonicaliser: two spellings compared here would put an in-checkout runDir
318
+ // outside its own checkout.
319
+ const resolved = canonicalPath(candidate);
299
320
  const rel = relative(checkout, resolved);
300
321
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
301
322
  };
@@ -364,14 +385,7 @@ export const clearLegacyUnattended = (selectedPath) => {
364
385
 
365
386
  const invokedDirectly = () => {
366
387
  if (!process.argv[1]) return false;
367
- const real = (p) => {
368
- try {
369
- return realpathSync(p);
370
- } catch {
371
- return p;
372
- }
373
- };
374
- return real(fileURLToPath(import.meta.url)) === real(process.argv[1]);
388
+ return canonicalPath(fileURLToPath(import.meta.url)) === canonicalPath(process.argv[1]);
375
389
  };
376
390
 
377
391
  if (invokedDirectly()) {
@@ -59,7 +59,11 @@ and the `board` command itself refuses a switch while the checkout is unattended
59
59
  does not prevent an arbitrary direct shell write to the selector — edit-tool
60
60
  hooks cannot see one. `.claude/queue.state.json` stays per config, not per board:
61
61
  the tier the last close recorded rations the next selection whichever board it
62
- lands on.
62
+ lands on. This is repository-risk state, not tracker metadata: switching from
63
+ one independent queue to another must not turn a previous mechanism close into
64
+ permission for a second one in the same checkout. Pinned in the generator's
65
+ `test/template/queue-board.test.ts` (absent in a generated rig) › "keeps
66
+ completed-tier spacing repository-global when the active board switches".
63
67
 
64
68
  Adding a fourth is an adapter, not a rewrite: `core.mjs` holds every selection
65
69
  decision and each adapter only maps its tracker's records onto the neutral shape.
@@ -7,23 +7,23 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-core-purity.mjs\"",
11
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQBjAG8AcgBlAC0AcAB1AHIAaQB0AHkALgBtAGoAcwAnADsAIAAmACAAbgBvAGQAZQAgACQAaABvAG8AawBQAGEAdABoADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQA="
10
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/guard-core-purity.mjs\"",
11
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGcAdQBhAHIAZAAtAGMAbwByAGUALQBwAHUAcgBpAHQAeQAuAG0AagBzACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8AIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ARABpAGEAZwBuAG8AcwB0AGkAYwBzAC4AUAByAG8AYwBlAHMAcwBTAHQAYQByAHQASQBuAGYAbwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAEYAaQBsAGUATgBhAG0AZQAgAD0AIAAnAG4AbwBkAGUAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAEEAcgBnAHUAbQBlAG4AdABzACAAPQAgACcAIgAnACAAKwAgACQAaABvAG8AawBQAGEAdABoACAAKwAgACcAIgAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AVQBzAGUAUwBoAGUAbABsAEUAeABlAGMAdQB0AGUAIAA9ACAAJABmAGEAbABzAGUAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBSAGUAZABpAHIAZQBjAHQAUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAgAD0AIAAkAHQAcgB1AGUAOwAgACQAYwBoAGkAbABkACAAPQAgAFsAUwB5AHMAdABlAG0ALgBEAGkAYQBnAG4AbwBzAHQAaQBjAHMALgBQAHIAbwBjAGUAcwBzAF0AOgA6AFMAdABhAHIAdAAoACQAcwB0AGEAcgB0AEkAbgBmAG8AKQA7ACAAWwBDAG8AbgBzAG8AbABlAF0AOgA6AE8AcABlAG4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAoACkALgBDAG8AcAB5AFQAbwAoACQAYwBoAGkAbABkAC4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAuAEIAYQBzAGUAUwB0AHIAZQBhAG0AKQA7ACAAJABjAGgAaQBsAGQALgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0AC4AQwBsAG8AcwBlACgAKQA7ACAAJABjAGgAaQBsAGQALgBXAGEAaQB0AEYAbwByAEUAeABpAHQAKAApADsAIABlAHgAaQB0ACAAJABjAGgAaQBsAGQALgBFAHgAaQB0AEMAbwBkAGUA"
12
12
  },
13
13
  {
14
14
  "type": "command",
15
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-web-boundary.mjs\"",
16
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQB3AGUAYgAtAGIAbwB1AG4AZABhAHIAeQAuAG0AagBzACcAOwAgACYAIABuAG8AZABlACAAJABoAG8AbwBrAFAAYQB0AGgAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA=="
15
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/guard-web-boundary.mjs\"",
16
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGcAdQBhAHIAZAAtAHcAZQBiAC0AYgBvAHUAbgBkAGEAcgB5AC4AbQBqAHMAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBEAGkAYQBnAG4AbwBzAHQAaQBjAHMALgBQAHIAbwBjAGUAcwBzAFMAdABhAHIAdABJAG4AZgBvADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4ARgBpAGwAZQBOAGEAbQBlACAAPQAgACcAbgBvAGQAZQAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AQQByAGcAdQBtAGUAbgB0AHMAIAA9ACAAJwAiACcAIAArACAAJABoAG8AbwBrAFAAYQB0AGgAIAArACAAJwAiACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBVAHMAZQBTAGgAZQBsAGwARQB4AGUAYwB1AHQAZQAgAD0AIAAkAGYAYQBsAHMAZQA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAFIAZQBkAGkAcgBlAGMAdABTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0ACAAPQAgACQAdAByAHUAZQA7ACAAJABjAGgAaQBsAGQAIAA9ACAAWwBTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAFAAcgBvAGMAZQBzAHMAXQA6ADoAUwB0AGEAcgB0ACgAJABzAHQAYQByAHQASQBuAGYAbwApADsAIABbAEMAbwBuAHMAbwBsAGUAXQA6ADoATwBwAGUAbgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0ACgAKQAuAEMAbwBwAHkAVABvACgAJABjAGgAaQBsAGQALgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0AC4AQgBhAHMAZQBTAHQAcgBlAGEAbQApADsAIAAkAGMAaABpAGwAZAAuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQALgBDAGwAbwBzAGUAKAApADsAIAAkAGMAaABpAGwAZAAuAFcAYQBpAHQARgBvAHIARQB4AGkAdAAoACkAOwAgAGUAeABpAHQAIAAkAGMAaABpAGwAZAAuAEUAeABpAHQAQwBvAGQAZQA="
17
17
  },
18
18
  {
19
19
  "type": "command",
20
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-secret-file.mjs\"",
21
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQBzAGUAYwByAGUAdAAtAGYAaQBsAGUALgBtAGoAcwAnADsAIAAmACAAbgBvAGQAZQAgACQAaABvAG8AawBQAGEAdABoADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQA="
20
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/guard-secret-file.mjs\"",
21
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGcAdQBhAHIAZAAtAHMAZQBjAHIAZQB0AC0AZgBpAGwAZQAuAG0AagBzACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8AIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ARABpAGEAZwBuAG8AcwB0AGkAYwBzAC4AUAByAG8AYwBlAHMAcwBTAHQAYQByAHQASQBuAGYAbwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAEYAaQBsAGUATgBhAG0AZQAgAD0AIAAnAG4AbwBkAGUAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAEEAcgBnAHUAbQBlAG4AdABzACAAPQAgACcAIgAnACAAKwAgACQAaABvAG8AawBQAGEAdABoACAAKwAgACcAIgAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AVQBzAGUAUwBoAGUAbABsAEUAeABlAGMAdQB0AGUAIAA9ACAAJABmAGEAbABzAGUAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBSAGUAZABpAHIAZQBjAHQAUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAgAD0AIAAkAHQAcgB1AGUAOwAgACQAYwBoAGkAbABkACAAPQAgAFsAUwB5AHMAdABlAG0ALgBEAGkAYQBnAG4AbwBzAHQAaQBjAHMALgBQAHIAbwBjAGUAcwBzAF0AOgA6AFMAdABhAHIAdAAoACQAcwB0AGEAcgB0AEkAbgBmAG8AKQA7ACAAWwBDAG8AbgBzAG8AbABlAF0AOgA6AE8AcABlAG4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAoACkALgBDAG8AcAB5AFQAbwAoACQAYwBoAGkAbABkAC4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAuAEIAYQBzAGUAUwB0AHIAZQBhAG0AKQA7ACAAJABjAGgAaQBsAGQALgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0AC4AQwBsAG8AcwBlACgAKQA7ACAAJABjAGgAaQBsAGQALgBXAGEAaQB0AEYAbwByAEUAeABpAHQAKAApADsAIABlAHgAaQB0ACAAJABjAGgAaQBsAGQALgBFAHgAaQB0AEMAbwBkAGUA"
22
22
  },
23
23
  {
24
24
  "type": "command",
25
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-rulebook.mjs\"",
26
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQByAHUAbABlAGIAbwBvAGsALgBtAGoAcwAnADsAIAAmACAAbgBvAGQAZQAgACQAaABvAG8AawBQAGEAdABoADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQA="
25
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/guard-rulebook.mjs\"",
26
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGcAdQBhAHIAZAAtAHIAdQBsAGUAYgBvAG8AawAuAG0AagBzACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8AIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ARABpAGEAZwBuAG8AcwB0AGkAYwBzAC4AUAByAG8AYwBlAHMAcwBTAHQAYQByAHQASQBuAGYAbwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAEYAaQBsAGUATgBhAG0AZQAgAD0AIAAnAG4AbwBkAGUAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAEEAcgBnAHUAbQBlAG4AdABzACAAPQAgACcAIgAnACAAKwAgACQAaABvAG8AawBQAGEAdABoACAAKwAgACcAIgAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AVQBzAGUAUwBoAGUAbABsAEUAeABlAGMAdQB0AGUAIAA9ACAAJABmAGEAbABzAGUAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBSAGUAZABpAHIAZQBjAHQAUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAgAD0AIAAkAHQAcgB1AGUAOwAgACQAYwBoAGkAbABkACAAPQAgAFsAUwB5AHMAdABlAG0ALgBEAGkAYQBnAG4AbwBzAHQAaQBjAHMALgBQAHIAbwBjAGUAcwBzAF0AOgA6AFMAdABhAHIAdAAoACQAcwB0AGEAcgB0AEkAbgBmAG8AKQA7ACAAWwBDAG8AbgBzAG8AbABlAF0AOgA6AE8AcABlAG4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAoACkALgBDAG8AcAB5AFQAbwAoACQAYwBoAGkAbABkAC4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAuAEIAYQBzAGUAUwB0AHIAZQBhAG0AKQA7ACAAJABjAGgAaQBsAGQALgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0AC4AQwBsAG8AcwBlACgAKQA7ACAAJABjAGgAaQBsAGQALgBXAGEAaQB0AEYAbwByAEUAeABpAHQAKAApADsAIABlAHgAaQB0ACAAJABjAGgAaQBsAGQALgBFAHgAaQB0AEMAbwBkAGUA"
27
27
  }
28
28
  ]
29
29
  },
@@ -32,13 +32,13 @@
32
32
  "hooks": [
33
33
  {
34
34
  "type": "command",
35
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/block-no-verify.mjs\"",
36
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBiAGwAbwBjAGsALQBuAG8ALQB2AGUAcgBpAGYAeQAuAG0AagBzACcAOwAgACYAIABuAG8AZABlACAAJABoAG8AbwBrAFAAYQB0AGgAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA=="
35
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/block-no-verify.mjs\"",
36
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGIAbABvAGMAawAtAG4AbwAtAHYAZQByAGkAZgB5AC4AbQBqAHMAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBEAGkAYQBnAG4AbwBzAHQAaQBjAHMALgBQAHIAbwBjAGUAcwBzAFMAdABhAHIAdABJAG4AZgBvADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4ARgBpAGwAZQBOAGEAbQBlACAAPQAgACcAbgBvAGQAZQAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AQQByAGcAdQBtAGUAbgB0AHMAIAA9ACAAJwAiACcAIAArACAAJABoAG8AbwBrAFAAYQB0AGgAIAArACAAJwAiACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBVAHMAZQBTAGgAZQBsAGwARQB4AGUAYwB1AHQAZQAgAD0AIAAkAGYAYQBsAHMAZQA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAFIAZQBkAGkAcgBlAGMAdABTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0ACAAPQAgACQAdAByAHUAZQA7ACAAJABjAGgAaQBsAGQAIAA9ACAAWwBTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAFAAcgBvAGMAZQBzAHMAXQA6ADoAUwB0AGEAcgB0ACgAJABzAHQAYQByAHQASQBuAGYAbwApADsAIABbAEMAbwBuAHMAbwBsAGUAXQA6ADoATwBwAGUAbgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0ACgAKQAuAEMAbwBwAHkAVABvACgAJABjAGgAaQBsAGQALgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0AC4AQgBhAHMAZQBTAHQAcgBlAGEAbQApADsAIAAkAGMAaABpAGwAZAAuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQALgBDAGwAbwBzAGUAKAApADsAIAAkAGMAaABpAGwAZAAuAFcAYQBpAHQARgBvAHIARQB4AGkAdAAoACkAOwAgAGUAeABpAHQAIAAkAGMAaABpAGwAZAAuAEUAeABpAHQAQwBvAGQAZQA="
37
37
  },
38
38
  {
39
39
  "type": "command",
40
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/guard-bash.mjs\"",
41
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAHUAYQByAGQALQBiAGEAcwBoAC4AbQBqAHMAJwA7ACAAJgAgAG4AbwBkAGUAIAAkAGgAbwBvAGsAUABhAHQAaAA7ACAAZQB4AGkAdAAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUA"
40
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/guard-bash.mjs\"",
41
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGcAdQBhAHIAZAAtAGIAYQBzAGgALgBtAGoAcwAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAFAAcgBvAGMAZQBzAHMAUwB0AGEAcgB0AEkAbgBmAG8AOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBGAGkAbABlAE4AYQBtAGUAIAA9ACAAJwBuAG8AZABlACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBBAHIAZwB1AG0AZQBuAHQAcwAgAD0AIAAnACIAJwAgACsAIAAkAGgAbwBvAGsAUABhAHQAaAAgACsAIAAnACIAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAFUAcwBlAFMAaABlAGwAbABFAHgAZQBjAHUAdABlACAAPQAgACQAZgBhAGwAcwBlADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AUgBlAGQAaQByAGUAYwB0AFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQAIAA9ACAAJAB0AHIAdQBlADsAIAAkAGMAaABpAGwAZAAgAD0AIABbAFMAeQBzAHQAZQBtAC4ARABpAGEAZwBuAG8AcwB0AGkAYwBzAC4AUAByAG8AYwBlAHMAcwBdADoAOgBTAHQAYQByAHQAKAAkAHMAdABhAHIAdABJAG4AZgBvACkAOwAgAFsAQwBvAG4AcwBvAGwAZQBdADoAOgBPAHAAZQBuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQAKAApAC4AQwBvAHAAeQBUAG8AKAAkAGMAaABpAGwAZAAuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQALgBCAGEAcwBlAFMAdAByAGUAYQBtACkAOwAgACQAYwBoAGkAbABkAC4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAuAEMAbABvAHMAZQAoACkAOwAgACQAYwBoAGkAbABkAC4AVwBhAGkAdABGAG8AcgBFAHgAaQB0ACgAKQA7ACAAZQB4AGkAdAAgACQAYwBoAGkAbABkAC4ARQB4AGkAdABDAG8AZABlAA=="
42
42
  }
43
43
  ]
44
44
  }
@@ -48,9 +48,9 @@
48
48
  "hooks": [
49
49
  {
50
50
  "type": "command",
51
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/gate-stop-dod.mjs\"",
51
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/gate-stop-dod.mjs\"",
52
52
  "timeout": 900,
53
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBnAGEAdABlAC0AcwB0AG8AcAAtAGQAbwBkAC4AbQBqAHMAJwA7ACAAJgAgAG4AbwBkAGUAIAAkAGgAbwBvAGsAUABhAHQAaAA7ACAAZQB4AGkAdAAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUA"
53
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGcAYQB0AGUALQBzAHQAbwBwAC0AZABvAGQALgBtAGoAcwAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAFAAcgBvAGMAZQBzAHMAUwB0AGEAcgB0AEkAbgBmAG8AOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBGAGkAbABlAE4AYQBtAGUAIAA9ACAAJwBuAG8AZABlACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBBAHIAZwB1AG0AZQBuAHQAcwAgAD0AIAAnACIAJwAgACsAIAAkAGgAbwBvAGsAUABhAHQAaAAgACsAIAAnACIAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAFUAcwBlAFMAaABlAGwAbABFAHgAZQBjAHUAdABlACAAPQAgACQAZgBhAGwAcwBlADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AUgBlAGQAaQByAGUAYwB0AFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQAIAA9ACAAJAB0AHIAdQBlADsAIAAkAGMAaABpAGwAZAAgAD0AIABbAFMAeQBzAHQAZQBtAC4ARABpAGEAZwBuAG8AcwB0AGkAYwBzAC4AUAByAG8AYwBlAHMAcwBdADoAOgBTAHQAYQByAHQAKAAkAHMAdABhAHIAdABJAG4AZgBvACkAOwAgAFsAQwBvAG4AcwBvAGwAZQBdADoAOgBPAHAAZQBuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQAKAApAC4AQwBvAHAAeQBUAG8AKAAkAGMAaABpAGwAZAAuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQALgBCAGEAcwBlAFMAdAByAGUAYQBtACkAOwAgACQAYwBoAGkAbABkAC4AUwB0AGEAbgBkAGEAcgBkAEkAbgBwAHUAdAAuAEMAbABvAHMAZQAoACkAOwAgACQAYwBoAGkAbABkAC4AVwBhAGkAdABGAG8AcgBFAHgAaQB0ACgAKQA7ACAAZQB4AGkAdAAgACQAYwBoAGkAbABkAC4ARQB4AGkAdABDAG8AZABlAA=="
54
54
  }
55
55
  ]
56
56
  }
@@ -60,8 +60,8 @@
60
60
  "hooks": [
61
61
  {
62
62
  "type": "command",
63
- "command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/inject-rules.mjs\"",
64
- "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAaABvAG8AawBQAGEAdABoACAAPQAgAEoAbwBpAG4ALQBQAGEAdABoACAAJAByAGUAcABvAFIAbwBvAHQAIAAnAC4AYwBsAGEAdQBkAGUALwBoAG8AbwBrAHMALwBpAG4AagBlAGMAdAAtAHIAdQBsAGUAcwAuAG0AagBzACcAOwAgACYAIABuAG8AZABlACAAJABoAG8AbwBrAFAAYQB0AGgAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA=="
63
+ "command": "repoRoot=\"$(git rev-parse --show-toplevel)\" && CLAUDE_PROJECT_DIR=\"$repoRoot\" node \"$repoRoot/.claude/hooks/inject-rules.mjs\"",
64
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJAByAGUAcABvAFIAbwBvAHQAIAA9ACAAZwBpAHQAIAByAGUAdgAtAHAAYQByAHMAZQAgAC0ALQBzAGgAbwB3AC0AdABvAHAAbABlAHYAZQBsADsAIABpAGYAIAAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAAtAG4AZQAgADAAKQAgAHsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0AOwAgACQAZQBuAHYAOgBDAEwAQQBVAEQARQBfAFAAUgBPAEoARQBDAFQAXwBEAEkAUgAgAD0AIAAkAHIAZQBwAG8AUgBvAG8AdAA7ACAAJABoAG8AbwBrAFAAYQB0AGgAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAHIAZQBwAG8AUgBvAG8AdAAgACcALgBjAGwAYQB1AGQAZQAvAGgAbwBvAGsAcwAvAGkAbgBqAGUAYwB0AC0AcgB1AGwAZQBzAC4AbQBqAHMAJwA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBEAGkAYQBnAG4AbwBzAHQAaQBjAHMALgBQAHIAbwBjAGUAcwBzAFMAdABhAHIAdABJAG4AZgBvADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4ARgBpAGwAZQBOAGEAbQBlACAAPQAgACcAbgBvAGQAZQAnADsAIAAkAHMAdABhAHIAdABJAG4AZgBvAC4AQQByAGcAdQBtAGUAbgB0AHMAIAA9ACAAJwAiACcAIAArACAAJABoAG8AbwBrAFAAYQB0AGgAIAArACAAJwAiACcAOwAgACQAcwB0AGEAcgB0AEkAbgBmAG8ALgBVAHMAZQBTAGgAZQBsAGwARQB4AGUAYwB1AHQAZQAgAD0AIAAkAGYAYQBsAHMAZQA7ACAAJABzAHQAYQByAHQASQBuAGYAbwAuAFIAZQBkAGkAcgBlAGMAdABTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0ACAAPQAgACQAdAByAHUAZQA7ACAAJABjAGgAaQBsAGQAIAA9ACAAWwBTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAFAAcgBvAGMAZQBzAHMAXQA6ADoAUwB0AGEAcgB0ACgAJABzAHQAYQByAHQASQBuAGYAbwApADsAIABbAEMAbwBuAHMAbwBsAGUAXQA6ADoATwBwAGUAbgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0ACgAKQAuAEMAbwBwAHkAVABvACgAJABjAGgAaQBsAGQALgBTAHQAYQBuAGQAYQByAGQASQBuAHAAdQB0AC4AQgBhAHMAZQBTAHQAcgBlAGEAbQApADsAIAAkAGMAaABpAGwAZAAuAFMAdABhAG4AZABhAHIAZABJAG4AcAB1AHQALgBDAGwAbwBzAGUAKAApADsAIAAkAGMAaABpAGwAZAAuAFcAYQBpAHQARgBvAHIARQB4AGkAdAAoACkAOwAgAGUAeABpAHQAIAAkAGMAaABpAGwAZAAuAEUAeABpAHQAQwBvAGQAZQA="
65
65
  }
66
66
  ]
67
67
  }
@@ -116,6 +116,21 @@ with no refusal, and then **holds** the elevated item with `causes: ["spacing"]`
116
116
  It holds because `core.mjs` is the layer that reads every unrecognised value
117
117
  restrictively — the same layer that holds when `selectNext` is called directly.
118
118
 
119
+ ## A board switch does not reset repository risk
120
+
121
+ `lastCompletedTier` is intentionally checkout-global, not board-scoped. Boards
122
+ partition ownership and selection; they do not partition the repository a merge
123
+ changes. If switching the selector chose a fresh tier slot, a run could land an
124
+ unreviewed mechanism from one board, switch to an independent queue, and land a
125
+ second mechanism in the same checkout — exactly the back-to-back chain the
126
+ ration exists to stop.
127
+
128
+ The selector therefore changes only the adapter options. The tier stays in the
129
+ one `.claude/queue.state.json` paired with the checkout's config, and the next
130
+ selection reads it whichever board is active. This is pinned in the generator's
131
+ `test/template/queue-board.test.ts` (absent in a generated rig) › "keeps
132
+ completed-tier spacing repository-global when the active board switches".
133
+
119
134
  ## What this does not fix
120
135
 
121
136
  The livelock where *every* remaining item is elevated-by-mechanism still exists —
@@ -17,6 +17,7 @@
17
17
  ".claude/hooks/guard-secret-file.mjs",
18
18
  ".claude/hooks/guard-rulebook.mjs",
19
19
  ".claude/hooks/lib/edit-input.mjs",
20
+ ".claude/hooks/lib/hook-input.mjs",
20
21
  ".claude/skills/pr-ship/SKILL.md",
21
22
  ".claude/skills/loop/SKILL.md",
22
23
  ".claude/skills/check-premises/SKILL.md",
@@ -6,7 +6,8 @@
6
6
  "0.3.2",
7
7
  "0.4.0",
8
8
  "0.5.0",
9
- "0.6.0"
9
+ "0.6.0",
10
+ "0.6.1"
10
11
  ],
11
12
  "files": {
12
13
  ".agents/skills/check-premises/SKILL.md": {
@@ -20,7 +21,8 @@
20
21
  "since": "0.5.0",
21
22
  "hashes": [
22
23
  "eb52ef41269018107534d8dc2f552c57698e1677a1e76b5e666b5d09c007cac0",
23
- "47922b182915cb92696f851174f2e88c4ae8c67dc119ef39a3a597ef1300fab5"
24
+ "47922b182915cb92696f851174f2e88c4ae8c67dc119ef39a3a597ef1300fab5",
25
+ "57f6add0504db45e7577474f97705fe474310e4cd3310cdb348a2c64eabe88cd"
24
26
  ]
25
27
  },
26
28
  ".agents/skills/new-invariant/SKILL.md": {
@@ -90,7 +92,8 @@
90
92
  "hashes": [
91
93
  "8d5762849afd328fdc1aabf053ad207f2e306e013665e3ff934b06cbb1bbda38",
92
94
  "0e4307cd9cf8ef723d79e98b1af46a34c6912a6aa925e2179d939cb83c8f2a44",
93
- "7e8eaa2b4af9d65e1d10bf50c32c1d138fb2f8b9ba6e6d0880cade0524324d9d"
95
+ "7e8eaa2b4af9d65e1d10bf50c32c1d138fb2f8b9ba6e6d0880cade0524324d9d",
96
+ "ed1c05d3938c0b4f277f1bf4af8641460def830569049ff0697c9bda14518b78"
94
97
  ]
95
98
  },
96
99
  ".claude/agents/security-scanner.md": {
@@ -126,14 +129,16 @@
126
129
  "de1f331eaf93bad6ef0e1734789fd3c4ab07550a85ed2d8ff8888bc6f0bcfb0f",
127
130
  "9fb8fad4d95756fb2424a97550aa85dfab0ca8dbcf91e6d0385263ba3b929135",
128
131
  "7852205961cb1647b49de5f7e79820c1735b4be897adcf8f5bc3eeb49318ff78",
129
- "33edb11904e5c4f88626b41c7e649b85d20957a11ff7e1062538917647c3e7f0"
132
+ "33edb11904e5c4f88626b41c7e649b85d20957a11ff7e1062538917647c3e7f0",
133
+ "19202a28355dac1d7a95d29411668cf970006b3044d89a772768ce21793931aa"
130
134
  ]
131
135
  },
132
136
  ".claude/hooks/guard-bash.mjs": {
133
137
  "since": "0.3.0",
134
138
  "hashes": [
135
139
  "4d396f412fc53094188b14348c5af4e79c7542a182630c20c4687d79899c5b0e",
136
- "e7a513216ebbceaa7f11ec8fb611de8bb5d5925dfdfd75b779dcb7c0ee6e83cc"
140
+ "e7a513216ebbceaa7f11ec8fb611de8bb5d5925dfdfd75b779dcb7c0ee6e83cc",
141
+ "78047f42dda65076dd2cdb73ad530553db3d4a6f3ea7f0729590ac7286323cb1"
137
142
  ]
138
143
  },
139
144
  ".claude/hooks/guard-core-purity.mjs": {
@@ -146,13 +151,15 @@
146
151
  ".claude/hooks/guard-rulebook.mjs": {
147
152
  "since": "0.6.0",
148
153
  "hashes": [
149
- "4524d715f2e41f4cfdd3d0f9c858817f913aaca5192bda97dff876b4d8c51f19"
154
+ "4524d715f2e41f4cfdd3d0f9c858817f913aaca5192bda97dff876b4d8c51f19",
155
+ "f9e3e4b7c22c4a64f521402419707ff382b05c8deb6310f62e5545db87f7cb4f"
150
156
  ]
151
157
  },
152
158
  ".claude/hooks/guard-secret-file.mjs": {
153
159
  "since": "0.5.0",
154
160
  "hashes": [
155
- "f6b81cb7640924d2e56aced58e6b48f65df6638fb5a9099985fe5d673245c3ad"
161
+ "f6b81cb7640924d2e56aced58e6b48f65df6638fb5a9099985fe5d673245c3ad",
162
+ "8c333879dba1f2bf87bbb8e7b257ac51315a74a4cb45252408ca615e2a42d7b5"
156
163
  ]
157
164
  },
158
165
  ".claude/hooks/guard-web-boundary.mjs": {
@@ -173,7 +180,8 @@
173
180
  "since": "0.5.0",
174
181
  "hashes": [
175
182
  "4f836bc3ff75a868ffe035b179ac9e7dc1ad7715d520677ff9f6aa5bfdb200eb",
176
- "0c191d326a7bb0bbfc110e2210f44b4aa39276e8db3779aaa4545cffd4e11863"
183
+ "0c191d326a7bb0bbfc110e2210f44b4aa39276e8db3779aaa4545cffd4e11863",
184
+ "572c671f8f2cad190fe2f144071954ac4d3b1357cab80a19bad584dd3b3768c8"
177
185
  ]
178
186
  },
179
187
  ".claude/queue.json": {
@@ -194,7 +202,8 @@
194
202
  "c3aa6ea4487adf6d542174f491bf469d8f8563ec7c8c4de63fcd82d6a5ca4fba",
195
203
  "ce2104ce697d3e5ed9210e85fe0daeb66b432a36868493f06fec54073a5fff31",
196
204
  "530f63a4b29a5cc67867fa7b23575db81d42609ae401bfc4089b05194c586c21",
197
- "a7359408d56e6bdbf268019ad9193d5fbf4a872385a65ba08564204e6a8f5b5f"
205
+ "a7359408d56e6bdbf268019ad9193d5fbf4a872385a65ba08564204e6a8f5b5f",
206
+ "619999500386b3d2867bbd4799f86a800fe7e168083c6e35fb2e629a2c926c5f"
198
207
  ]
199
208
  },
200
209
  ".claude/rules/aws-cdk.md": {
@@ -211,7 +220,8 @@
211
220
  "b24bbf3e596033719eff463d317ba1e933d0661e48bfa8eafba8d6dca1453a59",
212
221
  "518d9b3d7cbae50f30c86d33b2b4bbea11ab3ba6cfef270876e92604b2af5f50",
213
222
  "389484f0846c3dd8207c0f41bd56409103fbcc7f58b94b1bf4bd1d81b5af0e87",
214
- "a2e10499a0bcd2cdc32f3986d27b59077c86e524d40323faabb2e5d78afc002c"
223
+ "a2e10499a0bcd2cdc32f3986d27b59077c86e524d40323faabb2e5d78afc002c",
224
+ "c94c33d7ccbd46f191948325dfc7f68ce13b0d3a9470215de1de3f06b1499111"
215
225
  ]
216
226
  },
217
227
  ".claude/rules/node-ts.md": {
@@ -233,7 +243,8 @@
233
243
  "since": "0.5.0",
234
244
  "hashes": [
235
245
  "783b5b455a56c6a3e3723f3c6da861e0a945562acb686b86ab9d0e16dbd552a6",
236
- "c7a045d3687429c5047f09b4033ffffcecfede2edba736106f5e1048eb3cf27f"
246
+ "c7a045d3687429c5047f09b4033ffffcecfede2edba736106f5e1048eb3cf27f",
247
+ "8e9fe877949955e8f698701f969784b288bc95943c3bcea3bd4368db82e8e43e"
237
248
  ]
238
249
  },
239
250
  ".claude/scripts/detect-missed-gate.mjs": {
@@ -247,13 +258,15 @@
247
258
  ".claude/scripts/doctor.mjs": {
248
259
  "since": "0.6.0",
249
260
  "hashes": [
250
- "eb31d2726ab95d6b8fd091b1daf8a21f403a46c801590baf4fecee5c1bec64b9"
261
+ "eb31d2726ab95d6b8fd091b1daf8a21f403a46c801590baf4fecee5c1bec64b9",
262
+ "5154ad9abdcb37fea58b0da78fd71842c705774ffe48fd8050c302da17e73913"
251
263
  ]
252
264
  },
253
265
  ".claude/scripts/git-env.mjs": {
254
266
  "since": "0.5.0",
255
267
  "hashes": [
256
- "ff9f3c848a3881b67a8f795a4d172f407d896447e21dd3a9c4903365977d4519"
268
+ "ff9f3c848a3881b67a8f795a4d172f407d896447e21dd3a9c4903365977d4519",
269
+ "b022399f287cdce1bcb07329c0aab44bf99714b5f0515f274c962f41ea072b86"
257
270
  ]
258
271
  },
259
272
  ".claude/scripts/lib/gate-coverage.mjs": {
@@ -265,13 +278,15 @@
265
278
  ".claude/scripts/lib/revalidation-points.mjs": {
266
279
  "since": "0.6.0",
267
280
  "hashes": [
268
- "41c4f70e5914636bccec7995172c322c875774027e0c626abb7c593c43fddd65"
281
+ "41c4f70e5914636bccec7995172c322c875774027e0c626abb7c593c43fddd65",
282
+ "b48e4959862906e279eca481f484f07a7fd4aa082f2778224a6574a7871bc9d8"
269
283
  ]
270
284
  },
271
285
  ".claude/scripts/lib/secrets.mjs": {
272
286
  "since": "0.5.0",
273
287
  "hashes": [
274
- "dc4a300646f12459c0c37e380b325451474e1ec113482381e0fad80aceb88049"
288
+ "dc4a300646f12459c0c37e380b325451474e1ec113482381e0fad80aceb88049",
289
+ "60fab2cc9f2028bccf6139e6a744406b4a86a9039d1367f3cef409902e9b327d"
275
290
  ]
276
291
  },
277
292
  ".claude/scripts/lib/verdict.mjs": {
@@ -332,7 +347,8 @@
332
347
  "hashes": [
333
348
  "cd149245dc29ef7489ae0e5394fa5a063dea2cc017bd3cbbface1ace6f8d7b46",
334
349
  "7db3c17b781e6876b4822afd09e6affa97e46607b63d443975d987daf1f14464",
335
- "9edd70b5960f367958e8e7500150b4edd285c99869cca803e40e0537d2d347b0"
350
+ "9edd70b5960f367958e8e7500150b4edd285c99869cca803e40e0537d2d347b0",
351
+ "4cf917ac06708e1068dfe9f8bbebc9376c4748526bd669b9bea32929ba996d0a"
336
352
  ]
337
353
  },
338
354
  ".claude/scripts/queue/jira.mjs": {
@@ -341,7 +357,8 @@
341
357
  "13e187a4005136a9a1fae5fb5a3d2a61c3cf5dc097b467c12622ad82a0fd5d52",
342
358
  "86591a48b9264d641c2530238ea3720705f12e17af2ec8dccecabfa3a742a2e7",
343
359
  "13c301f828d4402fcf81e7e0b2b1d22472cbdb50f6fd606770056af4ec215e51",
344
- "b8be5855809c0d44ece94858456098663296c17c5ad9abb1ea2cf1a8f48ca491"
360
+ "b8be5855809c0d44ece94858456098663296c17c5ad9abb1ea2cf1a8f48ca491",
361
+ "3fc8ff9a06a2e22355a660a31e2943a97f50869f7e5726f768a7931d17b64b5b"
345
362
  ]
346
363
  },
347
364
  ".claude/scripts/queue/plan-md.mjs": {
@@ -368,13 +385,15 @@
368
385
  ".claude/scripts/revalidate.mjs": {
369
386
  "since": "0.6.0",
370
387
  "hashes": [
371
- "32f847a2a3a5136cabcb42f6447da9f8cf98e2d20512212ee5201461128d7798"
388
+ "32f847a2a3a5136cabcb42f6447da9f8cf98e2d20512212ee5201461128d7798",
389
+ "b62348a27bcaa1d62a0e4ddd53873fab6cb75f9af8e8c69d24f6ce81c592c4e3"
372
390
  ]
373
391
  },
374
392
  ".claude/scripts/revalidation-report.mjs": {
375
393
  "since": "0.6.0",
376
394
  "hashes": [
377
- "b99a3537158c2851698bfac027f7061811d7d428c3eea57f3ce0f58ea75dc249"
395
+ "b99a3537158c2851698bfac027f7061811d7d428c3eea57f3ce0f58ea75dc249",
396
+ "78adac5c8f696f2c9cc86f404abf1d55604ccb10f11dd612501c14d4cf5935b1"
378
397
  ]
379
398
  },
380
399
  ".claude/scripts/run-journal.mjs": {
@@ -400,7 +419,8 @@
400
419
  ".claude/scripts/unattended-flag.mjs": {
401
420
  "since": "0.6.0",
402
421
  "hashes": [
403
- "060b001927d8b471f1c1a8c8851fbb4266d3b2edd674cccbd6c16c44b57b91dd"
422
+ "060b001927d8b471f1c1a8c8851fbb4266d3b2edd674cccbd6c16c44b57b91dd",
423
+ "7721ee19fd1bcf6902be3d359def40742d79b88f5d83e7e3ca22ba45ca3211aa"
404
424
  ]
405
425
  },
406
426
  ".claude/scripts/verdict.mjs": {
@@ -435,7 +455,8 @@
435
455
  "8f687f92fa60d4cc679fa13bff997cec92227f6bc0b90c1dc5b8a5400f2abef0",
436
456
  "e442ca58c4249ac10d5759bba2d8360544642e03b2b158e6253e71f5be3767fb",
437
457
  "eb52ef41269018107534d8dc2f552c57698e1677a1e76b5e666b5d09c007cac0",
438
- "47922b182915cb92696f851174f2e88c4ae8c67dc119ef39a3a597ef1300fab5"
458
+ "47922b182915cb92696f851174f2e88c4ae8c67dc119ef39a3a597ef1300fab5",
459
+ "57f6add0504db45e7577474f97705fe474310e4cd3310cdb348a2c64eabe88cd"
439
460
  ]
440
461
  },
441
462
  ".claude/skills/new-invariant/SKILL.md": {
@@ -503,7 +524,8 @@
503
524
  "since": "0.5.0",
504
525
  "hashes": [
505
526
  "80b4631502d95c619ad060ce02cc8702b12574758ba7df8337ed63efa15d764d",
506
- "cfd516f72acdfd197c59ac3cbdd39d7b175976dbd3d9eef52825dfdc4f1e5b60"
527
+ "cfd516f72acdfd197c59ac3cbdd39d7b175976dbd3d9eef52825dfdc4f1e5b60",
528
+ "8687eacf0ad707a4f887e0714e82fca23fd005fa9ec8824faedfe99ca46333e1"
507
529
  ]
508
530
  },
509
531
  ".codex/agents/security-scanner.toml": {
@@ -6,5 +6,6 @@
6
6
  "0.3.2": "bbcb24fd8ff5290826ee3b152b5130f6bd4697da",
7
7
  "0.4.0": "c77c6c3e5d991a55b733a05a3b5ae01e2a55e578",
8
8
  "0.5.0": "d78d0e77307a2be128de46ff4f64e3c146b5df2b",
9
- "0.6.0": "29ca0543368e922349945e52541a92b46ddcc5bd"
9
+ "0.6.0": "29ca0543368e922349945e52541a92b46ddcc5bd",
10
+ "0.6.1": "f1d1e3dbd2161545d77ee5e7f90fa8e74b8a9f3e"
10
11
  }