@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.
- package/.agents/plugins/marketplace.json +21 -0
- package/.codex-plugin/plugin.json +29 -0
- package/.cursorrules +197 -194
- package/AGENTS.md +214 -214
- package/CLAUDE.md +29 -29
- package/README.md +368 -278
- 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 -98
- package/docs/orchestration.md +168 -168
- package/frontier/cli.cjs +279 -248
- package/frontier/config.cjs +468 -441
- package/frontier/dispatch.cjs +267 -255
- package/frontier/judge.cjs +92 -92
- package/frontier/run.cjs +201 -148
- package/frontier/schema.cjs +112 -112
- package/frontier/semaphore.cjs +49 -49
- package/frontier/synthesize.cjs +79 -79
- package/hooks/frontier-autorun.cjs +127 -124
- 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 -94
- package/integrations/cline/skills/frontier/SKILL.md +75 -75
- package/integrations/codex/prompts/frontier.md +70 -66
- package/integrations/codex/prompts/update.md +39 -36
- package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -0
- package/integrations/codex/skills/{settings → maestro-settings}/SKILL.md +55 -46
- package/integrations/codex/skills/{terse → maestro-terse}/SKILL.md +58 -49
- package/integrations/codex/skills/maestro-update/SKILL.md +31 -0
- 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 -55
- package/scripts/install.cjs +1014 -605
- package/settings/cli.cjs +140 -140
- package/settings/config.cjs +309 -309
- package/skills/maestro-frontier/SKILL.md +122 -0
- package/skills/maestro-settings/SKILL.md +55 -0
- package/skills/maestro-terse/SKILL.md +58 -0
- package/skills/maestro-update/SKILL.md +31 -0
- package/skills/terse/SKILL.md +74 -0
- package/integrations/codex/skills/frontier/SKILL.md +0 -91
- package/integrations/codex/skills/update/SKILL.md +0 -29
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro terse-mode hook. One file, two events (dispatch on
|
|
3
|
-
// hook_event_name):
|
|
4
|
-
// - SessionStart: resolve the level (env > config > off), write the
|
|
5
|
-
// flag file, inject the level-filtered ruleset from
|
|
6
|
-
// skills/terse/SKILL.md (single source of truth) as
|
|
7
|
-
// additionalContext.
|
|
8
|
-
// - UserPromptSubmit: track level switches (/maestro:terse, /terse,
|
|
9
|
-
// natural-language deactivation) in the flag file and emit a
|
|
10
|
-
// one-line reminder every turn while active -- per-turn
|
|
11
|
-
// reinforcement defeats style drift after context compression
|
|
12
|
-
// (same pattern as maestro-gate-reminder).
|
|
13
|
-
//
|
|
14
|
-
// Level resolution: MAESTRO_TERSE_LEVEL env var, then terseLevel in
|
|
15
|
-
// $XDG_CONFIG_HOME/maestro/config.json (~/.config/maestro fallback,
|
|
16
|
-
// %APPDATA%\maestro on Windows), then 'off'. Off by default:
|
|
17
|
-
// installing the plugin must not change anyone's output style.
|
|
18
|
-
//
|
|
19
|
-
// Flag I/O ported from Caveman (MIT): symlink-refusing, O_NOFOLLOW,
|
|
20
|
-
// atomic temp+rename, 0600, 64-byte read cap, level whitelist. A
|
|
21
|
-
// predictable flag path under ~/.claude is a symlink-attack target;
|
|
22
|
-
// never write through one, never inject unvalidated bytes into model
|
|
23
|
-
// context.
|
|
24
|
-
//
|
|
25
|
-
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
26
|
-
// package.json in a parent directory of the install location.
|
|
27
|
-
|
|
28
|
-
const fs = require('fs');
|
|
29
|
-
const os = require('os');
|
|
30
|
-
const path = require('path');
|
|
31
|
-
|
|
32
|
-
const LEVELS = ['lite', 'full', 'ultra'];
|
|
33
|
-
const MAX_FLAG_BYTES = 64;
|
|
34
|
-
|
|
35
|
-
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
36
|
-
const flagPath = path.join(claudeDir, '.maestro-terse');
|
|
37
|
-
|
|
38
|
-
function configDir() {
|
|
39
|
-
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'maestro');
|
|
40
|
-
if (process.platform === 'win32') {
|
|
41
|
-
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'maestro');
|
|
42
|
-
}
|
|
43
|
-
return path.join(os.homedir(), '.config', 'maestro');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function defaultLevel() {
|
|
47
|
-
const env = String(process.env.MAESTRO_TERSE_LEVEL || '').toLowerCase();
|
|
48
|
-
if (env === 'off' || LEVELS.includes(env)) return env;
|
|
49
|
-
try {
|
|
50
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(configDir(), 'config.json'), 'utf8'));
|
|
51
|
-
const v = String(cfg.terseLevel || '').toLowerCase();
|
|
52
|
-
if (v === 'off' || LEVELS.includes(v)) return v;
|
|
53
|
-
} catch {}
|
|
54
|
-
return 'off';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function safeWriteFlag(level) {
|
|
58
|
-
try {
|
|
59
|
-
const dir = path.dirname(flagPath);
|
|
60
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
-
try { if (fs.lstatSync(dir).isSymbolicLink()) return; } catch { return; }
|
|
62
|
-
try {
|
|
63
|
-
if (fs.lstatSync(flagPath).isSymbolicLink()) return;
|
|
64
|
-
} catch (e) {
|
|
65
|
-
if (e.code !== 'ENOENT') return;
|
|
66
|
-
}
|
|
67
|
-
const tempPath = path.join(dir, `.maestro-terse.${process.pid}.${Date.now()}`);
|
|
68
|
-
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
69
|
-
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
|
|
70
|
-
let fd;
|
|
71
|
-
try {
|
|
72
|
-
if (O_NOFOLLOW === 0) { try { if (fs.lstatSync(tempPath).isSymbolicLink()) return; } catch {} }
|
|
73
|
-
fd = fs.openSync(tempPath, flags, 0o600);
|
|
74
|
-
fs.writeSync(fd, String(level));
|
|
75
|
-
try { fs.fchmodSync(fd, 0o600); } catch {}
|
|
76
|
-
} finally {
|
|
77
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
78
|
-
}
|
|
79
|
-
fs.renameSync(tempPath, flagPath);
|
|
80
|
-
} catch {}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function readFlag() {
|
|
84
|
-
try {
|
|
85
|
-
let st;
|
|
86
|
-
try { st = fs.lstatSync(flagPath); } catch { return null; }
|
|
87
|
-
if (st.isSymbolicLink() || !st.isFile()) return null;
|
|
88
|
-
if (st.size > MAX_FLAG_BYTES) return null;
|
|
89
|
-
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
90
|
-
let fd, out;
|
|
91
|
-
try {
|
|
92
|
-
if (O_NOFOLLOW === 0) { try { if (fs.lstatSync(flagPath).isSymbolicLink()) return null; } catch {} }
|
|
93
|
-
fd = fs.openSync(flagPath, fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
94
|
-
const buf = Buffer.alloc(MAX_FLAG_BYTES);
|
|
95
|
-
const n = fs.readSync(fd, buf, 0, MAX_FLAG_BYTES, 0);
|
|
96
|
-
out = buf.slice(0, n).toString('utf8');
|
|
97
|
-
} finally {
|
|
98
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
99
|
-
}
|
|
100
|
-
const raw = out.trim().toLowerCase();
|
|
101
|
-
return LEVELS.includes(raw) ? raw : null;
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function removeFlag() {
|
|
108
|
-
try { fs.unlinkSync(flagPath); } catch {}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function sessionStart() {
|
|
112
|
-
const level = defaultLevel();
|
|
113
|
-
if (level === 'off' || !LEVELS.includes(level)) { removeFlag(); return; }
|
|
114
|
-
safeWriteFlag(level);
|
|
115
|
-
|
|
116
|
-
let body = '';
|
|
117
|
-
try {
|
|
118
|
-
const skill = fs.readFileSync(path.join(__dirname, '..', 'skills', 'terse', 'SKILL.md'), 'utf8');
|
|
119
|
-
// Strip frontmatter and maintainer HTML comments, then keep only
|
|
120
|
-
// the active level's intensity row and example lines.
|
|
121
|
-
body = skill
|
|
122
|
-
.replace(/^---[\s\S]*?---\s*/, '')
|
|
123
|
-
.replace(/<!--[\s\S]*?-->\s*/g, '')
|
|
124
|
-
.split('\n')
|
|
125
|
-
.filter(line => {
|
|
126
|
-
const row = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/);
|
|
127
|
-
if (row) return row[1] === level;
|
|
128
|
-
const ex = line.match(/^- (\S+?):\s/);
|
|
129
|
-
if (ex) return ex[1] === level;
|
|
130
|
-
return true;
|
|
131
|
-
})
|
|
132
|
-
.join('\n');
|
|
133
|
-
} catch {
|
|
134
|
-
body = 'Respond terse. All technical substance stay. Only fluff die.\n' +
|
|
135
|
-
'Drop articles/filler/pleasantries/hedging. Fragments OK. ' +
|
|
136
|
-
'Code/commits/PRs: write normal. Off: "stop terse" / "normal mode".';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
process.stdout.write(JSON.stringify({
|
|
140
|
-
hookSpecificOutput: {
|
|
141
|
-
hookEventName: 'SessionStart',
|
|
142
|
-
additionalContext: 'MAESTRO TERSE ACTIVE — level: ' + level + '\n\n' + body
|
|
143
|
-
}
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function promptSubmit(data) {
|
|
148
|
-
const prompt = String(data.prompt || '').trim().toLowerCase();
|
|
149
|
-
|
|
150
|
-
if (prompt.startsWith('/maestro:terse') || prompt.startsWith('/terse')) {
|
|
151
|
-
const arg = (prompt.split(/\s+/)[1] || '').toLowerCase();
|
|
152
|
-
if (arg === 'off') {
|
|
153
|
-
removeFlag();
|
|
154
|
-
} else if (LEVELS.includes(arg)) {
|
|
155
|
-
safeWriteFlag(arg);
|
|
156
|
-
} else {
|
|
157
|
-
// Bare invocation: explicit opt-in. Use the configured default,
|
|
158
|
-
// or 'full' when the default is off.
|
|
159
|
-
const d = defaultLevel();
|
|
160
|
-
safeWriteFlag(LEVELS.includes(d) ? d : 'full');
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (/\b(stop|disable|deactivate|turn off)\b.*\bterse\b/.test(prompt) ||
|
|
165
|
-
/\bterse\b.*\b(stop|disable|off)\b/.test(prompt) ||
|
|
166
|
-
/\bnormal mode\b/.test(prompt)) {
|
|
167
|
-
removeFlag();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const active = readFlag();
|
|
171
|
-
if (active) {
|
|
172
|
-
process.stdout.write(JSON.stringify({
|
|
173
|
-
hookSpecificOutput: {
|
|
174
|
-
hookEventName: 'UserPromptSubmit',
|
|
175
|
-
additionalContext: 'MAESTRO TERSE ACTIVE (' + active + '). ' +
|
|
176
|
-
'Drop articles/filler/pleasantries/hedging. Fragments OK. ' +
|
|
177
|
-
'Code/commits/security: write normal.'
|
|
178
|
-
}
|
|
179
|
-
}));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let data = {};
|
|
184
|
-
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
185
|
-
|
|
186
|
-
if (data.hook_event_name === 'SessionStart') sessionStart();
|
|
187
|
-
else if (data.hook_event_name === 'UserPromptSubmit') promptSubmit(data);
|
|
188
|
-
|
|
189
|
-
process.exit(0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro terse-mode hook. One file, two events (dispatch on
|
|
3
|
+
// hook_event_name):
|
|
4
|
+
// - SessionStart: resolve the level (env > config > off), write the
|
|
5
|
+
// flag file, inject the level-filtered ruleset from
|
|
6
|
+
// skills/terse/SKILL.md (single source of truth) as
|
|
7
|
+
// additionalContext.
|
|
8
|
+
// - UserPromptSubmit: track level switches (/maestro:terse, /terse,
|
|
9
|
+
// natural-language deactivation) in the flag file and emit a
|
|
10
|
+
// one-line reminder every turn while active -- per-turn
|
|
11
|
+
// reinforcement defeats style drift after context compression
|
|
12
|
+
// (same pattern as maestro-gate-reminder).
|
|
13
|
+
//
|
|
14
|
+
// Level resolution: MAESTRO_TERSE_LEVEL env var, then terseLevel in
|
|
15
|
+
// $XDG_CONFIG_HOME/maestro/config.json (~/.config/maestro fallback,
|
|
16
|
+
// %APPDATA%\maestro on Windows), then 'off'. Off by default:
|
|
17
|
+
// installing the plugin must not change anyone's output style.
|
|
18
|
+
//
|
|
19
|
+
// Flag I/O ported from Caveman (MIT): symlink-refusing, O_NOFOLLOW,
|
|
20
|
+
// atomic temp+rename, 0600, 64-byte read cap, level whitelist. A
|
|
21
|
+
// predictable flag path under ~/.claude is a symlink-attack target;
|
|
22
|
+
// never write through one, never inject unvalidated bytes into model
|
|
23
|
+
// context.
|
|
24
|
+
//
|
|
25
|
+
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
26
|
+
// package.json in a parent directory of the install location.
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const LEVELS = ['lite', 'full', 'ultra'];
|
|
33
|
+
const MAX_FLAG_BYTES = 64;
|
|
34
|
+
|
|
35
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
36
|
+
const flagPath = path.join(claudeDir, '.maestro-terse');
|
|
37
|
+
|
|
38
|
+
function configDir() {
|
|
39
|
+
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'maestro');
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'maestro');
|
|
42
|
+
}
|
|
43
|
+
return path.join(os.homedir(), '.config', 'maestro');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function defaultLevel() {
|
|
47
|
+
const env = String(process.env.MAESTRO_TERSE_LEVEL || '').toLowerCase();
|
|
48
|
+
if (env === 'off' || LEVELS.includes(env)) return env;
|
|
49
|
+
try {
|
|
50
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(configDir(), 'config.json'), 'utf8'));
|
|
51
|
+
const v = String(cfg.terseLevel || '').toLowerCase();
|
|
52
|
+
if (v === 'off' || LEVELS.includes(v)) return v;
|
|
53
|
+
} catch {}
|
|
54
|
+
return 'off';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeWriteFlag(level) {
|
|
58
|
+
try {
|
|
59
|
+
const dir = path.dirname(flagPath);
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
try { if (fs.lstatSync(dir).isSymbolicLink()) return; } catch { return; }
|
|
62
|
+
try {
|
|
63
|
+
if (fs.lstatSync(flagPath).isSymbolicLink()) return;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e.code !== 'ENOENT') return;
|
|
66
|
+
}
|
|
67
|
+
const tempPath = path.join(dir, `.maestro-terse.${process.pid}.${Date.now()}`);
|
|
68
|
+
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
69
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
|
|
70
|
+
let fd;
|
|
71
|
+
try {
|
|
72
|
+
if (O_NOFOLLOW === 0) { try { if (fs.lstatSync(tempPath).isSymbolicLink()) return; } catch {} }
|
|
73
|
+
fd = fs.openSync(tempPath, flags, 0o600);
|
|
74
|
+
fs.writeSync(fd, String(level));
|
|
75
|
+
try { fs.fchmodSync(fd, 0o600); } catch {}
|
|
76
|
+
} finally {
|
|
77
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
fs.renameSync(tempPath, flagPath);
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readFlag() {
|
|
84
|
+
try {
|
|
85
|
+
let st;
|
|
86
|
+
try { st = fs.lstatSync(flagPath); } catch { return null; }
|
|
87
|
+
if (st.isSymbolicLink() || !st.isFile()) return null;
|
|
88
|
+
if (st.size > MAX_FLAG_BYTES) return null;
|
|
89
|
+
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
90
|
+
let fd, out;
|
|
91
|
+
try {
|
|
92
|
+
if (O_NOFOLLOW === 0) { try { if (fs.lstatSync(flagPath).isSymbolicLink()) return null; } catch {} }
|
|
93
|
+
fd = fs.openSync(flagPath, fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
94
|
+
const buf = Buffer.alloc(MAX_FLAG_BYTES);
|
|
95
|
+
const n = fs.readSync(fd, buf, 0, MAX_FLAG_BYTES, 0);
|
|
96
|
+
out = buf.slice(0, n).toString('utf8');
|
|
97
|
+
} finally {
|
|
98
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
99
|
+
}
|
|
100
|
+
const raw = out.trim().toLowerCase();
|
|
101
|
+
return LEVELS.includes(raw) ? raw : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function removeFlag() {
|
|
108
|
+
try { fs.unlinkSync(flagPath); } catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sessionStart() {
|
|
112
|
+
const level = defaultLevel();
|
|
113
|
+
if (level === 'off' || !LEVELS.includes(level)) { removeFlag(); return; }
|
|
114
|
+
safeWriteFlag(level);
|
|
115
|
+
|
|
116
|
+
let body = '';
|
|
117
|
+
try {
|
|
118
|
+
const skill = fs.readFileSync(path.join(__dirname, '..', 'skills', 'terse', 'SKILL.md'), 'utf8');
|
|
119
|
+
// Strip frontmatter and maintainer HTML comments, then keep only
|
|
120
|
+
// the active level's intensity row and example lines.
|
|
121
|
+
body = skill
|
|
122
|
+
.replace(/^---[\s\S]*?---\s*/, '')
|
|
123
|
+
.replace(/<!--[\s\S]*?-->\s*/g, '')
|
|
124
|
+
.split('\n')
|
|
125
|
+
.filter(line => {
|
|
126
|
+
const row = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/);
|
|
127
|
+
if (row) return row[1] === level;
|
|
128
|
+
const ex = line.match(/^- (\S+?):\s/);
|
|
129
|
+
if (ex) return ex[1] === level;
|
|
130
|
+
return true;
|
|
131
|
+
})
|
|
132
|
+
.join('\n');
|
|
133
|
+
} catch {
|
|
134
|
+
body = 'Respond terse. All technical substance stay. Only fluff die.\n' +
|
|
135
|
+
'Drop articles/filler/pleasantries/hedging. Fragments OK. ' +
|
|
136
|
+
'Code/commits/PRs: write normal. Off: "stop terse" / "normal mode".';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
process.stdout.write(JSON.stringify({
|
|
140
|
+
hookSpecificOutput: {
|
|
141
|
+
hookEventName: 'SessionStart',
|
|
142
|
+
additionalContext: 'MAESTRO TERSE ACTIVE — level: ' + level + '\n\n' + body
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function promptSubmit(data) {
|
|
148
|
+
const prompt = String(data.prompt || '').trim().toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (prompt.startsWith('/maestro:terse') || prompt.startsWith('/terse')) {
|
|
151
|
+
const arg = (prompt.split(/\s+/)[1] || '').toLowerCase();
|
|
152
|
+
if (arg === 'off') {
|
|
153
|
+
removeFlag();
|
|
154
|
+
} else if (LEVELS.includes(arg)) {
|
|
155
|
+
safeWriteFlag(arg);
|
|
156
|
+
} else {
|
|
157
|
+
// Bare invocation: explicit opt-in. Use the configured default,
|
|
158
|
+
// or 'full' when the default is off.
|
|
159
|
+
const d = defaultLevel();
|
|
160
|
+
safeWriteFlag(LEVELS.includes(d) ? d : 'full');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (/\b(stop|disable|deactivate|turn off)\b.*\bterse\b/.test(prompt) ||
|
|
165
|
+
/\bterse\b.*\b(stop|disable|off)\b/.test(prompt) ||
|
|
166
|
+
/\bnormal mode\b/.test(prompt)) {
|
|
167
|
+
removeFlag();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const active = readFlag();
|
|
171
|
+
if (active) {
|
|
172
|
+
process.stdout.write(JSON.stringify({
|
|
173
|
+
hookSpecificOutput: {
|
|
174
|
+
hookEventName: 'UserPromptSubmit',
|
|
175
|
+
additionalContext: 'MAESTRO TERSE ACTIVE (' + active + '). ' +
|
|
176
|
+
'Drop articles/filler/pleasantries/hedging. Fragments OK. ' +
|
|
177
|
+
'Code/commits/security: write normal.'
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let data = {};
|
|
184
|
+
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
185
|
+
|
|
186
|
+
if (data.hook_event_name === 'SessionStart') sessionStart();
|
|
187
|
+
else if (data.hook_event_name === 'UserPromptSubmit') promptSubmit(data);
|
|
188
|
+
|
|
189
|
+
process.exit(0);
|
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro PostToolUse tool-call budget advisory (Fable T1). Log-only,
|
|
3
|
-
// zero prompt tokens, never blocks.
|
|
4
|
-
//
|
|
5
|
-
// Fable scales tool calls to task complexity (1 for single facts; 3-5
|
|
6
|
-
// medium; 5-10 deep research). Maestro caps subagent tool budgets (S9)
|
|
7
|
-
// but nothing watches the ORCHESTRATOR's own pre-edit exploration. This
|
|
8
|
-
// hook measures how many exploration (non-edit) tool calls happened this
|
|
9
|
-
// turn before the FIRST file edit; if that exceeds a budget it appends
|
|
10
|
-
// one advisory row to a local log. It is a behavioural lever, evidence-
|
|
11
|
-
// gated: log-only first, so a preregistered OFF/ON fixture can show the
|
|
12
|
-
// signal separates before anyone promotes it to an enforcing warning.
|
|
13
|
-
//
|
|
14
|
-
// Design choices:
|
|
15
|
-
// - Fires on PostToolUse for edit tools; evaluates once per turn at the
|
|
16
|
-
// first-edit boundary (a per-turn marker file makes it idempotent).
|
|
17
|
-
// - "Turn" = everything since the last genuine user prompt, located from
|
|
18
|
-
// the transcript with the same heuristic as maestro-phase-scope.
|
|
19
|
-
// - ZERO prompt tokens: never writes stdout / additionalContext, so it
|
|
20
|
-
// adds nothing to context. It only appends a counts-only JSON row.
|
|
21
|
-
// - NEVER blocks: PostToolUse cannot block and this emits no decision.
|
|
22
|
-
// - Privacy: records counts + project folder basename only -- no
|
|
23
|
-
// prompts, no file contents, no paths. No network, ever.
|
|
24
|
-
//
|
|
25
|
-
// Env:
|
|
26
|
-
// - MAESTRO_TOOLBUDGET=0 disable entirely (default: active, log-only)
|
|
27
|
-
// - MAESTRO_TOOLBUDGET_THRESHOLD exploration-call budget (default 20)
|
|
28
|
-
// - MAESTRO_TOOLBUDGET_LOG override log path (default ~/.claude/maestro-toolbudget.jsonl)
|
|
29
|
-
// - MAESTRO_TOOLBUDGET_MARKERDIR override per-turn marker dir (default OS tmp)
|
|
30
|
-
//
|
|
31
|
-
// Promotion path (NOT shipped, pending fixture evidence): a `warn` mode
|
|
32
|
-
// that emits additionalContext at the first edit. Kept out until the log
|
|
33
|
-
// shows the budget separates real over-exploration from normal work.
|
|
34
|
-
//
|
|
35
|
-
// Payload fields verified against code.claude.com/docs/en/hooks
|
|
36
|
-
// (PostToolUse input: session_id, transcript_path, cwd, tool_name,
|
|
37
|
-
// tool_input; PostToolUse output cannot block), 2026-06-16.
|
|
38
|
-
//
|
|
39
|
-
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
40
|
-
// package.json in a parent directory of the install location.
|
|
41
|
-
|
|
42
|
-
'use strict';
|
|
43
|
-
|
|
44
|
-
const fs = require('fs');
|
|
45
|
-
const os = require('os');
|
|
46
|
-
const path = require('path');
|
|
47
|
-
const crypto = require('crypto');
|
|
48
|
-
|
|
49
|
-
if (process.env.MAESTRO_TOOLBUDGET === '0') process.exit(0);
|
|
50
|
-
|
|
51
|
-
const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);
|
|
52
|
-
|
|
53
|
-
let data = {};
|
|
54
|
-
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
55
|
-
|
|
56
|
-
// Only the first edit of a turn matters; ignore every non-edit tool.
|
|
57
|
-
if (!EDIT_TOOLS.has(data.tool_name)) process.exit(0);
|
|
58
|
-
|
|
59
|
-
// Counting exploration before the first edit needs the transcript.
|
|
60
|
-
if (!data.transcript_path || !fs.existsSync(data.transcript_path)) process.exit(0);
|
|
61
|
-
|
|
62
|
-
let lines = [];
|
|
63
|
-
try {
|
|
64
|
-
const buf = fs.readFileSync(data.transcript_path, 'utf8');
|
|
65
|
-
lines = (buf.length > 4000000 ? buf.slice(-4000000) : buf).split(/\r?\n/);
|
|
66
|
-
} catch { process.exit(0); }
|
|
67
|
-
|
|
68
|
-
const parsed = lines.map(l => { try { return JSON.parse(l); } catch { return null; } });
|
|
69
|
-
|
|
70
|
-
// Locate the last genuine user prompt (typed text, not a tool_result
|
|
71
|
-
// carrier); everything after it is the current turn.
|
|
72
|
-
let turnStart = 0;
|
|
73
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
74
|
-
const e = parsed[i];
|
|
75
|
-
if (!e || e.type !== 'user' || e.isMeta || !e.message) continue;
|
|
76
|
-
const c = e.message.content;
|
|
77
|
-
const genuine = typeof c === 'string'
|
|
78
|
-
? true
|
|
79
|
-
: Array.isArray(c) && c.some(x => x && x.type === 'text') && !c.some(x => x && x.type === 'tool_result');
|
|
80
|
-
if (genuine) turnStart = i;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Evaluate at most once per turn, at the first-edit boundary.
|
|
84
|
-
const markerDir = process.env.MAESTRO_TOOLBUDGET_MARKERDIR || os.tmpdir();
|
|
85
|
-
const turnKey = crypto.createHash('sha1')
|
|
86
|
-
.update(String(data.session_id || '') + ':' + turnStart + ':' + (lines[turnStart] || ''))
|
|
87
|
-
.digest('hex').slice(0, 16);
|
|
88
|
-
const marker = path.join(markerDir, 'maestro-toolbudget-' + turnKey);
|
|
89
|
-
if (fs.existsSync(marker)) process.exit(0);
|
|
90
|
-
try { fs.mkdirSync(markerDir, { recursive: true }); fs.writeFileSync(marker, '1'); } catch {}
|
|
91
|
-
|
|
92
|
-
// Count exploration (non-edit) tool calls that ran before the first edit
|
|
93
|
-
// this turn. priorEdit stops the count at the first edit, whether or not
|
|
94
|
-
// the triggering edit is already in the transcript.
|
|
95
|
-
let explore = 0;
|
|
96
|
-
let priorEdit = false;
|
|
97
|
-
for (let i = turnStart; i < parsed.length; i++) {
|
|
98
|
-
const e = parsed[i];
|
|
99
|
-
if (!e || e.type !== 'assistant' || !e.message || !Array.isArray(e.message.content)) continue;
|
|
100
|
-
for (const item of e.message.content) {
|
|
101
|
-
if (!item || item.type !== 'tool_use') continue;
|
|
102
|
-
if (EDIT_TOOLS.has(item.name)) { priorEdit = true; break; }
|
|
103
|
-
explore++;
|
|
104
|
-
}
|
|
105
|
-
if (priorEdit) break;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const threshold = parseInt(process.env.MAESTRO_TOOLBUDGET_THRESHOLD, 10) || 20;
|
|
109
|
-
if (explore > threshold) {
|
|
110
|
-
const row = {
|
|
111
|
-
ts: new Date().toISOString(),
|
|
112
|
-
session_id: data.session_id || null,
|
|
113
|
-
kind: 'toolbudget-advisory',
|
|
114
|
-
explore_calls: explore,
|
|
115
|
-
threshold,
|
|
116
|
-
first_edit_tool: data.tool_name,
|
|
117
|
-
project: data.cwd ? path.basename(data.cwd) : null
|
|
118
|
-
};
|
|
119
|
-
const logPath = process.env.MAESTRO_TOOLBUDGET_LOG
|
|
120
|
-
|| path.join(os.homedir(), '.claude', 'maestro-toolbudget.jsonl');
|
|
121
|
-
try {
|
|
122
|
-
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
123
|
-
fs.appendFileSync(logPath, JSON.stringify(row) + '\n');
|
|
124
|
-
} catch { /* advisory is best-effort; never disrupt the session */ }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
process.exit(0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro PostToolUse tool-call budget advisory (Fable T1). Log-only,
|
|
3
|
+
// zero prompt tokens, never blocks.
|
|
4
|
+
//
|
|
5
|
+
// Fable scales tool calls to task complexity (1 for single facts; 3-5
|
|
6
|
+
// medium; 5-10 deep research). Maestro caps subagent tool budgets (S9)
|
|
7
|
+
// but nothing watches the ORCHESTRATOR's own pre-edit exploration. This
|
|
8
|
+
// hook measures how many exploration (non-edit) tool calls happened this
|
|
9
|
+
// turn before the FIRST file edit; if that exceeds a budget it appends
|
|
10
|
+
// one advisory row to a local log. It is a behavioural lever, evidence-
|
|
11
|
+
// gated: log-only first, so a preregistered OFF/ON fixture can show the
|
|
12
|
+
// signal separates before anyone promotes it to an enforcing warning.
|
|
13
|
+
//
|
|
14
|
+
// Design choices:
|
|
15
|
+
// - Fires on PostToolUse for edit tools; evaluates once per turn at the
|
|
16
|
+
// first-edit boundary (a per-turn marker file makes it idempotent).
|
|
17
|
+
// - "Turn" = everything since the last genuine user prompt, located from
|
|
18
|
+
// the transcript with the same heuristic as maestro-phase-scope.
|
|
19
|
+
// - ZERO prompt tokens: never writes stdout / additionalContext, so it
|
|
20
|
+
// adds nothing to context. It only appends a counts-only JSON row.
|
|
21
|
+
// - NEVER blocks: PostToolUse cannot block and this emits no decision.
|
|
22
|
+
// - Privacy: records counts + project folder basename only -- no
|
|
23
|
+
// prompts, no file contents, no paths. No network, ever.
|
|
24
|
+
//
|
|
25
|
+
// Env:
|
|
26
|
+
// - MAESTRO_TOOLBUDGET=0 disable entirely (default: active, log-only)
|
|
27
|
+
// - MAESTRO_TOOLBUDGET_THRESHOLD exploration-call budget (default 20)
|
|
28
|
+
// - MAESTRO_TOOLBUDGET_LOG override log path (default ~/.claude/maestro-toolbudget.jsonl)
|
|
29
|
+
// - MAESTRO_TOOLBUDGET_MARKERDIR override per-turn marker dir (default OS tmp)
|
|
30
|
+
//
|
|
31
|
+
// Promotion path (NOT shipped, pending fixture evidence): a `warn` mode
|
|
32
|
+
// that emits additionalContext at the first edit. Kept out until the log
|
|
33
|
+
// shows the budget separates real over-exploration from normal work.
|
|
34
|
+
//
|
|
35
|
+
// Payload fields verified against code.claude.com/docs/en/hooks
|
|
36
|
+
// (PostToolUse input: session_id, transcript_path, cwd, tool_name,
|
|
37
|
+
// tool_input; PostToolUse output cannot block), 2026-06-16.
|
|
38
|
+
//
|
|
39
|
+
// .cjs so Node treats it as CommonJS regardless of any "type": "module"
|
|
40
|
+
// package.json in a parent directory of the install location.
|
|
41
|
+
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const os = require('os');
|
|
46
|
+
const path = require('path');
|
|
47
|
+
const crypto = require('crypto');
|
|
48
|
+
|
|
49
|
+
if (process.env.MAESTRO_TOOLBUDGET === '0') process.exit(0);
|
|
50
|
+
|
|
51
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);
|
|
52
|
+
|
|
53
|
+
let data = {};
|
|
54
|
+
try { data = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
55
|
+
|
|
56
|
+
// Only the first edit of a turn matters; ignore every non-edit tool.
|
|
57
|
+
if (!EDIT_TOOLS.has(data.tool_name)) process.exit(0);
|
|
58
|
+
|
|
59
|
+
// Counting exploration before the first edit needs the transcript.
|
|
60
|
+
if (!data.transcript_path || !fs.existsSync(data.transcript_path)) process.exit(0);
|
|
61
|
+
|
|
62
|
+
let lines = [];
|
|
63
|
+
try {
|
|
64
|
+
const buf = fs.readFileSync(data.transcript_path, 'utf8');
|
|
65
|
+
lines = (buf.length > 4000000 ? buf.slice(-4000000) : buf).split(/\r?\n/);
|
|
66
|
+
} catch { process.exit(0); }
|
|
67
|
+
|
|
68
|
+
const parsed = lines.map(l => { try { return JSON.parse(l); } catch { return null; } });
|
|
69
|
+
|
|
70
|
+
// Locate the last genuine user prompt (typed text, not a tool_result
|
|
71
|
+
// carrier); everything after it is the current turn.
|
|
72
|
+
let turnStart = 0;
|
|
73
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
74
|
+
const e = parsed[i];
|
|
75
|
+
if (!e || e.type !== 'user' || e.isMeta || !e.message) continue;
|
|
76
|
+
const c = e.message.content;
|
|
77
|
+
const genuine = typeof c === 'string'
|
|
78
|
+
? true
|
|
79
|
+
: Array.isArray(c) && c.some(x => x && x.type === 'text') && !c.some(x => x && x.type === 'tool_result');
|
|
80
|
+
if (genuine) turnStart = i;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Evaluate at most once per turn, at the first-edit boundary.
|
|
84
|
+
const markerDir = process.env.MAESTRO_TOOLBUDGET_MARKERDIR || os.tmpdir();
|
|
85
|
+
const turnKey = crypto.createHash('sha1')
|
|
86
|
+
.update(String(data.session_id || '') + ':' + turnStart + ':' + (lines[turnStart] || ''))
|
|
87
|
+
.digest('hex').slice(0, 16);
|
|
88
|
+
const marker = path.join(markerDir, 'maestro-toolbudget-' + turnKey);
|
|
89
|
+
if (fs.existsSync(marker)) process.exit(0);
|
|
90
|
+
try { fs.mkdirSync(markerDir, { recursive: true }); fs.writeFileSync(marker, '1'); } catch {}
|
|
91
|
+
|
|
92
|
+
// Count exploration (non-edit) tool calls that ran before the first edit
|
|
93
|
+
// this turn. priorEdit stops the count at the first edit, whether or not
|
|
94
|
+
// the triggering edit is already in the transcript.
|
|
95
|
+
let explore = 0;
|
|
96
|
+
let priorEdit = false;
|
|
97
|
+
for (let i = turnStart; i < parsed.length; i++) {
|
|
98
|
+
const e = parsed[i];
|
|
99
|
+
if (!e || e.type !== 'assistant' || !e.message || !Array.isArray(e.message.content)) continue;
|
|
100
|
+
for (const item of e.message.content) {
|
|
101
|
+
if (!item || item.type !== 'tool_use') continue;
|
|
102
|
+
if (EDIT_TOOLS.has(item.name)) { priorEdit = true; break; }
|
|
103
|
+
explore++;
|
|
104
|
+
}
|
|
105
|
+
if (priorEdit) break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const threshold = parseInt(process.env.MAESTRO_TOOLBUDGET_THRESHOLD, 10) || 20;
|
|
109
|
+
if (explore > threshold) {
|
|
110
|
+
const row = {
|
|
111
|
+
ts: new Date().toISOString(),
|
|
112
|
+
session_id: data.session_id || null,
|
|
113
|
+
kind: 'toolbudget-advisory',
|
|
114
|
+
explore_calls: explore,
|
|
115
|
+
threshold,
|
|
116
|
+
first_edit_tool: data.tool_name,
|
|
117
|
+
project: data.cwd ? path.basename(data.cwd) : null
|
|
118
|
+
};
|
|
119
|
+
const logPath = process.env.MAESTRO_TOOLBUDGET_LOG
|
|
120
|
+
|| path.join(os.homedir(), '.claude', 'maestro-toolbudget.jsonl');
|
|
121
|
+
try {
|
|
122
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
123
|
+
fs.appendFileSync(logPath, JSON.stringify(row) + '\n');
|
|
124
|
+
} catch { /* advisory is best-effort; never disrupt the session */ }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
process.exit(0);
|