@maestrofrontier/frontier 1.4.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 (43) hide show
  1. package/AGENTS.md +214 -0
  2. package/CLAUDE.md +29 -0
  3. package/LICENSE +21 -0
  4. package/README.md +521 -0
  5. package/bin/maestro.cjs +75 -0
  6. package/commands/compress.md +36 -0
  7. package/commands/context-bar.md +30 -0
  8. package/commands/frontier.md +124 -0
  9. package/commands/settings.md +101 -0
  10. package/commands/terse.md +23 -0
  11. package/commands/update.md +59 -0
  12. package/docs/orchestration.md +168 -0
  13. package/frontier/cli.cjs +248 -0
  14. package/frontier/config.cjs +441 -0
  15. package/frontier/dispatch.cjs +255 -0
  16. package/frontier/judge.cjs +92 -0
  17. package/frontier/run.cjs +148 -0
  18. package/frontier/schema.cjs +112 -0
  19. package/frontier/semaphore.cjs +49 -0
  20. package/frontier/synthesize.cjs +79 -0
  21. package/hooks/frontier-autorun.cjs +124 -0
  22. package/hooks/hooks.json +103 -0
  23. package/hooks/maestro-doctrine-guard.cjs +81 -0
  24. package/hooks/maestro-gate-reminder.cjs +58 -0
  25. package/hooks/maestro-gate-telemetry.cjs +77 -0
  26. package/hooks/maestro-loop-guard.cjs +76 -0
  27. package/hooks/maestro-phase-scope.cjs +118 -0
  28. package/hooks/maestro-statusline-sync.cjs +152 -0
  29. package/hooks/maestro-subagent-guard.cjs +148 -0
  30. package/hooks/maestro-terse-mode.cjs +189 -0
  31. package/hooks/maestro-toolbudget-advisory.cjs +127 -0
  32. package/integrations/README.md +87 -0
  33. package/integrations/cline/skills/frontier/SKILL.md +75 -0
  34. package/integrations/codex/prompts/frontier.md +66 -0
  35. package/integrations/codex/prompts/update.md +36 -0
  36. package/integrations/cursor/commands/frontier.md +63 -0
  37. package/integrations/cursor/commands/update.md +34 -0
  38. package/integrations/gemini/commands/frontier.toml +76 -0
  39. package/integrations/windsurf/workflows/frontier.md +70 -0
  40. package/package.json +52 -0
  41. package/scripts/install.cjs +490 -0
  42. package/settings/cli.cjs +140 -0
  43. package/settings/config.cjs +309 -0
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // Maestro Frontier — synthesis stage: build prompt + invoke Opus for final answer.
3
+
4
+ 'use strict';
5
+
6
+ const dispatch = require('./dispatch.cjs');
7
+
8
+ /**
9
+ * Build the synthesis prompt for Opus.
10
+ * @param {string} userPrompt
11
+ * @param {{ analysis?: import('./schema.cjs').Analysis, responses: import('./schema.cjs').PanelResponse[] }} bundle
12
+ * @param {object} cfg
13
+ * @returns {string}
14
+ */
15
+ function buildSynthPrompt(userPrompt, bundle, cfg) {
16
+ const antiMajority =
17
+ 'Do NOT majority-vote or pick the most common answer; weigh correctness and evidence — ' +
18
+ 'a single correct minority response outweighs a popular wrong one.';
19
+
20
+ let groundingSection;
21
+ if (bundle.analysis) {
22
+ groundingSection =
23
+ `PANEL ANALYSIS (structured):
24
+ ${JSON.stringify(bundle.analysis, null, 2)}
25
+
26
+ Ground your final answer in this analysis:
27
+ - Adopt the consensus points as established facts.
28
+ - RESOLVE contradictions by reasoning about which stance is most correct; do not dodge them.
29
+ - Preserve unique insights that add value.
30
+ - Address any blind spots the analysis identified.`;
31
+ } else {
32
+ const raw = bundle.responses.map(
33
+ r => `### Response from ${r.model}\n${r.content}`
34
+ ).join('\n\n');
35
+ groundingSection =
36
+ `RAW PANEL RESPONSES:
37
+ ${raw}
38
+
39
+ Ground your final answer in these responses.`;
40
+ }
41
+
42
+ return `You are a SYNTHESIZER producing the definitive final answer to a user question.
43
+
44
+ USER QUESTION:
45
+ ${userPrompt}
46
+
47
+ ${groundingSection}
48
+
49
+ IMPORTANT: ${antiMajority}
50
+
51
+ Write the final answer as clear, direct prose. No JSON, no meta-commentary, no preamble about your process. Output the answer only.`;
52
+ }
53
+
54
+ /**
55
+ * Run the synthesis stage. Returns the final answer string or '' on failure (degrades gracefully).
56
+ * @param {string} userPrompt
57
+ * @param {{ analysis?: import('./schema.cjs').Analysis, responses: import('./schema.cjs').PanelResponse[] }} bundle
58
+ * @param {object} cfg
59
+ * @param {{ spawn?: Function }} [deps]
60
+ * @returns {Promise<string>}
61
+ */
62
+ async function runSynth(userPrompt, bundle, cfg, deps) {
63
+ const spawn = (deps && deps.spawn) || dispatch.spawnOne;
64
+ let r;
65
+ try {
66
+ r = await spawn(
67
+ buildSynthPrompt(userPrompt, bundle, cfg),
68
+ cfg.adapters[cfg.synthModel],
69
+ { timeoutMs: cfg.timeoutMs, fusionDepth: 1 }
70
+ );
71
+ } catch {
72
+ return '';
73
+ }
74
+
75
+ if (r && r.ok && r.content) return r.content;
76
+ return '';
77
+ }
78
+
79
+ module.exports = { buildSynthPrompt, runSynth };
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ // Maestro Frontier autorun hook (UserPromptSubmit). When the engine is
3
+ // armed (frontier-state mode != 'off'), every user prompt is run through
4
+ // the configured preset/model via runFrontier, and the synthesized answer
5
+ // is injected back as additionalContext with a relay instruction + a
6
+ // one-line preset header, so the live session relays it. mode == 'off' ->
7
+ // zero overhead: no engine require, no spawn, no injected context.
8
+ //
9
+ // Recursion guard (load-bearing): the engine spawns child `claude -p`
10
+ // CLIs, and headless `claude -p` re-fires UserPromptSubmit hooks (verified
11
+ // against the Claude Code hooks contract). dispatch.cjs sets FUSION_DEPTH
12
+ // on every spawned child; this hook no-ops whenever FUSION_DEPTH is present,
13
+ // BEFORE any engine require or spawn. Without it the first armed prompt
14
+ // would fork the engine recursively.
15
+ //
16
+ // Degrade-to-normal: any engine error, empty answer, or thrown exception
17
+ // exits 0 with empty stdout (logging only to stderr), so a broken engine
18
+ // never blocks or corrupts a turn — the session just answers normally. A
19
+ // UserPromptSubmit hook can only ADD context; it cannot suppress the main
20
+ // turn, which is exactly the relay model.
21
+ //
22
+ // .cjs so Node treats it as CommonJS regardless of any parent "type":
23
+ // "module" package.json. configDir/state patterns reused from
24
+ // frontier/config.cjs; structure ported from maestro-terse-mode.cjs.
25
+
26
+ 'use strict';
27
+
28
+ const fs = require('fs');
29
+
30
+ function noop() { process.exit(0); }
31
+
32
+ let data = {};
33
+ try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { noop(); }
34
+
35
+ if (data.hook_event_name !== 'UserPromptSubmit') noop();
36
+
37
+ // Recursion guard FIRST: never run the engine inside an engine-spawned CLI.
38
+ // The engine sets FUSION_DEPTH on every child (dispatch.cjs:108); depth >= 1
39
+ // means we are inside a spawned panel/judge/synth process. Mirror run.cjs's
40
+ // own parseInt check (run.cjs:27) so the two layers agree and a stray
41
+ // FUSION_DEPTH='0'/'' in the environment reads as "not a child".
42
+ const fusionDepth = parseInt(process.env.FUSION_DEPTH || '0', 10);
43
+ if (Number.isFinite(fusionDepth) && fusionDepth >= 1) noop();
44
+
45
+ let state;
46
+ try {
47
+ const cfg = require('../frontier/config.cjs');
48
+ const cwd = data.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
49
+ // This hook only ever runs under Claude Code, so force a per-workspace
50
+ // scope; resolveScope honors --scope/MAESTRO_SCOPE first, then we ensure
51
+ // we never fall back to the shared legacy 'default' file.
52
+ let scope = cfg.resolveScope([], { cwd });
53
+ if (scope === 'default') scope = 'cc-' + cfg.workspaceHash(cwd);
54
+ state = cfg.loadState(scope);
55
+ } catch {
56
+ noop();
57
+ }
58
+
59
+ // Off -> zero overhead: no run.cjs require, no spawn, no injected context.
60
+ if (!state || state.mode === 'off') noop();
61
+
62
+ const prompt = String(data.prompt || '');
63
+
64
+ // Optional length gate (default 0 = every prompt). Skips trivially short
65
+ // prompts ("yes"/"ok") so they don't pay a full engine run.
66
+ const rawMin = Number(state.autorunMinChars);
67
+ const minChars = Number.isFinite(rawMin) && rawMin > 0 ? rawMin : 0;
68
+ if (prompt.trim().length < minChars) noop();
69
+
70
+ // Any unexpected throw after the await boundary degrades to a normal turn,
71
+ // never a non-zero exit / unhandled rejection.
72
+ run().catch((e) => {
73
+ process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
74
+ process.exit(0);
75
+ });
76
+
77
+ async function run() {
78
+ let result;
79
+ try {
80
+ const { runFrontier } = require('../frontier/run.cjs');
81
+ result = await runFrontier({ prompt, state });
82
+ } catch (e) {
83
+ process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
84
+ noop();
85
+ }
86
+
87
+ if (!result || result.status !== 'ok' || !result.final) {
88
+ if (result && result.status === 'error') {
89
+ process.stderr.write(
90
+ 'frontier-autorun: engine error [' + result.failure_reason + ']: ' + result.error + '\n');
91
+ }
92
+ noop();
93
+ }
94
+
95
+ const context =
96
+ 'MAESTRO FRONTIER AUTORUN — ' + presetHeader(state) + '\n\n' +
97
+ 'The Maestro Frontier engine already ran this prompt through the panel ' +
98
+ 'above and produced the answer below. Relay it as your response — you ' +
99
+ 'may reformat for clarity, but do not redo the work or contradict it:\n\n' +
100
+ result.final;
101
+
102
+ // fs.writeSync (synchronous, unbuffered) guarantees the full payload reaches
103
+ // stdout before exit. process.exit() does NOT drain a pipe-backed
104
+ // process.stdout, which would truncate a large engine answer (multi-KB
105
+ // synthesis) into malformed JSON the hook consumer cannot parse.
106
+ fs.writeSync(1, JSON.stringify({
107
+ hookSpecificOutput: {
108
+ hookEventName: 'UserPromptSubmit',
109
+ additionalContext: context,
110
+ },
111
+ }));
112
+ process.exit(0);
113
+ }
114
+
115
+ function presetHeader(st) {
116
+ if (st.mode === 'single') return 'single · ' + st.model;
117
+ if (st.mode === 'fusion') {
118
+ const preset = st.preset === 'custom'
119
+ ? 'custom (' + (Array.isArray(st.models) ? st.models.join(', ') : '') + ')'
120
+ : st.preset;
121
+ return 'fusion · ' + preset;
122
+ }
123
+ return String(st.mode);
124
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Read",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-doctrine-guard.cjs\""
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionStart": [
15
+ {
16
+ "matcher": "",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-terse-mode.cjs\""
21
+ },
22
+ {
23
+ "type": "command",
24
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-statusline-sync.cjs\""
25
+ }
26
+ ]
27
+ }
28
+ ],
29
+ "UserPromptSubmit": [
30
+ {
31
+ "matcher": "",
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-gate-reminder.cjs\""
36
+ },
37
+ {
38
+ "type": "command",
39
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-terse-mode.cjs\""
40
+ },
41
+ {
42
+ "type": "command",
43
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/frontier-autorun.cjs\"",
44
+ "timeout": 300
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "SubagentStop": [
50
+ {
51
+ "matcher": "",
52
+ "hooks": [
53
+ {
54
+ "type": "command",
55
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-subagent-guard.cjs\""
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ "Stop": [
61
+ {
62
+ "matcher": "",
63
+ "hooks": [
64
+ {
65
+ "type": "command",
66
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-loop-guard.cjs\""
67
+ }
68
+ ]
69
+ }
70
+ ],
71
+ "PostToolUse": [
72
+ {
73
+ "matcher": "Edit|Write|NotebookEdit|Bash",
74
+ "hooks": [
75
+ {
76
+ "type": "command",
77
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-phase-scope.cjs\""
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "matcher": "Edit|Write|NotebookEdit",
83
+ "hooks": [
84
+ {
85
+ "type": "command",
86
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-toolbudget-advisory.cjs\""
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "SessionEnd": [
92
+ {
93
+ "matcher": "",
94
+ "hooks": [
95
+ {
96
+ "type": "command",
97
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/maestro-gate-telemetry.cjs\""
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ }
103
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // Maestro PreToolUse doctrine-read guard. Enforces AGENTS.md S7.2
3
+ // structurally: when the doctrine is autoloaded (CLAUDE.md/AGENTS.md
4
+ // present at cwd, which Claude Code injects at session start), a Read
5
+ // of AGENTS.md or CLAUDE.md re-buys tokens for content already in
6
+ // context. This hook replaces the probabilistic S7.2 prose line with a
7
+ // deterministic deny.
8
+ //
9
+ // Modes via MAESTRO_DOCTRINE_GUARD:
10
+ // - "always" (default): deny every doctrine Read while doctrine files
11
+ // exist at cwd. Safe on Claude Code, where subagents receive the
12
+ // project doctrine automatically; the deny reason tells the model
13
+ // to use the in-context copy.
14
+ // - "once": allow the first doctrine Read per session (marker file in
15
+ // the OS temp dir keyed by session_id), deny repeats. For runtimes
16
+ // whose subagents genuinely lack the doctrine in context (S7.2:
17
+ // "a subagent without it in context reads AGENTS.md once").
18
+ // - "0": disabled.
19
+ //
20
+ // When no doctrine file exists at cwd nothing was autoloaded, so reads
21
+ // pass through untouched (e.g. inspecting another repo's AGENTS.md).
22
+ // docs/orchestration.md is never guarded -- it is the on-demand layer
23
+ // and reading it is the intended path. Fails open on any error.
24
+ //
25
+ // Payload fields verified against code.claude.com/docs/en/hooks
26
+ // (PreToolUse input: session_id, cwd, tool_name, tool_input; output:
27
+ // hookSpecificOutput.permissionDecision allow|deny|ask + reason),
28
+ // 2026-06-11.
29
+ //
30
+ // .cjs so Node treats it as CommonJS regardless of any "type": "module"
31
+ // package.json in a parent directory of the install location.
32
+
33
+ const fs = require('fs');
34
+ const os = require('os');
35
+ const path = require('path');
36
+
37
+ const mode = process.env.MAESTRO_DOCTRINE_GUARD || 'always';
38
+ if (mode === '0') process.exit(0);
39
+
40
+ let data = {};
41
+ try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
42
+ if (data.tool_name !== 'Read' || !data.tool_input) process.exit(0);
43
+
44
+ const fp = data.tool_input.file_path;
45
+ if (typeof fp !== 'string') process.exit(0);
46
+ const base = path.basename(fp).toLowerCase();
47
+ if (base !== 'agents.md' && base !== 'claude.md') process.exit(0);
48
+
49
+ // Guard only when the doctrine was actually autoloaded: a doctrine file
50
+ // at cwd is what Claude Code injects at session start.
51
+ const cwd = typeof data.cwd === 'string' ? data.cwd : process.cwd();
52
+ let autoloaded = false;
53
+ try {
54
+ autoloaded = fs.existsSync(path.join(cwd, 'CLAUDE.md'))
55
+ || fs.existsSync(path.join(cwd, 'AGENTS.md'));
56
+ } catch { autoloaded = false; }
57
+ if (!autoloaded) process.exit(0);
58
+
59
+ if (mode === 'once' && data.session_id) {
60
+ const marker = path.join(
61
+ os.tmpdir(),
62
+ `maestro-doctrine-guard-${String(data.session_id).replace(/[^a-zA-Z0-9-]/g, '_')}`
63
+ );
64
+ if (!fs.existsSync(marker)) {
65
+ try { fs.writeFileSync(marker, '1'); } catch { /* still allow */ }
66
+ process.exit(0);
67
+ }
68
+ }
69
+
70
+ process.stdout.write(JSON.stringify({
71
+ hookSpecificOutput: {
72
+ hookEventName: 'PreToolUse',
73
+ permissionDecision: 'deny',
74
+ permissionDecisionReason: 'maestro-doctrine-guard: denied Read of '
75
+ + path.basename(fp) + ' -- the doctrine is autoloaded into context '
76
+ + 'at session start (AGENTS.md S7.2); use the in-context copy '
77
+ + 'instead of re-reading it from disk. The on-demand protocol '
78
+ + 'layer lives in docs/orchestration.md, which is not blocked.'
79
+ }
80
+ }));
81
+ process.exit(0);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // Maestro UserPromptSubmit gate reminder. Soft, additive, fire-once.
3
+ //
4
+ // Injects the Decision Gate checklist (AGENTS.md S1) as additional
5
+ // context on the FIRST user prompt of a session, so the gate survives
6
+ // attention decay in long interactive sessions. Hooks inject context;
7
+ // they cannot force a verdict or a spawn — this is a reminder, not an
8
+ // enforcement gate.
9
+ //
10
+ // Fire-once: a marker file keyed by session_id under the OS temp dir.
11
+ // Opt-out: MAESTRO_GATE_REMINDER=0 disables entirely.
12
+ // The measured default includes the spawn imperative. A shorter
13
+ // verdict-only variant was tested and removed after increasing turns
14
+ // and cost in a 2026-06-12 smoke run.
15
+ //
16
+ // Payload fields verified against code.claude.com/docs/en/hooks
17
+ // (UserPromptSubmit input: session_id, transcript_path, cwd, prompt;
18
+ // stdout JSON hookSpecificOutput.additionalContext is added to
19
+ // context), 2026-06-11.
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
+ const fs = require('fs');
25
+ const os = require('os');
26
+ const path = require('path');
27
+
28
+ if (process.env.MAESTRO_GATE_REMINDER === '0') process.exit(0);
29
+
30
+ let data = {};
31
+ try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
32
+ if (!data.session_id) process.exit(0);
33
+
34
+ const marker = path.join(
35
+ os.tmpdir(),
36
+ `maestro-gate-reminder-${String(data.session_id).replace(/[^a-zA-Z0-9-]/g, '_')}`
37
+ );
38
+ if (fs.existsSync(marker)) process.exit(0);
39
+ try { fs.writeFileSync(marker, '1'); } catch { /* still remind */ }
40
+
41
+ const checklistLines = [
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.'
49
+ ];
50
+ const checklist = checklistLines.join('\n');
51
+
52
+ process.stdout.write(JSON.stringify({
53
+ hookSpecificOutput: {
54
+ hookEventName: 'UserPromptSubmit',
55
+ additionalContext: checklist,
56
+ },
57
+ }));
58
+ process.exit(0);
@@ -0,0 +1,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
+ 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);
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ // Maestro Stop-event loop guard. Enforces AGENTS.md S10 structurally.
3
+ //
4
+ // Fires only when the session shows loop evidence: active session crons
5
+ // (/loop <interval>) in the Stop payload's `session_crons`, or
6
+ // ScheduleWakeup calls (self-paced loops) in the transcript. Then:
7
+ // - Warn when no checkpoint artifact (_*.md) exists in cwd -- S10
8
+ // requires one durable checkpoint file, read first on every wakeup.
9
+ // - Warn when wakeup count exceeds MAESTRO_LOOP_MAX_ITER (default 50)
10
+ // -- S10 hard caps: the end condition set at start wins over anything
11
+ // encountered mid-run.
12
+ //
13
+ // Fires at most once per session (transcript marker), never blocks,
14
+ // degrades silently on missing payload fields. Payload fields verified
15
+ // against code.claude.com/docs/en/hooks and the Claude Code changelog
16
+ // (Stop input: session_crons, transcript_path, cwd;
17
+ // output: hookSpecificOutput.additionalContext), 2026-06-10.
18
+ //
19
+ // .cjs so Node treats it as CommonJS regardless of any "type": "module"
20
+ // package.json in a parent directory of the install location.
21
+ //
22
+ // Install: see README "Claude Code: Loop Guard".
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ let data = {};
28
+ try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
29
+
30
+ // Defensive: never re-enter a stop-hook continuation loop.
31
+ if (data.stop_hook_active === true) process.exit(0);
32
+
33
+ let txText = '';
34
+ const txPath = data.transcript_path;
35
+ if (txPath && fs.existsSync(txPath)) {
36
+ try {
37
+ const buf = fs.readFileSync(txPath, 'utf8');
38
+ txText = buf.length > 2000000 ? buf.slice(-2000000) : buf;
39
+ } catch {}
40
+ }
41
+
42
+ // Fire once per session.
43
+ if (txText.includes('Maestro loop guard:')) process.exit(0);
44
+
45
+ const crons = Array.isArray(data.session_crons) ? data.session_crons : [];
46
+ const wakeups = (txText.match(/"name"\s*:\s*"ScheduleWakeup"/g) || []).length;
47
+ const looping = crons.length > 0 || wakeups > 0;
48
+ if (!looping) process.exit(0);
49
+
50
+ const warnings = [];
51
+
52
+ const cap = parseInt(process.env.MAESTRO_LOOP_MAX_ITER, 10) || 50;
53
+ if (wakeups > cap) {
54
+ warnings.push(`${wakeups} wakeups exceed the iteration cap (${cap}). S10: hard caps bound autonomous runs -- re-check the end condition declared at start; if it is met, deliver the final report and stop scheduling.`);
55
+ }
56
+
57
+ if (data.cwd) {
58
+ let hasCheckpoint = false;
59
+ try {
60
+ hasCheckpoint = fs.readdirSync(data.cwd).some(f => /^_.+\.md$/i.test(f) && f !== '_.md');
61
+ } catch { hasCheckpoint = true; } // unreadable cwd: assume fine, fail open
62
+ if (!hasCheckpoint) {
63
+ warnings.push('Session is looping but no checkpoint artifact (_<task>.md) exists in the working directory. S10: externalize phase status, findings, and decisions to one durable gitignored file and read it first on every wakeup.');
64
+ }
65
+ }
66
+
67
+ if (warnings.length) {
68
+ process.stdout.write(JSON.stringify({
69
+ hookSpecificOutput: {
70
+ hookEventName: 'Stop',
71
+ additionalContext: 'Maestro loop guard:\n- ' + warnings.join('\n- ')
72
+ }
73
+ }));
74
+ }
75
+
76
+ process.exit(0);