@maestrofrontier/frontier 1.4.4 → 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.
Files changed (53) hide show
  1. package/.agents/plugins/marketplace.json +21 -0
  2. package/.codex-plugin/plugin.json +29 -0
  3. package/.cursorrules +197 -194
  4. package/AGENTS.md +214 -214
  5. package/CLAUDE.md +29 -29
  6. package/README.md +368 -278
  7. package/bin/maestro.cjs +75 -75
  8. package/commands/compress.md +36 -36
  9. package/commands/frontier.md +124 -124
  10. package/commands/terse.md +23 -23
  11. package/docs/codex.md +167 -98
  12. package/docs/orchestration.md +168 -168
  13. package/frontier/cli.cjs +279 -248
  14. package/frontier/config.cjs +468 -441
  15. package/frontier/dispatch.cjs +267 -255
  16. package/frontier/judge.cjs +92 -92
  17. package/frontier/run.cjs +201 -148
  18. package/frontier/schema.cjs +112 -112
  19. package/frontier/semaphore.cjs +49 -49
  20. package/frontier/synthesize.cjs +79 -79
  21. package/hooks/frontier-autorun.cjs +127 -124
  22. package/hooks/hooks.json +103 -103
  23. package/hooks/maestro-doctrine-guard.cjs +81 -81
  24. package/hooks/maestro-gate-reminder.cjs +22 -7
  25. package/hooks/maestro-gate-telemetry.cjs +79 -77
  26. package/hooks/maestro-phase-scope.cjs +118 -118
  27. package/hooks/maestro-statusline-sync.cjs +152 -152
  28. package/hooks/maestro-subagent-guard.cjs +148 -148
  29. package/hooks/maestro-terse-mode.cjs +189 -189
  30. package/hooks/maestro-toolbudget-advisory.cjs +127 -127
  31. package/integrations/README.md +111 -94
  32. package/integrations/cline/skills/frontier/SKILL.md +75 -75
  33. package/integrations/codex/prompts/frontier.md +70 -66
  34. package/integrations/codex/prompts/update.md +39 -36
  35. package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -0
  36. package/integrations/codex/skills/{settings → maestro-settings}/SKILL.md +55 -46
  37. package/integrations/codex/skills/{terse → maestro-terse}/SKILL.md +58 -49
  38. package/integrations/codex/skills/maestro-update/SKILL.md +31 -0
  39. package/integrations/cursor/commands/frontier.md +63 -63
  40. package/integrations/cursor/commands/update.md +34 -34
  41. package/integrations/gemini/commands/frontier.toml +76 -76
  42. package/integrations/windsurf/workflows/frontier.md +70 -70
  43. package/package.json +58 -55
  44. package/scripts/install.cjs +1014 -605
  45. package/settings/cli.cjs +140 -140
  46. package/settings/config.cjs +309 -309
  47. package/skills/maestro-frontier/SKILL.md +122 -0
  48. package/skills/maestro-settings/SKILL.md +55 -0
  49. package/skills/maestro-terse/SKILL.md +58 -0
  50. package/skills/maestro-update/SKILL.md +31 -0
  51. package/skills/terse/SKILL.md +74 -0
  52. package/integrations/codex/skills/frontier/SKILL.md +0 -91
  53. package/integrations/codex/skills/update/SKILL.md +0 -29
@@ -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 `GATE: files=<n> concerns=<m> -> single-agent',
44
- '| multi-agent — <reason>`. files>=5 across 2+ concerns = multi-agent:',
45
- 'spawn the Planner via the Agent/Task tool BEFORE any edit. A met',
46
- 'trigger downgrades ONLY on >60% file overlap between subtasks or',
47
- '<=3 files total in one dependency chain. Sub-trigger tasks stay',
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
- const checklist = checklistLines.join('\n');
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
- const verdictRe = /GATE:\s*files=\S+\s+concerns=\S+\s*->\s*(single|multi)-agent/;
38
- let agentCount = 0;
39
- let verdict = null;
40
- if (data.transcript_path && fs.existsSync(data.transcript_path)) {
41
- try {
42
- const buf = fs.readFileSync(data.transcript_path, 'utf8');
43
- const text = buf.length > 8000000 ? buf.slice(-8000000) : buf;
44
- for (const line of text.split(/\r?\n/)) {
45
- let e;
46
- try { e = JSON.parse(line); } catch { continue; }
47
- if (!e || e.type !== 'assistant' || !e.message || !Array.isArray(e.message.content)) continue;
48
- for (const item of e.message.content) {
49
- if (!item) continue;
50
- if (item.type === 'tool_use' && (item.name === 'Task' || item.name === 'Agent')) agentCount++;
51
- if (item.type === 'text' && typeof item.text === 'string') {
52
- const m = item.text.match(verdictRe);
53
- if (m) verdict = m[1];
54
- }
55
- }
56
- }
57
- } catch {}
58
- }
59
-
60
- const row = {
61
- ts: new Date().toISOString(),
62
- session_id: data.session_id || null,
63
- gate: agentCount > 0 ? 'multi' : 'single',
64
- verdict,
65
- agent_count: agentCount,
66
- mismatch: verdict !== null && ((verdict === 'multi') !== (agentCount > 0)),
67
- reason: data.reason || null,
68
- project: data.cwd ? path.basename(data.cwd) : null
69
- };
70
-
71
- try {
72
- const dir = path.join(os.homedir(), '.claude');
73
- fs.mkdirSync(dir, { recursive: true });
74
- fs.appendFileSync(path.join(dir, 'maestro-telemetry.jsonl'), JSON.stringify(row) + '\n');
75
- } catch {}
76
-
77
- process.exit(0);
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);