@xhulz/nina 0.38.0 → 0.39.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.
Files changed (128) hide show
  1. package/package.json +1 -1
  2. package/releases/0.39.0/core/GAPS.md +125 -0
  3. package/releases/0.39.0/core/answered.json +32 -0
  4. package/releases/0.39.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  5. package/releases/0.39.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  6. package/releases/0.39.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  7. package/releases/0.39.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  8. package/releases/0.39.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  9. package/releases/0.39.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  10. package/releases/0.39.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  11. package/releases/0.39.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  12. package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  13. package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  14. package/releases/0.39.0/core/tree/.claude/agents/architect.md +127 -0
  15. package/releases/0.39.0/core/tree/.claude/agents/dba.md +110 -0
  16. package/releases/0.39.0/core/tree/.claude/agents/devops.md +114 -0
  17. package/releases/0.39.0/core/tree/.claude/agents/implementer.md +156 -0
  18. package/releases/0.39.0/core/tree/.claude/agents/integration-tester.md +168 -0
  19. package/releases/0.39.0/core/tree/.claude/agents/planner.md +132 -0
  20. package/releases/0.39.0/core/tree/.claude/agents/qa.md +152 -0
  21. package/releases/0.39.0/core/tree/.claude/agents/reviewer.md +183 -0
  22. package/releases/0.39.0/core/tree/.claude/agents/secops.md +121 -0
  23. package/releases/0.39.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  24. package/releases/0.39.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  25. package/releases/0.39.0/core/tree/.claude/agents-overview.md +64 -0
  26. package/releases/0.39.0/core/tree/.claude/frontend.md +10 -0
  27. package/releases/0.39.0/core/tree/.claude/graph.md +75 -0
  28. package/releases/0.39.0/core/tree/.claude/patterns.md +213 -0
  29. package/releases/0.39.0/core/tree/.claude/pills/README.md +147 -0
  30. package/releases/0.39.0/core/tree/.claude/pipeline.md +101 -0
  31. package/releases/0.39.0/core/tree/.claude/retrieval.md +103 -0
  32. package/releases/0.39.0/core/tree/.claude/router.md +198 -0
  33. package/releases/0.39.0/core/tree/.claude/templates/integration.md +45 -0
  34. package/releases/0.39.0/core/tree/CLAUDE.md +163 -0
  35. package/releases/0.39.0/core/tree/scripts/cost-watch.mjs +18 -0
  36. package/releases/0.39.0/core/tree/scripts/edit-guard.mjs +24 -0
  37. package/releases/0.39.0/core/tree/scripts/harness-check.mjs +71 -0
  38. package/releases/0.39.0/core/tree/scripts/loop-gate.mjs +19 -0
  39. package/releases/0.39.0/core/vocabulary.json +13 -0
  40. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  41. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  42. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  43. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  44. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  45. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  46. package/releases/0.39.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  47. package/releases/0.39.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  48. package/releases/0.39.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  49. package/releases/0.39.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  50. package/releases/0.39.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  51. package/releases/0.39.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  52. package/releases/0.39.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  53. package/releases/0.39.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  54. package/releases/0.39.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  55. package/releases/0.39.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  56. package/releases/0.39.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  57. package/releases/0.39.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  58. package/releases/0.39.0/surfaces/db/tree/.claude/graph.md +8 -0
  59. package/releases/0.39.0/surfaces/db/tree/.claude/patterns.md +19 -0
  60. package/releases/0.39.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  61. package/releases/0.39.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  62. package/releases/0.39.0/surfaces/db/tree/.claude/router.md +4 -0
  63. package/releases/0.39.0/surfaces/db/tree/CLAUDE.md +17 -0
  64. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  65. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  66. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  67. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  68. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  69. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  70. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  71. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  72. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  73. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  74. package/releases/0.39.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  75. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  76. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  77. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  78. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  79. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  80. package/releases/0.39.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  81. package/releases/0.39.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  82. package/releases/0.39.0/surfaces/frontend/tree/.claude/router.md +14 -0
  83. package/releases/0.39.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  84. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  85. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  86. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  87. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  88. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  89. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  90. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  91. package/releases/0.39.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  92. package/releases/0.39.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  93. package/releases/0.39.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  94. package/releases/0.39.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  95. package/releases/0.39.0/surfaces/integrations/tree/.claude/router.md +6 -0
  96. package/releases/0.39.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  97. package/releases/0.39.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  98. package/releases/0.39.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  99. package/releases/0.39.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  100. package/releases/0.39.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  101. package/releases/0.39.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  102. package/releases/0.39.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  103. package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  104. package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  105. package/releases/0.39.0/surfaces/money/tree/.claude/graph.md +2 -0
  106. package/releases/0.39.0/surfaces/money/tree/.claude/patterns.md +49 -0
  107. package/releases/0.39.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  108. package/releases/0.39.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  109. package/releases/0.39.0/surfaces/money/tree/CLAUDE.md +11 -0
  110. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  111. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  112. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  113. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  114. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  115. package/releases/0.39.0/surfaces/pii/tree/CLAUDE.md +5 -0
  116. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  117. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  118. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  119. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  120. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  121. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  122. package/releases/0.39.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  123. package/releases/0.39.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  124. package/src/commands/compose.mjs +7 -3
  125. package/src/commands/init.mjs +1 -1
  126. package/src/gate.mjs +49 -1
  127. package/src/guard.mjs +188 -0
  128. package/src/wiring.mjs +3 -1
