@maestrofrontier/frontier 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +214 -0
- package/CLAUDE.md +29 -0
- package/LICENSE +21 -0
- package/README.md +521 -0
- package/bin/maestro.cjs +75 -0
- package/commands/compress.md +36 -0
- package/commands/context-bar.md +30 -0
- package/commands/frontier.md +124 -0
- package/commands/settings.md +101 -0
- package/commands/terse.md +23 -0
- package/commands/update.md +59 -0
- package/docs/orchestration.md +168 -0
- package/frontier/cli.cjs +248 -0
- package/frontier/config.cjs +441 -0
- package/frontier/dispatch.cjs +255 -0
- package/frontier/judge.cjs +92 -0
- package/frontier/run.cjs +148 -0
- package/frontier/schema.cjs +112 -0
- package/frontier/semaphore.cjs +49 -0
- package/frontier/synthesize.cjs +79 -0
- package/hooks/frontier-autorun.cjs +124 -0
- package/hooks/hooks.json +103 -0
- package/hooks/maestro-doctrine-guard.cjs +81 -0
- package/hooks/maestro-gate-reminder.cjs +58 -0
- package/hooks/maestro-gate-telemetry.cjs +77 -0
- package/hooks/maestro-loop-guard.cjs +76 -0
- package/hooks/maestro-phase-scope.cjs +118 -0
- package/hooks/maestro-statusline-sync.cjs +152 -0
- package/hooks/maestro-subagent-guard.cjs +148 -0
- package/hooks/maestro-terse-mode.cjs +189 -0
- package/hooks/maestro-toolbudget-advisory.cjs +127 -0
- package/integrations/README.md +87 -0
- package/integrations/cline/skills/frontier/SKILL.md +75 -0
- package/integrations/codex/prompts/frontier.md +66 -0
- package/integrations/codex/prompts/update.md +36 -0
- package/integrations/cursor/commands/frontier.md +63 -0
- package/integrations/cursor/commands/update.md +34 -0
- package/integrations/gemini/commands/frontier.toml +76 -0
- package/integrations/windsurf/workflows/frontier.md +70 -0
- package/package.json +52 -0
- package/scripts/install.cjs +490 -0
- package/settings/cli.cjs +140 -0
- package/settings/config.cjs +309 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — CLI-spawn + parallel-fanout for panel adapters.
|
|
3
|
+
// spawnOne: one adapter invocation -> PanelResponse (never rejects).
|
|
4
|
+
// fanOut: bounded-concurrent map over modelIds -> PanelResponse[].
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const { stripLlmWrapper } = require('./schema.cjs');
|
|
14
|
+
const { mapLimit } = require('./semaphore.cjs');
|
|
15
|
+
|
|
16
|
+
const MAX_BUF = 2 * 1024 * 1024; // 2 MB cap per stream
|
|
17
|
+
|
|
18
|
+
/** @param {string} p @returns {string} */
|
|
19
|
+
function safeReadFile(p) {
|
|
20
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Residual cmd.exe hazards for a prompt passed as an ARGUMENT on win32.
|
|
24
|
+
// Win32 npm shims (codex/gemini, extensionless) cannot be spawned with
|
|
25
|
+
// shell:false (Node v18.20+/20.12+/24 throw EINVAL on .cmd/.bat), and
|
|
26
|
+
// shell:true concatenates args unescaped (DEP0190). So we wrap them in an
|
|
27
|
+
// explicit `cmd.exe /d /s /c <shim> ...` with shell:false: Node then applies
|
|
28
|
+
// its standard argv quoting to every element, so spaces and &|<>() inside the
|
|
29
|
+
// quoted prompt are literal to cmd and there is no unescaped concat. What
|
|
30
|
+
// Node's C-runtime quoting does NOT reconcile with cmd.exe is the double quote
|
|
31
|
+
// and percent (cmd still expands %VAR% on the /c line). stdin adapters
|
|
32
|
+
// (claude, codex) are unaffected; only the promptVia:'arg' shim path (gemini)
|
|
33
|
+
// is, so we refuse a prompt bearing those residual chars there. Plain prose —
|
|
34
|
+
// spaces and most punctuation included — passes.
|
|
35
|
+
const CMD_UNSAFE_RE = new RegExp('["%\\r\\n]');
|
|
36
|
+
function unsafeForShellArg(s) {
|
|
37
|
+
return CMD_UNSAFE_RE.test(String(s));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Spawn one adapter process and return a PanelResponse (never rejects).
|
|
42
|
+
* @param {string} prompt
|
|
43
|
+
* @param {object} adapter — DEFAULTS.adapters[id] shape
|
|
44
|
+
* @param {{ timeoutMs?: number, fusionDepth?: number }} [opts]
|
|
45
|
+
* @returns {Promise<import('./schema.cjs').PanelResponse>}
|
|
46
|
+
*/
|
|
47
|
+
function spawnOne(prompt, adapter, opts) {
|
|
48
|
+
const timeoutMs = (opts && opts.timeoutMs != null) ? opts.timeoutMs : 180000;
|
|
49
|
+
const fusionDepth = (opts && opts.fusionDepth != null) ? opts.fusionDepth : 1;
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
|
|
54
|
+
// Guard: missing/invalid adapter
|
|
55
|
+
if (!adapter || typeof adapter !== 'object') {
|
|
56
|
+
return resolve({
|
|
57
|
+
model: '', content: '', ok: false, durationMs: 0, tokensEst: 0,
|
|
58
|
+
error: 'spawn error: adapter is undefined',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- build invocation ----
|
|
63
|
+
// Node scripts (test stubs) run via process.execPath; win32 npm shims go
|
|
64
|
+
// through an explicit cmd.exe wrapper (see CMD_UNSAFE_RE note); everything
|
|
65
|
+
// else (claude.exe, POSIX bins) spawns directly. shell is always false.
|
|
66
|
+
const isNodeScript = /\.[cm]?js$/i.test(adapter.bin);
|
|
67
|
+
const isWinShim = !isNodeScript && process.platform === 'win32' && !path.extname(adapter.bin);
|
|
68
|
+
|
|
69
|
+
// program args: base flags (+ optional output file) (+ optional prompt arg)
|
|
70
|
+
const progArgs = [...(adapter.baseArgs || [])];
|
|
71
|
+
|
|
72
|
+
// last-message-file: unique tmp path
|
|
73
|
+
let tmpPath = null;
|
|
74
|
+
if (adapter.output === 'last-message-file') {
|
|
75
|
+
tmpPath = path.join(
|
|
76
|
+
os.tmpdir(),
|
|
77
|
+
'frontier-' + process.pid + '-' + Date.now() + '-' +
|
|
78
|
+
Math.random().toString(36).slice(2) + '.txt'
|
|
79
|
+
);
|
|
80
|
+
progArgs.push('--output-last-message', tmpPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// prompt delivery via arg (stdin path is written after spawn)
|
|
84
|
+
if (adapter.promptVia === 'arg') {
|
|
85
|
+
if (isWinShim && unsafeForShellArg(prompt)) {
|
|
86
|
+
return resolve({
|
|
87
|
+
model: adapter.model, content: '', ok: false,
|
|
88
|
+
durationMs: Date.now() - start, tokensEst: 0,
|
|
89
|
+
error: 'unsafe prompt for win32 arg-path adapter (contains a quote, ' +
|
|
90
|
+
'percent, or newline); use a stdin-capable model or remove those characters',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
progArgs.push(adapter.promptFlag || '-p', prompt);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let bin, spawnArgs;
|
|
97
|
+
if (isNodeScript) {
|
|
98
|
+
bin = process.execPath;
|
|
99
|
+
spawnArgs = [adapter.bin, ...progArgs];
|
|
100
|
+
} else if (isWinShim) {
|
|
101
|
+
bin = process.env.ComSpec || 'cmd.exe';
|
|
102
|
+
spawnArgs = ['/d', '/s', '/c', adapter.bin, ...progArgs];
|
|
103
|
+
} else {
|
|
104
|
+
bin = adapter.bin;
|
|
105
|
+
spawnArgs = progArgs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const env = { ...process.env, FUSION_DEPTH: String(fusionDepth), ...(adapter.env || {}) };
|
|
109
|
+
|
|
110
|
+
let child;
|
|
111
|
+
try {
|
|
112
|
+
child = spawn(bin, spawnArgs, { shell: false, env, windowsHide: true });
|
|
113
|
+
} catch (spawnErr) {
|
|
114
|
+
return resolve({
|
|
115
|
+
model: adapter.model, content: '', ok: false,
|
|
116
|
+
durationMs: Date.now() - start, tokensEst: 0,
|
|
117
|
+
error: 'spawn error: ' + spawnErr.message,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// stdin delivery
|
|
122
|
+
if (adapter.promptVia !== 'arg') {
|
|
123
|
+
child.stdin.on('error', () => {});
|
|
124
|
+
try { child.stdin.write(prompt); child.stdin.end(); } catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let stdoutBuf = '';
|
|
128
|
+
let stderrBuf = '';
|
|
129
|
+
|
|
130
|
+
child.stdout.setEncoding('utf8');
|
|
131
|
+
child.stdout.on('data', (chunk) => {
|
|
132
|
+
stdoutBuf += chunk;
|
|
133
|
+
if (stdoutBuf.length > MAX_BUF) stdoutBuf = stdoutBuf.slice(-MAX_BUF);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
child.stderr.setEncoding('utf8');
|
|
137
|
+
child.stderr.on('data', (chunk) => {
|
|
138
|
+
stderrBuf += chunk;
|
|
139
|
+
if (stderrBuf.length > MAX_BUF) stderrBuf = stderrBuf.slice(-MAX_BUF);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// timeout
|
|
143
|
+
let timedOut = false;
|
|
144
|
+
let killTimer = null;
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
timedOut = true;
|
|
147
|
+
child.kill('SIGTERM');
|
|
148
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), 2000);
|
|
149
|
+
}, timeoutMs);
|
|
150
|
+
|
|
151
|
+
child.on('error', (err) => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
if (killTimer) clearTimeout(killTimer);
|
|
154
|
+
resolve({
|
|
155
|
+
model: adapter.model, content: '', ok: false,
|
|
156
|
+
durationMs: Date.now() - start, tokensEst: 0,
|
|
157
|
+
error: 'spawn error: ' + err.message,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
child.on('close', (code, signal) => {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
if (killTimer) clearTimeout(killTimer);
|
|
164
|
+
|
|
165
|
+
// ---- parse stdout -> content (read tmp file BEFORE cleanup) ----
|
|
166
|
+
let content = '';
|
|
167
|
+
let parseError = null;
|
|
168
|
+
|
|
169
|
+
if (adapter.parse === 'claude-json') {
|
|
170
|
+
let parsed;
|
|
171
|
+
try { parsed = JSON.parse(stdoutBuf); } catch {
|
|
172
|
+
parseError = 'claude json parse fail';
|
|
173
|
+
}
|
|
174
|
+
if (!parseError) {
|
|
175
|
+
if (parsed && parsed.is_error) {
|
|
176
|
+
parseError = String(parsed.result || 'is_error');
|
|
177
|
+
} else {
|
|
178
|
+
content = stripLlmWrapper(String((parsed && parsed.result) || '').trim());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} else if (adapter.parse === 'gemini-json') {
|
|
182
|
+
let parsed;
|
|
183
|
+
try { parsed = JSON.parse(stdoutBuf); } catch {
|
|
184
|
+
parseError = 'gemini json parse fail';
|
|
185
|
+
}
|
|
186
|
+
if (!parseError) {
|
|
187
|
+
content = stripLlmWrapper(String((parsed && parsed.response) || '').trim());
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// 'text': prefer last-message-file content, fall back to stdout
|
|
191
|
+
const raw = (adapter.output === 'last-message-file' ? safeReadFile(tmpPath) : '') || stdoutBuf;
|
|
192
|
+
content = stripLlmWrapper(String(raw).trim());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// tmp-file cleanup (after read)
|
|
196
|
+
if (tmpPath) {
|
|
197
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const ok = (code === 0) && !timedOut && content.length > 0 && !parseError;
|
|
201
|
+
let error;
|
|
202
|
+
if (!ok) {
|
|
203
|
+
error = parseError ||
|
|
204
|
+
(timedOut
|
|
205
|
+
? 'timeout'
|
|
206
|
+
: ('exit ' + code + (signal ? (' ' + signal) : '') +
|
|
207
|
+
(stderrBuf ? ': ' + stderrBuf.slice(0, 500) : '')));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const durationMs = Date.now() - start;
|
|
211
|
+
const tokensEst = Math.ceil(content.length / 4);
|
|
212
|
+
resolve({
|
|
213
|
+
model: adapter.model, content, ok, durationMs, tokensEst,
|
|
214
|
+
...(ok ? {} : { error }),
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* fanOut — run spawnOne over modelIds bounded by concurrency; order preserved.
|
|
222
|
+
* @param {string} prompt
|
|
223
|
+
* @param {string[]} modelIds
|
|
224
|
+
* @param {{ adapters: object, timeoutMs: number, concurrency: number }} cfg
|
|
225
|
+
* @param {{ fusionDepth?: number, concurrency?: number }} [opts]
|
|
226
|
+
* @returns {Promise<import('./schema.cjs').PanelResponse[]>}
|
|
227
|
+
*/
|
|
228
|
+
async function fanOut(prompt, modelIds, cfg, opts) {
|
|
229
|
+
const fusionDepth = (opts && opts.fusionDepth != null) ? opts.fusionDepth : 1;
|
|
230
|
+
const concurrency = (opts && opts.concurrency != null)
|
|
231
|
+
? opts.concurrency
|
|
232
|
+
: Math.min(modelIds.length, (cfg && cfg.concurrency) || 4);
|
|
233
|
+
|
|
234
|
+
const settled = await mapLimit(modelIds, concurrency, (id) => {
|
|
235
|
+
const adapter = cfg && cfg.adapters && cfg.adapters[id];
|
|
236
|
+
// undefined adapter -> spawnOne resolves a failed PanelResponse
|
|
237
|
+
return spawnOne(prompt, adapter, {
|
|
238
|
+
timeoutMs: (cfg && cfg.timeoutMs) || 180000,
|
|
239
|
+
fusionDepth,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return settled.map((s, i) => {
|
|
244
|
+
if (s.ok) return s.value;
|
|
245
|
+
// spawnOne never rejects, but be defensive
|
|
246
|
+
const adapter = cfg && cfg.adapters && cfg.adapters[modelIds[i]];
|
|
247
|
+
return {
|
|
248
|
+
model: adapter ? adapter.model : modelIds[i],
|
|
249
|
+
content: '', ok: false, durationMs: 0, tokensEst: 0,
|
|
250
|
+
error: String(s.error),
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { spawnOne, fanOut, unsafeForShellArg };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — judge stage: build prompt + invoke Opus to produce Analysis.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { parseAnalysis } = require('./schema.cjs');
|
|
7
|
+
const dispatch = require('./dispatch.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the judge prompt for Opus.
|
|
11
|
+
* @param {string} userPrompt
|
|
12
|
+
* @param {import('./schema.cjs').PanelResponse[]} responses
|
|
13
|
+
* @param {object} cfg
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function buildJudgePrompt(userPrompt, responses, cfg) {
|
|
17
|
+
const sections = responses.map(
|
|
18
|
+
r => `### Response from ${r.model}\n${r.content}`
|
|
19
|
+
).join('\n\n');
|
|
20
|
+
|
|
21
|
+
const domainNote = (cfg.excluded_domains && cfg.excluded_domains.length > 0)
|
|
22
|
+
? `\nDisregard any claims sourced only from the following domains: ${cfg.excluded_domains.join(', ')}.\n`
|
|
23
|
+
: '';
|
|
24
|
+
|
|
25
|
+
return `You are a neutral JUDGE evaluating multiple AI panel responses to a user question.
|
|
26
|
+
|
|
27
|
+
USER QUESTION:
|
|
28
|
+
${userPrompt}
|
|
29
|
+
|
|
30
|
+
PANEL RESPONSES:
|
|
31
|
+
${sections}
|
|
32
|
+
${domainNote}
|
|
33
|
+
INSTRUCTIONS:
|
|
34
|
+
COMPARE the panel responses — do NOT merge or summarize them. Perform a structured analysis:
|
|
35
|
+
- consensus: points that all or most models agree on
|
|
36
|
+
- contradictions: topics where models disagree, with each model's specific stance
|
|
37
|
+
- partial_coverage: points that only some models covered (with which models)
|
|
38
|
+
- unique_insights: insights raised by exactly one model (with which model)
|
|
39
|
+
- blind_spots: important points that NO model addressed
|
|
40
|
+
|
|
41
|
+
OUTPUT IS ONLY a single JSON object with EXACTLY these keys:
|
|
42
|
+
consensus (string[])
|
|
43
|
+
contradictions (array of {topic: string, stances: [{model: string, stance: string}]})
|
|
44
|
+
partial_coverage (array of {models: string[], point: string})
|
|
45
|
+
unique_insights (array of {model: string, insight: string})
|
|
46
|
+
blind_spots (string[])
|
|
47
|
+
|
|
48
|
+
No prose before or after. No markdown fence. No extra keys. Output the raw JSON object only.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the judge stage. Returns Analysis or undefined on any failure (degrades gracefully).
|
|
53
|
+
* @param {string} userPrompt
|
|
54
|
+
* @param {import('./schema.cjs').PanelResponse[]} responses
|
|
55
|
+
* @param {object} cfg
|
|
56
|
+
* @param {{ spawn?: Function }} [deps]
|
|
57
|
+
* @returns {Promise<import('./schema.cjs').Analysis | undefined>}
|
|
58
|
+
*/
|
|
59
|
+
async function runJudge(userPrompt, responses, cfg, deps) {
|
|
60
|
+
const spawn = (deps && deps.spawn) || dispatch.spawnOne;
|
|
61
|
+
let r;
|
|
62
|
+
try {
|
|
63
|
+
r = await spawn(
|
|
64
|
+
buildJudgePrompt(userPrompt, responses, cfg),
|
|
65
|
+
cfg.adapters[cfg.judgeModel],
|
|
66
|
+
{ timeoutMs: cfg.timeoutMs, fusionDepth: 1 }
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!r || !r.ok || !r.content) return undefined;
|
|
73
|
+
|
|
74
|
+
// Primary parse
|
|
75
|
+
try {
|
|
76
|
+
return parseAnalysis(r.content);
|
|
77
|
+
} catch {
|
|
78
|
+
// substring recovery: find outermost { ... }
|
|
79
|
+
const first = r.content.indexOf('{');
|
|
80
|
+
const last = r.content.lastIndexOf('}');
|
|
81
|
+
if (first !== -1 && last > first) {
|
|
82
|
+
try {
|
|
83
|
+
return parseAnalysis(r.content.slice(first, last + 1));
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { buildJudgePrompt, runJudge };
|
package/frontier/run.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — mode router (off / single / fusion).
|
|
3
|
+
// runFrontier({prompt, state, cfg, deps}) -> Promise<FusionResult>.
|
|
4
|
+
//
|
|
5
|
+
// Re-grounding note (Fable T2): the engine fans a prompt to model CLIs
|
|
6
|
+
// that do NOT share this session's file/context. A member or synthesis
|
|
7
|
+
// output asserting "I can't see X" / "no such file" reflects that
|
|
8
|
+
// subprocess's blank context, not ground truth — re-ground such claims
|
|
9
|
+
// against live context before relaying them to the user (AGENTS.md S7.7).
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { DEFAULTS } = require('./config.cjs');
|
|
14
|
+
const { resolvePanel, resolveJudgeModel, resolveSynthModel } = require('./config.cjs');
|
|
15
|
+
const { classify, toFailedModel } = require('./schema.cjs');
|
|
16
|
+
const dispatch = require('./dispatch.cjs');
|
|
17
|
+
const judge = require('./judge.cjs');
|
|
18
|
+
const synthesize = require('./synthesize.cjs');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{ prompt:string, state:object, cfg?:object, deps?:object }} opts
|
|
22
|
+
* @returns {Promise<object>}
|
|
23
|
+
*/
|
|
24
|
+
async function runFrontier({ prompt, state, cfg, deps }) {
|
|
25
|
+
cfg = cfg || DEFAULTS;
|
|
26
|
+
deps = deps || {};
|
|
27
|
+
|
|
28
|
+
const spawnOne = deps.spawnOne || dispatch.spawnOne;
|
|
29
|
+
const fanOut = deps.fanOut || dispatch.fanOut;
|
|
30
|
+
const runJudge = deps.runJudge || judge.runJudge;
|
|
31
|
+
const runSynth = deps.runSynth || synthesize.runSynth;
|
|
32
|
+
|
|
33
|
+
const rawDepth = parseInt(process.env.FUSION_DEPTH || '0', 10);
|
|
34
|
+
const depth = isNaN(rawDepth) ? 0 : rawDepth;
|
|
35
|
+
|
|
36
|
+
// ---- OFF ----
|
|
37
|
+
if (state.mode === 'off') {
|
|
38
|
+
return { status: 'off', mode: 'off', final: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- RECURSION GUARD (single + fusion) ----
|
|
42
|
+
if (depth >= 1) {
|
|
43
|
+
const base = {
|
|
44
|
+
status: 'error',
|
|
45
|
+
mode: state.mode,
|
|
46
|
+
error: 'fusion depth exceeded (one-level cap)',
|
|
47
|
+
failure_reason: 'fusion_invocation_capped',
|
|
48
|
+
};
|
|
49
|
+
if (state.mode === 'fusion') return { ...base, preset: state.preset };
|
|
50
|
+
if (state.mode === 'single') return { ...base, model: state.model };
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- SINGLE ----
|
|
55
|
+
if (state.mode === 'single') {
|
|
56
|
+
const adapter = cfg.adapters && cfg.adapters[state.model];
|
|
57
|
+
if (!adapter) {
|
|
58
|
+
return {
|
|
59
|
+
status: 'error',
|
|
60
|
+
mode: 'single',
|
|
61
|
+
model: state.model,
|
|
62
|
+
error: 'unknown model: ' + state.model,
|
|
63
|
+
failure_reason: 'unexpected_error',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const resp = await spawnOne(prompt, adapter, { timeoutMs: cfg.timeoutMs, fusionDepth: depth + 1 });
|
|
67
|
+
if (!resp.ok) {
|
|
68
|
+
return {
|
|
69
|
+
status: 'error',
|
|
70
|
+
mode: 'single',
|
|
71
|
+
model: state.model,
|
|
72
|
+
error: resp.error,
|
|
73
|
+
failure_reason: classify([toFailedModel(resp)]),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { status: 'ok', mode: 'single', model: state.model, final: resp.content, response: resp };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- FUSION ----
|
|
80
|
+
if (state.mode === 'fusion') {
|
|
81
|
+
// resolve panel
|
|
82
|
+
let panelIds;
|
|
83
|
+
try {
|
|
84
|
+
panelIds = resolvePanel(state, cfg);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return {
|
|
87
|
+
status: 'error',
|
|
88
|
+
mode: 'fusion',
|
|
89
|
+
preset: state.preset,
|
|
90
|
+
error: e.message,
|
|
91
|
+
failure_reason: 'unexpected_error',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// BUDGET (opt-in)
|
|
96
|
+
if (cfg.tokenBudget && cfg.tokenBudget > 0) {
|
|
97
|
+
const projected = Math.ceil(prompt.length / 4) * panelIds.length;
|
|
98
|
+
if (projected > cfg.tokenBudget) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'error',
|
|
101
|
+
mode: 'fusion',
|
|
102
|
+
preset: state.preset,
|
|
103
|
+
error: 'projected token budget exceeded (' + projected + '>' + cfg.tokenBudget + ')',
|
|
104
|
+
failure_reason: 'unexpected_error',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const panel = await fanOut(prompt, panelIds, cfg, { fusionDepth: depth + 1 });
|
|
110
|
+
const ok = panel.filter(p => p.ok);
|
|
111
|
+
const failed = panel.filter(p => !p.ok);
|
|
112
|
+
|
|
113
|
+
if (ok.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
status: 'error',
|
|
116
|
+
mode: 'fusion',
|
|
117
|
+
preset: state.preset,
|
|
118
|
+
error: 'all panels failed',
|
|
119
|
+
failure_reason: classify(failed.map(toFailedModel)),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Resolve judge/synth model (explicit flag -> preset override -> default)
|
|
124
|
+
// and hand the judge/synth stages a cfg pinned to those models, so a
|
|
125
|
+
// preset like gpt-duo runs entirely on its own provider.
|
|
126
|
+
const stageCfg = {
|
|
127
|
+
...cfg,
|
|
128
|
+
judgeModel: resolveJudgeModel(state, cfg),
|
|
129
|
+
synthModel: resolveSynthModel(state, cfg),
|
|
130
|
+
};
|
|
131
|
+
const analysis = await runJudge(prompt, ok, stageCfg);
|
|
132
|
+
let final = await runSynth(prompt, { analysis, responses: ok }, stageCfg);
|
|
133
|
+
if (!final) {
|
|
134
|
+
// synth-fail fallback: longest ok response
|
|
135
|
+
final = ok.reduce((a, b) => b.content.length > a.content.length ? b : a).content;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = { status: 'ok', mode: 'fusion', preset: state.preset, final, responses: ok };
|
|
139
|
+
if (analysis !== undefined) result.analysis = analysis;
|
|
140
|
+
if (failed.length) result.failed_models = failed.map(toFailedModel);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- UNKNOWN MODE ----
|
|
145
|
+
return { status: 'error', mode: state.mode, error: 'unknown mode', failure_reason: 'unexpected_error' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { runFrontier };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — shared types-as-validators + helpers.
|
|
3
|
+
// Zero deps, CJS. Ported stripLlmWrapper from scripts/compress.cjs.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ model:string, content:string, ok:boolean, durationMs:number, tokensEst:number, toolCalls?:unknown[], error?:string }} PanelResponse
|
|
9
|
+
* @typedef {{ model:string, reason:string }} FailedModel
|
|
10
|
+
* @typedef {{ consensus:string[], contradictions:{topic:string,stances:{model:string,stance:string}[]}[], partial_coverage:{models:string[],point:string}[], unique_insights:{model:string,insight:string}[], blind_spots:string[] }} Analysis
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {string[]} */
|
|
14
|
+
const FAILURE_REASONS = [
|
|
15
|
+
'all_panels_failed',
|
|
16
|
+
'insufficient_credits',
|
|
17
|
+
'rate_limited',
|
|
18
|
+
'fusion_invocation_capped',
|
|
19
|
+
'unexpected_error',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {unknown} x
|
|
24
|
+
* @returns {x is PanelResponse}
|
|
25
|
+
*/
|
|
26
|
+
function isPanelResponse(x) {
|
|
27
|
+
if (x === null || typeof x !== 'object') return false;
|
|
28
|
+
const o = /** @type {Record<string,unknown>} */ (x);
|
|
29
|
+
return (
|
|
30
|
+
typeof o.model === 'string' &&
|
|
31
|
+
typeof o.content === 'string' &&
|
|
32
|
+
typeof o.ok === 'boolean' &&
|
|
33
|
+
typeof o.durationMs === 'number' &&
|
|
34
|
+
typeof o.tokensEst === 'number'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {unknown} x
|
|
40
|
+
* @returns {x is Analysis}
|
|
41
|
+
*/
|
|
42
|
+
function isAnalysis(x) {
|
|
43
|
+
if (x === null || typeof x !== 'object') return false;
|
|
44
|
+
const o = /** @type {Record<string,unknown>} */ (x);
|
|
45
|
+
if (!Array.isArray(o.consensus)) return false;
|
|
46
|
+
if (!o.consensus.every(s => typeof s === 'string')) return false;
|
|
47
|
+
if (!Array.isArray(o.blind_spots)) return false;
|
|
48
|
+
if (!o.blind_spots.every(s => typeof s === 'string')) return false;
|
|
49
|
+
if (!Array.isArray(o.contradictions)) return false;
|
|
50
|
+
if (!o.contradictions.every(e => e !== null && typeof e === 'object')) return false;
|
|
51
|
+
if (!Array.isArray(o.partial_coverage)) return false;
|
|
52
|
+
if (!o.partial_coverage.every(e => e !== null && typeof e === 'object')) return false;
|
|
53
|
+
if (!Array.isArray(o.unique_insights)) return false;
|
|
54
|
+
if (!o.unique_insights.every(e => e !== null && typeof e === 'object')) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} str
|
|
60
|
+
* @returns {Analysis}
|
|
61
|
+
* @throws {Error}
|
|
62
|
+
*/
|
|
63
|
+
function parseAnalysis(str) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(str);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
throw new Error('parseAnalysis: invalid JSON — ' + e.message);
|
|
69
|
+
}
|
|
70
|
+
if (!isAnalysis(parsed)) {
|
|
71
|
+
throw new Error('parseAnalysis: object does not satisfy Analysis shape');
|
|
72
|
+
}
|
|
73
|
+
return /** @type {Analysis} */ (parsed);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {PanelResponse} panelResponse
|
|
78
|
+
* @returns {FailedModel}
|
|
79
|
+
*/
|
|
80
|
+
function toFailedModel(panelResponse) {
|
|
81
|
+
return { model: panelResponse.model, reason: panelResponse.error || 'unknown' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {FailedModel[]} failedModels
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function classify(failedModels) {
|
|
89
|
+
if (!failedModels || failedModels.length === 0) return 'all_panels_failed';
|
|
90
|
+
const combined = failedModels.map(f => f.reason || '').join(' ').toLowerCase();
|
|
91
|
+
if (/rate.?limit|429|too many request/.test(combined)) return 'rate_limited';
|
|
92
|
+
if (/insufficient|credit|quota|billing|payment|exceed.*(usage|plan)/.test(combined)) return 'insufficient_credits';
|
|
93
|
+
if (/enoent|spawn|signal|crash|killed/.test(combined)) return 'unexpected_error';
|
|
94
|
+
return 'all_panels_failed';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Port VERBATIM from scripts/compress.cjs — strips an outer ```fence
|
|
98
|
+
// wrapping the ENTIRE output.
|
|
99
|
+
function stripLlmWrapper(text) {
|
|
100
|
+
const m = text.match(/^\s*(`{3,}|~{3,})[^\n]*\n([\s\S]*)\n\1\s*$/);
|
|
101
|
+
return m ? m[2] : text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
FAILURE_REASONS,
|
|
106
|
+
isPanelResponse,
|
|
107
|
+
isAnalysis,
|
|
108
|
+
parseAnalysis,
|
|
109
|
+
toFailedModel,
|
|
110
|
+
classify,
|
|
111
|
+
stripLlmWrapper,
|
|
112
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — bounded-concurrency map utility.
|
|
3
|
+
// mapLimit(items, limit, asyncFn) -> Promise<settled[]>
|
|
4
|
+
// Each settled item is {ok:true,value} or {ok:false,error}.
|
|
5
|
+
// A rejected task releases its permit and does NOT starve the pool.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @template T, R
|
|
11
|
+
* @param {T[]} items
|
|
12
|
+
* @param {number} limit
|
|
13
|
+
* @param {(item: T, index: number) => Promise<R>} asyncFn
|
|
14
|
+
* @returns {Promise<({ok:true,value:R}|{ok:false,error:unknown})[]}
|
|
15
|
+
*/
|
|
16
|
+
function mapLimit(items, limit, asyncFn) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const n = items.length;
|
|
19
|
+
if (n === 0) { resolve([]); return; }
|
|
20
|
+
|
|
21
|
+
const results = new Array(n);
|
|
22
|
+
let nextIdx = 0; // index of next item to start
|
|
23
|
+
let inFlight = 0; // currently running tasks
|
|
24
|
+
let done = 0; // settled tasks
|
|
25
|
+
|
|
26
|
+
function run() {
|
|
27
|
+
while (inFlight < limit && nextIdx < n) {
|
|
28
|
+
const idx = nextIdx++;
|
|
29
|
+
inFlight++;
|
|
30
|
+
Promise.resolve()
|
|
31
|
+
.then(() => asyncFn(items[idx], idx))
|
|
32
|
+
.then(
|
|
33
|
+
(value) => { results[idx] = { ok: true, value }; },
|
|
34
|
+
(error) => { results[idx] = { ok: false, error }; }
|
|
35
|
+
)
|
|
36
|
+
.finally(() => {
|
|
37
|
+
inFlight--;
|
|
38
|
+
done++;
|
|
39
|
+
if (done === n) { resolve(results); return; }
|
|
40
|
+
run();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { mapLimit };
|