@yemi33/minions 0.1.2081 → 0.1.2083

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.
@@ -69,19 +69,19 @@ async function openSettings() {
69
69
  const agentRows = Object.entries(agents).map(function([id, a]) {
70
70
  return '<tr>' +
71
71
  '<td style="font-weight:600">' + escHtml(a.emoji || '') + ' ' + escHtml(a.name || id) + '</td>' +
72
- '<td><input data-agent="' + escHtml(id) + '" data-field="role" value="' + escHtml(a.role || '') + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></td>' +
73
- '<td><input data-agent="' + escHtml(id) + '" data-field="skills" value="' + escHtml((a.skills || []).join(', ')) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></td>' +
72
+ '<td><input data-agent="' + escHtml(id) + '" data-field="role" value="' + escHtml(a.role || '') + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)"></td>' +
73
+ '<td><input data-agent="' + escHtml(id) + '" data-field="skills" value="' + escHtml((a.skills || []).join(', ')) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)"></td>' +
74
74
  '<td data-runtime-cli="' + escHtml(id) + '" style="min-width:110px">' +
75
75
  // Initial loading placeholder — initRuntimeFleetUI() replaces this with a
76
76
  // <select> populated from /api/runtimes once the registry resolves.
77
- '<input value="' + escHtml(a.cli || '') + '" placeholder="' + escHtml(fleetCliLabel) + ' (fleet)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:11px">' +
77
+ '<input value="' + escHtml(a.cli || '') + '" placeholder="' + escHtml(fleetCliLabel) + ' (fleet)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-base)">' +
78
78
  '</td>' +
79
79
  '<td data-runtime-model="' + escHtml(id) + '" style="min-width:140px">' +
80
80
  // Loading placeholder — initRuntimeFleetUI() replaces this with a
81
81
  // <select> populated from /api/runtimes/<resolved-cli>/models.
82
- '<input value="' + escHtml(a.model || '') + '" placeholder="' + escHtml(fleetModelLabel) + ' (fleet)" disabled style="width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:11px">' +
82
+ '<input value="' + escHtml(a.model || '') + '" placeholder="' + escHtml(fleetModelLabel) + ' (fleet)" disabled style="width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-base)">' +
83
83
  '</td>' +
84
- '<td><input data-agent="' + escHtml(id) + '" data-field="monthlyBudgetUsd" value="' + escHtml(a.monthlyBudgetUsd != null ? String(a.monthlyBudgetUsd) : '') + '" placeholder="unlimited" style="width:70px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px;text-align:right"></td>' +
84
+ '<td><input data-agent="' + escHtml(id) + '" data-field="monthlyBudgetUsd" value="' + escHtml(a.monthlyBudgetUsd != null ? String(a.monthlyBudgetUsd) : '') + '" placeholder="unlimited" style="width:70px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base);text-align:right"></td>' +
85
85
  '</tr>';
86
86
  }).join('');
87
87
 
