@maestrofrontier/frontier 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +214 -0
  2. package/CLAUDE.md +29 -0
  3. package/LICENSE +21 -0
  4. package/README.md +521 -0
  5. package/bin/maestro.cjs +75 -0
  6. package/commands/compress.md +36 -0
  7. package/commands/context-bar.md +30 -0
  8. package/commands/frontier.md +124 -0
  9. package/commands/settings.md +101 -0
  10. package/commands/terse.md +23 -0
  11. package/commands/update.md +59 -0
  12. package/docs/orchestration.md +168 -0
  13. package/frontier/cli.cjs +248 -0
  14. package/frontier/config.cjs +441 -0
  15. package/frontier/dispatch.cjs +255 -0
  16. package/frontier/judge.cjs +92 -0
  17. package/frontier/run.cjs +148 -0
  18. package/frontier/schema.cjs +112 -0
  19. package/frontier/semaphore.cjs +49 -0
  20. package/frontier/synthesize.cjs +79 -0
  21. package/hooks/frontier-autorun.cjs +124 -0
  22. package/hooks/hooks.json +103 -0
  23. package/hooks/maestro-doctrine-guard.cjs +81 -0
  24. package/hooks/maestro-gate-reminder.cjs +58 -0
  25. package/hooks/maestro-gate-telemetry.cjs +77 -0
  26. package/hooks/maestro-loop-guard.cjs +76 -0
  27. package/hooks/maestro-phase-scope.cjs +118 -0
  28. package/hooks/maestro-statusline-sync.cjs +152 -0
  29. package/hooks/maestro-subagent-guard.cjs +148 -0
  30. package/hooks/maestro-terse-mode.cjs +189 -0
  31. package/hooks/maestro-toolbudget-advisory.cjs +127 -0
  32. package/integrations/README.md +87 -0
  33. package/integrations/cline/skills/frontier/SKILL.md +75 -0
  34. package/integrations/codex/prompts/frontier.md +66 -0
  35. package/integrations/codex/prompts/update.md +36 -0
  36. package/integrations/cursor/commands/frontier.md +63 -0
  37. package/integrations/cursor/commands/update.md +34 -0
  38. package/integrations/gemini/commands/frontier.toml +76 -0
  39. package/integrations/windsurf/workflows/frontier.md +70 -0
  40. package/package.json +52 -0
  41. package/scripts/install.cjs +490 -0
  42. package/settings/cli.cjs +140 -0
  43. package/settings/config.cjs +309 -0
@@ -0,0 +1,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 };
@@ -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 };