@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.
Files changed (50) hide show
  1. package/.agents/plugins/marketplace.json +21 -21
  2. package/.codex-plugin/plugin.json +29 -29
  3. package/.cursorrules +197 -194
  4. package/AGENTS.md +3 -3
  5. package/README.md +368 -368
  6. package/bin/maestro.cjs +75 -75
  7. package/commands/compress.md +36 -36
  8. package/commands/frontier.md +124 -124
  9. package/commands/terse.md +23 -23
  10. package/docs/codex.md +167 -167
  11. package/docs/orchestration.md +168 -168
  12. package/frontier/cli.cjs +279 -252
  13. package/frontier/config.cjs +468 -468
  14. package/frontier/dispatch.cjs +267 -255
  15. package/frontier/judge.cjs +92 -92
  16. package/frontier/run.cjs +201 -180
  17. package/frontier/schema.cjs +112 -112
  18. package/frontier/semaphore.cjs +49 -49
  19. package/frontier/synthesize.cjs +79 -79
  20. package/hooks/frontier-autorun.cjs +127 -120
  21. package/hooks/hooks.json +103 -103
  22. package/hooks/maestro-doctrine-guard.cjs +81 -81
  23. package/hooks/maestro-gate-reminder.cjs +22 -7
  24. package/hooks/maestro-gate-telemetry.cjs +79 -77
  25. package/hooks/maestro-phase-scope.cjs +118 -118
  26. package/hooks/maestro-statusline-sync.cjs +152 -152
  27. package/hooks/maestro-subagent-guard.cjs +148 -148
  28. package/hooks/maestro-terse-mode.cjs +189 -189
  29. package/hooks/maestro-toolbudget-advisory.cjs +127 -127
  30. package/integrations/README.md +111 -111
  31. package/integrations/cline/skills/frontier/SKILL.md +75 -75
  32. package/integrations/codex/prompts/frontier.md +70 -70
  33. package/integrations/codex/prompts/update.md +39 -39
  34. package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -122
  35. package/integrations/codex/skills/maestro-settings/SKILL.md +55 -55
  36. package/integrations/codex/skills/maestro-terse/SKILL.md +58 -58
  37. package/integrations/codex/skills/maestro-update/SKILL.md +31 -31
  38. package/integrations/cursor/commands/frontier.md +63 -63
  39. package/integrations/cursor/commands/update.md +34 -34
  40. package/integrations/gemini/commands/frontier.toml +76 -76
  41. package/integrations/windsurf/workflows/frontier.md +70 -70
  42. package/package.json +58 -58
  43. package/scripts/install.cjs +1014 -1014
  44. package/settings/cli.cjs +140 -140
  45. package/settings/config.cjs +309 -309
  46. package/skills/maestro-frontier/SKILL.md +122 -122
  47. package/skills/maestro-settings/SKILL.md +55 -55
  48. package/skills/maestro-terse/SKILL.md +58 -58
  49. package/skills/maestro-update/SKILL.md +31 -31
  50. package/skills/terse/SKILL.md +74 -74
