@maestrofrontier/frontier 1.4.5 → 1.5.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/.agents/plugins/marketplace.json +21 -21
- package/.codex-plugin/plugin.json +29 -29
- package/.cursorrules +197 -194
- package/AGENTS.md +3 -3
- package/README.md +368 -368
- package/bin/maestro.cjs +75 -75
- package/commands/compress.md +36 -36
- package/commands/frontier.md +124 -124
- package/commands/terse.md +23 -23
- package/docs/codex.md +167 -167
- package/docs/orchestration.md +168 -168
- package/frontier/cli.cjs +279 -252
- package/frontier/config.cjs +468 -468
- package/frontier/dispatch.cjs +267 -255
- package/frontier/judge.cjs +92 -92
- package/frontier/run.cjs +201 -180
- package/frontier/schema.cjs +112 -112
- package/frontier/semaphore.cjs +49 -49
- package/frontier/synthesize.cjs +79 -79
- package/hooks/frontier-autorun.cjs +127 -120
- package/hooks/hooks.json +103 -103
- package/hooks/maestro-doctrine-guard.cjs +81 -81
- package/hooks/maestro-gate-reminder.cjs +22 -7
- package/hooks/maestro-gate-telemetry.cjs +79 -77
- package/hooks/maestro-phase-scope.cjs +118 -118
- package/hooks/maestro-statusline-sync.cjs +152 -152
- package/hooks/maestro-subagent-guard.cjs +148 -148
- package/hooks/maestro-terse-mode.cjs +189 -189
- package/hooks/maestro-toolbudget-advisory.cjs +127 -127
- package/integrations/README.md +111 -111
- package/integrations/cline/skills/frontier/SKILL.md +75 -75
- package/integrations/codex/prompts/frontier.md +70 -70
- package/integrations/codex/prompts/update.md +39 -39
- package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -122
- package/integrations/codex/skills/maestro-settings/SKILL.md +55 -55
- package/integrations/codex/skills/maestro-terse/SKILL.md +58 -58
- package/integrations/codex/skills/maestro-update/SKILL.md +31 -31
- package/integrations/cursor/commands/frontier.md +63 -63
- package/integrations/cursor/commands/update.md +34 -34
- package/integrations/gemini/commands/frontier.toml +76 -76
- package/integrations/windsurf/workflows/frontier.md +70 -70
- package/package.json +58 -58
- package/scripts/install.cjs +1014 -1014
- package/settings/cli.cjs +140 -140
- package/settings/config.cjs +309 -309
- package/skills/maestro-frontier/SKILL.md +122 -122
- package/skills/maestro-settings/SKILL.md +55 -55
- package/skills/maestro-terse/SKILL.md +58 -58
- package/skills/maestro-update/SKILL.md +31 -31
- package/skills/terse/SKILL.md +74 -74
|
@@ -40,14 +40,29 @@ try { fs.writeFileSync(marker, '1'); } catch { /* still remind */ }
|
|
|
40
40
|
|
|
41
41
|
const checklistLines = [
|
|
42
42
|
'Maestro Decision Gate (S1): before the first file edit, output the',
|
|
43
|
-
'counted verdict line `
|
|
44
|
-
'| multi-agent — <reason>`. files>=5
|
|
45
|
-
'spawn the Planner via the
|
|
46
|
-
'
|
|
47
|
-
'<=3 files total in one
|
|
48
|
-
'single-agent.'
|
|
43
|
+
'counted verdict line `Maestro · frontier <on|off> — files=<n>',
|
|
44
|
+
'concerns=<m> -> single-agent | multi-agent — <reason>`. files>=5',
|
|
45
|
+
'across 2+ concerns = multi-agent: spawn the Planner via the',
|
|
46
|
+
'Agent/Task tool BEFORE any edit. A met trigger downgrades ONLY on',
|
|
47
|
+
'>60% file overlap between subtasks or <=3 files total in one',
|
|
48
|
+
'dependency chain. Sub-trigger tasks stay single-agent.'
|
|
49
49
|
];
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
// Inject the live frontier engine state so the badge in the verdict
|
|
52
|
+
// line is accurate. Degrades to 'off' on any failure — never throws.
|
|
53
|
+
let badge = 'off';
|
|
54
|
+
try {
|
|
55
|
+
const cfg = require('../frontier/config.cjs');
|
|
56
|
+
const cwd = data.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
57
|
+
const scope = cfg.resolveScope([], { cwd });
|
|
58
|
+
const st = cfg.loadState(scope);
|
|
59
|
+
badge = (!st || st.mode === 'off')
|
|
60
|
+
? 'off'
|
|
61
|
+
: ('on (' + st.mode + '/' + (st.preset || st.model || '') + ')');
|
|
62
|
+
} catch { badge = 'off'; }
|
|
63
|
+
|
|
64
|
+
const checklist = checklistLines.join('\n') +
|
|
65
|
+
'\nCurrent frontier state for the badge: frontier ' + badge;
|
|
51
66
|
|
|
52
67
|
process.stdout.write(JSON.stringify({
|
|
53
68
|
hookSpecificOutput: {
|
|
@@ -1,77 +1,79 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro SessionEnd gate telemetry. Strictly opt-in, strictly local.
|
|
3
|
-
//
|
|
4
|
-
// Records one JSON line per session so you can audit your own Decision
|
|
5
|
-
// Gate behavior over time (AGENTS.md S1): did the session route
|
|
6
|
-
// single-agent or multi-agent, how many specialists were spawned, and
|
|
7
|
-
// how the session ended.
|
|
8
|
-
//
|
|
9
|
-
// Privacy: does nothing unless MAESTRO_TELEMETRY=1. Writes only to
|
|
10
|
-
// ~/.claude/maestro-telemetry.jsonl on this machine. No network, ever.
|
|
11
|
-
// Captures counts and the project folder NAME only -- no prompts, no
|
|
12
|
-
// file contents, no paths beyond the basename.
|
|
13
|
-
//
|
|
14
|
-
// Payload fields verified against code.claude.com/docs/en/hooks
|
|
15
|
-
// (SessionEnd input: session_id, transcript_path, cwd, reason;
|
|
16
|
-
// SessionEnd output cannot block), 2026-06-10.
|
|
17
|
-
//
|
|
18
|
-
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
19
|
-
// package.json in a parent directory of the install location.
|
|
20
|
-
//
|
|
21
|
-
// Install: see README "Claude Code: Gate Telemetry".
|
|
22
|
-
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
const os = require('os');
|
|
25
|
-
const path = require('path');
|
|
26
|
-
|
|
27
|
-
if (process.env.MAESTRO_TELEMETRY !== '1') process.exit(0);
|
|
28
|
-
|
|
29
|
-
let data = {};
|
|
30
|
-
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
31
|
-
|
|
32
|
-
// Spawn count alone misses the measured failure mode: a multi-agent
|
|
33
|
-
// verdict stated in text but no specialist ever spawned. Parse the S1
|
|
34
|
-
// verdict line too and record verdict vs spawned separately; mismatch
|
|
35
|
-
// flags either direction (multi verdict with 0 spawns, single verdict
|
|
36
|
-
// with spawns). Last verdict line wins (re-gated mid-session).
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (item
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro SessionEnd gate telemetry. Strictly opt-in, strictly local.
|
|
3
|
+
//
|
|
4
|
+
// Records one JSON line per session so you can audit your own Decision
|
|
5
|
+
// Gate behavior over time (AGENTS.md S1): did the session route
|
|
6
|
+
// single-agent or multi-agent, how many specialists were spawned, and
|
|
7
|
+
// how the session ended.
|
|
8
|
+
//
|
|
9
|
+
// Privacy: does nothing unless MAESTRO_TELEMETRY=1. Writes only to
|
|
10
|
+
// ~/.claude/maestro-telemetry.jsonl on this machine. No network, ever.
|
|
11
|
+
// Captures counts and the project folder NAME only -- no prompts, no
|
|
12
|
+
// file contents, no paths beyond the basename.
|
|
13
|
+
//
|
|
14
|
+
// Payload fields verified against code.claude.com/docs/en/hooks
|
|
15
|
+
// (SessionEnd input: session_id, transcript_path, cwd, reason;
|
|
16
|
+
// SessionEnd output cannot block), 2026-06-10.
|
|
17
|
+
//
|
|
18
|
+
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
19
|
+
// package.json in a parent directory of the install location.
|
|
20
|
+
//
|
|
21
|
+
// Install: see README "Claude Code: Gate Telemetry".
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
if (process.env.MAESTRO_TELEMETRY !== '1') process.exit(0);
|
|
28
|
+
|
|
29
|
+
let data = {};
|
|
30
|
+
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
31
|
+
|
|
32
|
+
// Spawn count alone misses the measured failure mode: a multi-agent
|
|
33
|
+
// verdict stated in text but no specialist ever spawned. Parse the S1
|
|
34
|
+
// verdict line too and record verdict vs spawned separately; mismatch
|
|
35
|
+
// flags either direction (multi verdict with 0 spawns, single verdict
|
|
36
|
+
// with spawns). Last verdict line wins (re-gated mid-session).
|
|
37
|
+
// Matches BOTH the legacy `GATE: files=...` line and the rebranded
|
|
38
|
+
// `Maestro · frontier <on|off> — files=...` badge line.
|
|
39
|
+
const verdictRe = /(?:GATE|Maestro)[:\s·].*?files=\S+\s+concerns=\S+\s*->\s*(single|multi)-agent/;
|
|
40
|
+
let agentCount = 0;
|
|
41
|
+
let verdict = null;
|
|
42
|
+
if (data.transcript_path && fs.existsSync(data.transcript_path)) {
|
|
43
|
+
try {
|
|
44
|
+
const buf = fs.readFileSync(data.transcript_path, 'utf8');
|
|
45
|
+
const text = buf.length > 8000000 ? buf.slice(-8000000) : buf;
|
|
46
|
+
for (const line of text.split(/\r?\n/)) {
|
|
47
|
+
let e;
|
|
48
|
+
try { e = JSON.parse(line); } catch { continue; }
|
|
49
|
+
if (!e || e.type !== 'assistant' || !e.message || !Array.isArray(e.message.content)) continue;
|
|
50
|
+
for (const item of e.message.content) {
|
|
51
|
+
if (!item) continue;
|
|
52
|
+
if (item.type === 'tool_use' && (item.name === 'Task' || item.name === 'Agent')) agentCount++;
|
|
53
|
+
if (item.type === 'text' && typeof item.text === 'string') {
|
|
54
|
+
const m = item.text.match(verdictRe);
|
|
55
|
+
if (m) verdict = m[1];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const row = {
|
|
63
|
+
ts: new Date().toISOString(),
|
|
64
|
+
session_id: data.session_id || null,
|
|
65
|
+
gate: agentCount > 0 ? 'multi' : 'single',
|
|
66
|
+
verdict,
|
|
67
|
+
agent_count: agentCount,
|
|
68
|
+
mismatch: verdict !== null && ((verdict === 'multi') !== (agentCount > 0)),
|
|
69
|
+
reason: data.reason || null,
|
|
70
|
+
project: data.cwd ? path.basename(data.cwd) : null
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const dir = path.join(os.homedir(), '.claude');
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
fs.appendFileSync(path.join(dir, 'maestro-telemetry.jsonl'), JSON.stringify(row) + '\n');
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
process.exit(0);
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro PostToolUse phase-scope guard. Enforces AGENTS.md S7.1
|
|
3
|
-
// structurally: max 5 files modified per phase. Counts distinct files
|
|
4
|
-
// touched by Edit/Write/NotebookEdit -- plus Bash mutations whose
|
|
5
|
-
// target path is statically extractable (redirects, sed -i, tee, mv,
|
|
6
|
-
// cp, rm, mkdir, touch) -- since the last real user prompt (one turn
|
|
7
|
-
// ~ one phase for interactive work) and warns once per turn when the
|
|
8
|
-
// count exceeds MAESTRO_PHASE_FILE_CAP (default 5). Targets with
|
|
9
|
-
// shell expansion ($, backticks, globs) are skipped: a missed count
|
|
10
|
-
// is cheaper than a false warning. git commit / npm install mutate
|
|
11
|
-
// but name no file; they are out of scope for a file counter.
|
|
12
|
-
//
|
|
13
|
-
// Wire with matcher "Edit|Write|NotebookEdit|Bash" so it only runs on
|
|
14
|
-
// file-modifying tools. Soft warning via additionalContext -- never
|
|
15
|
-
// blocks; the agent decides whether the scope is justified (e.g. a
|
|
16
|
-
// user-approved bulk rename). Degrades silently on missing payload
|
|
17
|
-
// fields. Payload fields verified against code.claude.com/docs/en/hooks
|
|
18
|
-
// (PostToolUse input: tool_name, tool_input, transcript_path;
|
|
19
|
-
// output: hookSpecificOutput.additionalContext), 2026-06-10.
|
|
20
|
-
//
|
|
21
|
-
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
22
|
-
// package.json in a parent directory of the install location.
|
|
23
|
-
//
|
|
24
|
-
// Install: see README "Claude Code: Phase-Scope Guard".
|
|
25
|
-
|
|
26
|
-
const fs = require('fs');
|
|
27
|
-
|
|
28
|
-
const MUTATORS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
29
|
-
const MARKER = 'Maestro phase-scope guard:';
|
|
30
|
-
|
|
31
|
-
// Best-effort file targets of a Bash command's mutating constructs.
|
|
32
|
-
function bashTargets(cmd) {
|
|
33
|
-
const files = [];
|
|
34
|
-
if (typeof cmd !== 'string') return files;
|
|
35
|
-
const strip = t => t.replace(/^["']+|["']+$/g, '');
|
|
36
|
-
const ok = t => t && !t.startsWith('-') && !/[$`*?{}()\[\]<>]/.test(t) && t !== '/dev/null' && !/^nul$/i.test(t);
|
|
37
|
-
let m;
|
|
38
|
-
const redir = /(?<![-=<>])>{1,2}\s*([^\s;&|<>]+)/g;
|
|
39
|
-
while ((m = redir.exec(cmd))) { const t = strip(m[1]); if (ok(t)) files.push(t); }
|
|
40
|
-
for (let seg of cmd.split(/(?:&&|\|\||[;|\n])/)) {
|
|
41
|
-
seg = seg.replace(/(?<![-=<>])>{1,2}\s*[^\s;&|<>]+/g, ' ');
|
|
42
|
-
const toks = seg.trim().split(/\s+/).filter(Boolean);
|
|
43
|
-
while (toks.length && (toks[0] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[0]))) toks.shift();
|
|
44
|
-
const name = toks[0];
|
|
45
|
-
if (!name) continue;
|
|
46
|
-
const args = toks.slice(1).filter(a => !a.startsWith('-')).map(strip).filter(ok);
|
|
47
|
-
if ((name === 'mv' || name === 'cp') && args.length >= 2) files.push(args[args.length - 1]);
|
|
48
|
-
else if (name === 'rm' || name === 'mkdir' || name === 'touch' || name === 'tee') files.push(...args);
|
|
49
|
-
else if (name === 'sed' && /(^|\s)-i/.test(seg) && args.length) files.push(args[args.length - 1]);
|
|
50
|
-
}
|
|
51
|
-
return files;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let data = {};
|
|
55
|
-
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
56
|
-
|
|
57
|
-
let lines = [];
|
|
58
|
-
if (data.transcript_path && fs.existsSync(data.transcript_path)) {
|
|
59
|
-
try {
|
|
60
|
-
const buf = fs.readFileSync(data.transcript_path, 'utf8');
|
|
61
|
-
lines = (buf.length > 4000000 ? buf.slice(-4000000) : buf).split(/\r?\n/);
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Locate the last genuine user prompt (typed text, not a tool_result
|
|
66
|
-
// carrier). Everything after it is the current turn.
|
|
67
|
-
let turnStart = 0;
|
|
68
|
-
const parsed = lines.map(l => { try { return JSON.parse(l); } catch { return null; } });
|
|
69
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
70
|
-
const e = parsed[i];
|
|
71
|
-
if (!e || e.type !== 'user' || e.isMeta || !e.message) continue;
|
|
72
|
-
const c = e.message.content;
|
|
73
|
-
const genuine = typeof c === 'string'
|
|
74
|
-
? true
|
|
75
|
-
: Array.isArray(c) && c.some(x => x && x.type === 'text') && !c.some(x => x && x.type === 'tool_result');
|
|
76
|
-
if (genuine) turnStart = i;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Fire once per turn.
|
|
80
|
-
for (let i = turnStart; i < lines.length; i++) {
|
|
81
|
-
if (lines[i].includes(MARKER)) process.exit(0);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const files = new Set();
|
|
85
|
-
for (let i = turnStart; i < parsed.length; i++) {
|
|
86
|
-
const e = parsed[i];
|
|
87
|
-
if (!e || e.type !== 'assistant' || !e.message || !Array.isArray(e.message.content)) continue;
|
|
88
|
-
for (const item of e.message.content) {
|
|
89
|
-
if (!item || item.type !== 'tool_use' || !item.input) continue;
|
|
90
|
-
if (MUTATORS.has(item.name)) {
|
|
91
|
-
const p = item.input.file_path || item.input.notebook_path;
|
|
92
|
-
if (p) files.add(p);
|
|
93
|
-
} else if (item.name === 'Bash') {
|
|
94
|
-
for (const t of bashTargets(item.input.command)) files.add(t);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// The triggering call may not be in the transcript yet.
|
|
99
|
-
if (data.tool_input) {
|
|
100
|
-
if (MUTATORS.has(data.tool_name)) {
|
|
101
|
-
const p = data.tool_input.file_path || data.tool_input.notebook_path;
|
|
102
|
-
if (p) files.add(p);
|
|
103
|
-
} else if (data.tool_name === 'Bash') {
|
|
104
|
-
for (const t of bashTargets(data.tool_input.command)) files.add(t);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const cap = parseInt(process.env.MAESTRO_PHASE_FILE_CAP, 10) || 5;
|
|
109
|
-
if (files.size > cap) {
|
|
110
|
-
process.stdout.write(JSON.stringify({
|
|
111
|
-
hookSpecificOutput: {
|
|
112
|
-
hookEventName: 'PostToolUse',
|
|
113
|
-
additionalContext: `${MARKER} ${files.size} distinct files modified this turn exceeds the max-${cap}-files-per-phase rule (AGENTS.md S7.1). Complete and verify the current phase before expanding scope, or split the remaining work into a follow-up phase. If the user explicitly approved a wider batch (e.g. a bulk rename), proceed.`
|
|
114
|
-
}
|
|
115
|
-
}));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
process.exit(0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro PostToolUse phase-scope guard. Enforces AGENTS.md S7.1
|
|
3
|
+
// structurally: max 5 files modified per phase. Counts distinct files
|
|
4
|
+
// touched by Edit/Write/NotebookEdit -- plus Bash mutations whose
|
|
5
|
+
// target path is statically extractable (redirects, sed -i, tee, mv,
|
|
6
|
+
// cp, rm, mkdir, touch) -- since the last real user prompt (one turn
|
|
7
|
+
// ~ one phase for interactive work) and warns once per turn when the
|
|
8
|
+
// count exceeds MAESTRO_PHASE_FILE_CAP (default 5). Targets with
|
|
9
|
+
// shell expansion ($, backticks, globs) are skipped: a missed count
|
|
10
|
+
// is cheaper than a false warning. git commit / npm install mutate
|
|
11
|
+
// but name no file; they are out of scope for a file counter.
|
|
12
|
+
//
|
|
13
|
+
// Wire with matcher "Edit|Write|NotebookEdit|Bash" so it only runs on
|
|
14
|
+
// file-modifying tools. Soft warning via additionalContext -- never
|
|
15
|
+
// blocks; the agent decides whether the scope is justified (e.g. a
|
|
16
|
+
// user-approved bulk rename). Degrades silently on missing payload
|
|
17
|
+
// fields. Payload fields verified against code.claude.com/docs/en/hooks
|
|
18
|
+
// (PostToolUse input: tool_name, tool_input, transcript_path;
|
|
19
|
+
// output: hookSpecificOutput.additionalContext), 2026-06-10.
|
|
20
|
+
//
|
|
21
|
+
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
22
|
+
// package.json in a parent directory of the install location.
|
|
23
|
+
//
|
|
24
|
+
// Install: see README "Claude Code: Phase-Scope Guard".
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
|
|
28
|
+
const MUTATORS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
29
|
+
const MARKER = 'Maestro phase-scope guard:';
|
|
30
|
+
|
|
31
|
+
// Best-effort file targets of a Bash command's mutating constructs.
|
|
32
|
+
function bashTargets(cmd) {
|
|
33
|
+
const files = [];
|
|
34
|
+
if (typeof cmd !== 'string') return files;
|
|
35
|
+
const strip = t => t.replace(/^["']+|["']+$/g, '');
|
|
36
|
+
const ok = t => t && !t.startsWith('-') && !/[$`*?{}()\[\]<>]/.test(t) && t !== '/dev/null' && !/^nul$/i.test(t);
|
|
37
|
+
let m;
|
|
38
|
+
const redir = /(?<![-=<>])>{1,2}\s*([^\s;&|<>]+)/g;
|
|
39
|
+
while ((m = redir.exec(cmd))) { const t = strip(m[1]); if (ok(t)) files.push(t); }
|
|
40
|
+
for (let seg of cmd.split(/(?:&&|\|\||[;|\n])/)) {
|
|
41
|
+
seg = seg.replace(/(?<![-=<>])>{1,2}\s*[^\s;&|<>]+/g, ' ');
|
|
42
|
+
const toks = seg.trim().split(/\s+/).filter(Boolean);
|
|
43
|
+
while (toks.length && (toks[0] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[0]))) toks.shift();
|
|
44
|
+
const name = toks[0];
|
|
45
|
+
if (!name) continue;
|
|
46
|
+
const args = toks.slice(1).filter(a => !a.startsWith('-')).map(strip).filter(ok);
|
|
47
|
+
if ((name === 'mv' || name === 'cp') && args.length >= 2) files.push(args[args.length - 1]);
|
|
48
|
+
else if (name === 'rm' || name === 'mkdir' || name === 'touch' || name === 'tee') files.push(...args);
|
|
49
|
+
else if (name === 'sed' && /(^|\s)-i/.test(seg) && args.length) files.push(args[args.length - 1]);
|
|
50
|
+
}
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let data = {};
|
|
55
|
+
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
56
|
+
|
|
57
|
+
let lines = [];
|
|
58
|
+
if (data.transcript_path && fs.existsSync(data.transcript_path)) {
|
|
59
|
+
try {
|
|
60
|
+
const buf = fs.readFileSync(data.transcript_path, 'utf8');
|
|
61
|
+
lines = (buf.length > 4000000 ? buf.slice(-4000000) : buf).split(/\r?\n/);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Locate the last genuine user prompt (typed text, not a tool_result
|
|
66
|
+
// carrier). Everything after it is the current turn.
|
|
67
|
+
let turnStart = 0;
|
|
68
|
+
const parsed = lines.map(l => { try { return JSON.parse(l); } catch { return null; } });
|
|
69
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
70
|
+
const e = parsed[i];
|
|
71
|
+
if (!e || e.type !== 'user' || e.isMeta || !e.message) continue;
|
|
72
|
+
const c = e.message.content;
|
|
73
|
+
const genuine = typeof c === 'string'
|
|
74
|
+
? true
|
|
75
|
+
: Array.isArray(c) && c.some(x => x && x.type === 'text') && !c.some(x => x && x.type === 'tool_result');
|
|
76
|
+
if (genuine) turnStart = i;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fire once per turn.
|
|
80
|
+
for (let i = turnStart; i < lines.length; i++) {
|
|
81
|
+
if (lines[i].includes(MARKER)) process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const files = new Set();
|
|
85
|
+
for (let i = turnStart; i < parsed.length; i++) {
|
|
86
|
+
const e = parsed[i];
|
|
87
|
+
if (!e || e.type !== 'assistant' || !e.message || !Array.isArray(e.message.content)) continue;
|
|
88
|
+
for (const item of e.message.content) {
|
|
89
|
+
if (!item || item.type !== 'tool_use' || !item.input) continue;
|
|
90
|
+
if (MUTATORS.has(item.name)) {
|
|
91
|
+
const p = item.input.file_path || item.input.notebook_path;
|
|
92
|
+
if (p) files.add(p);
|
|
93
|
+
} else if (item.name === 'Bash') {
|
|
94
|
+
for (const t of bashTargets(item.input.command)) files.add(t);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// The triggering call may not be in the transcript yet.
|
|
99
|
+
if (data.tool_input) {
|
|
100
|
+
if (MUTATORS.has(data.tool_name)) {
|
|
101
|
+
const p = data.tool_input.file_path || data.tool_input.notebook_path;
|
|
102
|
+
if (p) files.add(p);
|
|
103
|
+
} else if (data.tool_name === 'Bash') {
|
|
104
|
+
for (const t of bashTargets(data.tool_input.command)) files.add(t);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const cap = parseInt(process.env.MAESTRO_PHASE_FILE_CAP, 10) || 5;
|
|
109
|
+
if (files.size > cap) {
|
|
110
|
+
process.stdout.write(JSON.stringify({
|
|
111
|
+
hookSpecificOutput: {
|
|
112
|
+
hookEventName: 'PostToolUse',
|
|
113
|
+
additionalContext: `${MARKER} ${files.size} distinct files modified this turn exceeds the max-${cap}-files-per-phase rule (AGENTS.md S7.1). Complete and verify the current phase before expanding scope, or split the remaining work into a follow-up phase. If the user explicitly approved a wider batch (e.g. a bulk rename), proceed.`
|
|
114
|
+
}
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process.exit(0);
|