@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.
- package/package.json +1 -1
- package/releases/0.39.0/core/GAPS.md +125 -0
- package/releases/0.39.0/core/answered.json +32 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.39.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.39.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.39.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.39.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.39.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.39.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.39.0/core/tree/.claude/agents/qa.md +152 -0
- package/releases/0.39.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.39.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.39.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.39.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.39.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.39.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.39.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.39.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.39.0/core/tree/.claude/pills/README.md +147 -0
- package/releases/0.39.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.39.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.39.0/core/tree/.claude/router.md +198 -0
- package/releases/0.39.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.39.0/core/tree/CLAUDE.md +163 -0
- package/releases/0.39.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.39.0/core/tree/scripts/edit-guard.mjs +24 -0
- package/releases/0.39.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.39.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.39.0/core/vocabulary.json +13 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.39.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.39.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.39.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.39.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.39.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.39.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.39.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/commands/compose.mjs +7 -3
- package/src/commands/init.mjs +1 -1
- package/src/gate.mjs +49 -1
- package/src/guard.mjs +188 -0
- 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
|
-
|
|
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,
|
|
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.
|