@maestrofrontier/frontier 1.4.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +21 -21
- package/.codex-plugin/plugin.json +29 -29
- package/.cursorrules +197 -194
- package/AGENTS.md +3 -3
- package/README.md +368 -368
- package/bin/maestro.cjs +75 -75
- package/commands/compress.md +36 -36
- package/commands/frontier.md +124 -124
- package/commands/terse.md +23 -23
- package/docs/codex.md +167 -167
- package/docs/orchestration.md +168 -168
- package/frontier/cli.cjs +279 -252
- package/frontier/config.cjs +468 -468
- package/frontier/dispatch.cjs +267 -255
- package/frontier/judge.cjs +92 -92
- package/frontier/progress.cjs +138 -0
- package/frontier/run.cjs +201 -180
- package/frontier/schema.cjs +112 -112
- package/frontier/semaphore.cjs +49 -49
- package/frontier/synthesize.cjs +79 -79
- package/hooks/frontier-autorun.cjs +135 -120
- package/hooks/hooks.json +103 -103
- package/hooks/maestro-doctrine-guard.cjs +81 -81
- package/hooks/maestro-gate-reminder.cjs +22 -7
- package/hooks/maestro-gate-telemetry.cjs +79 -77
- package/hooks/maestro-phase-scope.cjs +118 -118
- package/hooks/maestro-statusline-sync.cjs +152 -152
- package/hooks/maestro-subagent-guard.cjs +148 -148
- package/hooks/maestro-terse-mode.cjs +189 -189
- package/hooks/maestro-toolbudget-advisory.cjs +127 -127
- package/integrations/README.md +111 -111
- package/integrations/cline/skills/frontier/SKILL.md +75 -75
- package/integrations/codex/prompts/frontier.md +70 -70
- package/integrations/codex/prompts/update.md +39 -39
- package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -122
- package/integrations/codex/skills/maestro-settings/SKILL.md +55 -55
- package/integrations/codex/skills/maestro-terse/SKILL.md +58 -58
- package/integrations/codex/skills/maestro-update/SKILL.md +31 -31
- package/integrations/cursor/commands/frontier.md +63 -63
- package/integrations/cursor/commands/update.md +34 -34
- package/integrations/gemini/commands/frontier.toml +76 -76
- package/integrations/windsurf/workflows/frontier.md +70 -70
- package/package.json +59 -58
- package/scripts/install.cjs +1014 -1014
- package/settings/cli.cjs +140 -140
- package/settings/config.cjs +309 -309
- package/skills/maestro-frontier/SKILL.md +122 -122
- package/skills/maestro-settings/SKILL.md +55 -55
- package/skills/maestro-terse/SKILL.md +58 -58
- package/skills/maestro-update/SKILL.md +31 -31
- package/skills/terse/SKILL.md +74 -74
package/frontier/config.cjs
CHANGED
|
@@ -1,468 +1,468 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro Frontier — config defaults + state persistence.
|
|
3
|
-
// Zero deps, CJS. configDir + safeWriteFlag patterns ported from
|
|
4
|
-
// hooks/maestro-terse-mode.cjs. tokenBudget=0 means budget abort
|
|
5
|
-
// DISABLED (opt-in feature; set to a positive integer to enable).
|
|
6
|
-
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
const crypto = require('crypto');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
// ---------- configDir (ported from maestro-terse-mode.cjs) ----------
|
|
15
|
-
|
|
16
|
-
function configDir() {
|
|
17
|
-
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'maestro');
|
|
18
|
-
if (process.platform === 'win32') {
|
|
19
|
-
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'maestro');
|
|
20
|
-
}
|
|
21
|
-
return path.join(os.homedir(), '.config', 'maestro');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------- arg helper (used by resolveScope) ----------
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {string[]} argv
|
|
28
|
-
* @param {string} flag
|
|
29
|
-
* @returns {string|null}
|
|
30
|
-
*/
|
|
31
|
-
function getFlag(argv, flag) {
|
|
32
|
-
const i = argv.indexOf(flag);
|
|
33
|
-
return i !== -1 && i + 1 < argv.length ? argv[i + 1] : null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---------- workspace hash ----------
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Derive a stable 8-hex workspace identifier from cwd by walking up to the
|
|
40
|
-
* nearest .git root, normalizing the path, and hashing it.
|
|
41
|
-
* On win32 the path is lowercased (filesystem is case-insensitive).
|
|
42
|
-
* @param {string} cwd
|
|
43
|
-
* @returns {string} 8 hex characters
|
|
44
|
-
*/
|
|
45
|
-
function workspaceHash(cwd) {
|
|
46
|
-
let normalized = path.resolve(cwd);
|
|
47
|
-
let last = null;
|
|
48
|
-
while (normalized !== last && !fs.existsSync(path.join(normalized, '.git'))) {
|
|
49
|
-
last = normalized;
|
|
50
|
-
normalized = path.dirname(normalized);
|
|
51
|
-
}
|
|
52
|
-
if (process.platform === 'win32') {
|
|
53
|
-
normalized = normalized.replace(/\\/g, '/').toLowerCase().replace(/\/+$/g, '');
|
|
54
|
-
} else {
|
|
55
|
-
normalized = normalized.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
56
|
-
}
|
|
57
|
-
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------- scope helpers ----------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Sanitize a raw scope value: lowercase, strip non-[a-z0-9-] chars entirely,
|
|
64
|
-
* then return 'default' if the result is empty.
|
|
65
|
-
* Examples: 'Foo' -> 'foo', 'a b!c' -> 'abc', '' -> 'default'.
|
|
66
|
-
* @param {*} v
|
|
67
|
-
* @returns {string}
|
|
68
|
-
*/
|
|
69
|
-
function sanitizeScope(v) {
|
|
70
|
-
const s = String(v).toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
71
|
-
return s.length > 0 ? s : 'default';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function workspaceCwd(opts) {
|
|
75
|
-
return (opts && opts.cwd) || process.env.CLAUDE_PROJECT_DIR || process.env.CODEX_PROJECT_DIR || process.cwd();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function resolveScopeAlias(scope, opts) {
|
|
79
|
-
const clean = sanitizeScope(scope);
|
|
80
|
-
if (['codex-project', 'codex-workspace', 'codex-repo'].includes(clean)) {
|
|
81
|
-
return 'codex-' + workspaceHash(workspaceCwd(opts));
|
|
82
|
-
}
|
|
83
|
-
if (['claude-project', 'claude-workspace', 'cc-project', 'cc-workspace'].includes(clean)) {
|
|
84
|
-
return 'cc-' + workspaceHash(workspaceCwd(opts));
|
|
85
|
-
}
|
|
86
|
-
return clean;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Resolve the active scope from argv + environment. Precedence:
|
|
91
|
-
* 1. --scope <value> flag in argv
|
|
92
|
-
* 2. process.env.MAESTRO_SCOPE
|
|
93
|
-
* 3. Autodetect: PLUGIN_ROOT truthy -> 'codex-<8hex>'
|
|
94
|
-
* 4. Autodetect: CLAUDE_PLUGIN_ROOT || CLAUDECODE truthy -> 'cc-<8hex>'
|
|
95
|
-
* 5. Autodetect: PLUGIN_DATA truthy -> 'codex-<8hex>'
|
|
96
|
-
* where <8hex> is derived from the workspace root (opts.cwd,
|
|
97
|
-
* CLAUDE_PROJECT_DIR, CODEX_PROJECT_DIR, or process.cwd()) via
|
|
98
|
-
* workspaceHash().
|
|
99
|
-
* 6. 'default'
|
|
100
|
-
* The chosen value for steps 1-2 is sanitized, and project aliases such as
|
|
101
|
-
* codex-project/codex-workspace are expanded to per-workspace scopes.
|
|
102
|
-
* @param {string[]} argv
|
|
103
|
-
* @param {{ cwd?: string }} [opts]
|
|
104
|
-
* @returns {string}
|
|
105
|
-
*/
|
|
106
|
-
function resolveScope(argv, opts) {
|
|
107
|
-
const flagVal = getFlag(argv, '--scope');
|
|
108
|
-
if (flagVal !== null) return resolveScopeAlias(flagVal, opts);
|
|
109
|
-
if (process.env.MAESTRO_SCOPE) return resolveScopeAlias(process.env.MAESTRO_SCOPE, opts);
|
|
110
|
-
if (process.env.PLUGIN_ROOT) {
|
|
111
|
-
return 'codex-' + workspaceHash(workspaceCwd(opts));
|
|
112
|
-
}
|
|
113
|
-
if (process.env.CLAUDE_PLUGIN_ROOT || process.env.CLAUDECODE) {
|
|
114
|
-
return 'cc-' + workspaceHash(workspaceCwd(opts));
|
|
115
|
-
}
|
|
116
|
-
if (process.env.PLUGIN_DATA) {
|
|
117
|
-
return 'codex-' + workspaceHash(workspaceCwd(opts));
|
|
118
|
-
}
|
|
119
|
-
return 'default';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ---------- path helpers ----------
|
|
123
|
-
|
|
124
|
-
/** Pre-scope global state file; migration source only, never written/deleted. */
|
|
125
|
-
function legacyStatePath() {
|
|
126
|
-
return path.join(configDir(), 'frontier-state.json');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Scope-aware state path.
|
|
131
|
-
* scope === 'default' => frontier-state.json (legacy-compatible, no suffix).
|
|
132
|
-
* Any other scope => frontier-state.<scope>.json.
|
|
133
|
-
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
134
|
-
* @returns {string}
|
|
135
|
-
*/
|
|
136
|
-
function statePath(scope) {
|
|
137
|
-
if (scope === undefined) scope = resolveScope([]);
|
|
138
|
-
else scope = resolveScopeAlias(scope);
|
|
139
|
-
if (scope === 'default') return path.join(configDir(), 'frontier-state.json');
|
|
140
|
-
return path.join(configDir(), 'frontier-state.' + scope + '.json');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ---------- state I/O ----------
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Read and validate a state file. Returns:
|
|
147
|
-
* - null if the file is absent (ENOENT) — caller decides the fallback.
|
|
148
|
-
* - {mode:'off'} on symlink, corrupt JSON, or invalid mode.
|
|
149
|
-
* - parsed object on success.
|
|
150
|
-
* @param {string} p
|
|
151
|
-
* @returns {object|null}
|
|
152
|
-
*/
|
|
153
|
-
function _readStateFile(p) {
|
|
154
|
-
let st;
|
|
155
|
-
try { st = fs.lstatSync(p); } catch { return null; }
|
|
156
|
-
if (st.isSymbolicLink()) return { mode: 'off' };
|
|
157
|
-
try {
|
|
158
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
159
|
-
const parsed = JSON.parse(raw);
|
|
160
|
-
if (!parsed || typeof parsed !== 'object') return { mode: 'off' };
|
|
161
|
-
if (!['off', 'single', 'fusion'].includes(parsed.mode)) return { mode: 'off' };
|
|
162
|
-
return parsed;
|
|
163
|
-
} catch {
|
|
164
|
-
return { mode: 'off' };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Strict state read for explicit adoption paths. Unlike _readStateFile, this
|
|
170
|
-
* distinguishes invalid legacy content from a valid {mode:'off'} state.
|
|
171
|
-
* @param {string} p
|
|
172
|
-
* @returns {{ ok: true, state: object }|{ ok: false, reason: 'missing'|'invalid' }}
|
|
173
|
-
*/
|
|
174
|
-
function _readValidatedStateFile(p) {
|
|
175
|
-
let st;
|
|
176
|
-
try { st = fs.lstatSync(p); } catch { return { ok: false, reason: 'missing' }; }
|
|
177
|
-
if (st.isSymbolicLink()) return { ok: false, reason: 'invalid' };
|
|
178
|
-
try {
|
|
179
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
180
|
-
const parsed = JSON.parse(raw);
|
|
181
|
-
if (!parsed || typeof parsed !== 'object') return { ok: false, reason: 'invalid' };
|
|
182
|
-
if (!['off', 'single', 'fusion'].includes(parsed.mode)) return { ok: false, reason: 'invalid' };
|
|
183
|
-
return { ok: true, state: parsed };
|
|
184
|
-
} catch {
|
|
185
|
-
return { ok: false, reason: 'invalid' };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Load frontier state for the given scope.
|
|
191
|
-
* D3 MIGRATION: if scope !== 'default' and scope does NOT match /^(cc|codex)-/
|
|
192
|
-
* and the scoped file does NOT exist AND the legacy frontier-state.json
|
|
193
|
-
* DOES exist, seed from the legacy file (read-only — never write during
|
|
194
|
-
* load). Same symlink/parse guards apply. cc-* and codex-* scopes are
|
|
195
|
-
* excluded from migration because they are per-workspace and must not inherit
|
|
196
|
-
* global legacy state. Named scopes (e.g. 'codex', 'cursor') still migrate.
|
|
197
|
-
* Falls back to {mode:'off'} on any failure.
|
|
198
|
-
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
199
|
-
* @returns {object}
|
|
200
|
-
*/
|
|
201
|
-
function loadState(scope) {
|
|
202
|
-
if (scope === undefined) scope = resolveScope([]);
|
|
203
|
-
else scope = resolveScopeAlias(scope);
|
|
204
|
-
try {
|
|
205
|
-
const p = statePath(scope);
|
|
206
|
-
const result = _readStateFile(p);
|
|
207
|
-
if (result !== null) return result;
|
|
208
|
-
|
|
209
|
-
// File absent. For non-default, non-workspace scopes attempt migration from legacy file.
|
|
210
|
-
if (scope !== 'default' && !/^(cc|codex)-/.test(scope)) {
|
|
211
|
-
const legacyResult = _readStateFile(legacyStatePath());
|
|
212
|
-
if (legacyResult !== null) return legacyResult;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { mode: 'off' };
|
|
216
|
-
} catch {
|
|
217
|
-
return { mode: 'off' };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Atomic temp+rename write, 0600, symlink-refusing.
|
|
223
|
-
* Ported from safeWriteFlag in hooks/maestro-terse-mode.cjs.
|
|
224
|
-
* @param {object} state
|
|
225
|
-
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
226
|
-
* @returns {boolean}
|
|
227
|
-
*/
|
|
228
|
-
function saveState(state, scope) {
|
|
229
|
-
if (scope === undefined) scope = resolveScope([]);
|
|
230
|
-
else scope = resolveScopeAlias(scope);
|
|
231
|
-
try {
|
|
232
|
-
const p = statePath(scope);
|
|
233
|
-
const dir = path.dirname(p);
|
|
234
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
235
|
-
try { if (fs.lstatSync(dir).isSymbolicLink()) return false; } catch { return false; }
|
|
236
|
-
try {
|
|
237
|
-
if (fs.lstatSync(p).isSymbolicLink()) return false;
|
|
238
|
-
} catch (e) {
|
|
239
|
-
if (e.code !== 'ENOENT') return false;
|
|
240
|
-
}
|
|
241
|
-
const tempPath = path.join(dir, `.frontier-state.${process.pid}.${Date.now()}.tmp`);
|
|
242
|
-
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
243
|
-
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
|
|
244
|
-
let fd;
|
|
245
|
-
try {
|
|
246
|
-
if (O_NOFOLLOW === 0) {
|
|
247
|
-
try { if (fs.lstatSync(tempPath).isSymbolicLink()) return false; } catch {}
|
|
248
|
-
}
|
|
249
|
-
fd = fs.openSync(tempPath, flags, 0o600);
|
|
250
|
-
fs.writeSync(fd, JSON.stringify(state));
|
|
251
|
-
try { fs.fchmodSync(fd, 0o600); } catch {}
|
|
252
|
-
} finally {
|
|
253
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
254
|
-
}
|
|
255
|
-
fs.renameSync(tempPath, p);
|
|
256
|
-
return true;
|
|
257
|
-
} catch {
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Explicitly adopt the validated legacy frontier-state.json into a cc-* scope.
|
|
264
|
-
* This copies legacy content into frontier-state.<cc-scope>.json only, never
|
|
265
|
-
* deletes legacy, and refuses to overwrite an existing scoped file unless
|
|
266
|
-
* opts.force is true.
|
|
267
|
-
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
268
|
-
* @param {{ force?: boolean }} [opts]
|
|
269
|
-
* @returns {{ ok: true, scope: string, path: string }|{ ok: false, reason: string, scope: string, path?: string }}
|
|
270
|
-
*/
|
|
271
|
-
function adoptLegacyState(scope, opts) {
|
|
272
|
-
if (scope === undefined) scope = resolveScope([]);
|
|
273
|
-
const targetScope = sanitizeScope(scope);
|
|
274
|
-
const targetPath = statePath(targetScope);
|
|
275
|
-
if (!/^cc-/.test(targetScope)) {
|
|
276
|
-
return { ok: false, reason: 'not-cc-scope', scope: targetScope, path: targetPath };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const legacy = _readValidatedStateFile(legacyStatePath());
|
|
280
|
-
if (!legacy.ok) {
|
|
281
|
-
return {
|
|
282
|
-
ok: false,
|
|
283
|
-
reason: legacy.reason === 'missing' ? 'missing-legacy' : 'invalid-legacy',
|
|
284
|
-
scope: targetScope,
|
|
285
|
-
path: targetPath,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const st = fs.lstatSync(targetPath);
|
|
291
|
-
if (st.isSymbolicLink()) return { ok: false, reason: 'unsafe-target', scope: targetScope, path: targetPath };
|
|
292
|
-
if (!(opts && opts.force)) return { ok: false, reason: 'exists', scope: targetScope, path: targetPath };
|
|
293
|
-
} catch (e) {
|
|
294
|
-
if (e.code !== 'ENOENT') return { ok: false, reason: 'unsafe-target', scope: targetScope, path: targetPath };
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (!saveState(legacy.state, targetScope)) {
|
|
298
|
-
return { ok: false, reason: 'write-failed', scope: targetScope, path: targetPath };
|
|
299
|
-
}
|
|
300
|
-
return { ok: true, scope: targetScope, path: targetPath };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ---------- DEFAULTS ----------
|
|
304
|
-
|
|
305
|
-
const DEFAULTS = {
|
|
306
|
-
adapters: {
|
|
307
|
-
opus: {
|
|
308
|
-
model: 'opus',
|
|
309
|
-
bin: process.env.MAESTRO_CLAUDE_BIN || 'claude',
|
|
310
|
-
baseArgs: ['-p', '--output-format', 'json', '--dangerously-skip-permissions'],
|
|
311
|
-
promptVia: 'stdin',
|
|
312
|
-
webTools: false,
|
|
313
|
-
output: 'stdout',
|
|
314
|
-
parse: 'claude-json',
|
|
315
|
-
},
|
|
316
|
-
'gpt-5.5': {
|
|
317
|
-
model: 'gpt-5.5',
|
|
318
|
-
bin: process.env.MAESTRO_CODEX_BIN || 'codex',
|
|
319
|
-
baseArgs: [
|
|
320
|
-
'exec',
|
|
321
|
-
'--skip-git-repo-check',
|
|
322
|
-
'--dangerously-bypass-approvals-and-sandbox',
|
|
323
|
-
'-m', 'gpt-5.5',
|
|
324
|
-
'--color', 'never',
|
|
325
|
-
],
|
|
326
|
-
promptVia: 'stdin',
|
|
327
|
-
webTools: true,
|
|
328
|
-
output: 'last-message-file',
|
|
329
|
-
parse: 'text',
|
|
330
|
-
},
|
|
331
|
-
gemini: {
|
|
332
|
-
model: 'gemini',
|
|
333
|
-
bin: process.env.MAESTRO_GEMINI_BIN || 'gemini',
|
|
334
|
-
baseArgs: [
|
|
335
|
-
'--output-format', 'json',
|
|
336
|
-
'--approval-mode', 'yolo',
|
|
337
|
-
'--model', 'gemini-3.1-pro-preview',
|
|
338
|
-
],
|
|
339
|
-
promptVia: 'arg',
|
|
340
|
-
promptFlag: '-p',
|
|
341
|
-
webTools: false,
|
|
342
|
-
output: 'stdout',
|
|
343
|
-
parse: 'gemini-json',
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
presets: {
|
|
347
|
-
'opus-duo': ['opus', 'opus'],
|
|
348
|
-
'opus-gpt': ['opus', 'gpt-5.5'],
|
|
349
|
-
'gpt-duo': ['gpt-5.5', 'gpt-5.5'],
|
|
350
|
-
'frontier-trio': ['opus', 'gpt-5.5', 'gemini'],
|
|
351
|
-
},
|
|
352
|
-
// Per-preset judge/synth model overrides. A preset listed here runs its
|
|
353
|
-
// judge + synthesizer on the named model instead of the global default
|
|
354
|
-
// below; this is what lets gpt-duo run end-to-end on Codex alone (no
|
|
355
|
-
// claude). Presets NOT listed use judgeModel/synthModel. An explicit
|
|
356
|
-
// --judge/--synth flag (state.judgeModel/synthModel) overrides both.
|
|
357
|
-
presetStages: {
|
|
358
|
-
'gpt-duo': { judge: 'gpt-5.5', synth: 'gpt-5.5' },
|
|
359
|
-
},
|
|
360
|
-
judgeModel: 'opus',
|
|
361
|
-
synthModel: 'opus',
|
|
362
|
-
concurrency: 4,
|
|
363
|
-
timeoutMs: 180000,
|
|
364
|
-
// tokenBudget=0 means budget abort DISABLED (opt-in).
|
|
365
|
-
// Set to a positive integer (e.g. 50000) to enable hard budget cutoff.
|
|
366
|
-
tokenBudget: 0,
|
|
367
|
-
excluded_domains: [],
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
// ---------- resolution helpers ----------
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* @param {object} state
|
|
374
|
-
* @param {typeof DEFAULTS} cfg
|
|
375
|
-
* @returns {string[]}
|
|
376
|
-
*/
|
|
377
|
-
function resolvePanel(state, cfg) {
|
|
378
|
-
if (state.preset === 'custom') {
|
|
379
|
-
const models = state.models;
|
|
380
|
-
if (!Array.isArray(models) || models.length === 0) {
|
|
381
|
-
throw new Error('resolvePanel: custom preset requires a non-empty models array');
|
|
382
|
-
}
|
|
383
|
-
if (models.length > 8) {
|
|
384
|
-
throw new Error('resolvePanel: custom preset exceeds 8-model limit');
|
|
385
|
-
}
|
|
386
|
-
const unknown = models.filter(m => !cfg.adapters[m]);
|
|
387
|
-
if (unknown.length > 0) {
|
|
388
|
-
throw new Error('resolvePanel: unknown model(s): ' + unknown.join(', '));
|
|
389
|
-
}
|
|
390
|
-
return models;
|
|
391
|
-
}
|
|
392
|
-
const resolved = cfg.presets[state.preset];
|
|
393
|
-
if (!resolved) {
|
|
394
|
-
throw new Error('resolvePanel: unknown preset: ' + state.preset);
|
|
395
|
-
}
|
|
396
|
-
return resolved;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Resolve the judge or synth model for a fusion state. Precedence:
|
|
401
|
-
* explicit flag (state.judgeModel/synthModel) -> per-preset override
|
|
402
|
-
* (cfg.presetStages) -> global default (cfg.judgeModel/synthModel).
|
|
403
|
-
* @param {'judge'|'synth'} stage
|
|
404
|
-
* @param {object} state
|
|
405
|
-
* @param {typeof DEFAULTS} cfg
|
|
406
|
-
* @returns {string}
|
|
407
|
-
*/
|
|
408
|
-
function resolveStageModel(stage, state, cfg) {
|
|
409
|
-
const explicit = stage === 'judge' ? state.judgeModel : state.synthModel;
|
|
410
|
-
if (explicit) return explicit;
|
|
411
|
-
const ps = cfg.presetStages && cfg.presetStages[state.preset];
|
|
412
|
-
if (ps && ps[stage]) return ps[stage];
|
|
413
|
-
return stage === 'judge' ? cfg.judgeModel : cfg.synthModel;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** @param {object} state @param {typeof DEFAULTS} cfg @returns {string} */
|
|
417
|
-
function resolveJudgeModel(state, cfg) {
|
|
418
|
-
return resolveStageModel('judge', state, cfg);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/** @param {object} state @param {typeof DEFAULTS} cfg @returns {string} */
|
|
422
|
-
function resolveSynthModel(state, cfg) {
|
|
423
|
-
return resolveStageModel('synth', state, cfg);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** @param {string} m @returns {boolean} */
|
|
427
|
-
function validateMode(m) {
|
|
428
|
-
return ['off', 'single', 'fusion'].includes(m);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* @param {string} p
|
|
433
|
-
* @param {typeof DEFAULTS} [cfg]
|
|
434
|
-
* @returns {boolean}
|
|
435
|
-
*/
|
|
436
|
-
function validatePreset(p, cfg) {
|
|
437
|
-
const c = cfg || DEFAULTS;
|
|
438
|
-
return p === 'custom' || Object.prototype.hasOwnProperty.call(c.presets, p);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* @param {string} m
|
|
443
|
-
* @param {typeof DEFAULTS} [cfg]
|
|
444
|
-
* @returns {boolean}
|
|
445
|
-
*/
|
|
446
|
-
function validateModel(m, cfg) {
|
|
447
|
-
const c = cfg || DEFAULTS;
|
|
448
|
-
return Object.prototype.hasOwnProperty.call(c.adapters, m);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
module.exports = {
|
|
452
|
-
DEFAULTS,
|
|
453
|
-
configDir,
|
|
454
|
-
sanitizeScope,
|
|
455
|
-
workspaceHash,
|
|
456
|
-
resolveScope,
|
|
457
|
-
statePath,
|
|
458
|
-
legacyStatePath,
|
|
459
|
-
loadState,
|
|
460
|
-
saveState,
|
|
461
|
-
adoptLegacyState,
|
|
462
|
-
resolvePanel,
|
|
463
|
-
resolveJudgeModel,
|
|
464
|
-
resolveSynthModel,
|
|
465
|
-
validateMode,
|
|
466
|
-
validatePreset,
|
|
467
|
-
validateModel,
|
|
468
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — config defaults + state persistence.
|
|
3
|
+
// Zero deps, CJS. configDir + safeWriteFlag patterns ported from
|
|
4
|
+
// hooks/maestro-terse-mode.cjs. tokenBudget=0 means budget abort
|
|
5
|
+
// DISABLED (opt-in feature; set to a positive integer to enable).
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// ---------- configDir (ported from maestro-terse-mode.cjs) ----------
|
|
15
|
+
|
|
16
|
+
function configDir() {
|
|
17
|
+
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'maestro');
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'maestro');
|
|
20
|
+
}
|
|
21
|
+
return path.join(os.homedir(), '.config', 'maestro');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------- arg helper (used by resolveScope) ----------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string[]} argv
|
|
28
|
+
* @param {string} flag
|
|
29
|
+
* @returns {string|null}
|
|
30
|
+
*/
|
|
31
|
+
function getFlag(argv, flag) {
|
|
32
|
+
const i = argv.indexOf(flag);
|
|
33
|
+
return i !== -1 && i + 1 < argv.length ? argv[i + 1] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------- workspace hash ----------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive a stable 8-hex workspace identifier from cwd by walking up to the
|
|
40
|
+
* nearest .git root, normalizing the path, and hashing it.
|
|
41
|
+
* On win32 the path is lowercased (filesystem is case-insensitive).
|
|
42
|
+
* @param {string} cwd
|
|
43
|
+
* @returns {string} 8 hex characters
|
|
44
|
+
*/
|
|
45
|
+
function workspaceHash(cwd) {
|
|
46
|
+
let normalized = path.resolve(cwd);
|
|
47
|
+
let last = null;
|
|
48
|
+
while (normalized !== last && !fs.existsSync(path.join(normalized, '.git'))) {
|
|
49
|
+
last = normalized;
|
|
50
|
+
normalized = path.dirname(normalized);
|
|
51
|
+
}
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
normalized = normalized.replace(/\\/g, '/').toLowerCase().replace(/\/+$/g, '');
|
|
54
|
+
} else {
|
|
55
|
+
normalized = normalized.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
56
|
+
}
|
|
57
|
+
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------- scope helpers ----------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sanitize a raw scope value: lowercase, strip non-[a-z0-9-] chars entirely,
|
|
64
|
+
* then return 'default' if the result is empty.
|
|
65
|
+
* Examples: 'Foo' -> 'foo', 'a b!c' -> 'abc', '' -> 'default'.
|
|
66
|
+
* @param {*} v
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function sanitizeScope(v) {
|
|
70
|
+
const s = String(v).toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
71
|
+
return s.length > 0 ? s : 'default';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function workspaceCwd(opts) {
|
|
75
|
+
return (opts && opts.cwd) || process.env.CLAUDE_PROJECT_DIR || process.env.CODEX_PROJECT_DIR || process.cwd();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveScopeAlias(scope, opts) {
|
|
79
|
+
const clean = sanitizeScope(scope);
|
|
80
|
+
if (['codex-project', 'codex-workspace', 'codex-repo'].includes(clean)) {
|
|
81
|
+
return 'codex-' + workspaceHash(workspaceCwd(opts));
|
|
82
|
+
}
|
|
83
|
+
if (['claude-project', 'claude-workspace', 'cc-project', 'cc-workspace'].includes(clean)) {
|
|
84
|
+
return 'cc-' + workspaceHash(workspaceCwd(opts));
|
|
85
|
+
}
|
|
86
|
+
return clean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the active scope from argv + environment. Precedence:
|
|
91
|
+
* 1. --scope <value> flag in argv
|
|
92
|
+
* 2. process.env.MAESTRO_SCOPE
|
|
93
|
+
* 3. Autodetect: PLUGIN_ROOT truthy -> 'codex-<8hex>'
|
|
94
|
+
* 4. Autodetect: CLAUDE_PLUGIN_ROOT || CLAUDECODE truthy -> 'cc-<8hex>'
|
|
95
|
+
* 5. Autodetect: PLUGIN_DATA truthy -> 'codex-<8hex>'
|
|
96
|
+
* where <8hex> is derived from the workspace root (opts.cwd,
|
|
97
|
+
* CLAUDE_PROJECT_DIR, CODEX_PROJECT_DIR, or process.cwd()) via
|
|
98
|
+
* workspaceHash().
|
|
99
|
+
* 6. 'default'
|
|
100
|
+
* The chosen value for steps 1-2 is sanitized, and project aliases such as
|
|
101
|
+
* codex-project/codex-workspace are expanded to per-workspace scopes.
|
|
102
|
+
* @param {string[]} argv
|
|
103
|
+
* @param {{ cwd?: string }} [opts]
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function resolveScope(argv, opts) {
|
|
107
|
+
const flagVal = getFlag(argv, '--scope');
|
|
108
|
+
if (flagVal !== null) return resolveScopeAlias(flagVal, opts);
|
|
109
|
+
if (process.env.MAESTRO_SCOPE) return resolveScopeAlias(process.env.MAESTRO_SCOPE, opts);
|
|
110
|
+
if (process.env.PLUGIN_ROOT) {
|
|
111
|
+
return 'codex-' + workspaceHash(workspaceCwd(opts));
|
|
112
|
+
}
|
|
113
|
+
if (process.env.CLAUDE_PLUGIN_ROOT || process.env.CLAUDECODE) {
|
|
114
|
+
return 'cc-' + workspaceHash(workspaceCwd(opts));
|
|
115
|
+
}
|
|
116
|
+
if (process.env.PLUGIN_DATA) {
|
|
117
|
+
return 'codex-' + workspaceHash(workspaceCwd(opts));
|
|
118
|
+
}
|
|
119
|
+
return 'default';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------- path helpers ----------
|
|
123
|
+
|
|
124
|
+
/** Pre-scope global state file; migration source only, never written/deleted. */
|
|
125
|
+
function legacyStatePath() {
|
|
126
|
+
return path.join(configDir(), 'frontier-state.json');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Scope-aware state path.
|
|
131
|
+
* scope === 'default' => frontier-state.json (legacy-compatible, no suffix).
|
|
132
|
+
* Any other scope => frontier-state.<scope>.json.
|
|
133
|
+
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
function statePath(scope) {
|
|
137
|
+
if (scope === undefined) scope = resolveScope([]);
|
|
138
|
+
else scope = resolveScopeAlias(scope);
|
|
139
|
+
if (scope === 'default') return path.join(configDir(), 'frontier-state.json');
|
|
140
|
+
return path.join(configDir(), 'frontier-state.' + scope + '.json');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------- state I/O ----------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Read and validate a state file. Returns:
|
|
147
|
+
* - null if the file is absent (ENOENT) — caller decides the fallback.
|
|
148
|
+
* - {mode:'off'} on symlink, corrupt JSON, or invalid mode.
|
|
149
|
+
* - parsed object on success.
|
|
150
|
+
* @param {string} p
|
|
151
|
+
* @returns {object|null}
|
|
152
|
+
*/
|
|
153
|
+
function _readStateFile(p) {
|
|
154
|
+
let st;
|
|
155
|
+
try { st = fs.lstatSync(p); } catch { return null; }
|
|
156
|
+
if (st.isSymbolicLink()) return { mode: 'off' };
|
|
157
|
+
try {
|
|
158
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
159
|
+
const parsed = JSON.parse(raw);
|
|
160
|
+
if (!parsed || typeof parsed !== 'object') return { mode: 'off' };
|
|
161
|
+
if (!['off', 'single', 'fusion'].includes(parsed.mode)) return { mode: 'off' };
|
|
162
|
+
return parsed;
|
|
163
|
+
} catch {
|
|
164
|
+
return { mode: 'off' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Strict state read for explicit adoption paths. Unlike _readStateFile, this
|
|
170
|
+
* distinguishes invalid legacy content from a valid {mode:'off'} state.
|
|
171
|
+
* @param {string} p
|
|
172
|
+
* @returns {{ ok: true, state: object }|{ ok: false, reason: 'missing'|'invalid' }}
|
|
173
|
+
*/
|
|
174
|
+
function _readValidatedStateFile(p) {
|
|
175
|
+
let st;
|
|
176
|
+
try { st = fs.lstatSync(p); } catch { return { ok: false, reason: 'missing' }; }
|
|
177
|
+
if (st.isSymbolicLink()) return { ok: false, reason: 'invalid' };
|
|
178
|
+
try {
|
|
179
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
if (!parsed || typeof parsed !== 'object') return { ok: false, reason: 'invalid' };
|
|
182
|
+
if (!['off', 'single', 'fusion'].includes(parsed.mode)) return { ok: false, reason: 'invalid' };
|
|
183
|
+
return { ok: true, state: parsed };
|
|
184
|
+
} catch {
|
|
185
|
+
return { ok: false, reason: 'invalid' };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Load frontier state for the given scope.
|
|
191
|
+
* D3 MIGRATION: if scope !== 'default' and scope does NOT match /^(cc|codex)-/
|
|
192
|
+
* and the scoped file does NOT exist AND the legacy frontier-state.json
|
|
193
|
+
* DOES exist, seed from the legacy file (read-only — never write during
|
|
194
|
+
* load). Same symlink/parse guards apply. cc-* and codex-* scopes are
|
|
195
|
+
* excluded from migration because they are per-workspace and must not inherit
|
|
196
|
+
* global legacy state. Named scopes (e.g. 'codex', 'cursor') still migrate.
|
|
197
|
+
* Falls back to {mode:'off'} on any failure.
|
|
198
|
+
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
199
|
+
* @returns {object}
|
|
200
|
+
*/
|
|
201
|
+
function loadState(scope) {
|
|
202
|
+
if (scope === undefined) scope = resolveScope([]);
|
|
203
|
+
else scope = resolveScopeAlias(scope);
|
|
204
|
+
try {
|
|
205
|
+
const p = statePath(scope);
|
|
206
|
+
const result = _readStateFile(p);
|
|
207
|
+
if (result !== null) return result;
|
|
208
|
+
|
|
209
|
+
// File absent. For non-default, non-workspace scopes attempt migration from legacy file.
|
|
210
|
+
if (scope !== 'default' && !/^(cc|codex)-/.test(scope)) {
|
|
211
|
+
const legacyResult = _readStateFile(legacyStatePath());
|
|
212
|
+
if (legacyResult !== null) return legacyResult;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { mode: 'off' };
|
|
216
|
+
} catch {
|
|
217
|
+
return { mode: 'off' };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Atomic temp+rename write, 0600, symlink-refusing.
|
|
223
|
+
* Ported from safeWriteFlag in hooks/maestro-terse-mode.cjs.
|
|
224
|
+
* @param {object} state
|
|
225
|
+
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
function saveState(state, scope) {
|
|
229
|
+
if (scope === undefined) scope = resolveScope([]);
|
|
230
|
+
else scope = resolveScopeAlias(scope);
|
|
231
|
+
try {
|
|
232
|
+
const p = statePath(scope);
|
|
233
|
+
const dir = path.dirname(p);
|
|
234
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
235
|
+
try { if (fs.lstatSync(dir).isSymbolicLink()) return false; } catch { return false; }
|
|
236
|
+
try {
|
|
237
|
+
if (fs.lstatSync(p).isSymbolicLink()) return false;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
if (e.code !== 'ENOENT') return false;
|
|
240
|
+
}
|
|
241
|
+
const tempPath = path.join(dir, `.frontier-state.${process.pid}.${Date.now()}.tmp`);
|
|
242
|
+
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
243
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
|
|
244
|
+
let fd;
|
|
245
|
+
try {
|
|
246
|
+
if (O_NOFOLLOW === 0) {
|
|
247
|
+
try { if (fs.lstatSync(tempPath).isSymbolicLink()) return false; } catch {}
|
|
248
|
+
}
|
|
249
|
+
fd = fs.openSync(tempPath, flags, 0o600);
|
|
250
|
+
fs.writeSync(fd, JSON.stringify(state));
|
|
251
|
+
try { fs.fchmodSync(fd, 0o600); } catch {}
|
|
252
|
+
} finally {
|
|
253
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
254
|
+
}
|
|
255
|
+
fs.renameSync(tempPath, p);
|
|
256
|
+
return true;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Explicitly adopt the validated legacy frontier-state.json into a cc-* scope.
|
|
264
|
+
* This copies legacy content into frontier-state.<cc-scope>.json only, never
|
|
265
|
+
* deletes legacy, and refuses to overwrite an existing scoped file unless
|
|
266
|
+
* opts.force is true.
|
|
267
|
+
* @param {string} [scope] Omit to autodetect the runtime scope via resolveScope([]).
|
|
268
|
+
* @param {{ force?: boolean }} [opts]
|
|
269
|
+
* @returns {{ ok: true, scope: string, path: string }|{ ok: false, reason: string, scope: string, path?: string }}
|
|
270
|
+
*/
|
|
271
|
+
function adoptLegacyState(scope, opts) {
|
|
272
|
+
if (scope === undefined) scope = resolveScope([]);
|
|
273
|
+
const targetScope = sanitizeScope(scope);
|
|
274
|
+
const targetPath = statePath(targetScope);
|
|
275
|
+
if (!/^cc-/.test(targetScope)) {
|
|
276
|
+
return { ok: false, reason: 'not-cc-scope', scope: targetScope, path: targetPath };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const legacy = _readValidatedStateFile(legacyStatePath());
|
|
280
|
+
if (!legacy.ok) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
reason: legacy.reason === 'missing' ? 'missing-legacy' : 'invalid-legacy',
|
|
284
|
+
scope: targetScope,
|
|
285
|
+
path: targetPath,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const st = fs.lstatSync(targetPath);
|
|
291
|
+
if (st.isSymbolicLink()) return { ok: false, reason: 'unsafe-target', scope: targetScope, path: targetPath };
|
|
292
|
+
if (!(opts && opts.force)) return { ok: false, reason: 'exists', scope: targetScope, path: targetPath };
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (e.code !== 'ENOENT') return { ok: false, reason: 'unsafe-target', scope: targetScope, path: targetPath };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!saveState(legacy.state, targetScope)) {
|
|
298
|
+
return { ok: false, reason: 'write-failed', scope: targetScope, path: targetPath };
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, scope: targetScope, path: targetPath };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------- DEFAULTS ----------
|
|
304
|
+
|
|
305
|
+
const DEFAULTS = {
|
|
306
|
+
adapters: {
|
|
307
|
+
opus: {
|
|
308
|
+
model: 'opus',
|
|
309
|
+
bin: process.env.MAESTRO_CLAUDE_BIN || 'claude',
|
|
310
|
+
baseArgs: ['-p', '--output-format', 'json', '--dangerously-skip-permissions'],
|
|
311
|
+
promptVia: 'stdin',
|
|
312
|
+
webTools: false,
|
|
313
|
+
output: 'stdout',
|
|
314
|
+
parse: 'claude-json',
|
|
315
|
+
},
|
|
316
|
+
'gpt-5.5': {
|
|
317
|
+
model: 'gpt-5.5',
|
|
318
|
+
bin: process.env.MAESTRO_CODEX_BIN || 'codex',
|
|
319
|
+
baseArgs: [
|
|
320
|
+
'exec',
|
|
321
|
+
'--skip-git-repo-check',
|
|
322
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
323
|
+
'-m', 'gpt-5.5',
|
|
324
|
+
'--color', 'never',
|
|
325
|
+
],
|
|
326
|
+
promptVia: 'stdin',
|
|
327
|
+
webTools: true,
|
|
328
|
+
output: 'last-message-file',
|
|
329
|
+
parse: 'text',
|
|
330
|
+
},
|
|
331
|
+
gemini: {
|
|
332
|
+
model: 'gemini',
|
|
333
|
+
bin: process.env.MAESTRO_GEMINI_BIN || 'gemini',
|
|
334
|
+
baseArgs: [
|
|
335
|
+
'--output-format', 'json',
|
|
336
|
+
'--approval-mode', 'yolo',
|
|
337
|
+
'--model', 'gemini-3.1-pro-preview',
|
|
338
|
+
],
|
|
339
|
+
promptVia: 'arg',
|
|
340
|
+
promptFlag: '-p',
|
|
341
|
+
webTools: false,
|
|
342
|
+
output: 'stdout',
|
|
343
|
+
parse: 'gemini-json',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
presets: {
|
|
347
|
+
'opus-duo': ['opus', 'opus'],
|
|
348
|
+
'opus-gpt': ['opus', 'gpt-5.5'],
|
|
349
|
+
'gpt-duo': ['gpt-5.5', 'gpt-5.5'],
|
|
350
|
+
'frontier-trio': ['opus', 'gpt-5.5', 'gemini'],
|
|
351
|
+
},
|
|
352
|
+
// Per-preset judge/synth model overrides. A preset listed here runs its
|
|
353
|
+
// judge + synthesizer on the named model instead of the global default
|
|
354
|
+
// below; this is what lets gpt-duo run end-to-end on Codex alone (no
|
|
355
|
+
// claude). Presets NOT listed use judgeModel/synthModel. An explicit
|
|
356
|
+
// --judge/--synth flag (state.judgeModel/synthModel) overrides both.
|
|
357
|
+
presetStages: {
|
|
358
|
+
'gpt-duo': { judge: 'gpt-5.5', synth: 'gpt-5.5' },
|
|
359
|
+
},
|
|
360
|
+
judgeModel: 'opus',
|
|
361
|
+
synthModel: 'opus',
|
|
362
|
+
concurrency: 4,
|
|
363
|
+
timeoutMs: 180000,
|
|
364
|
+
// tokenBudget=0 means budget abort DISABLED (opt-in).
|
|
365
|
+
// Set to a positive integer (e.g. 50000) to enable hard budget cutoff.
|
|
366
|
+
tokenBudget: 0,
|
|
367
|
+
excluded_domains: [],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// ---------- resolution helpers ----------
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {object} state
|
|
374
|
+
* @param {typeof DEFAULTS} cfg
|
|
375
|
+
* @returns {string[]}
|
|
376
|
+
*/
|
|
377
|
+
function resolvePanel(state, cfg) {
|
|
378
|
+
if (state.preset === 'custom') {
|
|
379
|
+
const models = state.models;
|
|
380
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
381
|
+
throw new Error('resolvePanel: custom preset requires a non-empty models array');
|
|
382
|
+
}
|
|
383
|
+
if (models.length > 8) {
|
|
384
|
+
throw new Error('resolvePanel: custom preset exceeds 8-model limit');
|
|
385
|
+
}
|
|
386
|
+
const unknown = models.filter(m => !cfg.adapters[m]);
|
|
387
|
+
if (unknown.length > 0) {
|
|
388
|
+
throw new Error('resolvePanel: unknown model(s): ' + unknown.join(', '));
|
|
389
|
+
}
|
|
390
|
+
return models;
|
|
391
|
+
}
|
|
392
|
+
const resolved = cfg.presets[state.preset];
|
|
393
|
+
if (!resolved) {
|
|
394
|
+
throw new Error('resolvePanel: unknown preset: ' + state.preset);
|
|
395
|
+
}
|
|
396
|
+
return resolved;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Resolve the judge or synth model for a fusion state. Precedence:
|
|
401
|
+
* explicit flag (state.judgeModel/synthModel) -> per-preset override
|
|
402
|
+
* (cfg.presetStages) -> global default (cfg.judgeModel/synthModel).
|
|
403
|
+
* @param {'judge'|'synth'} stage
|
|
404
|
+
* @param {object} state
|
|
405
|
+
* @param {typeof DEFAULTS} cfg
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
function resolveStageModel(stage, state, cfg) {
|
|
409
|
+
const explicit = stage === 'judge' ? state.judgeModel : state.synthModel;
|
|
410
|
+
if (explicit) return explicit;
|
|
411
|
+
const ps = cfg.presetStages && cfg.presetStages[state.preset];
|
|
412
|
+
if (ps && ps[stage]) return ps[stage];
|
|
413
|
+
return stage === 'judge' ? cfg.judgeModel : cfg.synthModel;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** @param {object} state @param {typeof DEFAULTS} cfg @returns {string} */
|
|
417
|
+
function resolveJudgeModel(state, cfg) {
|
|
418
|
+
return resolveStageModel('judge', state, cfg);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** @param {object} state @param {typeof DEFAULTS} cfg @returns {string} */
|
|
422
|
+
function resolveSynthModel(state, cfg) {
|
|
423
|
+
return resolveStageModel('synth', state, cfg);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** @param {string} m @returns {boolean} */
|
|
427
|
+
function validateMode(m) {
|
|
428
|
+
return ['off', 'single', 'fusion'].includes(m);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @param {string} p
|
|
433
|
+
* @param {typeof DEFAULTS} [cfg]
|
|
434
|
+
* @returns {boolean}
|
|
435
|
+
*/
|
|
436
|
+
function validatePreset(p, cfg) {
|
|
437
|
+
const c = cfg || DEFAULTS;
|
|
438
|
+
return p === 'custom' || Object.prototype.hasOwnProperty.call(c.presets, p);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @param {string} m
|
|
443
|
+
* @param {typeof DEFAULTS} [cfg]
|
|
444
|
+
* @returns {boolean}
|
|
445
|
+
*/
|
|
446
|
+
function validateModel(m, cfg) {
|
|
447
|
+
const c = cfg || DEFAULTS;
|
|
448
|
+
return Object.prototype.hasOwnProperty.call(c.adapters, m);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = {
|
|
452
|
+
DEFAULTS,
|
|
453
|
+
configDir,
|
|
454
|
+
sanitizeScope,
|
|
455
|
+
workspaceHash,
|
|
456
|
+
resolveScope,
|
|
457
|
+
statePath,
|
|
458
|
+
legacyStatePath,
|
|
459
|
+
loadState,
|
|
460
|
+
saveState,
|
|
461
|
+
adoptLegacyState,
|
|
462
|
+
resolvePanel,
|
|
463
|
+
resolveJudgeModel,
|
|
464
|
+
resolveSynthModel,
|
|
465
|
+
validateMode,
|
|
466
|
+
validatePreset,
|
|
467
|
+
validateModel,
|
|
468
|
+
};
|