@yemi33/minions 0.1.2082 → 0.1.2084
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/qa.js +41 -2
- package/dashboard/js/settings.js +56 -56
- package/dashboard/js/state.js +1 -1
- package/dashboard/pages/qa.html +3 -2
- package/dashboard.js +27 -6
- package/docs/README.md +4 -1
- package/docs/command-center.md +30 -1
- package/docs/constants.md +32 -0
- package/docs/design-state-storage.md +3 -1
- package/docs/keep-processes.md +47 -0
- package/docs/qa-runbook-lifecycle.md +21 -1
- package/engine/features.js +7 -2
- package/engine/playbook.js +8 -0
- package/engine/qa-sessions.js +316 -42
- package/engine.js +17 -0
- package/package.json +1 -1
- package/playbooks/qa-session-setup.md +32 -0
- package/prompts/cc-system.md +14 -6
package/dashboard/js/qa.js
CHANGED
|
@@ -504,6 +504,7 @@ function qaOpenRunAgent(workItemId, agentId) {
|
|
|
504
504
|
loadQaTargets();
|
|
505
505
|
loadQaRunbooks();
|
|
506
506
|
loadQaRunners();
|
|
507
|
+
loadQaProjectsSelect();
|
|
507
508
|
loadQaSessions();
|
|
508
509
|
_startQaSessionsPoll();
|
|
509
510
|
_startQaRunsPoll();
|
|
@@ -594,6 +595,38 @@ async function loadQaRunners() {
|
|
|
594
595
|
}
|
|
595
596
|
}
|
|
596
597
|
|
|
598
|
+
// W-mpq6xqzj000606d0 — Populate the multi-select projects dropdown on the
|
|
599
|
+
// QA Session form. Prefers the in-memory `cmdProjects` cache (populated by
|
|
600
|
+
// /api/status renders), falling back to a direct /api/status fetch when the
|
|
601
|
+
// QA page is opened in isolation before any other page has loaded.
|
|
602
|
+
async function loadQaProjectsSelect() {
|
|
603
|
+
const sel = document.getElementById('qa-session-projects');
|
|
604
|
+
if (!sel) return;
|
|
605
|
+
let projects = [];
|
|
606
|
+
try {
|
|
607
|
+
if (typeof cmdProjects !== 'undefined' && Array.isArray(cmdProjects) && cmdProjects.length > 0) {
|
|
608
|
+
projects = cmdProjects.slice();
|
|
609
|
+
} else {
|
|
610
|
+
const res = await fetch('/api/status');
|
|
611
|
+
const json = res.ok ? await res.json() : {};
|
|
612
|
+
if (Array.isArray(json && json.projects)) {
|
|
613
|
+
projects = json.projects.map(p => ({ name: p.name, description: p.description || '' }));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch { projects = []; }
|
|
617
|
+
// Preserve current selection across re-renders.
|
|
618
|
+
const previouslySelected = new Set(Array.from(sel.selectedOptions || []).map(o => o.value));
|
|
619
|
+
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
|
620
|
+
for (const p of projects) {
|
|
621
|
+
if (!p || !p.name) continue;
|
|
622
|
+
const opt = document.createElement('option');
|
|
623
|
+
opt.value = p.name;
|
|
624
|
+
opt.textContent = p.name;
|
|
625
|
+
if (previouslySelected.has(p.name)) opt.selected = true;
|
|
626
|
+
sel.appendChild(opt);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
597
630
|
async function loadQaSessions() {
|
|
598
631
|
const root = document.getElementById('qa-sessions-content');
|
|
599
632
|
if (!root) return;
|
|
@@ -773,7 +806,13 @@ async function qaSubmitSessionForm() {
|
|
|
773
806
|
const flowsRaw = (document.getElementById('qa-session-flows') || {}).value || '';
|
|
774
807
|
const mode = (document.getElementById('qa-session-mode') || {}).value || 'confirm';
|
|
775
808
|
const runner = (document.getElementById('qa-session-runner') || {}).value || '';
|
|
776
|
-
|
|
809
|
+
// W-mpq6xqzj000606d0 — Multi-select projects dropdown. First selected =
|
|
810
|
+
// primary (drives DRAFT/EXECUTE); rest = co-services (dev-up only).
|
|
811
|
+
// Empty selection = central (no project).
|
|
812
|
+
const projectsSel = document.getElementById('qa-session-projects');
|
|
813
|
+
const projects = projectsSel
|
|
814
|
+
? Array.from(projectsSel.selectedOptions || []).map(o => o.value).filter(Boolean)
|
|
815
|
+
: [];
|
|
777
816
|
const capture = {
|
|
778
817
|
video: !!(document.getElementById('qa-session-capture-video') || {}).checked,
|
|
779
818
|
screenshots: !!(document.getElementById('qa-session-capture-screenshots') || {}).checked,
|
|
@@ -781,7 +820,7 @@ async function qaSubmitSessionForm() {
|
|
|
781
820
|
};
|
|
782
821
|
const body = { target, flowsRaw, mode, capture };
|
|
783
822
|
if (runner) body.runner = runner;
|
|
784
|
-
if (
|
|
823
|
+
if (projects.length > 0) body.projects = projects;
|
|
785
824
|
|
|
786
825
|
try {
|
|
787
826
|
const res = await fetch('/api/qa/session', {
|
package/dashboard/js/settings.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
108
|
-
'<div style="font-size:
|
|
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:
|
|
114
|
-
'<span style="font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
132
|
-
'<div style="font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
154
|
-
'<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
229
|
-
'<div style="font-size:
|
|
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:
|
|
235
|
-
'<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
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_<NAME>=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:
|
|
359
|
-
'<span style="font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
378
|
-
(f.description ? '<div style="font-size:
|
|
379
|
-
'<div style="font-size:
|
|
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:
|
|
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 & 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
739
|
-
(hint ? '<span style="font-size:
|
|
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:
|
|
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:
|
|
748
|
-
(hint ? '<div style="font-size:
|
|
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
|
|
package/dashboard/js/state.js
CHANGED
|
@@ -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
|
};
|
package/dashboard/pages/qa.html
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
</select>
|
|
18
18
|
</div>
|
|
19
19
|
<div>
|
|
20
|
-
<label class="qa-form-label" for="qa-session-
|
|
21
|
-
<
|
|
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
|
@@ -10273,13 +10273,31 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10273
10273
|
return { wiPath: target.wiPath, project: target.project ? target.project.name : null };
|
|
10274
10274
|
}
|
|
10275
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
|
+
|
|
10276
10294
|
async function handleQaSessionCreate(req, res) {
|
|
10277
10295
|
try {
|
|
10278
10296
|
const body = await readBody(req);
|
|
10279
10297
|
// Validate first so a bad spec returns 400 BEFORE we touch state. The
|
|
10280
10298
|
// body shape is identical to qaSessions.validateSpec — target, flowsRaw,
|
|
10281
|
-
// mode, capture, runner, project, createdBy — so we forward
|
|
10282
|
-
// verbatim.
|
|
10299
|
+
// mode, capture, runner, project | projects, createdBy — so we forward
|
|
10300
|
+
// it verbatim.
|
|
10283
10301
|
const qaSessions = require('./engine/qa-sessions');
|
|
10284
10302
|
const v = qaSessions.validateSpec(body);
|
|
10285
10303
|
if (!v.ok) return jsonReply(res, 400, { error: 'invalid spec', details: v.errors }, req);
|
|
@@ -10294,8 +10312,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10294
10312
|
createSpec.createdBy = originAgent;
|
|
10295
10313
|
}
|
|
10296
10314
|
|
|
10297
|
-
|
|
10298
|
-
|
|
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);
|
|
10299
10320
|
|
|
10300
10321
|
let session;
|
|
10301
10322
|
try { session = qaSessions.createSession(createSpec); }
|
|
@@ -10304,8 +10325,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10304
10325
|
let setupWiId = null;
|
|
10305
10326
|
try {
|
|
10306
10327
|
setupWiId = qaSessions.queueSetup(session.id, {
|
|
10307
|
-
wiPath:
|
|
10308
|
-
project: resolved.project,
|
|
10328
|
+
resolvedTargets: resolution.targets.map(t => ({ project: t.project, wiPath: t.wiPath })),
|
|
10309
10329
|
});
|
|
10310
10330
|
} catch (e) {
|
|
10311
10331
|
// queueSetup throws when pending→spawning is rejected (createSession
|
|
@@ -10324,6 +10344,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10324
10344
|
state: 'spawning',
|
|
10325
10345
|
setupWorkItemId: setupWiId,
|
|
10326
10346
|
managedSpawnName: session.managedSpawnName,
|
|
10347
|
+
projects: resolution.targets.map(t => t.project),
|
|
10327
10348
|
}, req);
|
|
10328
10349
|
} catch (e) {
|
|
10329
10350
|
return jsonReply(res, 500, { error: e.message }, req);
|
package/docs/README.md
CHANGED
|
@@ -15,10 +15,13 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
15
15
|
|
|
16
16
|
- [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
|
|
17
17
|
- [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
|
|
18
|
+
- [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
|
|
18
19
|
- [constellation-bridge.md](constellation-bridge.md) — Read-only cross-repo bridge: `engine.constellationBridge.enabled` flag, marker-file contract, and the `minions bridge` subcommand for local debugging.
|
|
20
|
+
- [cooldown-merge-semantics.md](cooldown-merge-semantics.md) — Scoping deliverable defining merge semantics for `saveCooldowns` (longer-of TTL merge, key-level upserts, gitignored on-disk format).
|
|
19
21
|
- [copilot-cli-schema.md](copilot-cli-schema.md) — Behavior and schema reference for the GitHub Copilot CLI adapter (capability flags, stdin vs `-p`, model discovery, effort levels).
|
|
20
|
-
- [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target.
|
|
22
|
+
- [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target (accepted; implementation tracked in CHANGELOG.md Phases 0–7).
|
|
21
23
|
- [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
|
|
24
|
+
- [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
|
|
22
25
|
- [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
|
|
23
26
|
- [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
|
|
24
27
|
- [pr-comment-followup.md](pr-comment-followup.md) — PR-comment follow-up dispatch contract: fix/review agents may spin off a new WI via `POST /api/work-items` with `meta.pr_followup` instead of broadening the current PR or rebutting the comment.
|
package/docs/command-center.md
CHANGED
|
@@ -4,4 +4,33 @@ The Command Center (CC) is the dashboard's conversational chat panel. It opens f
|
|
|
4
4
|
|
|
5
5
|
CC is intentionally a thin wrapper around the runtime CLI: state changes happen via `Bash`-tool `curl` calls to the dashboard's own REST API, not via parsed delimiter blocks. The end-to-end flow is `dashboard/js/command-center.js` `_ccDoSend()` → `POST /api/command-center` (or `/api/command-center/stream`) in `dashboard.js` (`handleCommandCenter`) → `engine/llm.js` `callLLM({ direct: true })` → claude/copilot CLI session persisted in `engine/cc-sessions.json`. Per-turn API mutations are correlated via the `X-CC-Turn-Id` header and surfaced as standalone `role='action'` chips rendered outside the assistant bubble (`_ccActionResultLine` + `addMsg('action', ...)`).
|
|
6
6
|
|
|
7
|
-
For canonical detail (system prompt, session lifecycle, turn-ID surfacing pipeline, doc-chat integration, and CC API contract), read [`CLAUDE.md`](../CLAUDE.md) — see the **CC API Contract** and **Sessions** sections — and the source in [`dashboard/js/command-center.js`](../dashboard/js/command-center.js), [`dashboard.js`](../dashboard.js) (`handleCommandCenter`), and [`prompts/cc-system.md`](../prompts/cc-system.md).
|
|
7
|
+
For canonical detail (system prompt, session lifecycle, turn-ID surfacing pipeline, doc-chat integration, and CC API contract), read [`CLAUDE.md`](../CLAUDE.md) — see the **CC API Contract** and **Sessions** sections — and the source in [`dashboard/js/command-center.js`](../dashboard/js/command-center.js), [`dashboard.js`](../dashboard.js) (`handleCommandCenter`), and [`prompts/cc-system.md`](../prompts/cc-system.md).
|
|
8
|
+
|
|
9
|
+
## Error surfacing contract (W-mpmwxni2000c25c7-d)
|
|
10
|
+
|
|
11
|
+
Both CC and Doc-Chat emit a typed error envelope so the dashboard can render a red `.cc-error` bubble (role=alert) with a Retry button and stop the spinner immediately. Errors arrive in two shapes:
|
|
12
|
+
|
|
13
|
+
1. **SSE mid-stream** (most common). `writeCcEvent` / `writeDocEvent` write the wire as `event: error\ndata: {…envelope…}\n\n` so consumers using `addEventListener('error', …)` see them as named events. The JSON payload still carries `type: 'error'` for clients that only read the `data:` line, so both parser strategies keep working.
|
|
14
|
+
2. **Non-2xx POST response** (pre-stream — `readBody` guard, prototype-pollution rejection, etc.). The body is the same envelope as JSON; the dashboard's `if (!res.ok)` branch parses it and stashes it on the thrown Error as `_ccErrorEnvelope`, then renders the same red bubble.
|
|
15
|
+
|
|
16
|
+
Canonical envelope (`_buildCcErrorEnvelope` in `dashboard.js`):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{ "type": "error",
|
|
20
|
+
"message": "Human-readable cause + remediation hint",
|
|
21
|
+
"code": "model-unavailable | auth-failure | context-limit | budget-exceeded | crash | cc-turn-timeout | worker-spawn-failed | acp-handshake-failed | worker-died",
|
|
22
|
+
"retryable": false,
|
|
23
|
+
"availableModels": ["gpt-4o", "gpt-5.4", "..."],
|
|
24
|
+
"runtime": "copilot"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`code` is clamped to the allowlist (`CC_ERROR_CODES` constant); unknown codes collapse to `crash`. `retryable: true` is informational — there is **no auto-retry**; the dashboard always offers a manual Retry button instead. Auto-retrying these errors is a footgun because most are operator-fix categories (auth, budget, missing model) where re-spawning makes no progress.
|
|
29
|
+
|
|
30
|
+
**Watchdog (`engine.ccTurnTimeoutMs`, default 5 min, clamped 10s–1h).** Each turn arms a `setTimeout` that fires `event: error` with `code: 'cc-turn-timeout'`, aborts the in-flight LLM call, and ends the stream when no terminal event (`done`/`error`) arrives in time. Independent of `CC_CALL_TIMEOUT_MS` (the outer 1h hard ceiling); the watchdog is the *visible-to-user* no-progress cap. Surfaced in Settings → CC overrides.
|
|
31
|
+
|
|
32
|
+
**No auto-retry policy.** The backend never re-spawns the LLM after an error envelope. The client never silently resends the user's turn. Retry is a single-click manual action — guards against silent budget burn on `budget-exceeded`, infinite loops on `auth-failure`, and accidental re-charges on `context-limit`. The 429 + reconnect paths (rate-limited fetch retry, SSE reconnect-after-disconnect) remain — those are transport-level, not error-envelope-level.
|
|
33
|
+
|
|
34
|
+
## Per-turn surfacing pipeline
|
|
35
|
+
|
|
36
|
+
CC handler generates `ccTurnId = 'cct-' + shared.uid()` per request; injected into sysprompt AND prompt body via `_ccTurnHeaderPart(turnId)` (load-bearing: on resumed sessions `engine/llm.js` skips re-sending the sysprompt, so without body injection CC keeps the stale turn ID). Handler reads via `_readCcTurnIdHeader(req)` and calls `_recordCcTurnCreation(turnId, ...)` on success. End-of-turn: `_buildSyntheticActionResultsForTurn` produces synthetic `{action, result}` pairs (`_serverExecuted: true`). Client renders as standalone `role='action'` messages outside the assistant bubble. TTL: 5 min. Endpoints wired: `/api/work-items`, `/api/notes`, `/api/plan`, `/api/knowledge`, `/api/watches`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Constants — No Magic Strings
|
|
2
|
+
|
|
3
|
+
All cross-cutting status / type / condition values are defined in [`engine/shared.js`](../engine/shared.js). Engine and dashboard code **never** compares against raw strings; tests enforce.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
WI_STATUS = { PENDING, DISPATCHED, DONE, FAILED, PAUSED, QUEUED, DECOMPOSED, CANCELLED }
|
|
7
|
+
DONE_STATUSES = Set([WI_STATUS.DONE, 'in-pr', 'implemented', 'complete']) // legacy aliases on read only
|
|
8
|
+
WORK_TYPE = { IMPLEMENT, IMPLEMENT_LARGE, FIX, REVIEW, VERIFY, PLAN, PLAN_TO_PRD,
|
|
9
|
+
DECOMPOSE, MEETING, EXPLORE, ASK, TEST, DOCS, SETUP }
|
|
10
|
+
PLAN_STATUS = { ACTIVE, AWAITING_APPROVAL, APPROVED, PAUSED, REJECTED, COMPLETED, REVISION_REQUESTED }
|
|
11
|
+
PRD_ITEM_STATUS = { MISSING, UPDATED, DONE }; PRD_MATERIALIZABLE = Set([MISSING, UPDATED])
|
|
12
|
+
PR_STATUS = { ACTIVE, MERGED, ABANDONED, CLOSED, LINKED }; PR_POLLABLE_STATUSES = Set([ACTIVE, LINKED])
|
|
13
|
+
DISPATCH_RESULT = { SUCCESS, ERROR, TIMEOUT }
|
|
14
|
+
WATCH_STATUS = { ACTIVE, PAUSED, TRIGGERED, EXPIRED }
|
|
15
|
+
WATCH_CONDITION = { MERGED, BUILD_FAIL, BUILD_PASS, COMPLETED, FAILED, STATUS_CHANGE, ANY, NEW_COMMENTS,
|
|
16
|
+
VOTE_CHANGE, CONCLUDED, APPROVED, REJECTED, STAGE_COMPLETE, RAN, ENABLED, DISABLED, ACTIVITY_CHANGE,
|
|
17
|
+
HEAD_COMMIT_CHANGE, MERGEABLE_FLIPPED, READY_FOR_MERGE, BEHIND_MASTER, DRAFT_FLIPPED,
|
|
18
|
+
STALLED, RETRY_LIMIT_REACHED, DEPENDENCY_MET, ALL_ITEMS_DONE, ITEM_FAILED_N_TIMES,
|
|
19
|
+
STAGE_ADVANCED, STUCK_IN_STAGE }
|
|
20
|
+
WATCH_ABSOLUTE_CONDITIONS = Set([MERGED, BUILD_FAIL, BUILD_PASS, COMPLETED, FAILED, CONCLUDED, APPROVED,
|
|
21
|
+
REJECTED, READY_FOR_MERGE, RETRY_LIMIT_REACHED, ALL_ITEMS_DONE, ITEM_FAILED_N_TIMES]) // fire-once
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Engine defaults
|
|
25
|
+
|
|
26
|
+
Retry/timeout limits, sweep cadences, fleet ceilings, and managed-spawn caps live in `ENGINE_DEFAULTS` (also in `engine/shared.js`). Read defaults from there rather than re-declaring; per-deployment overrides go in `config.engine.*` and resolve through the per-knob helpers (`resolveAgentMaxBudget`, etc.).
|
|
27
|
+
|
|
28
|
+
## Invariants
|
|
29
|
+
|
|
30
|
+
- **Write only `WI_STATUS.DONE`.** Legacy aliases (`in-pr`, `implemented`, `complete`) are accepted on read for backward compat but never written. `updateWorkItemStatus()` validates writes against `WI_STATUS`.
|
|
31
|
+
- **`mutateJsonFileLocked` for RMW.** All shared-JSON status writes go through the locked mutator wrappers (`mutateDispatch`, `mutateWorkItems`, `mutatePullRequests`) — see [`CLAUDE.md`](../CLAUDE.md) → **Concurrency**.
|
|
32
|
+
- **No string comparisons.** `pr.status === 'active'` ⇒ `pr.status === PR_STATUS.ACTIVE`. Source-inspection tests grep for the constant form.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Design: Replacing File-Based State with a Structured Database
|
|
2
2
|
|
|
3
|
-
> Author: Rebecca (Architect) | Date: 2026-04-07 | Status:
|
|
3
|
+
> Author: Rebecca (Architect) | Date: 2026-04-07 | Status: **Accepted — implementation in progress**
|
|
4
|
+
|
|
5
|
+
> **Implementation status (as of 2026-05):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–7 have shipped (events, dispatches, work_items, pull_requests, logs, metrics, watches, schedule_runs + pipeline_runs + managed_processes + worktree_pool — see `CHANGELOG.md`). The SQLite schema lives under `engine/db/migrations/` and the singleton opens `engine/state.db` in WAL mode. The "Phase 2: estimated Node 26 LTS" timeline in §3 is now historical context; treat sections 1–3 as design rationale rather than a forward plan.
|
|
4
6
|
|
|
5
7
|
## Executive Summary
|
|
6
8
|
|