package/src/gate.mjs CHANGED
@@ -54,6 +54,7 @@
54
54
  * gate that could block work on its own crash would be worse than the loops it exists to stop.
55
55
  */
56
56
 
57
+ import { spawnSync } from 'node:child_process';
57
58
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
58
59
  import { dirname, join } from 'node:path';
59
60
  import { fileURLToPath } from 'node:url';
@@ -432,7 +433,18 @@ export function replay(entries, loops, forward = new Map()) {
432
433
  }
433
434
  }
434
435
  });
435
- return { effect: (to) => effect(to, entries.length), rounds: made };
436
+ // What is open when the replay ends: each loop with its rounds, the rounds the owner's replies gave it and the
437
+ // issues it counted by, and each loop-back no fix has acted on yet — what a compaction takes from the orchestrator.
438
+ const open = [...streak.entries()]
439
+ .filter(([key]) => key.split('|').length === 2)
440
+ .map(([key, s]) => {
441
+ const [source, token] = key.split('|');
442
+ const cap = Math.max(0, ...(loops.get(source)?.get(token)?.values() ?? []));
443
+ const issues = [...streak.keys()].filter((k) => k.startsWith(`${key}|`)).map((k) => k.slice(key.length + 1));
444
+ return { source, token, rounds: s.n, cap, given: grace.get(key) ?? 0, issues };
445
+ });
446
+ const waiting = [...pending.entries()].flatMap(([source, batch]) => batch.filter((v) => isLoopBack(v.verdict)).map((v) => ({ source, verdict: v.verdict, issues: v.issues ?? [] })));
447
+ return { effect: (to) => effect(to, entries.length), rounds: made, open, waiting };
436
448
  }
437
449
 
438
450
  /**
@@ -660,6 +672,40 @@ function onPre(input, project, path, at) {
660
672
  return confirmation(worst);
661
673
  }
662
674
 
675
+ /**
676
+ * What the gate hands the orchestrator after its conversation was compacted: the loops still open and the
677
+ * loop-backs waiting on a fix, the latest dispatches, where the checkout stands, and the two documents to read
678
+ * again. The graph and the router are read once, at a session's start, and a compaction drops them with every
679
+ * report; the router told the orchestrator to write "round 2 of max 2" in a dispatch so "you after a
680
+ * compaction" could see it, and after a compaction it sees only a summary.
681
+ *
682
+ * @param {object} project - `loadProject`'s.
683
+ * @param {string} path - The session's ledger.
684
+ * @param {string} root - The project.
685
+ * @returns {object}
686
+ */
687
+ function afterCompaction(project, path, root) {
688
+ const entries = readLedger(path);
689
+ const { open, waiting } = replay(entries, project.loops, project.forward);
690
+ const lines = ['The conversation was compacted. What the NINA loop gate kept of this session:'];
691
+ for (const l of open) {
692
+ lines.push(
693
+ `- open loop: ${l.source} ${l.token} — ${l.rounds} round(s), capped at ${l.cap}${l.given ? ` and ${l.given} more from the owner's replies` : ''}` +
694
+ (l.issues.length ? `, issues ${l.issues.join(', ')}` : ''),
695
+ );
696
+ }
697
+ for (const w of waiting) lines.push(`- waiting on a fix: ${w.source} ${w.verdict}${w.issues.length ? `, issues ${w.issues.join(', ')}` : ''}`);
698
+ if (open.length === 0 && waiting.length === 0) lines.push('- no loop is open');
699
+ const dispatched = entries.filter((e) => e.k === 'dispatch' && e.role).slice(-4);
700
+ if (dispatched.length) lines.push(`- the latest dispatches: ${dispatched.map((e) => e.role).join(', ')}, the newest at ${String(dispatched.at(-1).at ?? '').slice(11, 16)} UTC`);
701
+ const git = (...args) => spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', timeout: 2000 }).stdout?.trim() ?? '';
702
+ const branch = git('rev-parse', '--abbrev-ref', 'HEAD');
703
+ const last = git('log', '-1', '--format=%h %s');
704
+ if (branch) lines.push(`- the checkout: branch ${branch}${last ? `, last commit ${last.slice(0, 100)}` : ''}`);
705
+ lines.push('Before the next dispatch, read .claude/graph.md and .claude/router.md again: they were read at the start of the session, and are no longer in your context.');
706
+ return { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: lines.join('\n') } };
707
+ }
708
+
663
709
  /** Drops a project's ledgers nobody has written to in a month, and error lines older than a week. */
