@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
@@ -1,255 +1,267 @@
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 };
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, onProgress?: function }} [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
+ const onProgress = (opts && typeof opts.onProgress === 'function') ? opts.onProgress : null;
234
+
235
+ const total = modelIds.length;
236
+ let done = 0;
237
+
238
+ const settled = await mapLimit(modelIds, concurrency, (id) => {
239
+ const adapter = cfg && cfg.adapters && cfg.adapters[id];
240
+ // undefined adapter -> spawnOne resolves a failed PanelResponse
241
+ return spawnOne(prompt, adapter, {
242
+ timeoutMs: (cfg && cfg.timeoutMs) || 180000,
243
+ fusionDepth,
244
+ }).then((result) => {
245
+ done++;
246
+ if (onProgress) {
247
+ try {
248
+ onProgress({ phase: 'panel-progress', done, total, model: result.model, ms: result.durationMs });
249
+ } catch (_) {}
250
+ }
251
+ return result;
252
+ });
253
+ });
254
+
255
+ return settled.map((s, i) => {
256
+ if (s.ok) return s.value;
257
+ // spawnOne never rejects, but be defensive
258
+ const adapter = cfg && cfg.adapters && cfg.adapters[modelIds[i]];
259
+ return {
260
+ model: adapter ? adapter.model : modelIds[i],
261
+ content: '', ok: false, durationMs: 0, tokensEst: 0,
262
+ error: String(s.error),
263
+ };
264
+ });
265
+ }
266
+
267
+ module.exports = { spawnOne, fanOut, unsafeForShellArg };