@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/run.cjs
CHANGED
|
@@ -1,180 +1,201 @@
|
|
|
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
|
-
const MODEL_ALIASES = {
|
|
21
|
-
chatgpt: 'gpt-5.5',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const PRESET_ALIASES = {
|
|
25
|
-
'chatgpt-duo': 'gpt-duo',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** @param {string} model @returns {string} */
|
|
29
|
-
function canonicalModelId(model) {
|
|
30
|
-
return MODEL_ALIASES[model] || model;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** @param {string} preset @returns {string} */
|
|
34
|
-
function canonicalPresetId(preset) {
|
|
35
|
-
return PRESET_ALIASES[preset] || preset;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** @param {object} state @returns {object} */
|
|
39
|
-
function normalizeStateAliases(state) {
|
|
40
|
-
const normalized = { ...state };
|
|
41
|
-
if (normalized.model) normalized.model = canonicalModelId(normalized.model);
|
|
42
|
-
if (normalized.preset) normalized.preset = canonicalPresetId(normalized.preset);
|
|
43
|
-
if (Array.isArray(normalized.models)) {
|
|
44
|
-
normalized.models = normalized.models.map(canonicalModelId);
|
|
45
|
-
}
|
|
46
|
-
if (normalized.judgeModel) normalized.judgeModel = canonicalModelId(normalized.judgeModel);
|
|
47
|
-
if (normalized.synthModel) normalized.synthModel = canonicalModelId(normalized.synthModel);
|
|
48
|
-
return normalized;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {{ prompt:string, state:object, cfg?:object, deps?:object }} opts
|
|
53
|
-
* @returns {Promise<object>}
|
|
54
|
-
*/
|
|
55
|
-
async function runFrontier({ prompt, state, cfg, deps }) {
|
|
56
|
-
cfg = cfg || DEFAULTS;
|
|
57
|
-
deps = deps || {};
|
|
58
|
-
state = normalizeStateAliases(state || {});
|
|
59
|
-
|
|
60
|
-
const spawnOne
|
|
61
|
-
const fanOut
|
|
62
|
-
const runJudge
|
|
63
|
-
const runSynth
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
model: state.model,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
const MODEL_ALIASES = {
|
|
21
|
+
chatgpt: 'gpt-5.5',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PRESET_ALIASES = {
|
|
25
|
+
'chatgpt-duo': 'gpt-duo',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** @param {string} model @returns {string} */
|
|
29
|
+
function canonicalModelId(model) {
|
|
30
|
+
return MODEL_ALIASES[model] || model;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {string} preset @returns {string} */
|
|
34
|
+
function canonicalPresetId(preset) {
|
|
35
|
+
return PRESET_ALIASES[preset] || preset;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @param {object} state @returns {object} */
|
|
39
|
+
function normalizeStateAliases(state) {
|
|
40
|
+
const normalized = { ...state };
|
|
41
|
+
if (normalized.model) normalized.model = canonicalModelId(normalized.model);
|
|
42
|
+
if (normalized.preset) normalized.preset = canonicalPresetId(normalized.preset);
|
|
43
|
+
if (Array.isArray(normalized.models)) {
|
|
44
|
+
normalized.models = normalized.models.map(canonicalModelId);
|
|
45
|
+
}
|
|
46
|
+
if (normalized.judgeModel) normalized.judgeModel = canonicalModelId(normalized.judgeModel);
|
|
47
|
+
if (normalized.synthModel) normalized.synthModel = canonicalModelId(normalized.synthModel);
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {{ prompt:string, state:object, cfg?:object, deps?:object }} opts
|
|
53
|
+
* @returns {Promise<object>}
|
|
54
|
+
*/
|
|
55
|
+
async function runFrontier({ prompt, state, cfg, deps }) {
|
|
56
|
+
cfg = cfg || DEFAULTS;
|
|
57
|
+
deps = deps || {};
|
|
58
|
+
state = normalizeStateAliases(state || {});
|
|
59
|
+
|
|
60
|
+
const spawnOne = deps.spawnOne || dispatch.spawnOne;
|
|
61
|
+
const fanOut = deps.fanOut || dispatch.fanOut;
|
|
62
|
+
const runJudge = deps.runJudge || judge.runJudge;
|
|
63
|
+
const runSynth = deps.runSynth || synthesize.runSynth;
|
|
64
|
+
const onProgress = (deps && typeof deps.onProgress === 'function') ? deps.onProgress : null;
|
|
65
|
+
|
|
66
|
+
const startMs = Date.now();
|
|
67
|
+
|
|
68
|
+
/** Emit a progress event; swallows any error thrown by the callback. */
|
|
69
|
+
function emit(eventObj) {
|
|
70
|
+
if (!onProgress) return;
|
|
71
|
+
try { onProgress(eventObj); } catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rawDepth = parseInt(process.env.FUSION_DEPTH || '0', 10);
|
|
75
|
+
const depth = isNaN(rawDepth) ? 0 : rawDepth;
|
|
76
|
+
|
|
77
|
+
// ---- OFF ----
|
|
78
|
+
if (state.mode === 'off') {
|
|
79
|
+
return { status: 'off', mode: 'off', final: null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- RECURSION GUARD (single + fusion) ----
|
|
83
|
+
if (depth >= 1) {
|
|
84
|
+
const base = {
|
|
85
|
+
status: 'error',
|
|
86
|
+
mode: state.mode,
|
|
87
|
+
error: 'fusion depth exceeded (one-level cap)',
|
|
88
|
+
failure_reason: 'fusion_invocation_capped',
|
|
89
|
+
};
|
|
90
|
+
if (state.mode === 'fusion') return { ...base, preset: state.preset };
|
|
91
|
+
if (state.mode === 'single') return { ...base, model: state.model };
|
|
92
|
+
return base;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- SINGLE ----
|
|
96
|
+
if (state.mode === 'single') {
|
|
97
|
+
const adapter = cfg.adapters && cfg.adapters[state.model];
|
|
98
|
+
if (!adapter) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'error',
|
|
101
|
+
mode: 'single',
|
|
102
|
+
model: state.model,
|
|
103
|
+
error: 'unknown model: ' + state.model,
|
|
104
|
+
failure_reason: 'unexpected_error',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
emit({ phase: 'single-start', model: state.model });
|
|
108
|
+
const resp = await spawnOne(prompt, adapter, { timeoutMs: cfg.timeoutMs, fusionDepth: depth + 1 });
|
|
109
|
+
if (!resp.ok) {
|
|
110
|
+
return {
|
|
111
|
+
status: 'error',
|
|
112
|
+
mode: 'single',
|
|
113
|
+
model: state.model,
|
|
114
|
+
error: resp.error,
|
|
115
|
+
failure_reason: classify([toFailedModel(resp)]),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
emit({ phase: 'done', models: 1, ms: Date.now() - startMs });
|
|
119
|
+
return { status: 'ok', mode: 'single', model: state.model, final: resp.content, response: resp };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- FUSION ----
|
|
123
|
+
if (state.mode === 'fusion') {
|
|
124
|
+
// resolve panel
|
|
125
|
+
let panelIds;
|
|
126
|
+
try {
|
|
127
|
+
panelIds = resolvePanel(state, cfg);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'error',
|
|
131
|
+
mode: 'fusion',
|
|
132
|
+
preset: state.preset,
|
|
133
|
+
error: e.message,
|
|
134
|
+
failure_reason: 'unexpected_error',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// BUDGET (opt-in)
|
|
139
|
+
if (cfg.tokenBudget && cfg.tokenBudget > 0) {
|
|
140
|
+
const projected = Math.ceil(prompt.length / 4) * panelIds.length;
|
|
141
|
+
if (projected > cfg.tokenBudget) {
|
|
142
|
+
return {
|
|
143
|
+
status: 'error',
|
|
144
|
+
mode: 'fusion',
|
|
145
|
+
preset: state.preset,
|
|
146
|
+
error: 'projected token budget exceeded (' + projected + '>' + cfg.tokenBudget + ')',
|
|
147
|
+
failure_reason: 'unexpected_error',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
emit({ phase: 'panel-start', models: panelIds });
|
|
153
|
+
const panel = await fanOut(prompt, panelIds, cfg, { fusionDepth: depth + 1, onProgress });
|
|
154
|
+
const ok = panel.filter(p => p.ok);
|
|
155
|
+
const failed = panel.filter(p => !p.ok);
|
|
156
|
+
|
|
157
|
+
emit({ phase: 'panel-done', ok: ok.length, failed: failed.length });
|
|
158
|
+
|
|
159
|
+
if (ok.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
status: 'error',
|
|
162
|
+
mode: 'fusion',
|
|
163
|
+
preset: state.preset,
|
|
164
|
+
error: 'all panels failed',
|
|
165
|
+
failure_reason: classify(failed.map(toFailedModel)),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Resolve judge/synth model (explicit flag -> preset override -> default)
|
|
170
|
+
// and hand the judge/synth stages a cfg pinned to those models, so a
|
|
171
|
+
// preset like gpt-duo runs entirely on its own provider.
|
|
172
|
+
const stageCfg = {
|
|
173
|
+
...cfg,
|
|
174
|
+
judgeModel: resolveJudgeModel(state, cfg),
|
|
175
|
+
synthModel: resolveSynthModel(state, cfg),
|
|
176
|
+
};
|
|
177
|
+
emit({ phase: 'judge-start', model: stageCfg.judgeModel });
|
|
178
|
+
const analysis = await runJudge(prompt, ok, stageCfg);
|
|
179
|
+
emit({ phase: 'synth-start', model: stageCfg.synthModel });
|
|
180
|
+
let final = await runSynth(prompt, { analysis, responses: ok }, stageCfg);
|
|
181
|
+
if (!final) {
|
|
182
|
+
// synth-fail fallback: longest ok response
|
|
183
|
+
final = ok.reduce((a, b) => b.content.length > a.content.length ? b : a).content;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (failed.length > 0 && ok.length > 0) {
|
|
187
|
+
emit({ phase: 'degraded', failed: failed.length });
|
|
188
|
+
}
|
|
189
|
+
emit({ phase: 'done', models: ok.length, ms: Date.now() - startMs });
|
|
190
|
+
|
|
191
|
+
const result = { status: 'ok', mode: 'fusion', preset: state.preset, final, responses: ok };
|
|
192
|
+
if (analysis !== undefined) result.analysis = analysis;
|
|
193
|
+
if (failed.length) result.failed_models = failed.map(toFailedModel);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- UNKNOWN MODE ----
|
|
198
|
+
return { status: 'error', mode: state.mode, error: 'unknown mode', failure_reason: 'unexpected_error' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { runFrontier, canonicalModelId, canonicalPresetId, normalizeStateAliases };
|
package/frontier/schema.cjs
CHANGED
|
@@ -1,112 +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
|
-
};
|
|
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
|
+
};
|