@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.
Files changed (51) hide show
  1. package/.agents/plugins/marketplace.json +21 -21
  2. package/.codex-plugin/plugin.json +29 -29
  3. package/.cursorrules +197 -194
  4. package/AGENTS.md +3 -3
  5. package/README.md +368 -368
  6. package/bin/maestro.cjs +75 -75
  7. package/commands/compress.md +36 -36
  8. package/commands/frontier.md +124 -124
  9. package/commands/terse.md +23 -23
  10. package/docs/codex.md +167 -167
  11. package/docs/orchestration.md +168 -168
  12. package/frontier/cli.cjs +279 -252
  13. package/frontier/config.cjs +468 -468
  14. package/frontier/dispatch.cjs +267 -255
  15. package/frontier/judge.cjs +92 -92
  16. package/frontier/progress.cjs +138 -0
  17. package/frontier/run.cjs +201 -180
  18. package/frontier/schema.cjs +112 -112
  19. package/frontier/semaphore.cjs +49 -49
  20. package/frontier/synthesize.cjs +79 -79
  21. package/hooks/frontier-autorun.cjs +135 -120
  22. package/hooks/hooks.json +103 -103
  23. package/hooks/maestro-doctrine-guard.cjs +81 -81
  24. package/hooks/maestro-gate-reminder.cjs +22 -7
  25. package/hooks/maestro-gate-telemetry.cjs +79 -77
  26. package/hooks/maestro-phase-scope.cjs +118 -118
  27. package/hooks/maestro-statusline-sync.cjs +152 -152
  28. package/hooks/maestro-subagent-guard.cjs +148 -148
  29. package/hooks/maestro-terse-mode.cjs +189 -189
  30. package/hooks/maestro-toolbudget-advisory.cjs +127 -127
  31. package/integrations/README.md +111 -111
  32. package/integrations/cline/skills/frontier/SKILL.md +75 -75
  33. package/integrations/codex/prompts/frontier.md +70 -70
  34. package/integrations/codex/prompts/update.md +39 -39
  35. package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -122
  36. package/integrations/codex/skills/maestro-settings/SKILL.md +55 -55
  37. package/integrations/codex/skills/maestro-terse/SKILL.md +58 -58
  38. package/integrations/codex/skills/maestro-update/SKILL.md +31 -31
  39. package/integrations/cursor/commands/frontier.md +63 -63
  40. package/integrations/cursor/commands/update.md +34 -34
  41. package/integrations/gemini/commands/frontier.toml +76 -76
  42. package/integrations/windsurf/workflows/frontier.md +70 -70
  43. package/package.json +59 -58
  44. package/scripts/install.cjs +1014 -1014
  45. package/settings/cli.cjs +140 -140
  46. package/settings/config.cjs +309 -309
  47. package/skills/maestro-frontier/SKILL.md +122 -122
  48. package/skills/maestro-settings/SKILL.md +55 -55
  49. package/skills/maestro-terse/SKILL.md +58 -58
  50. package/skills/maestro-update/SKILL.md +31 -31
  51. 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 = 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
-
65
- const rawDepth = parseInt(process.env.FUSION_DEPTH || '0', 10);
66
- const depth = isNaN(rawDepth) ? 0 : rawDepth;
67
-
68
- // ---- OFF ----
69
- if (state.mode === 'off') {
70
- return { status: 'off', mode: 'off', final: null };
71
- }
72
-
73
- // ---- RECURSION GUARD (single + fusion) ----
74
- if (depth >= 1) {
75
- const base = {
76
- status: 'error',
77
- mode: state.mode,
78
- error: 'fusion depth exceeded (one-level cap)',
79
- failure_reason: 'fusion_invocation_capped',
80
- };
81
- if (state.mode === 'fusion') return { ...base, preset: state.preset };
82
- if (state.mode === 'single') return { ...base, model: state.model };
83
- return base;
84
- }
85
-
86
- // ---- SINGLE ----
87
- if (state.mode === 'single') {
88
- const adapter = cfg.adapters && cfg.adapters[state.model];
89
- if (!adapter) {
90
- return {
91
- status: 'error',
92
- mode: 'single',
93
- model: state.model,
94
- error: 'unknown model: ' + state.model,
95
- failure_reason: 'unexpected_error',
96
- };
97
- }
98
- const resp = await spawnOne(prompt, adapter, { timeoutMs: cfg.timeoutMs, fusionDepth: depth + 1 });
99
- if (!resp.ok) {
100
- return {
101
- status: 'error',
102
- mode: 'single',
103
- model: state.model,
104
- error: resp.error,
105
- failure_reason: classify([toFailedModel(resp)]),
106
- };
107
- }
108
- return { status: 'ok', mode: 'single', model: state.model, final: resp.content, response: resp };
109
- }
110
-
111
- // ---- FUSION ----
112
- if (state.mode === 'fusion') {
113
- // resolve panel
114
- let panelIds;
115
- try {
116
- panelIds = resolvePanel(state, cfg);
117
- } catch (e) {
118
- return {
119
- status: 'error',
120
- mode: 'fusion',
121
- preset: state.preset,
122
- error: e.message,
123
- failure_reason: 'unexpected_error',
124
- };
125
- }
126
-
127
- // BUDGET (opt-in)
128
- if (cfg.tokenBudget && cfg.tokenBudget > 0) {
129
- const projected = Math.ceil(prompt.length / 4) * panelIds.length;
130
- if (projected > cfg.tokenBudget) {
131
- return {
132
- status: 'error',
133
- mode: 'fusion',
134
- preset: state.preset,
135
- error: 'projected token budget exceeded (' + projected + '>' + cfg.tokenBudget + ')',
136
- failure_reason: 'unexpected_error',
137
- };
138
- }
139
- }
140
-
141
- const panel = await fanOut(prompt, panelIds, cfg, { fusionDepth: depth + 1 });
142
- const ok = panel.filter(p => p.ok);
143
- const failed = panel.filter(p => !p.ok);
144
-
145
- if (ok.length === 0) {
146
- return {
147
- status: 'error',
148
- mode: 'fusion',
149
- preset: state.preset,
150
- error: 'all panels failed',
151
- failure_reason: classify(failed.map(toFailedModel)),
152
- };
153
- }
154
-
155
- // Resolve judge/synth model (explicit flag -> preset override -> default)
156
- // and hand the judge/synth stages a cfg pinned to those models, so a
157
- // preset like gpt-duo runs entirely on its own provider.
158
- const stageCfg = {
159
- ...cfg,
160
- judgeModel: resolveJudgeModel(state, cfg),
161
- synthModel: resolveSynthModel(state, cfg),
162
- };
163
- const analysis = await runJudge(prompt, ok, stageCfg);
164
- let final = await runSynth(prompt, { analysis, responses: ok }, stageCfg);
165
- if (!final) {
166
- // synth-fail fallback: longest ok response
167
- final = ok.reduce((a, b) => b.content.length > a.content.length ? b : a).content;
168
- }
169
-
170
- const result = { status: 'ok', mode: 'fusion', preset: state.preset, final, responses: ok };
171
- if (analysis !== undefined) result.analysis = analysis;
172
- if (failed.length) result.failed_models = failed.map(toFailedModel);
173
- return result;
174
- }
175
-
176
- // ---- UNKNOWN MODE ----
177
- return { status: 'error', mode: state.mode, error: 'unknown mode', failure_reason: 'unexpected_error' };
178
- }
179
-
180
- module.exports = { runFrontier, canonicalModelId, canonicalPresetId, normalizeStateAliases };
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 };
@@ -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
+ };