@@ -1,120 +1,127 @@
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.env.CODEX_PROJECT_DIR || process.cwd();
49
- const scope = cfg.resolveScope([], { cwd });
50
- state = cfg.loadState(scope);
51
- } catch {
52
- noop();
53
- }
54
-
55
- // Off -> zero overhead: no run.cjs require, no spawn, no injected context.
56
- if (!state || state.mode === 'off') noop();
57
-
58
- const prompt = String(data.prompt || '');
59
-
60
- // Optional length gate (default 0 = every prompt). Skips trivially short
61
- // prompts ("yes"/"ok") so they don't pay a full engine run.
62
- const rawMin = Number(state.autorunMinChars);
63
- const minChars = Number.isFinite(rawMin) && rawMin > 0 ? rawMin : 0;
64
- if (prompt.trim().length < minChars) noop();
65
-
66
- // Any unexpected throw after the await boundary degrades to a normal turn,
67
- // never a non-zero exit / unhandled rejection.
68
- run().catch((e) => {
69
- process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
70
- process.exit(0);
71
- });
72
-
73
- async function run() {
74
- let result;
75
- try {
76
- const { runFrontier } = require('../frontier/run.cjs');
77
- result = await runFrontier({ prompt, state });
78
- } catch (e) {
79
- process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
80
- noop();
81
- }
82
-
83
- if (!result || result.status !== 'ok' || !result.final) {
84
- if (result && result.status === 'error') {
85
- process.stderr.write(
86
- 'frontier-autorun: engine error [' + result.failure_reason + ']: ' + result.error + '\n');
87
- }
88
- noop();
89
- }
90
-
91
- const context =
92
- 'MAESTRO FRONTIER AUTORUN — ' + presetHeader(state) + '\n\n' +
93
- 'The Maestro Frontier engine already ran this prompt through the panel ' +
94
- 'above and produced the answer below. Relay it as your response you ' +
95
- 'may reformat for clarity, but do not redo the work or contradict it:\n\n' +
96
- result.final;
97
-
98
- // fs.writeSync (synchronous, unbuffered) guarantees the full payload reaches
99
- // stdout before exit. process.exit() does NOT drain a pipe-backed
100
- // process.stdout, which would truncate a large engine answer (multi-KB
101
- // synthesis) into malformed JSON the hook consumer cannot parse.
102
- fs.writeSync(1, JSON.stringify({
103
- hookSpecificOutput: {
104
- hookEventName: 'UserPromptSubmit',
105
- additionalContext: context,
106
- },
107
- }));
108
- process.exit(0);
109
- }
110
-
111
- function presetHeader(st) {
112
- if (st.mode === 'single') return 'single · ' + st.model;
113
- if (st.mode === 'fusion') {
114
- const preset = st.preset === 'custom'
115
- ? 'custom (' + (Array.isArray(st.models) ? st.models.join(', ') : '') + ')'
116
- : st.preset;
117
- return 'fusion · ' + preset;
118
- }
119
- return String(st.mode);
120
- }
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.env.CODEX_PROJECT_DIR || process.cwd();
49
+ const scope = cfg.resolveScope([], { cwd });
50
+ state = cfg.loadState(scope);
51
+ } catch {
52
+ noop();
53
+ }
54
+
55
+ // Off -> zero overhead: no run.cjs require, no spawn, no injected context.
56
+ if (!state || state.mode === 'off') noop();
57
+
58
+ const prompt = String(data.prompt || '');
59
+
60
+ // Optional length gate (default 0 = every prompt). Skips trivially short
61
+ // prompts ("yes"/"ok") so they don't pay a full engine run.
62
+ const rawMin = Number(state.autorunMinChars);
63
+ const minChars = Number.isFinite(rawMin) && rawMin > 0 ? rawMin : 0;
64
+ if (prompt.trim().length < minChars) noop();
65
+
66
+ // Any unexpected throw after the await boundary degrades to a normal turn,
67
+ // never a non-zero exit / unhandled rejection.
68
+ run().catch((e) => {
69
+ process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
70
+ process.exit(0);
71
+ });
72
+
73
+ async function run() {
74
+ let result;
75
+ const runStart = Date.now();
76
+ try {
77
+ const { runFrontier } = require('../frontier/run.cjs');
78
+ result = await runFrontier({ prompt, state });
79
+ } catch (e) {
80
+ process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
81
+ noop();
82
+ }
83
+ const runMs = Date.now() - runStart;
84
+
85
+ if (!result || result.status !== 'ok' || !result.final) {
86
+ if (result && result.status === 'error') {
87
+ process.stderr.write(
88
+ 'frontier-autorun: engine error [' + result.failure_reason + ']: ' + result.error + '\n');
89
+ }
90
+ noop();
91
+ }
92
+
93
+ const modelCount = result.responses ? result.responses.length : 1;
94
+ const banner = ' Frontier \xb7 ' + presetHeader(state) + ' \xb7 ' + modelCount + ' models \xb7 ' + Math.round(runMs / 1000) + 's';
95
+
96
+ const context =
97
+ 'MAESTRO FRONTIER AUTORUN — ' + presetHeader(state) + '\n\n' +
98
+ 'The Maestro Frontier engine already ran this prompt through the panel ' +
99
+ 'above and produced the answer below. Begin your response with this exact ' +
100
+ 'banner line (verbatim, on its own line): ' + banner + '\n\n' +
101
+ 'Then relay the answer you may reformat for clarity, but do not redo ' +
102
+ 'the work or contradict it:\n\n' +
103
+ result.final;
104
+
105
+ // fs.writeSync (synchronous, unbuffered) guarantees the full payload reaches
106
+ // stdout before exit. process.exit() does NOT drain a pipe-backed
107
+ // process.stdout, which would truncate a large engine answer (multi-KB
108
+ // synthesis) into malformed JSON the hook consumer cannot parse.
109
+ fs.writeSync(1, JSON.stringify({
110
+ hookSpecificOutput: {
111
+ hookEventName: 'UserPromptSubmit',
112
+ additionalContext: context,
113
+ },
114
+ }));
115
+ process.exit(0);
116
+ }
117
+
118
+ function presetHeader(st) {
119
+ if (st.mode === 'single') return 'single · ' + st.model;
120
+ if (st.mode === 'fusion') {
121
+ const preset = st.preset === 'custom'
122
+ ? 'custom (' + (Array.isArray(st.models) ? st.models.join(', ') : '') + ')'
123
+ : st.preset;
124
+ return 'fusion · ' + preset;
125
+ }
126
+ return String(st.mode);
127
+ }
package/hooks/hooks.json CHANGED
@@ -1,103 +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 -e \"require(require('path').join(process.env.CLAUDE_PLUGIN_ROOT || process.env.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
- }
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 -e \"require(require('path').join(process.env.CLAUDE_PLUGIN_ROOT || process.env.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
+ }
@@ -1,81 +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);
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);