@yemi33/minions 0.1.2305 → 0.1.2307
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/bin/minions.js +1 -0
- package/dashboard/js/refresh.js +17 -13
- package/dashboard/js/render-other.js +251 -3
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prs.js +4 -1
- package/dashboard/js/render-utils.js +13 -0
- package/dashboard/js/render-work-items.js +11 -13
- package/dashboard/js/settings.js +30 -3
- package/dashboard/slim/body.html +6 -22
- package/dashboard/slim/js/modals-tiles.js +32 -22
- package/dashboard/slim/js/plans.js +24 -516
- package/dashboard/slim/styles.css +10 -7
- package/dashboard/styles.css +58 -0
- package/dashboard.js +66 -9
- package/docs/completion-reports.md +27 -2
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/harness-transparency.md +18 -0
- package/docs/live-checkout-mode.md +57 -8
- package/engine/ado-comment.js +5 -2
- package/engine/ado.js +37 -0
- package/engine/cli.js +58 -3
- package/engine/comment-format.js +82 -2
- package/engine/consolidation.js +59 -2
- package/engine/discover-project-skills.js +4 -0
- package/engine/dispatch.js +36 -1
- package/engine/gh-comment.js +8 -3
- package/engine/lifecycle.js +241 -25
- package/engine/live-checkout.js +335 -7
- package/engine/playbook.js +2 -2
- package/engine/pre-dispatch-eval.js +54 -5
- package/engine/queries.js +11 -7
- package/engine/shared.js +96 -6
- package/engine/supervisor.js +144 -19
- package/engine/watchdog.js +10 -0
- package/engine.js +210 -5
- package/package.json +1 -1
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +9 -0
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
bindModalClose('slim-agent-modal', 'slim-agent-close', _stopAgentDetailRuntime);
|
|
18
18
|
bindModalClose('slim-tools-modal', 'slim-tools-close');
|
|
19
19
|
bindModalClose('slim-tile-modal', 'slim-tile-close');
|
|
20
|
-
bindModalClose('slim-plans-modal', 'slim-plans-close');
|
|
21
20
|
|
|
22
21
|
function tileEmpty(body, text) {
|
|
23
22
|
var d = document.createElement('div');
|
|
@@ -142,6 +141,25 @@
|
|
|
142
141
|
body.appendChild(frame);
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
// Plans tile body — reuses the LITERAL classic dashboard /plans screen instead
|
|
145
|
+
// of a slim-specific tabbed Plans/PRD flyout, so there is one Plans/PRD UI, not
|
|
146
|
+
// two (W-mr28ko750010199f / "reuse, don't fork" — mirrors renderQueuedWorkBody
|
|
147
|
+
// for the Work tile). Slim and classic are two separate IIFE bundles with no
|
|
148
|
+
// shared scope, so we embed the real /plans screen in an iframe with the
|
|
149
|
+
// chrome-off ?embed=1 mode. The iframed page IS the classic screen — the full
|
|
150
|
+
// Plans + PRD tabbed UI with every lifecycle action (approve/execute/reject/
|
|
151
|
+
// pause/regenerate/unarchive/archive/delete), backed by the same /api/plans +
|
|
152
|
+
// /api/prd data + endpoints — with zero duplicated rendering logic. Classic is
|
|
153
|
+
// reachable at /plans even with slim-ux ON (only / is taken over). Built with
|
|
154
|
+
// createElement (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
155
|
+
function renderPlansBody(body) {
|
|
156
|
+
var frame = document.createElement('iframe');
|
|
157
|
+
frame.className = 'slim-plans-embed';
|
|
158
|
+
frame.src = '/plans?embed=1';
|
|
159
|
+
frame.title = 'Plans';
|
|
160
|
+
body.appendChild(frame);
|
|
161
|
+
}
|
|
162
|
+
|
|
145
163
|
function renderPrTileBody(body, data) {
|
|
146
164
|
// Emoji glyphs make build/review status scannable at a glance; the
|
|
147
165
|
// accompanying label keeps the row readable when the glyph alone is
|
|
@@ -264,6 +282,7 @@
|
|
|
264
282
|
engine: { title: 'Engine', render: renderEngineTileBody },
|
|
265
283
|
dispatches: { title: 'Active dispatches', render: function(body, data) { renderDispatchTileBody(body, data, true); } },
|
|
266
284
|
queued: { title: 'Queued work', render: function(body) { renderQueuedWorkBody(body); } },
|
|
285
|
+
plans: { title: 'Plans', render: function(body) { renderPlansBody(body); } },
|
|
267
286
|
prs: { title: 'Pull requests', render: renderPrTileBody },
|
|
268
287
|
watches: { title: 'Watches', render: renderWatchTileBody },
|
|
269
288
|
};
|
|
@@ -274,9 +293,6 @@
|
|
|
274
293
|
// The Knowledge tile opens the unified Knowledge control panel (Pinned
|
|
275
294
|
// Context + Notes + KB tabs) rather than the read-only tile detail view.
|
|
276
295
|
if (key === 'knowledge') { openKnowledgeModal(); return; }
|
|
277
|
-
// The Plans tile opens the tabbed Plans/PRD control panel (rendered by
|
|
278
|
-
// dashboard/slim/js/plans.js) rather than the read-only tile detail view.
|
|
279
|
-
if (key === 'plans') { openPlansModal(); return; }
|
|
280
296
|
var view = TILE_VIEWS[key];
|
|
281
297
|
if (!view) return;
|
|
282
298
|
var modal = document.getElementById('slim-tile-modal');
|
|
@@ -288,9 +304,11 @@
|
|
|
288
304
|
// The "+ Link PR" header chip only applies to the PR list.
|
|
289
305
|
var headerChip = document.getElementById('slim-tile-modal-linkpr');
|
|
290
306
|
if (headerChip) headerChip.style.display = (key === 'prs') ? '' : 'none';
|
|
291
|
-
// The queued
|
|
292
|
-
// the modal + drop the body padding so the embedded
|
|
307
|
+
// The queued + plans tiles embed a full classic screen (/work, /plans) in an
|
|
308
|
+
// iframe — widen the modal + drop the body padding so the embedded screen
|
|
309
|
+
// gets real estate. Both reuse the shared wide-modal treatment.
|
|
293
310
|
modal.classList.toggle('tile-modal--work', key === 'queued');
|
|
311
|
+
modal.classList.toggle('tile-modal--plans', key === 'plans');
|
|
294
312
|
view.render(body, lastStatusData || {});
|
|
295
313
|
modal.classList.add('open');
|
|
296
314
|
}
|
|
@@ -316,22 +334,14 @@
|
|
|
316
334
|
}
|
|
317
335
|
})();
|
|
318
336
|
|
|
319
|
-
// ── Plans
|
|
320
|
-
// The Plans tile opens
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (tabBar) {
|
|
328
|
-
tabBar.addEventListener('click', function(ev) {
|
|
329
|
-
var btn = ev.target && ev.target.closest ? ev.target.closest('.kn-tab') : null;
|
|
330
|
-
if (!btn) return;
|
|
331
|
-
var tab = btn.getAttribute('data-plans-tab');
|
|
332
|
-
if (tab && typeof setPlansTab === 'function') setPlansTab(tab);
|
|
333
|
-
});
|
|
334
|
-
}
|
|
337
|
+
// ── Plans tile wiring ──────────────────────────────────────────────
|
|
338
|
+
// The Plans tile opens the classic /plans screen in an iframe (routed through
|
|
339
|
+
// the standard TILE_VIEWS path in openTileModal above) — there is no
|
|
340
|
+
// slim-specific Plans/PRD flyout to wire. The cockpit tile's at-a-glance count
|
|
341
|
+
// is still slim-owned (the plan/PRD count is not part of the /api/status poll),
|
|
342
|
+
// so it is fetched lazily from /api/plans shortly after first paint, mirroring
|
|
343
|
+
// knowledge.js#loadKnowledgeCounts. See dashboard/slim/js/plans.js.
|
|
344
|
+
(function bindPlansTileCount() {
|
|
335
345
|
if (typeof loadPlansCounts === 'function') setTimeout(loadPlansCounts, 1500);
|
|
336
346
|
})();
|
|
337
347
|
|
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
|
|
2
|
-
// ── Plans
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// regenerate,archive,delete,unarchive}.
|
|
10
|
-
// PRD — /api/prd (progress.items × PRs × verify), with the .json entries
|
|
11
|
-
// from /api/plans for the per-PRD lifecycle status, and
|
|
12
|
-
// /api/work-items for the verify task row.
|
|
2
|
+
// ── Plans cockpit tile (count only) ────────────────────────────────
|
|
3
|
+
// The Plans tile opens the LITERAL classic /plans screen in an iframe
|
|
4
|
+
// (W-mr28ko750010199f / "reuse, don't fork" — see modals-tiles.js
|
|
5
|
+
// renderPlansBody), so there is NO slim-specific Plans/PRD flyout, renderer, or
|
|
6
|
+
// lifecycle-action layer here anymore. The classic /plans screen is the single
|
|
7
|
+
// source of truth for Plans/PRD rendering + every lifecycle action
|
|
8
|
+
// (approve/execute/reject/pause/regenerate/unarchive/archive/delete).
|
|
13
9
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
10
|
+
// What survives in slim is only the cockpit tile's at-a-glance count: the
|
|
11
|
+
// plan/PRD count is not part of the /api/status poll, so it is fetched lazily
|
|
12
|
+
// from the existing /api/plans list (NO new server route), mirroring
|
|
13
|
+
// knowledge.js#loadKnowledgeCounts. status.js repaints the tile from the
|
|
14
|
+
// last-known cached count on each status render; modals-tiles.js seeds it once
|
|
15
|
+
// shortly after first paint.
|
|
20
16
|
|
|
21
|
-
var _plansActiveTab = 'plans';
|
|
22
|
-
var _plansData = null; // cached /api/plans payload (array)
|
|
23
|
-
var _prdData = null; // cached /api/prd payload ({ progress, status })
|
|
24
|
-
var _plansVerifyWis = null; // cached verify work items (itemType === 'verify')
|
|
25
17
|
var _plansCount = 0; // last-known active plan + PRD count (tile)
|
|
26
18
|
|
|
27
19
|
// Plan statuses that count as "active" (in-flight / needs attention) for the
|
|
@@ -31,32 +23,16 @@
|
|
|
31
23
|
var PLAN_TERMINAL_STATUSES = { completed: 1, converted: 1, rejected: 1 };
|
|
32
24
|
function _isActivePlanStatus(s) { return !!s && !PLAN_TERMINAL_STATUSES[s]; }
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (s === 'completed' || s === 'approved') return 'green';
|
|
43
|
-
if (s === 'dispatched' || s === 'converting' || s === 'converted') return 'blue';
|
|
44
|
-
if (s === 'awaiting-approval' || s === 'paused' || s === 'revision-requested') return 'amber';
|
|
45
|
-
if (s === 'rejected' || s === 'has-failures') return 'red';
|
|
46
|
-
return '';
|
|
47
|
-
}
|
|
48
|
-
// Map a work-item status (done / in-progress / missing / paused / …) onto a
|
|
49
|
-
// .tile-chip color class.
|
|
50
|
-
function _wiStatusChipClass(s) {
|
|
51
|
-
if (s === 'done' || s === 'decomposed') return 'green';
|
|
52
|
-
if (s === 'in-progress') return 'blue';
|
|
53
|
-
if (s === 'paused' || s === 'blocked') return 'amber';
|
|
54
|
-
if (s === 'failed') return 'red';
|
|
55
|
-
return '';
|
|
26
|
+
function _countActivePlans(plans) {
|
|
27
|
+
if (!Array.isArray(plans)) return 0;
|
|
28
|
+
var c = 0;
|
|
29
|
+
for (var i = 0; i < plans.length; i++) {
|
|
30
|
+
var p = plans[i];
|
|
31
|
+
if (p && !p.archived && _isActivePlanStatus(p.status)) c++;
|
|
32
|
+
}
|
|
33
|
+
return c;
|
|
56
34
|
}
|
|
57
|
-
function _chipClassAttr(cls) { return 'tile-chip' + (cls ? ' ' + cls : ''); }
|
|
58
35
|
|
|
59
|
-
// ── Cockpit tile ───────────────────────────────────────────────────
|
|
60
36
|
// Repaints the Plans tile. Called from status.js's render path and from the
|
|
61
37
|
// lazy count loader. The count is the number of active (non-terminal,
|
|
62
38
|
// non-archived) plan + PRD entries.
|
|
@@ -67,485 +43,17 @@
|
|
|
67
43
|
updateTile('plans', n, detail, n ? 'blue' : null);
|
|
68
44
|
}
|
|
69
45
|
|
|
70
|
-
function _countActivePlans(plans) {
|
|
71
|
-
if (!Array.isArray(plans)) return 0;
|
|
72
|
-
var c = 0;
|
|
73
|
-
for (var i = 0; i < plans.length; i++) {
|
|
74
|
-
var p = plans[i];
|
|
75
|
-
if (p && !p.archived && _isActivePlanStatus(p.status)) c++;
|
|
76
|
-
}
|
|
77
|
-
return c;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
46
|
// Best-effort lazy fetch of the plan list to compute the tile count, mirroring
|
|
81
47
|
// knowledge.js#loadKnowledgeCounts. A transient failure leaves the last-known
|
|
82
|
-
// count untouched.
|
|
48
|
+
// count untouched. Uses the existing /api/plans list endpoint — no new route.
|
|
83
49
|
async function loadPlansCounts() {
|
|
84
50
|
try {
|
|
85
51
|
var res = await fetch('/api/plans', { headers: { 'Accept': 'application/json' } });
|
|
86
52
|
if (res.ok) {
|
|
87
|
-
|
|
88
|
-
renderPlansTile(_countActivePlans(
|
|
53
|
+
var plans = await res.json();
|
|
54
|
+
renderPlansTile(_countActivePlans(plans));
|
|
89
55
|
return;
|
|
90
56
|
}
|
|
91
57
|
} catch (e) { /* keep last-known */ }
|
|
92
58
|
renderPlansTile();
|
|
93
59
|
}
|
|
94
|
-
|
|
95
|
-
// ── Modal open/close + tab switching ───────────────────────────────
|
|
96
|
-
function openPlansModal() {
|
|
97
|
-
var modal = document.getElementById('slim-plans-modal');
|
|
98
|
-
if (!modal) return;
|
|
99
|
-
renderPlansTab();
|
|
100
|
-
modal.classList.add('open');
|
|
101
|
-
}
|
|
102
|
-
function closePlansModal() {
|
|
103
|
-
var modal = document.getElementById('slim-plans-modal');
|
|
104
|
-
if (modal) modal.classList.remove('open');
|
|
105
|
-
}
|
|
106
|
-
function setPlansTab(tab) {
|
|
107
|
-
_plansActiveTab = tab;
|
|
108
|
-
var tabs = document.querySelectorAll('#slim-plans-tabs .kn-tab');
|
|
109
|
-
var activeTabId = null;
|
|
110
|
-
for (var i = 0; i < tabs.length; i++) {
|
|
111
|
-
var isActive = tabs[i].getAttribute('data-plans-tab') === tab;
|
|
112
|
-
tabs[i].classList.toggle('active', isActive);
|
|
113
|
-
tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
114
|
-
if (isActive) activeTabId = tabs[i].id;
|
|
115
|
-
}
|
|
116
|
-
var body = document.getElementById('slim-plans-body');
|
|
117
|
-
if (body && activeTabId) body.setAttribute('aria-labelledby', activeTabId);
|
|
118
|
-
renderPlansTab();
|
|
119
|
-
}
|
|
120
|
-
function renderPlansTab() {
|
|
121
|
-
var body = document.getElementById('slim-plans-body');
|
|
122
|
-
if (!body) return;
|
|
123
|
-
body.textContent = '';
|
|
124
|
-
if (_plansActiveTab === 'prd') renderPlansPrdTab(body);
|
|
125
|
-
else renderPlansPlansTab(body);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function _plansMsg(text, color) {
|
|
129
|
-
var msg = document.getElementById('slim-plans-msg');
|
|
130
|
-
if (!msg) return;
|
|
131
|
-
msg.style.color = color || 'var(--muted)';
|
|
132
|
-
msg.textContent = text || '';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Tab: Plans (lifecycle view, one card per source draft) ─────────
|
|
136
|
-
function renderPlansPlansTab(body) {
|
|
137
|
-
var intro = document.createElement('p');
|
|
138
|
-
intro.textContent = 'Plan drafts and their lifecycle. Approve to use the materialized PRD as-is, Execute to (re)generate a PRD, or Archive when done.';
|
|
139
|
-
body.appendChild(intro);
|
|
140
|
-
|
|
141
|
-
var msg = document.createElement('div');
|
|
142
|
-
msg.id = 'slim-plans-msg';
|
|
143
|
-
msg.className = 'kn-msg';
|
|
144
|
-
body.appendChild(msg);
|
|
145
|
-
|
|
146
|
-
var list = document.createElement('div');
|
|
147
|
-
list.id = 'slim-plans-list';
|
|
148
|
-
body.appendChild(list);
|
|
149
|
-
|
|
150
|
-
if (_plansData) renderPlansList(list);
|
|
151
|
-
else {
|
|
152
|
-
var loading = document.createElement('div');
|
|
153
|
-
loading.className = 'tile-empty';
|
|
154
|
-
loading.textContent = 'Loading plans…';
|
|
155
|
-
list.appendChild(loading);
|
|
156
|
-
}
|
|
157
|
-
// Always refresh on open so newly-created/converted plans appear.
|
|
158
|
-
fetch('/api/plans', { headers: { 'Accept': 'application/json' } })
|
|
159
|
-
.then(function(r) { return r.ok ? r.json() : null; })
|
|
160
|
-
.then(function(data) {
|
|
161
|
-
if (!data) return;
|
|
162
|
-
_plansData = data;
|
|
163
|
-
renderPlansTile(_countActivePlans(_plansData));
|
|
164
|
-
if (_plansActiveTab === 'plans') {
|
|
165
|
-
var lw = document.getElementById('slim-plans-list');
|
|
166
|
-
if (lw) renderPlansList(lw);
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
-
.catch(function() { /* keep cached */ });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Build the logical lifecycle cards: one per non-archived .md draft, plus any
|
|
173
|
-
// non-archived PRD whose source draft isn't itself listed (directly-created
|
|
174
|
-
// PRDs). Each card resolves its effective status + action target from the
|
|
175
|
-
// linked PRD when one exists.
|
|
176
|
-
function _buildPlanCards(plans) {
|
|
177
|
-
var drafts = [];
|
|
178
|
-
var prds = [];
|
|
179
|
-
for (var i = 0; i < plans.length; i++) {
|
|
180
|
-
var p = plans[i];
|
|
181
|
-
if (!p || p.archived) continue;
|
|
182
|
-
if (p.format === 'prd') prds.push(p);
|
|
183
|
-
else drafts.push(p);
|
|
184
|
-
}
|
|
185
|
-
var prdBySource = {};
|
|
186
|
-
for (var j = 0; j < prds.length; j++) {
|
|
187
|
-
var sp = prds[j].sourcePlan;
|
|
188
|
-
if (sp) prdBySource[sp.replace(/^plans\//, '')] = prds[j];
|
|
189
|
-
}
|
|
190
|
-
var cards = [];
|
|
191
|
-
var claimedPrd = {};
|
|
192
|
-
for (var k = 0; k < drafts.length; k++) {
|
|
193
|
-
var d = drafts[k];
|
|
194
|
-
var linked = prdBySource[d.file] || null;
|
|
195
|
-
if (linked) claimedPrd[linked.file] = true;
|
|
196
|
-
cards.push({
|
|
197
|
-
draftFile: d.file,
|
|
198
|
-
prdFile: linked ? linked.file : '',
|
|
199
|
-
target: linked ? linked.file : d.file,
|
|
200
|
-
summary: d.summary || d.file,
|
|
201
|
-
project: d.project || '',
|
|
202
|
-
status: linked ? (linked.status || 'active') : (d.status || 'draft'),
|
|
203
|
-
generatedBy: d.generatedBy || (linked && linked.generatedBy) || '',
|
|
204
|
-
generatedAt: d.generatedAt || (linked && linked.generatedAt) || '',
|
|
205
|
-
itemCount: linked ? linked.itemCount : d.itemCount,
|
|
206
|
-
planStale: !!(linked && linked.planStale),
|
|
207
|
-
archiveReady: !!(linked && linked.archiveReady),
|
|
208
|
-
requiresApproval: linked ? !!linked.requiresApproval : false,
|
|
209
|
-
readFile: d.file,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
// Directly-created PRDs with no listed source draft.
|
|
213
|
-
for (var m = 0; m < prds.length; m++) {
|
|
214
|
-
var pr = prds[m];
|
|
215
|
-
if (claimedPrd[pr.file]) continue;
|
|
216
|
-
cards.push({
|
|
217
|
-
draftFile: pr.file,
|
|
218
|
-
prdFile: pr.file,
|
|
219
|
-
target: pr.file,
|
|
220
|
-
summary: pr.summary || pr.file,
|
|
221
|
-
project: pr.project || '',
|
|
222
|
-
status: pr.status || 'active',
|
|
223
|
-
generatedBy: pr.generatedBy || '',
|
|
224
|
-
generatedAt: pr.generatedAt || '',
|
|
225
|
-
itemCount: pr.itemCount,
|
|
226
|
-
planStale: !!pr.planStale,
|
|
227
|
-
archiveReady: !!pr.archiveReady,
|
|
228
|
-
requiresApproval: !!pr.requiresApproval,
|
|
229
|
-
readFile: pr.file,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
cards.sort(function(a, b) { return (b.generatedAt || '').localeCompare(a.generatedAt || ''); });
|
|
233
|
-
return cards;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function renderPlansList(listWrap) {
|
|
237
|
-
listWrap.textContent = '';
|
|
238
|
-
var cards = _buildPlanCards(_plansData || []);
|
|
239
|
-
if (!cards.length) {
|
|
240
|
-
var empty = document.createElement('div');
|
|
241
|
-
empty.className = 'tile-empty';
|
|
242
|
-
empty.textContent = 'No active plans. Drafts you create land here for approval and execution.';
|
|
243
|
-
listWrap.appendChild(empty);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
cards.forEach(function(card) { listWrap.appendChild(buildPlanRow(card)); });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function buildPlanRow(card) {
|
|
250
|
-
var row = document.createElement('div');
|
|
251
|
-
row.className = 'kb-row';
|
|
252
|
-
|
|
253
|
-
var top = document.createElement('div');
|
|
254
|
-
top.className = 'kb-row-top';
|
|
255
|
-
|
|
256
|
-
var chip = document.createElement('span');
|
|
257
|
-
chip.className = _chipClassAttr(_planStatusChipClass(card.status));
|
|
258
|
-
chip.textContent = PLAN_STATUS_LABELS[card.status] || card.status;
|
|
259
|
-
top.appendChild(chip);
|
|
260
|
-
|
|
261
|
-
var title = document.createElement('span');
|
|
262
|
-
title.className = 'kb-row-title';
|
|
263
|
-
title.textContent = card.summary;
|
|
264
|
-
title.title = card.summary;
|
|
265
|
-
top.appendChild(title);
|
|
266
|
-
row.appendChild(top);
|
|
267
|
-
|
|
268
|
-
var meta = document.createElement('div');
|
|
269
|
-
meta.className = 'kb-row-meta';
|
|
270
|
-
var metaBits = [];
|
|
271
|
-
if (card.project) metaBits.push(card.project);
|
|
272
|
-
if (typeof card.itemCount === 'number') metaBits.push(card.itemCount + ' item' + (card.itemCount === 1 ? '' : 's'));
|
|
273
|
-
if (card.generatedBy) metaBits.push(card.generatedBy);
|
|
274
|
-
if (card.generatedAt) metaBits.push(card.generatedAt);
|
|
275
|
-
if (card.planStale) metaBits.push('STALE');
|
|
276
|
-
meta.textContent = metaBits.join(' · ');
|
|
277
|
-
row.appendChild(meta);
|
|
278
|
-
|
|
279
|
-
// Action buttons. Clicking the row (away from a button) opens the draft.
|
|
280
|
-
var actions = document.createElement('div');
|
|
281
|
-
actions.className = 'kn-toolbar kn-toolbar-end';
|
|
282
|
-
actions.addEventListener('click', function(ev) { ev.stopPropagation(); });
|
|
283
|
-
|
|
284
|
-
var status = card.status;
|
|
285
|
-
var awaitingApproval = status === 'awaiting-approval' || (card.requiresApproval && status !== 'completed' && status !== 'rejected' && status !== 'dispatched');
|
|
286
|
-
if (awaitingApproval) {
|
|
287
|
-
actions.appendChild(_planActionBtn('Approve', 'btn-primary', function() { plansApprove(card.target); }));
|
|
288
|
-
actions.appendChild(_planActionBtn('Reject', 'btn-secondary', function() { plansReject(card.target); }));
|
|
289
|
-
} else if (status === 'paused') {
|
|
290
|
-
actions.appendChild(_planActionBtn('Resume', 'btn-primary', function() { plansApprove(card.target); }));
|
|
291
|
-
} else if (status === 'dispatched') {
|
|
292
|
-
actions.appendChild(_planActionBtn('Pause', 'btn-secondary', function() { plansPause(card.target); }));
|
|
293
|
-
}
|
|
294
|
-
// Execute: a draft with no PRD yet (re)generates one.
|
|
295
|
-
if (!card.prdFile && (status === 'draft' || status === 'active')) {
|
|
296
|
-
actions.appendChild(_planActionBtn('Execute', 'btn-primary', function() { plansExecute(card.draftFile, card.project); }));
|
|
297
|
-
}
|
|
298
|
-
actions.appendChild(_planActionBtn(card.archiveReady ? '✓ Archive' : 'Archive', 'btn-secondary', function() { plansArchive(card.target); }));
|
|
299
|
-
actions.appendChild(_planActionBtn('Delete', 'btn-secondary', function() { plansDelete(card.draftFile); }));
|
|
300
|
-
row.appendChild(actions);
|
|
301
|
-
|
|
302
|
-
row.addEventListener('click', function() { openPlanDraft(card.readFile); });
|
|
303
|
-
return row;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function _planActionBtn(label, cls, onClick) {
|
|
307
|
-
var b = document.createElement('button');
|
|
308
|
-
b.className = cls;
|
|
309
|
-
b.type = 'button';
|
|
310
|
-
b.textContent = label;
|
|
311
|
-
b.addEventListener('click', function(ev) { ev.stopPropagation(); onClick(); });
|
|
312
|
-
return b;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Read a plan draft (.md markdown or .json PRD) into a <pre>.
|
|
316
|
-
async function openPlanDraft(file) {
|
|
317
|
-
var body = document.getElementById('slim-plans-body');
|
|
318
|
-
if (!body) return;
|
|
319
|
-
body.textContent = '';
|
|
320
|
-
|
|
321
|
-
var backRow = document.createElement('div');
|
|
322
|
-
backRow.className = 'kn-toolbar';
|
|
323
|
-
var backBtn = document.createElement('button');
|
|
324
|
-
backBtn.className = 'btn-secondary';
|
|
325
|
-
backBtn.type = 'button';
|
|
326
|
-
backBtn.textContent = '← Back to plans';
|
|
327
|
-
backBtn.addEventListener('click', function() { renderPlansTab(); });
|
|
328
|
-
backRow.appendChild(backBtn);
|
|
329
|
-
body.appendChild(backRow);
|
|
330
|
-
|
|
331
|
-
var head = document.createElement('div');
|
|
332
|
-
head.className = 'kn-section-head';
|
|
333
|
-
head.textContent = file;
|
|
334
|
-
body.appendChild(head);
|
|
335
|
-
|
|
336
|
-
var pre = document.createElement('pre');
|
|
337
|
-
pre.className = 'kb-entry-content';
|
|
338
|
-
pre.textContent = 'Loading…';
|
|
339
|
-
body.appendChild(pre);
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
var res = await fetch('/api/plans/' + encodeURIComponent(file));
|
|
343
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
344
|
-
var text = await res.text();
|
|
345
|
-
pre.textContent = text;
|
|
346
|
-
} catch (e) {
|
|
347
|
-
pre.textContent = 'Failed to load plan: ' + (e && e.message ? e.message : e);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ── Lifecycle actions (POST to existing endpoints, then refresh) ───
|
|
352
|
-
async function _plansPost(endpoint, payload, pending) {
|
|
353
|
-
_plansMsg(pending || 'Working…', 'var(--muted)');
|
|
354
|
-
try {
|
|
355
|
-
var res = await fetch(endpoint, {
|
|
356
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
357
|
-
body: JSON.stringify(payload),
|
|
358
|
-
});
|
|
359
|
-
var d = await res.json().catch(function() { return {}; });
|
|
360
|
-
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
361
|
-
_plansMsg('Done.', 'var(--green)');
|
|
362
|
-
await refreshPlans();
|
|
363
|
-
return true;
|
|
364
|
-
} catch (e) {
|
|
365
|
-
_plansMsg('Error: ' + (e && e.message ? e.message : 'failed'), 'var(--red)');
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
function plansApprove(file) { return _plansPost('/api/plans/approve', { file: file }, 'Approving…'); }
|
|
370
|
-
function plansExecute(file, project) { return _plansPost('/api/plans/execute', { file: file, project: project || '' }, 'Executing…'); }
|
|
371
|
-
function plansReject(file) { return _plansPost('/api/plans/reject', { file: file }, 'Rejecting…'); }
|
|
372
|
-
function plansPause(file) { return _plansPost('/api/plans/pause', { file: file }, 'Pausing…'); }
|
|
373
|
-
function plansRegenerate(source) { return _plansPost('/api/plans/regenerate', { source: source }, 'Regenerating…'); }
|
|
374
|
-
function plansUnarchive(file) { return _plansPost('/api/plans/unarchive', { file: file }, 'Restoring…'); }
|
|
375
|
-
function plansArchive(file) { return _plansPost('/api/plans/archive', { file: file }, 'Archiving…'); }
|
|
376
|
-
async function plansDelete(file) {
|
|
377
|
-
if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete this plan and clean up its work items?')) return false;
|
|
378
|
-
return _plansPost('/api/plans/delete', { file: file }, 'Deleting…');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Re-fetch the plan list (cache-busting the local copy), update the tile, and
|
|
382
|
-
// re-render whichever tab is active.
|
|
383
|
-
async function refreshPlans() {
|
|
384
|
-
try {
|
|
385
|
-
var res = await fetch('/api/plans', { headers: { 'Accept': 'application/json' } });
|
|
386
|
-
if (res.ok) {
|
|
387
|
-
_plansData = await res.json();
|
|
388
|
-
renderPlansTile(_countActivePlans(_plansData));
|
|
389
|
-
}
|
|
390
|
-
} catch (e) { /* keep cached */ }
|
|
391
|
-
if (_plansActiveTab === 'plans') {
|
|
392
|
-
var lw = document.getElementById('slim-plans-list');
|
|
393
|
-
if (lw) renderPlansList(lw);
|
|
394
|
-
} else {
|
|
395
|
-
_prdData = null;
|
|
396
|
-
renderPlansTab();
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// ── Tab: PRD (materialized PRDs × work items × verify × PRs) ────────
|
|
401
|
-
function renderPlansPrdTab(body) {
|
|
402
|
-
var intro = document.createElement('p');
|
|
403
|
-
intro.textContent = 'Materialized PRDs — each plan item, its status, linked PRs, and the verify task.';
|
|
404
|
-
body.appendChild(intro);
|
|
405
|
-
|
|
406
|
-
var list = document.createElement('div');
|
|
407
|
-
list.id = 'slim-prd-list';
|
|
408
|
-
body.appendChild(list);
|
|
409
|
-
|
|
410
|
-
var loading = document.createElement('div');
|
|
411
|
-
loading.className = 'tile-empty';
|
|
412
|
-
loading.textContent = 'Loading PRDs…';
|
|
413
|
-
list.appendChild(loading);
|
|
414
|
-
|
|
415
|
-
Promise.all([
|
|
416
|
-
fetch('/api/prd', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
|
|
417
|
-
fetch('/api/work-items', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
|
|
418
|
-
]).then(function(results) {
|
|
419
|
-
_prdData = results[0];
|
|
420
|
-
_plansVerifyWis = Array.isArray(results[1])
|
|
421
|
-
? results[1].filter(function(w) { return w && w.itemType === 'verify'; })
|
|
422
|
-
: [];
|
|
423
|
-
if (_plansActiveTab !== 'prd') return;
|
|
424
|
-
var lw = document.getElementById('slim-prd-list');
|
|
425
|
-
if (lw) renderPrdList(lw);
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function renderPrdList(listWrap) {
|
|
430
|
-
listWrap.textContent = '';
|
|
431
|
-
var progress = _prdData && _prdData.progress;
|
|
432
|
-
var items = progress && Array.isArray(progress.items) ? progress.items : [];
|
|
433
|
-
if (!items.length) {
|
|
434
|
-
var empty = document.createElement('div');
|
|
435
|
-
empty.className = 'tile-empty';
|
|
436
|
-
empty.textContent = 'No materialized PRDs yet. Execute a plan from the Plans tab to generate one.';
|
|
437
|
-
listWrap.appendChild(empty);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
// Group items by their source PRD file (preserve first-seen order).
|
|
441
|
-
var groups = [];
|
|
442
|
-
var bySource = {};
|
|
443
|
-
items.forEach(function(it) {
|
|
444
|
-
var src = it.source || '(unknown)';
|
|
445
|
-
if (!bySource[src]) {
|
|
446
|
-
bySource[src] = { source: src, summary: it.planSummary || src, status: it.planStatus || 'active', items: [] };
|
|
447
|
-
groups.push(bySource[src]);
|
|
448
|
-
}
|
|
449
|
-
bySource[src].items.push(it);
|
|
450
|
-
});
|
|
451
|
-
groups.forEach(function(g) { listWrap.appendChild(buildPrdGroup(g)); });
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function buildPrdGroup(g) {
|
|
455
|
-
var wrap = document.createElement('div');
|
|
456
|
-
wrap.className = 'kb-row';
|
|
457
|
-
|
|
458
|
-
var head = document.createElement('div');
|
|
459
|
-
head.className = 'kb-row-top';
|
|
460
|
-
var chip = document.createElement('span');
|
|
461
|
-
chip.className = _chipClassAttr(_planStatusChipClass(g.status));
|
|
462
|
-
chip.textContent = PLAN_STATUS_LABELS[g.status] || g.status;
|
|
463
|
-
head.appendChild(chip);
|
|
464
|
-
var title = document.createElement('span');
|
|
465
|
-
title.className = 'kb-row-title';
|
|
466
|
-
title.textContent = g.summary;
|
|
467
|
-
title.title = g.source;
|
|
468
|
-
head.appendChild(title);
|
|
469
|
-
wrap.appendChild(head);
|
|
470
|
-
|
|
471
|
-
var done = g.items.filter(function(i) { return i.status === 'done' || i.status === 'decomposed'; }).length;
|
|
472
|
-
var meta = document.createElement('div');
|
|
473
|
-
meta.className = 'kb-row-meta';
|
|
474
|
-
meta.textContent = g.source + ' · ' + done + '/' + g.items.length + ' done';
|
|
475
|
-
wrap.appendChild(meta);
|
|
476
|
-
|
|
477
|
-
// Per-item rows.
|
|
478
|
-
g.items.forEach(function(it) { wrap.appendChild(buildPrdItemRow(it)); });
|
|
479
|
-
|
|
480
|
-
// Verify task row, if a verify work item exists for this PRD.
|
|
481
|
-
var verify = (_plansVerifyWis || []).filter(function(w) { return w.sourcePlan === g.source; });
|
|
482
|
-
if (verify.length) {
|
|
483
|
-
verify.forEach(function(w) {
|
|
484
|
-
var vrow = document.createElement('div');
|
|
485
|
-
vrow.className = 'kb-row-meta';
|
|
486
|
-
var vchip = document.createElement('span');
|
|
487
|
-
vchip.className = _chipClassAttr(_wiStatusChipClass(w.status));
|
|
488
|
-
vchip.textContent = 'Verify: ' + (w.status || 'pending');
|
|
489
|
-
vrow.appendChild(vchip);
|
|
490
|
-
wrap.appendChild(vrow);
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
return wrap;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function buildPrdItemRow(it) {
|
|
497
|
-
var row = document.createElement('div');
|
|
498
|
-
row.className = 'kb-row-meta';
|
|
499
|
-
row.style.display = 'flex';
|
|
500
|
-
row.style.alignItems = 'center';
|
|
501
|
-
row.style.gap = '6px';
|
|
502
|
-
row.style.flexWrap = 'wrap';
|
|
503
|
-
|
|
504
|
-
var chip = document.createElement('span');
|
|
505
|
-
chip.className = _chipClassAttr(_wiStatusChipClass(it.status));
|
|
506
|
-
chip.textContent = it.status || 'missing';
|
|
507
|
-
row.appendChild(chip);
|
|
508
|
-
|
|
509
|
-
var name = document.createElement('span');
|
|
510
|
-
name.textContent = it.name || it.id || '(item)';
|
|
511
|
-
name.style.color = 'var(--text)';
|
|
512
|
-
row.appendChild(name);
|
|
513
|
-
|
|
514
|
-
(it.projects || []).forEach(function(p) {
|
|
515
|
-
var pb = document.createElement('span');
|
|
516
|
-
pb.className = 'tile-chip';
|
|
517
|
-
pb.textContent = p;
|
|
518
|
-
row.appendChild(pb);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
(it.prs || []).forEach(function(pr) { row.appendChild(_buildPlansPrLink(pr)); });
|
|
522
|
-
return row;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function _buildPlansPrLink(pr) {
|
|
526
|
-
var icon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
|
|
527
|
-
var span = document.createElement('span');
|
|
528
|
-
span.style.display = 'inline-flex';
|
|
529
|
-
span.style.alignItems = 'center';
|
|
530
|
-
span.style.gap = '3px';
|
|
531
|
-
span.style.marginLeft = '4px';
|
|
532
|
-
var dot = document.createElement('span');
|
|
533
|
-
dot.textContent = icon;
|
|
534
|
-
dot.title = pr.status || 'active';
|
|
535
|
-
dot.style.color = pr.status === 'merged' ? 'var(--green)' : pr.status === 'abandoned' ? 'var(--red)' : 'var(--blue)';
|
|
536
|
-
span.appendChild(dot);
|
|
537
|
-
if (pr.url) {
|
|
538
|
-
var a = document.createElement('a');
|
|
539
|
-
a.href = pr.url;
|
|
540
|
-
a.target = '_blank';
|
|
541
|
-
a.rel = 'noopener';
|
|
542
|
-
a.textContent = pr.id || 'PR';
|
|
543
|
-
a.title = (pr.title || '') + ' (' + (pr.status || 'active') + ')';
|
|
544
|
-
span.appendChild(a);
|
|
545
|
-
} else {
|
|
546
|
-
var code = document.createElement('code');
|
|
547
|
-
code.textContent = pr.id || 'PR';
|
|
548
|
-
span.appendChild(code);
|
|
549
|
-
}
|
|
550
|
-
return span;
|
|
551
|
-
}
|