@@ -96,50 +96,50 @@ async function openSettings() {
96
96
  '<div id="set-runtime-section" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:16px">' +
97
97
  '<div style="display:grid;grid-template-columns:1fr 2fr;gap:8px;margin-bottom:8px">' +
98
98
  '<div data-search="default cli fleet runtime">' +
99
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Default CLI</label>' +
100
- '<select id="set-defaultCli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
99
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Default CLI</label>' +
100
+ '<select id="set-defaultCli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
101
101
  '<option value="">Loading…</option>' +
102
102
  '</select>' +
103
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Fleet-wide runtime — registered adapters from <code>/api/runtimes</code></div>' +
103
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Fleet-wide runtime — registered adapters from <code>/api/runtimes</code></div>' +
104
104
  '</div>' +
105
105
  '<div data-search="default model fleet">' +
106
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Default Model</label>' +
107
- '<div id="set-defaultModel-wrap"><input id="set-defaultModel" value="' + escHtml(e.defaultModel || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px"></div>' +
108
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
106
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Default Model</label>' +
107
+ '<div id="set-defaultModel-wrap"><input id="set-defaultModel" value="' + escHtml(e.defaultModel || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)"></div>' +
108
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
109
109
  '</div>' +
110
110
  '</div>' +
111
111
  // CC overrides — collapsed by default
112
112
  '<details id="set-cc-overrides-details"' + ((e.ccCli || e.ccModel) ? ' open' : '') + ' style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px">' +
113
- '<summary style="cursor:pointer;font-size:11px;color:var(--text);user-select:none">Customize CC separately ' +
114
- '<span style="font-size:9px;color:var(--muted)">(Command Center + doc-chat use the fleet defaults unless overridden)</span>' +
113
+ '<summary style="cursor:pointer;font-size:var(--text-base);color:var(--text);user-select:none">Customize CC separately ' +
114
+ '<span style="font-size:var(--text-xs);color:var(--muted)">(Command Center + doc-chat use the fleet defaults unless overridden)</span>' +
115
115
  '</summary>' +
116
116
  '<div style="display:grid;grid-template-columns:1fr 2fr 1fr;gap:8px;margin-top:8px">' +
117
117
  '<div data-search="cc cli command center runtime">' +
118
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">CC CLI</label>' +
119
- '<select id="set-ccCli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
118
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">CC CLI</label>' +
119
+ '<select id="set-ccCli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
120
120
  '<option value="">Loading…</option>' +
121
121
  '</select>' +
122
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = inherit Default CLI</div>' +
122
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = inherit Default CLI</div>' +
123
123
  '</div>' +
124
124
  '<div data-search="cc model command center">' +
125
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">CC Model</label>' +
125
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">CC Model</label>' +
126
126
  // Wrap the input in a dedicated container so loadModelsForRuntime
127
127
  // (which does `wrap.innerHTML = …` on the input's parent) only
128
128
  // swaps THIS element. Without the wrap it would wipe the label
129
129
  // and the hint below, breaking vertical alignment with the
130
130
  // sibling CC CLI / Effort columns.
131
- '<div id="set-ccModel-wrap"><input id="set-ccModel" value="' + escHtml(e.ccModel || '') + '" placeholder="(inherits Default Model)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px"></div>' +
132
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
131
+ '<div id="set-ccModel-wrap"><input id="set-ccModel" value="' + escHtml(e.ccModel || '') + '" placeholder="(inherits Default Model)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)"></div>' +
132
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
133
133
  '</div>' +
134
134
  '<div data-search="cc effort reasoning">' +
135
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Effort</label>' +
136
- '<select id="set-ccEffort" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
135
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Effort</label>' +
136
+ '<select id="set-ccEffort" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
137
137
  '<option value=""' + (!e.ccEffort ? ' selected' : '') + '>Default</option>' +
138
138
  '<option value="low"' + (e.ccEffort === 'low' ? ' selected' : '') + '>Low</option>' +
139
139
  '<option value="medium"' + (e.ccEffort === 'medium' ? ' selected' : '') + '>Medium</option>' +
140
140
  '<option value="high"' + (e.ccEffort === 'high' ? ' selected' : '') + '>High</option>' +
141
141
  '</select>' +
142
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">CC reasoning depth</div>' +
142
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">CC reasoning depth</div>' +
143
143
  '</div>' +
144
144
  '</div>' +
145
145
  // W-mpmwxni2000c25c7-d — per-turn watchdog. Surfaced under CC overrides
@@ -150,8 +150,8 @@ async function openSettings() {
150
150
  '</details>' +
151
151
  '</div>' +
152
152
  '<h4>Agents</h4>' +
153
- '<div style="font-size:10px;color:var(--muted);margin-bottom:6px">CLI / Model placeholders show the fleet default each agent will inherit. Pick a value to pin per-agent; clear to re-inherit. Per-agent monthly budget overrides the fleet ceiling.</div>' +
154
- '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:11px">' +
153
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:6px">CLI / Model placeholders show the fleet default each agent will inherit. Pick a value to pin per-agent; clear to re-inherit. Per-agent monthly budget overrides the fleet ceiling.</div>' +
154
+ '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:var(--text-base)">' +
155
155
  '<tr style="text-align:left;color:var(--muted)"><th style="padding:4px">Agent</th><th style="padding:4px">Role</th><th style="padding:4px">Skills</th><th style="padding:4px">CLI</th><th style="padding:4px">Model</th><th style="padding:4px">Budget $/mo</th></tr>' +
156
156
  agentRows +
157
157
  '</table>';
@@ -187,8 +187,8 @@ async function openSettings() {
187
187
  '<div class="settings-pane-sub">Dashboard-wide visual preferences. Persisted in browser localStorage + server-side settings so they survive a reload from a cold cache.</div>' +
188
188
  '<div class="settings-grid-2">' +
189
189
  '<div data-search="font size scale appearance dashboard">' +
190
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Font Size</label>' +
191
- '<select id="set-fontSize" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
190
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Font Size</label>' +
191
+ '<select id="set-fontSize" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
192
192
  (function() {
193
193
  var current = (e.fontSize || 'small');
194
194
  var opts = [['small', 'Small (current default)'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
@@ -197,7 +197,7 @@ async function openSettings() {
197
197
  }).join('');
198
198
  })() +
199
199
  '</select>' +
200
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Scales the whole dashboard. Persisted in browser + server.</div>' +
200
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Scales the whole dashboard. Persisted in browser + server.</div>' +
201
201
  '</div>' +
202
202
  '</div>';
203
203
 
@@ -216,23 +216,23 @@ async function openSettings() {
216
216
  var mismatch = !!(liveProj && liveProj.branchMismatch);
217
217
  var localBranch = (liveProj && liveProj.gitBranch) || '';
218
218
  var driftNote = mismatch
219
- ? '<div style="font-size:10px;color:var(--yellow);margin-top:4px">⚠ Configured main (<code>' + escHtml(p.mainBranch || '') + '</code>) differs from origin/HEAD (<code>' + escHtml(remoteDefault) + '</code>) — config is likely stale.</div>'
219
+ ? '<div style="font-size:var(--text-sm);color:var(--yellow);margin-top:4px">⚠ Configured main (<code>' + escHtml(p.mainBranch || '') + '</code>) differs from origin/HEAD (<code>' + escHtml(remoteDefault) + '</code>) — config is likely stale.</div>'
220
220
  : '';
221
221
  var pathRow = p.localPath
222
- ? '<div style="font-size:10px;color:var(--muted);margin-bottom:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' + escHtml(p.localPath) + '</div>'
222
+ ? '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' + escHtml(p.localPath) + '</div>'
223
223
  : '';
224
224
  var branchGrid = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:6px">' +
225
225
  settingsField('Configured main branch', 'set-mainBranch-' + p.name, p.mainBranch || '', '', 'Used by branch-naming + dependency-merge to identify mainline. Empty = auto-detect from origin/HEAD.') +
226
226
  '<div>' +
227
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Remote default <span style="opacity:0.6">(read-only)</span></label>' +
228
- '<input id="set-remoteDefaultBranch-' + p.name + '" value="' + escHtml(remoteDefault || '(unset)') + '" readonly style="width:100%;padding:4px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' +
229
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
227
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Remote default <span style="opacity:0.6">(read-only)</span></label>' +
228
+ '<input id="set-remoteDefaultBranch-' + p.name + '" value="' + escHtml(remoteDefault || '(unset)') + '" readonly style="width:100%;padding:4px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-md);font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' +
229
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
230
230
  '</div>' +
231
231
  '</div>';
232
232
  return '<div data-settings-project="' + escHtml(p.name) + '" data-search="project ' + escHtml(p.name.toLowerCase()) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:12px">' +
233
233
  '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
234
- '<div style="font-size:12px;font-weight:600">' + escHtml(p.name) + '</div>' +
235
- '<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:9px;padding:2px 8px;background:transparent;color:var(--red);border:1px solid var(--red);border-radius:3px;cursor:pointer">Remove</button>' +
234
+ '<div style="font-size:var(--text-md);font-weight:600">' + escHtml(p.name) + '</div>' +
235
+ '<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:var(--text-xs);padding:2px 8px;background:transparent;color:var(--red);border:1px solid var(--red);border-radius:3px;cursor:pointer">Remove</button>' +
236
236
  '</div>' +
237
237
  pathRow +
238
238
  branchGrid +
@@ -300,8 +300,8 @@ async function openSettings() {
300
300
  '</div>' +
301
301
  '<div class="settings-grid-2">' +
302
302
  '<div data-search="copilot stream mode incremental batched">' +
303
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Copilot stream</label>' +
304
- '<select id="set-copilotStreamMode" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
303
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Copilot stream</label>' +
304
+ '<select id="set-copilotStreamMode" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
305
305
  '<option value="on"' + ((e.copilotStreamMode || 'on') === 'on' ? ' selected' : '') + '>on (incremental)</option>' +
306
306
  '<option value="off"' + (e.copilotStreamMode === 'off' ? ' selected' : '') + '>off (batched)</option>' +
307
307
  '</select>' +
@@ -321,7 +321,7 @@ async function openSettings() {
321
321
  '<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px">' +
322
322
  settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Claude allow-list passed through for compatibility; runtime bypass flags are adapter-owned.') +
323
323
  '</div>' +
324
- '<div data-search="permission bypass dangerously-skip-permissions autopilot allow-all" style="font-size:10px;color:var(--muted);margin-top:12px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--surface-subtle, rgba(130,160,210,0.08))">' +
324
+ '<div data-search="permission bypass dangerously-skip-permissions autopilot allow-all" style="font-size:var(--text-sm);color:var(--muted);margin-top:12px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--surface-subtle, rgba(130,160,210,0.08))">' +
325
325
  'Permission bypass is runtime-owned: Claude agents use <code>--dangerously-skip-permissions</code>; Copilot agents use <code>--autopilot --allow-all --no-ask-user</code>. There is no dashboard permission-mode setting.' +
326
326
  '</div>';
327
327
 
@@ -355,16 +355,16 @@ async function openSettings() {
355
355
  '<h3>Feature Flags</h3>' +
356
356
  '<div class="settings-pane-sub">In-progress UX or behavior gates. Toggles persist immediately. Registry: <code>engine/features.js</code>. Env override: <code>MINIONS_FEATURE_&lt;NAME&gt;=1</code>.</div>' +
357
357
  '<details id="settings-features-details" open style="border:1px solid var(--border);border-radius:6px;padding:12px">' +
358
- '<summary style="cursor:pointer;font-size:12px;color:var(--text);user-select:none;margin-bottom:8px">Show experimental flags ' +
359
- '<span style="font-size:10px;color:var(--muted)">(' + featuresList.length + ' registered)</span>' +
358
+ '<summary style="cursor:pointer;font-size:var(--text-md);color:var(--text);user-select:none;margin-bottom:8px">Show experimental flags ' +
359
+ '<span style="font-size:var(--text-sm);color:var(--muted)">(' + featuresList.length + ' registered)</span>' +
360
360
  '</summary>' +
361
361
  (featuresList.length === 0
362
- ? '<div style="font-size:11px;color:var(--muted);padding:12px;border:1px dashed var(--border);border-radius:4px;text-align:center">No experimental features registered. Add entries to <code>engine/features.js</code> to gate new work.</div>'
362
+ ? '<div style="font-size:var(--text-base);color:var(--muted);padding:12px;border:1px dashed var(--border);border-radius:4px;text-align:center">No experimental features registered. Add entries to <code>engine/features.js</code> to gate new work.</div>'
363
363
  : '<div style="display:flex;flex-direction:column;gap:8px">' +
364
364
  featuresList.map(function(f) {
365
365
  const checked = f.enabled ? ' checked' : '';
366
366
  const expiredBadge = f.expired
367
- ? ' <span style="font-size:9px;padding:1px 5px;background:rgba(220,80,80,0.15);color:var(--red);border-radius:3px;margin-left:4px">EXPIRED</span>'
367
+ ? ' <span style="font-size:var(--text-xs);padding:1px 5px;background:rgba(220,80,80,0.15);color:var(--red);border-radius:3px;margin-left:4px">EXPIRED</span>'
368
368
  : '';
369
369
  const meta = [];
370
370
  if (f.addedIn) meta.push('added in ' + escHtml(f.addedIn));
@@ -374,9 +374,9 @@ async function openSettings() {
374
374
  return '<label data-feature-id="' + escHtml(f.id) + '" data-search="' + escHtml(searchText) + '" style="display:flex;align-items:flex-start;gap:8px;padding:8px;border:1px solid var(--border);border-radius:4px;cursor:pointer">' +
375
375
  '<input type="checkbox" data-feature-toggle="' + escHtml(f.id) + '"' + checked + ' style="margin-top:3px;cursor:pointer">' +
376
376
  '<div style="flex:1">' +
377
- '<div style="font-size:11px;font-weight:600;color:var(--text)">' + escHtml(f.id) + expiredBadge + '</div>' +
378
- (f.description ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(f.description) + '</div>' : '') +
379
- '<div style="font-size:9px;color:var(--muted);margin-top:3px">' + meta.join(' · ') + '</div>' +
377
+ '<div style="font-size:var(--text-base);font-weight:600;color:var(--text)">' + escHtml(f.id) + expiredBadge + '</div>' +
378
+ (f.description ? '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:2px">' + escHtml(f.description) + '</div>' : '') +
379
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:3px">' + meta.join(' · ') + '</div>' +
380
380
  '</div>' +
381
381
  '</label>';
382
382
  }).join('') +
@@ -388,7 +388,7 @@ async function openSettings() {
388
388
  '<div class="settings-pane-sub">Routing table + low-level engine knobs that most operators never touch. Change with care.</div>' +
389
389
  '<h4>Routing Table</h4>' +
390
390
  '<div data-search="routing table playbook agent assignment" style="margin-bottom:16px">' +
391
- '<textarea id="set-routing" rows="10" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:11px;resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
391
+ '<textarea id="set-routing" rows="10" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:var(--text-base);resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
392
392
  '</div>' +
393
393
  '<h4>Inbox &amp; Status Retention</h4>' +
394
394
  '<div class="settings-grid-2">' +
@@ -457,7 +457,7 @@ async function openSettings() {
457
457
  '<nav class="settings-rail-nav" id="settings-rail-nav">' + railHtml + '</nav>' +
458
458
  '</aside>' +
459
459
  '<div class="settings-content" id="settings-content">' +
460
- '<div id="settings-status" style="font-size:11px;min-height:16px;margin-bottom:8px"></div>' +
460
+ '<div id="settings-status" style="font-size:var(--text-base);min-height:16px;margin-bottom:8px"></div>' +
461
461
  panesHtml +
462
462
  '</div>' +
463
463
  '</div>';
@@ -588,7 +588,7 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
588
588
  const current = agent.cli || '';
589
589
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agentId, runtime names)
590
590
  cell.innerHTML =
591
- '<select data-agent="' + escHtml(agentId) + '" data-field="cli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px">' +
591
+ '<select data-agent="' + escHtml(agentId) + '" data-field="cli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)">' +
592
592
  '<option value=""' + (!current ? ' selected' : '') + '>(fleet default)</option>' +
593
593
  names.map(n =>
594
594
  '<option value="' + escHtml(n) + '"' + (n === current ? ' selected' : '') + '>' + escHtml(n) + '</option>'
@@ -647,7 +647,7 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
647
647
  const token = _nextModelLoadToken('runtime', inputId);
648
648
  if (!runtimeName) {
649
649
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: currentValue; inputId is an internal fixed DOM id)
650
- wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="(no runtime selected)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:12px">';
650
+ wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="(no runtime selected)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-md)">';
651
651
  return;
652
652
  }
653
653
  let payload = { models: null };
@@ -662,7 +662,7 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
662
662
  // Free-text fallback — let the user type anything (custom Anthropic /
663
663
  // OpenAI model IDs, future models, etc.).
664
664
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: currentValue; inputId is an internal fixed DOM id)
665
- wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">';
665
+ wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">';
666
666
  return;
667
667
  }
668
668
  // Dropdown. The first option submits empty string → "Default (CLI chooses)".
@@ -679,7 +679,7 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
679
679
  opts += '<option value="' + escHtml(currentValue) + '" selected>' + escHtml(currentValue) + ' (custom)</option>';
680
680
  }
681
681
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: model id, model label, currentValue; inputId is an internal fixed DOM id)
682
- wrap.innerHTML = '<select id="' + inputId + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' + opts + '</select>';
682
+ wrap.innerHTML = '<select id="' + inputId + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' + opts + '</select>';
683
683
  }
684
684
 
685
685
  /**
@@ -693,7 +693,7 @@ async function loadModelsForAgent(agentId, runtimeName, currentValue) {
693
693
  const cell = document.querySelector('[data-runtime-model="' + agentId + '"]');
694
694
  if (!cell) return;
695
695
  const baseAttrs = 'data-agent="' + escHtml(agentId) + '" data-field="model"';
696
- const baseStyle = 'width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px';
696
+ const baseStyle = 'width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)';
697
697
  const token = _nextModelLoadToken('agent', agentId);
698
698
  if (!runtimeName) {
699
699
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agentId, currentValue)
@@ -735,17 +735,17 @@ function settingsToggle(label, id, checked, hint) {
735
735
  const searchKey = (String(label || '') + ' ' + String(hint || '')).toLowerCase().replace(/"/g, '');
736
736
  return '<div class="settings-row" data-search="' + escHtml(searchKey) + '" style="display:flex;align-items:center;gap:8px;padding:4px 0">' +
737
737
  '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
738
- '<label for="' + id + '" style="font-size:12px;color:var(--text);cursor:pointer">' + escHtml(label) + '</label>' +
739
- (hint ? '<span style="font-size:9px;color:var(--muted)">' + escHtml(hint) + '</span>' : '') +
738
+ '<label for="' + id + '" style="font-size:var(--text-md);color:var(--text);cursor:pointer">' + escHtml(label) + '</label>' +
739
+ (hint ? '<span style="font-size:var(--text-xs);color:var(--muted)">' + escHtml(hint) + '</span>' : '') +
740
740
  '</div>';
741
741
  }
742
742
 
743
743
  function settingsField(label, id, value, unit, hint) {
744
744
  const searchKey = (String(label || '') + ' ' + String(hint || '') + ' ' + String(unit || '')).toLowerCase().replace(/"/g, '');
745
745
  return '<div class="settings-row" data-search="' + escHtml(searchKey) + '">' +
746
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">' + escHtml(label) + (unit ? ' <span style="opacity:0.6">(' + escHtml(unit) + ')</span>' : '') + '</label>' +
747
- '<input id="' + id + '" value="' + escHtml(String(value)) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
748
- (hint ? '<div style="font-size:9px;color:var(--muted);margin-top:1px">' + escHtml(hint) + '</div>' : '') +
746
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">' + escHtml(label) + (unit ? ' <span style="opacity:0.6">(' + escHtml(unit) + ')</span>' : '') + '</label>' +
747
+ '<input id="' + id + '" value="' + escHtml(String(value)) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
748
+ (hint ? '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">' + escHtml(hint) + '</div>' : '') +
749
749
  '</div>';
750
750
  }
751
751
 
@@ -43,7 +43,7 @@ let currentPage = getPageFromUrl();
43
43
  // _stopMeetingPoll, _stopQaRunsPoll all clearInterval on a nullable handle;
44
44
  // closeDetail / closeManagedLog short-circuit when no panel/stream is open.
45
45
  const PAGE_LAZY_LOADERS = {
46
- qa: ['loadQaTargets', 'loadQaRunbooks', 'loadQaRunners', 'loadQaSessions', '_startQaSessionsPoll', '_startQaRunsPoll'],
46
+ qa: ['loadQaTargets', 'loadQaRunbooks', 'loadQaRunners', 'loadQaProjectsSelect', 'loadQaSessions', '_startQaSessionsPoll', '_startQaRunsPoll'],
47
47
  plans: ['refreshPlans'],
48
48
  inbox: ['refreshKnowledgeBase'],
49
49
  };
@@ -17,8 +17,9 @@
17
17
  </select>
18
18
  </div>
19
19
  <div>
20
- <label class="qa-form-label" for="qa-session-project">Project</label>
21
- <input id="qa-session-project" type="text" class="qa-form-input" placeholder="(blank = central)">
20
+ <label class="qa-form-label" for="qa-session-projects">Projects</label>
21
+ <select id="qa-session-projects" class="qa-form-input" multiple size="3"></select>
22
+ <p class="empty" style="margin-top:4px;font-size:0.85em">Hold Ctrl/Cmd to select multiple. First selected = primary (drives DRAFT/EXECUTE). Others = co-services (dev-up only). Leave empty = central.</p>
22
23
  </div>
23
24
  </div>
24
25
  <div class="qa-form-row qa-session-target-fields">
package/dashboard.js CHANGED
@@ -5593,6 +5593,9 @@ const server = http.createServer(async (req, res) => {
5593
5593
  try {
5594
5594
  const body = await readBody(req);
5595
5595
  if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
5596
+ // Reject markdown filenames — mutateJsonFileLocked below silently overwrites
5597
+ // the file with JSON, destroying source plans (see /api/plans/approve trap).
5598
+ if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
5596
5599
  const planPath = resolvePlanPath(body.source);
5597
5600
  if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
5598
5601
  // Pre-check: verify item exists before taking the lock
@@ -5649,6 +5652,7 @@ const server = http.createServer(async (req, res) => {
5649
5652
  try {
5650
5653
  const body = await readBody(req);
5651
5654
  if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
5655
+ if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
5652
5656
  const planPath = resolvePlanPath(body.source);
5653
5657
  if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
5654
5658
  let removed = false;
@@ -6381,6 +6385,7 @@ const server = http.createServer(async (req, res) => {
6381
6385
  try {
6382
6386
  const body = await readBody(req);
6383
6387
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
6388
+ if (!body.file.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename (got `' + body.file + '`). To approve a plan, pass prd/<plan>.json, not the source plans/<plan>.md.' });
6384
6389
  const planPath = resolvePlanPath(body.file);
6385
6390
  let wasStale = false;
6386
6391
  const plan = mutateJsonFileLocked(planPath, (data) => {
@@ -6474,6 +6479,7 @@ const server = http.createServer(async (req, res) => {
6474
6479
  try {
6475
6480
  const body = await readBody(req);
6476
6481
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
6482
+ if (!body.file.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename (got `' + body.file + '`). Pass prd/<plan>.json, not the source plans/<plan>.md.' });
6477
6483
  const planPath = resolvePlanPath(body.file);
6478
6484
  mutateJsonFileLocked(planPath, (plan) => {
6479
6485
  if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = {};
@@ -6624,6 +6630,7 @@ const server = http.createServer(async (req, res) => {
6624
6630
  try {
6625
6631
  const body = await readBody(req);
6626
6632
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
6633
+ if (!body.file.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename (got `' + body.file + '`). Pass prd/<plan>.json, not the source plans/<plan>.md.' });
6627
6634
  const planPath = resolvePlanPath(body.file);
6628
6635
  const plan = mutateJsonFileLocked(planPath, (data) => {
6629
6636
  if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
@@ -6882,6 +6889,7 @@ const server = http.createServer(async (req, res) => {
6882
6889
  try {
6883
6890
  const body = await readBody(req);
6884
6891
  if (!body.file || !body.feedback) return jsonReply(res, 400, { error: 'file and feedback required' });
6892
+ if (!body.file.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename (got `' + body.file + '`). Pass prd/<plan>.json, not the source plans/<plan>.md.' });
6885
6893
  const planPath = resolvePlanPath(body.file);
6886
6894
  const plan = mutateJsonFileLocked(planPath, (data) => {
6887
6895
  if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
@@ -10265,13 +10273,31 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10265
10273
  return { wiPath: target.wiPath, project: target.project ? target.project.name : null };
10266
10274
  }
10267
10275
 
10276
+ // W-mpq6xqzj000606d0 — Multi-project resolver. Walks each requested project
10277
+ // through resolveWorkItemsCreateTarget, returning either the full list of
10278
+ // {wiPath, project} entries (with the first treated as primary) or
10279
+ // { error } on the first failure. Accepts the canonical `projects: string[]`
10280
+ // and falls back to the legacy single `project` string.
10281
+ function _qaSessionsResolveTargets(body) {
10282
+ const list = Array.isArray(body && body.projects) && body.projects.length > 0
10283
+ ? body.projects.slice()
10284
+ : [body && body.project ? body.project : ''];
10285
+ const resolved = [];
10286
+ for (const name of list) {
10287
+ const t = _qaSessionsResolveTarget(name);
10288
+ if (t.error) return { error: `project "${name || '(central)'}": ${t.error}` };
10289
+ resolved.push(t);
10290
+ }
10291
+ return { targets: resolved };
10292
+ }
10293
+
10268
10294
  async function handleQaSessionCreate(req, res) {
10269
10295
  try {
10270
10296
  const body = await readBody(req);
10271
10297
  // Validate first so a bad spec returns 400 BEFORE we touch state. The
10272
10298
  // body shape is identical to qaSessions.validateSpec — target, flowsRaw,
10273
- // mode, capture, runner, project, createdBy — so we forward it
10274
- // verbatim.
10299
+ // mode, capture, runner, project | projects, createdBy — so we forward
10300
+ // it verbatim.
10275
10301
  const qaSessions = require('./engine/qa-sessions');
10276
10302
  const v = qaSessions.validateSpec(body);
10277
10303
  if (!v.ok) return jsonReply(res, 400, { error: 'invalid spec', details: v.errors }, req);
@@ -10286,8 +10312,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10286
10312
  createSpec.createdBy = originAgent;
10287
10313
  }
10288
10314
 
10289
- const resolved = _qaSessionsResolveTarget(createSpec.project);
10290
- if (resolved.error) return jsonReply(res, 400, { error: resolved.error }, req);
10315
+ // W-mpq6xqzj000606d0 Resolve EACH project (multi-project fan-out)
10316
+ // before persisting the session, so a single bad project rejects the
10317
+ // whole submit instead of leaving an orphan session in PENDING.
10318
+ const resolution = _qaSessionsResolveTargets(createSpec);
10319
+ if (resolution.error) return jsonReply(res, 400, { error: resolution.error }, req);
10291
10320
 
10292
10321
  let session;
10293
10322
  try { session = qaSessions.createSession(createSpec); }
@@ -10296,8 +10325,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10296
10325
  let setupWiId = null;
10297
10326
  try {
10298
10327
  setupWiId = qaSessions.queueSetup(session.id, {
10299
- wiPath: resolved.wiPath,
10300
- project: resolved.project,
10328
+ resolvedTargets: resolution.targets.map(t => ({ project: t.project, wiPath: t.wiPath })),
10301
10329
  });
10302
10330
  } catch (e) {
10303
10331
  // queueSetup throws when pending→spawning is rejected (createSession
@@ -10316,6 +10344,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10316
10344
  state: 'spawning',
10317
10345
  setupWorkItemId: setupWiId,
10318
10346
  managedSpawnName: session.managedSpawnName,
10347
+ projects: resolution.targets.map(t => t.project),
10319
10348
  }, req);
10320
10349
  } catch (e) {
10321
10350
  return jsonReply(res, 500, { error: e.message }, req);
@@ -138,6 +138,26 @@ routing default):
138
138
  the session `spawning → drafting`. First-health failure → `failed` with
139
139
  `failure_class: qa-session-setup-failed`.
140
140
 
141
+ **Multi-project fan-out (W-mpq6xqzj000606d0):** when `spec.projects`
142
+ contains more than one project name, SETUP is fanned out — one work item
143
+ per project, queued in parallel into each project's own
144
+ `work-items.json`. The first project (`spec.projects[0]`) is the
145
+ **primary** (`meta.qaSession.primary === true`); the rest are
146
+ **co-services** (`primary === false`). Each WI's
147
+ `meta.qaSession.coServices` lists the co-service project names (only the
148
+ primary carries the full list; co-services see `[]`) and
149
+ `meta.qaSession.primaryProject` carries the canonical primary name.
150
+ The primary keeps the unsuffixed managed-spawn name; each co-service
151
+ gets `qa-session-<sessionId>-<project>`. The session tracks per-project
152
+ completion in `session.setupStatus[<project>]: { state, wiId, error,
153
+ completedAt }`, where `state ∈ {pending, success, failed}`. **All**
154
+ projects must succeed before `spawning → drafting`; any failure flips
155
+ the session to `failed` with `failure_class: qa-session-setup-failed`
156
+ and a JSON-encoded per-project error array in `session.error`. DRAFT
157
+ queues on `session.primaryWiPath` (stamped at queueSetup time).
158
+ Single-project sessions skip `setupStatus` entirely (fast path uses
159
+ `session.state` directly).
160
+
141
161
  2. **DRAFT** (`playbooks/qa-session-draft.md`) reads the live spawn
142
162
  metadata via `/api/managed-processes/by-name/qa-session-<id>`, calls the
143
163
  resolved runner's `generateBrief({target, flowsRaw, capture})` hook, and
@@ -168,7 +188,7 @@ Documented in `dashboard.js`; routes are visible at `GET /api/routes`.
168
188
 
169
189
  | Method | Path | Behavior |
170
190
  |--------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
171
- | POST | `/api/qa/session` | Create session; validates spec, calls `createSession` + `queueSetup` (`pending → spawning`). Returns `sessionId`, `setupWorkItemId`, `managedSpawnName`. |
191
+ | POST | `/api/qa/session` | Create session; validates spec, calls `createSession` + `queueSetup` (`pending → spawning`). Body accepts `project: string` (single) OR `projects: string[]` (multi, ≤5; first is primary, rest are co-services). Returns `sessionId`, `setupWorkItemId` (primary's WI), `managedSpawnName`, and (multi only) `projects: string[]`. |
172
192
  | GET | `/api/qa/sessions` | List sessions newest-first. Optional `?limit=N` and `?state=pending\|spawning\|drafting\|awaiting-approval\|executing\|done\|failed\|killed`. |
173
193
  | GET | `/api/qa/sessions/<id>` | Fetch a single session record by id. |
174
194
  | POST | `/api/qa/sessions/<id>/approve` | `awaiting-approval → executing`. Server-side creates the linked `qa-runs` record (synthetic `runbookId='qa-session-<id>'`), queues EXECUTE WI, stamps `qaRunId` on the session. |
@@ -51,14 +51,19 @@ const FEATURES = {
51
51
  // is the canonical predicate every dashboard.js call site uses. Explicit
52
52
  // `engine.ccUseWorkerPool` true/false in config wins ONLY when CC runtime
53
53
  // is copilot; otherwise the helper forces `false`. PR #2492 flipped the
54
- // copilot default ON; see that PR for the cold-spawn measurements.
54
+ // copilot resolver default ON; W-mpqf3ybq000455f2 flipped this registry
55
+ // default ON to match the resolved behavior (so the Settings/`/api/features`
56
+ // surface reports the same default the runtime actually uses). The pool
57
+ // has been stable since PR #2492's cold-spawn measurements; opt-out via
58
+ // `config.features.ccUseWorkerPool: false` or `engine.ccUseWorkerPool: false`
59
+ // still works.
55
60
  //
56
61
  // `requiredCcRuntime: 'copilot'` here is a UX hint — the Settings panel
57
62
  // greys out the toggle when the resolved CC runtime mismatches so users
58
63
  // see the constraint instead of toggling a no-op.
59
64
  'ccUseWorkerPool': {
60
65
  description: 'Route Command Center / doc-chat through a persistent `copilot --acp` worker per tab instead of spawning a fresh CLI per turn (~14s cold-start savings). Copilot-only — has no effect when the CC runtime is Claude.',
61
- default: false,
66
+ default: true,
62
67
  addedIn: '0.1.1916',
63
68
  requiredCcRuntime: 'copilot',
64
69
  },
@@ -325,6 +325,14 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
325
325
  'runner_brief', // generateBrief() output — used by qa-session-draft
326
326
  'runner_execute_brief', // executeBrief() output — used by qa-session-execute
327
327
  'test_file', // session.testFile — set after DRAFT, used by EXECUTE
328
+ // W-mpq6xqzj000606d0 — Multi-project QA Session fan-out. SETUP playbook
329
+ // gets one work item per project; these vars distinguish the "primary"
330
+ // (which owns DRAFT/EXECUTE handoff and the canonical managed-spawn name)
331
+ // from N "co-service" WIs (which just declare their own managed-spawn and
332
+ // exit). Optional because single-project sessions never set them.
333
+ 'role', // 'primary' | 'co-service'
334
+ 'primary_project', // canonical primary project name; empty on single-project sessions
335
+ 'co_services_json', // JSON-encoded co-service project names (e.g. ["api","worker"]); empty when none
328
336
  ]);
329
337
 
330
338
  const PLAYBOOK_REQUIRED_VARS = {