664
710
  function prune(root, now) {
665
711
  const dir = projectGateDir(root);
@@ -719,6 +765,8 @@ export function handle(input, { root, now = new Date(), pkg = PACKAGE }) {
719
765
  return null;
720
766
  case 'SubagentStop':
721
767
  return onStop(input, project, path, at);
768
+ case 'SessionStart':
769
+ return input.source === 'compact' ? afterCompaction(project, path, root) : null;
722
770
  case 'SubagentStart': {
723
771
  // Not the loop's business, but the one hook every stage passes through as it begins: its lessons. Claude
724
772
  // Code reads `additionalContext` from this event's `hookSpecificOutput` into the subagent's context as it
package/src/guard.mjs CHANGED
@@ -32,6 +32,190 @@ const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
32
32
  /** The tools that change a file, and where each names it. */
33
33
  const EDITS = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' };
34
34
 
35
+ /** The git subcommands that move the checkout or write its history — Hard Rule #18's. */
36
+ const MOVES = new Set(['stash', 'checkout', 'switch', 'reset', 'restore', 'clean', 'commit', 'merge', 'rebase', 'cherry-pick', 'revert', 'am', 'apply', 'pull', 'push']);
37
+
38
+ /** Words before the command word that leave it the command: `env X=1 git …`, `command git …`, `{ git …; }`. */
39
+ const RUNNERS = new Set(['env', 'command', 'exec', 'time', 'nohup', 'nice', '{', '}', '!']);
40
+
41
+ /** A `-C` or a `cd` to one of these is still the tree the stage was dispatched into. */
42
+ const HERE = new Set(['.', './']);
43
+
44
+ /** Where a heredoc's delimiter word ends. */
45
+ const DELIMITER_ENDS = new Set([' ', '\t', '\r', '\n', ';', '&', '|', '(', ')', '`', '<', '>']);
46
+
47
+ /** The characters that can end a run of plain word characters. */
48
+ const SPECIAL = new Set(["'", '"', '\\', '#', '\n', '<', '&', ';', '|', '(', ')', '`', ' ', '\t', '\r']);
49
+
50
+ /**
51
+ * A shell command as the shell reads it: its simple commands, in order, each as its words with the quotes removed.
52
+ * `&&`, `||`, `;`, `|`, `&`, a newline, a parenthesis and a backquote end one; a quoted string is part of a word — `"git" stash`
53
+ * is git — and never a separator or a command of its own, across lines too; a comment and a heredoc's body are not
54
+ * read. One pass, no pattern: the command is a model's, and a pattern that backtracks on it is one a stage could
55
+ * stall the guard with.
56
+ *
57
+ * @param {string} command
58
+ * @returns {string[][]}
59
+ */
60
+ export function simpleCommands(command) {
61
+ const text = String(command);
62
+ const commands = [[]];
63
+ const heredocs = [];
64
+ let word = null;
65
+ const end = () => {
66
+ if (word !== null) commands.at(-1).push(word);
67
+ word = null;
68
+ };
69
+ const next = () => {
70
+ end();
71
+ if (commands.at(-1).length) commands.push([]);
72
+ };
73
+ let i = 0;
74
+ while (i < text.length) {
75
+ const c = text[i];
76
+ if (c === "'") {
77
+ const close = text.indexOf("'", i + 1);
78
+ const stop = close === -1 ? text.length : close;
79
+ word = (word ?? '') + text.slice(i + 1, stop);
80
+ i = stop + 1;
81
+ } else if (c === '"') {
82
+ const parts = [];
83
+ i += 1;
84
+ let from = i;
85
+ while (i < text.length && text[i] !== '"') {
86
+ if (text[i] === '\\' && i + 1 < text.length) {
87
+ parts.push(text.slice(from, i));
88
+ from = i + 1;
89
+ i += 2;
90
+ } else i += 1;
91
+ }
92
+ parts.push(text.slice(from, i));
93
+ word = (word ?? '') + parts.join('');
94
+ i += 1;
95
+ } else if (c === '\\') {
96
+ if (text[i + 1] !== '\n') word = (word ?? '') + (text[i + 1] ?? '');
97
+ i += 2;
98
+ } else if (c === '#' && word === null) {
99
+ const line = text.indexOf('\n', i);
100
+ i = line === -1 ? text.length : line;
101
+ } else if (c === '\n') {
102
+ next();
103
+ i += 1;
104
+ for (const delimiter of heredocs.splice(0)) {
105
+ while (i < text.length) {
106
+ const line = text.indexOf('\n', i);
107
+ const stop = line === -1 ? text.length : line;
108
+ const body = text.slice(i, stop);
109
+ i = stop + 1;
110
+ if (body.trim() === delimiter) break;
111
+ }
112
+ }
113
+ } else if (c === '<' && text.startsWith('<<<', i)) {
114
+ word = (word ?? '') + '<<<';
115
+ i += 3;
116
+ } else if (c === '<' && text[i + 1] === '<') {
117
+ end();
118
+ i += text[i + 2] === '-' ? 3 : 2;
119
+ while (text[i] === ' ' || text[i] === '\t') i += 1;
120
+ const from = i;
121
+ while (i < text.length && !DELIMITER_ENDS.has(text[i])) i += 1;
122
+ const delimiter = text.slice(from, i).replaceAll("'", '').replaceAll('"', '').replaceAll('\\', '');
123
+ if (delimiter) heredocs.push(delimiter);
124
+ } else if (c === '&' && (text[i + 1] === '>' || word?.endsWith('>') || word?.endsWith('<'))) {
125
+ word = (word ?? '') + c;
126
+ i += 1;
127
+ } else if (c === ';' || c === '&' || c === '|' || c === '(' || c === ')' || c === '`') {
128
+ next();
129
+ i += (c === '&' || c === '|') && text[i + 1] === c ? 2 : 1;
130
+ } else if (c === ' ' || c === '\t' || c === '\r') {
131
+ end();
132
+ i += 1;
133
+ } else {
134
+ const from = i;
135
+ i += 1;
136
+ while (i < text.length && !SPECIAL.has(text[i])) i += 1;
137
+ word = (word ?? '') + text.slice(from, i);
138
+ }
139
+ }
140
+ end();
141
+ return commands.filter((words) => words.length);
142
+ }
143
+
144
+ /**
145
+ * The git subcommand in a shell command that would move the checkout the command runs in, or null. Read command
146
+ * by command, in order, as the shell runs it: a `cd` exempts only what comes after it — `git stash && cd /tmp`
147
+ * stashed the tree all the same, and a test of the whole string for a `cd` let it through — and `-C .` is not
148
+ * another directory. A git command is one whose command word is `git`, not a phrase in a quoted string or a
149
+ * heredoc's body; its subcommand is the word after its global options, so `merge-base` is not `merge`; and a
150
+ * stash's `list` or `show`, and an `apply` that only checks, read.
151
+ *
152
+ * @param {string} command - A Bash tool call's command.
153
+ * @returns {string|null}
154
+ */
155
+ export function checkoutMove(command) {
156
+ let moved = false;
157
+ for (const words of simpleCommands(command)) {
158
+ while (words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || RUNNERS.has(words[0]))) words.shift();
159
+ if (words[0] === 'cd' || words[0] === 'pushd') {
160
+ moved ||= !HERE.has(words[1] ?? '');
161
+ continue;
162
+ }
163
+ if (words[0] !== 'git') continue;
164
+ let i = 1;
165
+ let elsewhere = false;
166
+ while (words[i]?.startsWith('-')) {
167
+ if (words[i] === '-C') {
168
+ elsewhere ||= !HERE.has(words[i + 1] ?? '.');
169
+ i += 2;
170
+ } else if (words[i] === '-c') {
171
+ i += 2;
172
+ } else {
173
+ if (/^--(git-dir|work-tree)\b/.test(words[i])) elsewhere = true;
174
+ i += 1;
175
+ }
176
+ }
177
+ const sub = words[i];
178
+ if (!MOVES.has(sub) || moved || elsewhere) continue;
179
+ const rest = words.slice(i + 1);
180
+ if (sub === 'stash' && ['list', 'show'].includes(rest[0])) continue;
181
+ if (sub === 'apply' && rest.some((w) => ['--check', '--stat', '--numstat', '--summary'].includes(w)) && !rest.includes('--apply')) continue;
182
+ return sub;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * A stage's shell command that would move the checkout it was dispatched into, refused. Only the orchestrator
189
+ * commits, and the work in flight lives in that tree, uncommitted: one reviewer, told not to edit, stashed it to
190
+ * compare with the baseline, and its pop failed on a conflict.
191
+ *
192
+ * @param {object} input - The hook's stdin, parsed.
193
+ * @param {{layers: string}} pinned - The pinned layers, whose hard rules say whether this one is the project's.
194
+ * @returns {object|null}
195
+ */
196
+ function handleShell(input, pinned) {
197
+ if (!input.agent_id) return null;
198
+ const moves = checkoutMove(input.tool_input?.command ?? '');
199
+ if (!moves) return null;
200
+ let rules = '';
201
+ try {
202
+ rules = readFileSync(join(pinned.layers, 'core', 'tree', 'CLAUDE.md'), 'utf8');
203
+ } catch {
204
+ return null;
205
+ }
206
+ if (!rules.includes('Only the orchestrator commits')) return null;
207
+ return {
208
+ hookSpecificOutput: {
209
+ hookEventName: 'PreToolUse',
210
+ permissionDecision: 'deny',
211
+ permissionDecisionReason:
212
+ `NINA: \`git ${moves}\` would move the checkout the pipeline's work in flight lives in — Hard Rule #18: only the orchestrator ` +
213
+ 'commits, and no stage moves the checkout. Read history with git diff, log, show, status or blame, and build or test another ' +
214
+ 'revision in a `git worktree` you remove afterwards.',
215
+ },
216
+ };
217
+ }
218
+
35
219
  /** How much of a file is read to find it: the head, not the whole of a large file on every edit. */
36
220
  const HEAD_BYTES = 16 * 1024;
37
221
 
@@ -83,6 +267,10 @@ function head(path) {
83
267
  export async function handleEdit(input, { root, pkg = PACKAGE }) {
84
268
  try {
85
269
  if (input?.hook_event_name !== 'PreToolUse') return null;
270
+ if (input.tool_name === 'Bash') {
271
+ const pinnedShell = input.agent_id ? pinnedLayers(root, pkg) : null;
272
+ return pinnedShell ? handleShell(input, pinnedShell) : null;
273
+ }
86
274
  const key = EDITS[input.tool_name];
87
275
  const path = key ? input.tool_input?.[key] : null;
88
276
  const pinned = typeof path === 'string' ? pinnedLayers(root, pkg) : null;
package/src/wiring.mjs CHANGED
@@ -133,8 +133,10 @@ export const HOOKS = [
133
133
  { event: 'PostToolUse', matcher: 'Agent|Task|SendMessage|AskUserQuestion|SubagentHandback', script: GATE, timeout: 10, why: 'the gate never learns which rounds went out, or that the owner answered' },
134
134
  { event: 'SubagentStop', script: GATE, timeout: 10, why: 'the gate misses the verdict of a stage that wrote its report as its last message' },
135
135
  { event: 'SubagentStart', script: GATE, timeout: 10, why: 'a stage is not handed its lessons, and opens every shared pill to find them, or none' },
136
+ { event: 'SessionStart', matcher: 'compact', script: GATE, timeout: 10, why: 'after a compaction the orchestrator has lost the graph, the router and its open loops, and nothing gives them back' },
136
137
  {
137
- event: 'PreToolUse', matcher: 'Edit|Write|MultiEdit|NotebookEdit', script: GUARD, timeout: 10, why: 'a hand edit to a composed file is found only after the turn, as drift, and the next compose overwrites it',
138
+ event: 'PreToolUse', matcher: 'Edit|Write|MultiEdit|NotebookEdit|Bash', script: GUARD, timeout: 10,
139
+ why: "a hand edit to a composed file is found only after the turn, as drift, and a stage's git stash or checkout takes the work in flight",
138
140
  crash: (cannot) => ({ systemMessage: `NINA: the edit guard could not start, so composed files can be edited in place — ${cannot}` }),
139
141
  },
140
142
  // Every tool, since what it watches is a subagent's every turn; a call outside a subagent returns at once.