@yemi33/minions 0.1.1155 → 0.1.1157
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/CHANGELOG.md +53 -0
- package/dashboard/js/render-agents.js +24 -15
- package/dashboard/js/render-inbox.js +9 -9
- package/dashboard/js/render-kb.js +5 -5
- package/dashboard/js/render-plans.js +33 -33
- package/dashboard/js/render-prs.js +12 -12
- package/dashboard/js/render-work-items.js +60 -60
- package/dashboard/js/utils.js +20 -1
- package/dashboard.js +35 -14
- package/engine/ado.js +13 -1
- package/engine/scheduler.js +26 -4
- package/engine/shared.js +56 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1157 (2026-04-18)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- escapeHtml helper, hotspot XSS migration, regression gate (#1323)
|
|
7
|
+
- Prototype pollution guard in readBody (SEC-13) (#1300)
|
|
8
|
+
- redact ADO tokens and JWTs from engine/log.json writes (#1297)
|
|
9
|
+
- SEC-02 — replace curl shell-out in ado.js with adoFetch (#1296)
|
|
10
|
+
- validate project name and path on POST /api/projects/add (SEC-04, SEC-05) (#1298)
|
|
11
|
+
- seed realActivityMap at spawn time, stamp pid in live-output (#1200)
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
- ado merge-commit mismatch preserves prior buildStatus (closes #1233) (#1326)
|
|
15
|
+
- scheduler substitutes {{date}} in title and description (#1275)
|
|
16
|
+
- prevent archived plans from re-triggering via orphaned .backup restore
|
|
17
|
+
- pinned context optimistic save (closes #1295) (#1316)
|
|
18
|
+
- re-check throttle state per-PR iteration to avoid stale fixThrottled
|
|
19
|
+
- preserve buildErrorLog through transient states, persist poll time (#1273)
|
|
20
|
+
- auto-fetch PR title on link-pr (closes #1283) (#1299)
|
|
21
|
+
- scheduler double-fire within same cron minute (#1277)
|
|
22
|
+
- gate auto-fix dispatch on throttle state to prevent stale-data spurious fixes
|
|
23
|
+
- preserve buildErrorLog through transient build states (#1232) (#1274)
|
|
24
|
+
- annotate fast-exit empty-output failures with diagnostic hint (#1276)
|
|
25
|
+
- invalidate PRD cache on pr-links.json change + guard aggregate PRs (#1220) (#1272)
|
|
26
|
+
- pass --add-dir for minions + ~/.claude to agents (#1271)
|
|
27
|
+
- preserve VERDICT marker by tail-slicing agent output (#1234) (#1270)
|
|
28
|
+
- PRD info cache staleness and aggregate PR bleed-through (#1222)
|
|
29
|
+
- avoid no-op work item writes
|
|
30
|
+
- resilient claude binary resolution + surface spawn errors
|
|
31
|
+
- resolve native claude.exe from npm wrapper on Windows
|
|
32
|
+
- cap temp-agent creation at maxConcurrent per tick (#1219)
|
|
33
|
+
- reassign pending items from unspawned temp agents to idle named agents (#1204) (#1212)
|
|
34
|
+
|
|
35
|
+
### Other
|
|
36
|
+
- refactor: hoist fixThrottled before PR loop and drop underscore prefix
|
|
37
|
+
- docs(skill): add substitute-scheduler-template-vars (#1278)
|
|
38
|
+
- Clarify PR poll labels
|
|
39
|
+
- Prevent modal opens on text selection
|
|
40
|
+
- refactor: clarify settings page section structure and PR polling dependencies
|
|
41
|
+
- refactor: extract _probeClaudePackage helper, use shared.log for spawn errors
|
|
42
|
+
- Make work item descriptions scrollable
|
|
43
|
+
- Use PAT for publish merges
|
|
44
|
+
- test(queries): add unit tests for invalidateDispatchCache/getInbox/getAgentCharter (#1214)
|
|
45
|
+
- test(shared): add unit tests for truncateTextBytes/tailTextBytes/execSilent/trackReviewMetric/parseCanonicalPrId (#1215)
|
|
46
|
+
- Fix publish workflow merge
|
|
47
|
+
- chore: raise default meeting round timeout
|
|
48
|
+
- Harden prompt context handling
|
|
49
|
+
- Harden loop watch conversion
|
|
50
|
+
- Add watches sidebar activity badge
|
|
51
|
+
- test(cli): add unit tests for handleCommand, start, stop, kill, spawn (#1191)
|
|
52
|
+
- chore: untrack pipeline files — local config only
|
|
53
|
+
- restore: recover daily-arch-improvement and weekly-dead-code-cleanup pipelines
|
|
54
|
+
- Harden CC stream resilience
|
|
55
|
+
|
|
3
56
|
## 0.1.1155 (2026-04-18)
|
|
4
57
|
|
|
5
58
|
### Features
|
|
@@ -4,23 +4,23 @@ function renderAgents(agents) {
|
|
|
4
4
|
agentData = agents;
|
|
5
5
|
const grid = document.getElementById('agents-grid');
|
|
6
6
|
grid.innerHTML = agents.map(a => `
|
|
7
|
-
<div class="agent-card ${statusColor(a.status)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail('${
|
|
7
|
+
<div class="agent-card ${statusColor(a.status)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail('${escapeHtml(a.id)}')">
|
|
8
8
|
<div class="agent-card-header">
|
|
9
|
-
<span class="agent-name"><span class="agent-emoji">${
|
|
10
|
-
<span class="status-badge ${
|
|
9
|
+
<span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${escapeHtml(a.name)}</span>
|
|
10
|
+
<span class="status-badge ${escapeHtml(a.status)}">${escapeHtml(a.status)}</span>
|
|
11
11
|
</div>
|
|
12
|
-
<div class="agent-role">${
|
|
13
|
-
<div class="agent-action" title="${
|
|
12
|
+
<div class="agent-role">${escapeHtml(a.role)}</div>
|
|
13
|
+
<div class="agent-action" title="${escapeHtml(a.lastAction)}">${escapeHtml(a.lastAction)}</div>
|
|
14
14
|
${(function() {
|
|
15
15
|
var s = a.started_at, c = a.completed_at;
|
|
16
16
|
if (s && c) { var d = new Date(c) - new Date(s); if (d > 0) { var sec = Math.floor(d/1000)%60, min = Math.floor(d/60000)%60, hr = Math.floor(d/3600000); return '<div style="font-size:9px;color:var(--muted)">Last run: ' + (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's</div>'; } }
|
|
17
17
|
if (s && a.status === 'working') return '<div class="agent-runtime-tick" data-started="' + s + '" style="font-size:9px;color:var(--yellow)"></div>';
|
|
18
18
|
return '';
|
|
19
19
|
})()}
|
|
20
|
-
${a._blockingToolCall ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:10px;color:var(--muted)">⏳ Blocking tool call (${
|
|
21
|
-
${a._warning ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:4px;font-size:10px;color:var(--yellow)">⚠ ${
|
|
22
|
-
${a._permissionMode && a._permissionMode !== 'bypassPermissions' && !a._warning ? `<div style="margin-top:4px;font-size:9px;color:var(--muted)">Permission mode: ${
|
|
23
|
-
${a.resultSummary ? `<div class="agent-result" title="${
|
|
20
|
+
${a._blockingToolCall ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:10px;color:var(--muted)">⏳ Blocking tool call (${escapeHtml(a._blockingToolCall.tool)}) — silent ${Math.round(a._blockingToolCall.silentMs/60000)}min, timeout in ${Math.round(a._blockingToolCall.remainingMs/60000)}min</div>` : ''}
|
|
21
|
+
${a._warning ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:4px;font-size:10px;color:var(--yellow)">⚠ ${escapeHtml(a._warning)}</div>` : ''}
|
|
22
|
+
${a._permissionMode && a._permissionMode !== 'bypassPermissions' && !a._warning ? `<div style="margin-top:4px;font-size:9px;color:var(--muted)">Permission mode: ${escapeHtml(a._permissionMode)}</div>` : ''}
|
|
23
|
+
${a.resultSummary ? `<div class="agent-result" title="${escapeHtml(a.resultSummary)}">${renderMd(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
|
|
24
24
|
</div>
|
|
25
25
|
`).join('');
|
|
26
26
|
}
|
|
@@ -31,14 +31,23 @@ async function openAgentDetail(id) {
|
|
|
31
31
|
currentAgentId = id;
|
|
32
32
|
currentTab = (agent.status === 'working') ? 'live' : 'thought-process';
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// SEC-03 Phase A: Build the detail header via DOM + textContent instead of innerHTML.
|
|
35
|
+
// Emoji, name and role are all user-controlled fields; routing them through textContent
|
|
36
|
+
// guarantees no HTML interpretation even if the escape function were ever bypassed.
|
|
37
|
+
const nameEl = document.getElementById('detail-agent-name');
|
|
38
|
+
const emojiSpan = document.createElement('span');
|
|
39
|
+
emojiSpan.style.fontSize = '22px';
|
|
40
|
+
emojiSpan.textContent = agent.emoji || '';
|
|
41
|
+
nameEl.replaceChildren(
|
|
42
|
+
emojiSpan,
|
|
43
|
+
document.createTextNode(' ' + (agent.name || '') + ' \u2014 ' + (agent.role || ''))
|
|
44
|
+
);
|
|
36
45
|
|
|
37
46
|
const badgeClass = agent.status;
|
|
38
47
|
document.getElementById('detail-status-line').innerHTML =
|
|
39
48
|
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
40
|
-
'<span style="color:var(--muted)">' +
|
|
41
|
-
(agent._blockingToolCall ? '<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:11px;color:var(--muted)">⏳ Blocking tool call (' +
|
|
49
|
+
'<span style="color:var(--muted)">' + escapeHtml(agent.lastAction) + '</span>' +
|
|
50
|
+
(agent._blockingToolCall ? '<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:11px;color:var(--muted)">⏳ Blocking tool call (' + escapeHtml(agent._blockingToolCall.tool) + ') — silent ' + Math.round(agent._blockingToolCall.silentMs/60000) + 'min, timeout in ' + Math.round(agent._blockingToolCall.remainingMs/60000) + 'min</div>' : '') +
|
|
42
51
|
(agent.resultSummary ? '<div style="margin-top:4px;font-size:11px;color:var(--text);line-height:1.4">' + renderMd(agent.resultSummary.slice(0, 300)) + '</div>' : '');
|
|
43
52
|
|
|
44
53
|
// Show panel immediately with loading state — don't wait for API
|
|
@@ -53,8 +62,8 @@ async function openAgentDetail(id) {
|
|
|
53
62
|
} catch(e) {
|
|
54
63
|
document.getElementById('detail-content').innerHTML =
|
|
55
64
|
'<div style="padding:24px;text-align:center">' +
|
|
56
|
-
'<div style="color:var(--red);margin-bottom:12px">Error loading agent detail: ' +
|
|
57
|
-
'<button onclick="openAgentDetail(\'' +
|
|
65
|
+
'<div style="color:var(--red);margin-bottom:12px">Error loading agent detail: ' + escapeHtml(e.message) + '</div>' +
|
|
66
|
+
'<button onclick="openAgentDetail(\'' + escapeHtml(id) + '\')" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:12px">Retry</button>' +
|
|
58
67
|
' <button onclick="closeDetail()" style="padding:6px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:12px;color:var(--text)">Close</button>' +
|
|
59
68
|
'</div>';
|
|
60
69
|
}
|
|
@@ -29,16 +29,16 @@ function renderInbox(inbox) {
|
|
|
29
29
|
const idx = inboxStart + i;
|
|
30
30
|
const pk = inboxPinKey(item.name);
|
|
31
31
|
const pinned = isPinned(pk);
|
|
32
|
-
return `<div class="inbox-item${pinned ? ' item-pinned' : ''}" data-file="notes/inbox/${
|
|
32
|
+
return `<div class="inbox-item${pinned ? ' item-pinned' : ''}" data-file="notes/inbox/${escapeHtml(item.name)}">
|
|
33
33
|
<div class="inbox-name" onclick="openModal(${idx})" style="cursor:pointer">
|
|
34
|
-
<span>${
|
|
34
|
+
<span>${escapeHtml(item.name)}</span><span>${escapeHtml(item.age || '')}</span>
|
|
35
35
|
</div>
|
|
36
|
-
<div class="inbox-preview" onclick="openModal(${idx})" style="cursor:pointer">${
|
|
36
|
+
<div class="inbox-preview" onclick="openModal(${idx})" style="cursor:pointer">${escapeHtml(item.content.slice(0,200))}</div>
|
|
37
37
|
<div style="display:flex;gap:6px;margin-top:6px;align-items:center">
|
|
38
|
-
<button class="pr-pager-btn pin-btn${pinned ? ' pinned' : ''}" style="font-size:9px;padding:2px 8px" data-pin-key="${
|
|
39
|
-
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" data-inbox-name="${
|
|
40
|
-
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" data-inbox-name="${
|
|
41
|
-
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" data-inbox-name="${
|
|
38
|
+
<button class="pr-pager-btn pin-btn${pinned ? ' pinned' : ''}" style="font-size:9px;padding:2px 8px" data-pin-key="${escapeHtml(pk)}" onclick="event.stopPropagation();_togglePinAndRefresh(this.dataset.pinKey,'inbox')">${pinned ? 'Unpin' : 'Pin'}</button>
|
|
39
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();promoteToKB(this.dataset.inboxName)">Add to Knowledge Base</button>
|
|
40
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();openInboxInExplorer(this.dataset.inboxName)">Open in Explorer</button>
|
|
41
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();deleteInboxItem(this.dataset.inboxName)">Delete</button>
|
|
42
42
|
</div>
|
|
43
43
|
</div>`;
|
|
44
44
|
}).join('');
|
|
@@ -62,10 +62,10 @@ function promoteToKB(name) {
|
|
|
62
62
|
{ id: 'reviews', label: 'Reviews' },
|
|
63
63
|
];
|
|
64
64
|
const picker = '<div style="padding:16px 20px">' +
|
|
65
|
-
'<p style="font-size:13px;color:var(--text);margin-bottom:12px">Choose a category for <strong>' +
|
|
65
|
+
'<p style="font-size:13px;color:var(--text);margin-bottom:12px">Choose a category for <strong>' + escapeHtml(name) + '</strong>:</p>' +
|
|
66
66
|
'<div style="display:flex;flex-direction:column;gap:8px">' +
|
|
67
67
|
categories.map(c =>
|
|
68
|
-
'<button class="pr-pager-btn" style="font-size:12px;padding:8px 16px;text-align:left" onclick="doPromoteToKB(\'' +
|
|
68
|
+
'<button class="pr-pager-btn" style="font-size:12px;padding:8px 16px;text-align:left" onclick="doPromoteToKB(\'' + escapeHtml(name) + '\',\'' + c.id + '\')">' + c.label + '</button>'
|
|
69
69
|
).join('') +
|
|
70
70
|
'</div></div>';
|
|
71
71
|
document.getElementById('modal-title').textContent = 'Add to Knowledge Base';
|
|
@@ -112,18 +112,18 @@ function renderKnowledgeBase() {
|
|
|
112
112
|
const label = KB_CAT_LABELS[item.category] || item.category;
|
|
113
113
|
var pinKey = kbPinKey(item.category, item.file);
|
|
114
114
|
var pinned = isPinned(pinKey);
|
|
115
|
-
return '<div class="kb-item' + (pinned ? ' item-pinned' : '') + '" data-file="knowledge/' +
|
|
115
|
+
return '<div class="kb-item' + (pinned ? ' item-pinned' : '') + '" data-file="knowledge/' + escapeHtml(item.category) + '/' + escapeHtml(item.file) + '" onclick="kbOpenItem(\'' + escapeHtml(item.category) + '\', \'' + escapeHtml(item.file) + '\')">' +
|
|
116
116
|
'<div class="kb-item-body">' +
|
|
117
|
-
'<div class="kb-item-title">' + icon + ' ' +
|
|
118
|
-
' <button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') + '" style="font-size:9px;padding:1px 6px;margin-left:6px;vertical-align:middle" data-pin-key="' +
|
|
117
|
+
'<div class="kb-item-title">' + icon + ' ' + escapeHtml(item.title) +
|
|
118
|
+
' <button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') + '" style="font-size:9px;padding:1px 6px;margin-left:6px;vertical-align:middle" data-pin-key="' + escapeHtml(pinKey) + '" onclick="event.stopPropagation();_togglePinAndRefresh(this.dataset.pinKey,\'kb\')">' + (pinned ? 'Unpin' : 'Pin') + '</button>' +
|
|
119
119
|
'</div>' +
|
|
120
120
|
'<div class="kb-item-meta">' +
|
|
121
121
|
'<span>' + label + '</span>' +
|
|
122
|
-
(item.agent ? '<span>' +
|
|
122
|
+
(item.agent ? '<span>' + escapeHtml(item.agent) + '</span>' : '') +
|
|
123
123
|
'<span>' + (item.date || '') + '</span>' +
|
|
124
124
|
'<span>' + Math.round(item.size / 1024) + 'KB</span>' +
|
|
125
125
|
'</div>' +
|
|
126
|
-
(item.preview ? '<div class="kb-item-preview">' +
|
|
126
|
+
(item.preview ? '<div class="kb-item-preview">' + escapeHtml(item.preview) + '</div>' : '') +
|
|
127
127
|
'</div>' +
|
|
128
128
|
'</div>';
|
|
129
129
|
}).join('');
|
|
@@ -8,7 +8,7 @@ function _plansNext() { _plansPage++; refresh(); }
|
|
|
8
8
|
|
|
9
9
|
function openCreatePlanModal() {
|
|
10
10
|
const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p =>
|
|
11
|
-
'<option value="' +
|
|
11
|
+
'<option value="' + escapeHtml(p) + '">' + escapeHtml(p) + '</option>'
|
|
12
12
|
).join('');
|
|
13
13
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
14
14
|
|
|
@@ -218,23 +218,23 @@ function renderPlans(plans) {
|
|
|
218
218
|
// For awaiting-approval: show Execute (re-generate PRD from updated plan) + Approve (use current PRD as-is)
|
|
219
219
|
if (effectiveStatus === 'awaiting-approval' && isDraft && prdFile) {
|
|
220
220
|
actions = '<div class="plan-card-actions" onclick="event.stopPropagation()">' +
|
|
221
|
-
'<button class="plan-btn approve" onclick="planApprove(\'' +
|
|
222
|
-
'<button class="plan-btn approve" style="opacity:0.7" onclick="planExecute(\'' +
|
|
223
|
-
'<button class="plan-btn reject" onclick="planReject(\'' +
|
|
221
|
+
'<button class="plan-btn approve" onclick="planApprove(\'' + escapeHtml(actionTarget) + '\')">Approve</button>' +
|
|
222
|
+
'<button class="plan-btn approve" style="opacity:0.7" onclick="planExecute(\'' + escapeHtml(p.file) + '\',\'' + escapeHtml(p.project || '') + '\',this)">Re-execute</button>' +
|
|
223
|
+
'<button class="plan-btn reject" onclick="planReject(\'' + escapeHtml(actionTarget) + '\')">Reject</button>' +
|
|
224
224
|
'</div>';
|
|
225
225
|
} else {
|
|
226
226
|
const actionLabel = effectiveStatus === 'paused' ? 'Resume' : 'Approve';
|
|
227
227
|
actions = '<div class="plan-card-actions" onclick="event.stopPropagation()">' +
|
|
228
|
-
'<button class="plan-btn approve" onclick="planApprove(\'' +
|
|
229
|
-
'<button class="plan-btn reject" onclick="planReject(\'' +
|
|
228
|
+
'<button class="plan-btn approve" onclick="planApprove(\'' + escapeHtml(actionTarget) + '\')">' + actionLabel + '</button>' +
|
|
229
|
+
'<button class="plan-btn reject" onclick="planReject(\'' + escapeHtml(actionTarget) + '\')">Reject</button>' +
|
|
230
230
|
'</div>';
|
|
231
231
|
}
|
|
232
232
|
} else if (isRevision) {
|
|
233
|
-
actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' +
|
|
233
|
+
actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escapeHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
|
|
237
|
-
'onclick="event.stopPropagation();planExecute(\'' +
|
|
237
|
+
'onclick="event.stopPropagation();planExecute(\'' + escapeHtml(p.file) + '\',\'' + escapeHtml(p.project) + '\',this)">Execute</button>' : '';
|
|
238
238
|
const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
|
|
239
239
|
// Resume pill not needed — paused state is handled by the actions block above
|
|
240
240
|
const showResume = false;
|
|
@@ -242,37 +242,37 @@ function renderPlans(plans) {
|
|
|
242
242
|
const hasVerifyWi = !!verifyWi;
|
|
243
243
|
const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived && !hasVerifyWi;
|
|
244
244
|
const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow)" ' +
|
|
245
|
-
'onclick="event.stopPropagation();planPause(\'' +
|
|
245
|
+
'onclick="event.stopPropagation();planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button>' : '';
|
|
246
246
|
const resumeBtn = showResume
|
|
247
247
|
? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green)" ' +
|
|
248
|
-
'onclick="event.stopPropagation();planApprove(\'' +
|
|
248
|
+
'onclick="event.stopPropagation();planApprove(\'' + escapeHtml(prdFile) + '\',this)">Resume</button>'
|
|
249
249
|
: '';
|
|
250
250
|
const verifyBtn = showVerify ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green)" ' +
|
|
251
|
-
'onclick="event.stopPropagation();triggerVerify(\'' +
|
|
251
|
+
'onclick="event.stopPropagation();triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button>' : '';
|
|
252
252
|
const showArchive = !isArchived;
|
|
253
253
|
const archiveFile = prdFile || p.file;
|
|
254
254
|
const archiveReady = p.archiveReady && !isArchived;
|
|
255
255
|
const archiveBtn = showArchive ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px' +
|
|
256
256
|
(archiveReady ? ';color:var(--green);font-weight:600;border:1px solid var(--green)' : '') + '" ' +
|
|
257
|
-
'onclick="event.stopPropagation();planArchive(\'' +
|
|
257
|
+
'onclick="event.stopPropagation();planArchive(\'' + escapeHtml(archiveFile) + '\',this)">' +
|
|
258
258
|
(archiveReady ? '✓ Archive' : 'Archive') + '</button>' : '';
|
|
259
259
|
const archiveReadyBadge = archiveReady ? '<span style="font-size:9px;font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);vertical-align:middle" title="Verification passed — ready to archive">Ready to archive</span>' : '';
|
|
260
260
|
const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" ' +
|
|
261
|
-
'onclick="event.stopPropagation();planDelete(\'' +
|
|
261
|
+
'onclick="event.stopPropagation();planDelete(\'' + escapeHtml(p.file) + '\')">Delete</button>' : '';
|
|
262
262
|
|
|
263
263
|
const versionBadge = p.version ? ' <span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
|
|
264
264
|
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
|
|
265
265
|
const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
|
|
266
|
-
return '<div class="plan-card ' + cardClass + '" data-file="plans/' +
|
|
266
|
+
return '<div class="plan-card ' + cardClass + '" data-file="plans/' + escapeHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="if(shouldIgnoreSelectionClick(event))return;planView(\'' + escapeHtml(p.file) + '\')">' +
|
|
267
267
|
'<div class="plan-card-header">' +
|
|
268
|
-
'<div><div class="plan-card-title">' +
|
|
268
|
+
'<div><div class="plan-card-title">' + escapeHtml(p.summary || p.file) + versionBadge + '</div>' +
|
|
269
269
|
'<div class="plan-card-meta">' +
|
|
270
270
|
'<span style="font-weight:600;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + label + '</span>' +
|
|
271
|
-
(p.project ? '<span>' +
|
|
271
|
+
(p.project ? '<span>' + escapeHtml(p.project) + '</span>' : '') +
|
|
272
272
|
'<span>' + p.itemCount + ' items</span>' +
|
|
273
273
|
(p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
|
|
274
274
|
(p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
|
|
275
|
-
(p.generatedBy ? '<span>by ' +
|
|
275
|
+
(p.generatedBy ? '<span>by ' + escapeHtml(p.generatedBy) + '</span>' : '') +
|
|
276
276
|
executeBtn + pauseBtn + resumeBtn + verifyBtn + (hasVerifyWi ? _renderVerifyBadge(verifyWi) : '') + archiveReadyBadge + archiveBtn + deleteBtn +
|
|
277
277
|
'</div>' +
|
|
278
278
|
'</div>' +
|
|
@@ -323,15 +323,15 @@ function openArchivedPlansModal() {
|
|
|
323
323
|
const html = plans.map(p => {
|
|
324
324
|
const itemCount = p.itemCount || 0;
|
|
325
325
|
const completed = p.completedAt ? p.completedAt.slice(0, 10) : '';
|
|
326
|
-
return '<div class="plan-card" data-file="plans/' +
|
|
326
|
+
return '<div class="plan-card" data-file="plans/' + escapeHtml(p.file) + '" style="cursor:pointer;opacity:0.8" onclick="if(shouldIgnoreSelectionClick(event))return;planView(\'' + escapeHtml(p.file) + '\')">' +
|
|
327
327
|
'<div class="plan-card-header">' +
|
|
328
|
-
'<div><div class="plan-card-title" style="font-size:13px">' +
|
|
328
|
+
'<div><div class="plan-card-title" style="font-size:13px">' + escapeHtml(p.summary || p.file) + '</div>' +
|
|
329
329
|
'<div class="plan-card-meta">' +
|
|
330
330
|
'<span style="color:var(--green);font-weight:600">Completed</span>' +
|
|
331
|
-
(p.project ? '<span>' +
|
|
331
|
+
(p.project ? '<span>' + escapeHtml(p.project) + '</span>' : '') +
|
|
332
332
|
'<span>' + itemCount + ' items</span>' +
|
|
333
333
|
(completed ? '<span>' + completed + '</span>' : '') +
|
|
334
|
-
(p.generatedBy ? '<span>by ' +
|
|
334
|
+
(p.generatedBy ? '<span>by ' + escapeHtml(p.generatedBy) + '</span>' : '') +
|
|
335
335
|
'</div>' +
|
|
336
336
|
'</div>' +
|
|
337
337
|
'</div>' +
|
|
@@ -417,7 +417,7 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
417
417
|
if (normalizedFile.endsWith('.json')) {
|
|
418
418
|
let plan;
|
|
419
419
|
try { plan = JSON.parse(raw); } catch (e) {
|
|
420
|
-
document.getElementById('modal-body').innerHTML = '<p style="color:var(--red)">Failed to parse plan JSON: ' +
|
|
420
|
+
document.getElementById('modal-body').innerHTML = '<p style="color:var(--red)">Failed to parse plan JSON: ' + escapeHtml(e.message) + '</p><pre style="font-size:10px;max-height:200px;overflow:auto">' + escapeHtml((raw || '').slice(0, 500)) + '</pre>';
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
423
|
title = plan.plan_summary || normalizedFile;
|
|
@@ -466,20 +466,20 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
466
466
|
if (isNeedsAction) {
|
|
467
467
|
const target = prdFile || normalizedFile;
|
|
468
468
|
const label = effectiveStatus === 'paused' ? 'Resume' : 'Approve';
|
|
469
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="planApprove(\'' +
|
|
469
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="planApprove(\'' + escapeHtml(target) + '\',this)">' + label + '</button> ';
|
|
470
470
|
// Re-execute: re-generate PRD from updated plan (only for .md plans with existing awaiting PRD)
|
|
471
471
|
if (effectiveStatus === 'awaiting-approval' && isMdPlan && prdFile) {
|
|
472
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green);opacity:0.7" onclick="planExecute(\'' +
|
|
472
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green);opacity:0.7" onclick="planExecute(\'' + escapeHtml(normalizedFile) + '\',\'\',this)">Re-execute</button> ';
|
|
473
473
|
}
|
|
474
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planReject(\'' +
|
|
474
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planReject(\'' + escapeHtml(target) + '\')">Reject</button> ';
|
|
475
475
|
}
|
|
476
476
|
// Execute (draft .md without PRD)
|
|
477
477
|
if (isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived) {
|
|
478
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green);font-weight:600" onclick="planExecute(\'' +
|
|
478
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green);font-weight:600" onclick="planExecute(\'' + escapeHtml(normalizedFile) + '\',\'\',this)">Execute</button> ';
|
|
479
479
|
}
|
|
480
480
|
// Pause (active PRD, not completed)
|
|
481
481
|
if (effectiveStatus === 'dispatched' && prdFile && !isArchived) {
|
|
482
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--yellow)" onclick="planPause(\'' +
|
|
482
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--yellow)" onclick="planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button> ';
|
|
483
483
|
}
|
|
484
484
|
// Completed label
|
|
485
485
|
if (isCompleted && !isArchived) {
|
|
@@ -492,19 +492,19 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
492
492
|
// Verify / Verified badge
|
|
493
493
|
const modalVerifyWi = (window._lastWorkItems || []).find(w => w.itemType === 'verify' && w.sourcePlan === (prdFile || normalizedFile));
|
|
494
494
|
if (effectiveStatus === 'completed' && prdFile && !isArchived && !modalVerifyWi) {
|
|
495
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="triggerVerify(\'' +
|
|
495
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button> ';
|
|
496
496
|
}
|
|
497
497
|
if (modalVerifyWi) modalActions += _renderVerifyBadge(modalVerifyWi);
|
|
498
498
|
// Archive + Delete (always, unless archived)
|
|
499
499
|
if (!isArchived) {
|
|
500
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--muted)" onclick="planArchive(\'' +
|
|
501
|
-
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planDelete(\'' +
|
|
500
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--muted)" onclick="planArchive(\'' + escapeHtml(prdFile || normalizedFile) + '\')">Archive</button> ';
|
|
501
|
+
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planDelete(\'' + escapeHtml(normalizedFile) + '\')">Delete</button>';
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
|
|
505
505
|
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + modalActions + '</div>';
|
|
506
506
|
|
|
507
|
-
document.getElementById('modal-title').innerHTML =
|
|
507
|
+
document.getElementById('modal-title').innerHTML = escapeHtml(title) + (versionLabel ? ' <span style="font-size:11px;font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escapeHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
|
|
508
508
|
const modalBody = document.getElementById('modal-body');
|
|
509
509
|
const scrollTop = modalBody.scrollTop;
|
|
510
510
|
if (normalizedFile.endsWith('.json')) {
|
|
@@ -742,11 +742,11 @@ function _renderVerifyBadge(verifyWi) {
|
|
|
742
742
|
const planFile = verifyWi.sourcePlan || '';
|
|
743
743
|
const planSlug = planFile.replace('.json', '');
|
|
744
744
|
const verifyPr = allPrs.find(pr => (pr.prdItems || []).includes(verifyWi.id) || (pr.branch && pr.branch.includes(planSlug) && (pr.title || '').includes('[E2E]')));
|
|
745
|
-
const prLink = verifyPr?.url ? ' <a href="' +
|
|
745
|
+
const prLink = verifyPr?.url ? ' <a href="' + escapeHtml(verifyPr.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--blue);text-decoration:underline;font-size:9px">' + escapeHtml(verifyPr.id || 'E2E PR') + '</a>' : '';
|
|
746
746
|
// Testing guide
|
|
747
747
|
const guides = window._lastStatus?.verifyGuides || [];
|
|
748
748
|
const guide = guides.find(g => g.planFile === planFile);
|
|
749
|
-
const guideLink = guide ? ' <span onclick="event.stopPropagation();openVerifyGuide(\'' +
|
|
749
|
+
const guideLink = guide ? ' <span onclick="event.stopPropagation();openVerifyGuide(\'' + escapeHtml(guide.file) + '\')" style="color:var(--green);cursor:pointer;text-decoration:underline;font-size:9px">Testing Guide</span>' : '';
|
|
750
750
|
return '<span style="font-size:9px;font-weight:600;color:' + color + ';padding:0 4px">' + label + '</span>' + prLink + guideLink;
|
|
751
751
|
}
|
|
752
752
|
|
|
@@ -19,16 +19,16 @@ function prRow(pr) {
|
|
|
19
19
|
const url = pr.url || '#';
|
|
20
20
|
const prId = pr.id || '—';
|
|
21
21
|
return '<tr>' +
|
|
22
|
-
'<td><span class="pr-id">' +
|
|
23
|
-
'<td><a class="pr-title" href="' +
|
|
24
|
-
'<td><span class="pr-agent">' +
|
|
25
|
-
'<td><span class="pr-branch">' +
|
|
26
|
-
'<td><span class="pr-badge ' + reviewClass + '">' +
|
|
27
|
-
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' +
|
|
28
|
-
'<td><span class="pr-badge ' + buildClass + '">' +
|
|
29
|
-
'<td><span class="pr-badge ' + statusClass + '">' +
|
|
30
|
-
'<td><span class="pr-date">' +
|
|
31
|
-
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' +
|
|
22
|
+
'<td><span class="pr-id">' + escapeHtml(String(prId)) + '</span></td>' +
|
|
23
|
+
'<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(pr.title || 'Untitled') + '</a>' + (pr.description ? '<div class="pr-desc">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
|
|
24
|
+
'<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
|
|
25
|
+
'<td><span class="pr-branch">' + escapeHtml(pr.branch || '—') + '</span></td>' +
|
|
26
|
+
'<td><span class="pr-badge ' + reviewClass + '">' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
27
|
+
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
28
|
+
'<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
|
29
|
+
'<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
30
|
+
'<td><span class="pr-date">' + escapeHtml((pr.created || '—').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
31
|
+
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
|
|
32
32
|
'</tr>';
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -93,7 +93,7 @@ function openModal(i) {
|
|
|
93
93
|
if (!item) return;
|
|
94
94
|
document.getElementById('modal-title').textContent = item.name;
|
|
95
95
|
document.getElementById('modal-body').innerHTML =
|
|
96
|
-
'<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' +
|
|
96
|
+
'<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escapeHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
|
|
97
97
|
'<div style="font-size:12px;line-height:1.7;color:var(--muted)">' + renderMd(item.content) + '</div>';
|
|
98
98
|
_modalDocContext = { title: item.name, content: item.content, selection: '' };
|
|
99
99
|
_modalFilePath = 'notes/inbox/' + item.name; showModalQa();
|
|
@@ -106,7 +106,7 @@ function openModal(i) {
|
|
|
106
106
|
function openAddPrModal() {
|
|
107
107
|
const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p => {
|
|
108
108
|
const name = typeof p === 'object' ? p.name : p;
|
|
109
|
-
return '<option value="' +
|
|
109
|
+
return '<option value="' + escapeHtml(name) + '">' + escapeHtml(name) + '</option>';
|
|
110
110
|
}).join('');
|
|
111
111
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
112
112
|
|
|
@@ -23,55 +23,55 @@ function wiRetryBtn(item) {
|
|
|
23
23
|
return '<span style="font-size:9px;padding:1px 6px;color:var(--green);border:1px solid rgba(63,185,80,0.35);background:rgba(63,185,80,0.1);border-radius:3px;margin-left:4px">Requeued</span>';
|
|
24
24
|
}
|
|
25
25
|
if (rs && rs.status === 'error') {
|
|
26
|
-
return '<span style="font-size:9px;padding:1px 6px;color:var(--red);border:1px solid rgba(248,81,73,0.35);background:rgba(248,81,73,0.1);border-radius:3px;margin-left:4px;cursor:pointer" title="' +
|
|
26
|
+
return '<span style="font-size:9px;padding:1px 6px;color:var(--red);border:1px solid rgba(248,81,73,0.35);background:rgba(248,81,73,0.1);border-radius:3px;margin-left:4px;cursor:pointer" title="' + escapeHtml(rs.message || 'Retry failed') + ' — click to try again" onclick="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry failed</span>';
|
|
27
27
|
}
|
|
28
|
-
return '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' +
|
|
28
|
+
return '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry</button>';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function wiRow(item) {
|
|
32
32
|
const statusBadge = (s) => {
|
|
33
33
|
const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : s === 'decomposed' ? 'approved' : 'draft';
|
|
34
|
-
return '<span class="pr-badge ' + cls + '">' +
|
|
34
|
+
return '<span class="pr-badge ' + cls + '">' + escapeHtml(s) + '</span>';
|
|
35
35
|
};
|
|
36
|
-
const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' +
|
|
37
|
-
const priBadge = (p) => '<span class="prd-item-priority ' + (p || '') + '">' +
|
|
36
|
+
const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escapeHtml(t || 'implement') + '</span>';
|
|
37
|
+
const priBadge = (p) => '<span class="prd-item-priority ' + (p || '') + '">' + escapeHtml(p || 'medium') + '</span>';
|
|
38
38
|
const prLink = item._pr
|
|
39
|
-
? '<a class="pr-title" href="' +
|
|
39
|
+
? '<a class="pr-title" href="' + escapeHtml(item._prUrl || '#') + '" target="_blank" style="font-size:10px">' + escapeHtml(item._pr) + '</a>'
|
|
40
40
|
: (item.branchStrategy === 'shared-branch' && item.status === 'done')
|
|
41
41
|
? '<span style="font-size:9px;color:var(--muted)" title="Part of shared branch — aggregate PR created at verify stage">shared branch</span>'
|
|
42
42
|
: '<span style="color:var(--muted)">—</span>';
|
|
43
|
-
return '<tr style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' +
|
|
44
|
-
'<td><span class="pr-id">' +
|
|
45
|
-
'<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' +
|
|
46
|
-
'<td><span style="font-size:10px;color:var(--muted)">' +
|
|
43
|
+
return '<tr style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
|
|
44
|
+
'<td><span class="pr-id">' + escapeHtml(item.id || '') + '</span></td>' +
|
|
45
|
+
'<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + '</td>' +
|
|
46
|
+
'<td><span style="font-size:10px;color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
|
|
47
47
|
(item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:8px">fan-out</span>' : '') + '</td>' +
|
|
48
48
|
'<td>' + typeBadge(item.type) + '</td>' +
|
|
49
49
|
'<td>' + priBadge(item.priority) + '</td>' +
|
|
50
50
|
'<td>' + statusBadge(item.status || 'pending') +
|
|
51
51
|
(item._reopened ? ' <span style="font-size:9px;color:var(--purple);margin-left:4px" title="This item was reopened from a previously completed state">reopened</span>' : '') +
|
|
52
|
-
(item._pendingReason && item.status === 'pending' && item._pendingReason !== 'already_dispatched' ? ' <span style="font-size:9px;color:var(--muted);margin-left:4px" title="Pending reason: ' +
|
|
52
|
+
(item._pendingReason && item.status === 'pending' && item._pendingReason !== 'already_dispatched' ? ' <span style="font-size:9px;color:var(--muted);margin-left:4px" title="Pending reason: ' + escapeHtml(item._pendingReason) + '">' + escapeHtml(item._pendingReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
53
53
|
(item._pendingReason === 'already_dispatched' && item.status === 'pending' ? ' <span style="font-size:9px;color:var(--blue);margin-left:4px" title="In dispatch queue, waiting to be assigned">queued</span>' : '') +
|
|
54
|
-
(item._skipReason && item.status === 'pending' ? ' <span style="font-size:9px;color:var(--yellow);margin-left:4px" title="Dispatch blocked: ' +
|
|
54
|
+
(item._skipReason && item.status === 'pending' ? ' <span style="font-size:9px;color:var(--yellow);margin-left:4px" title="Dispatch blocked: ' + escapeHtml(item._skipReason) + (item._blockedBy ? ' (by ' + escapeHtml(item._blockedBy) + ')' : '') + '">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + (item._blockedBy ? ' <span style="color:var(--muted)">(' + escapeHtml(item._blockedBy) + ')</span>' : '') + '</span>' : '') +
|
|
55
55
|
(item.status === 'failed' ? ' ' + wiRetryBtn(item) : '') +
|
|
56
56
|
'</td>' +
|
|
57
57
|
'<td>' +
|
|
58
58
|
(item.completedAgents && item.completedAgents.length > 0
|
|
59
|
-
? '<span class="pr-agent">' +
|
|
60
|
-
: '<span class="pr-agent">' +
|
|
61
|
-
(item.failReason ? '<span style="display:block;font-size:9px;color:var(--red)" title="' +
|
|
59
|
+
? '<span class="pr-agent">' + escapeHtml(item.completedAgents.join(', ')) + '</span>'
|
|
60
|
+
: '<span class="pr-agent">' + escapeHtml(item.dispatched_to || item.agent || '—') + '</span>') +
|
|
61
|
+
(item.failReason ? '<span style="display:block;font-size:9px;color:var(--red)" title="' + escapeHtml(item.failReason) + '">' + escapeHtml(item.failReason.slice(0, 30)) + '</span>' : '') +
|
|
62
62
|
'</td>' +
|
|
63
63
|
'<td>' + prLink + '</td>' +
|
|
64
|
-
'<td><span class="pr-date">' +
|
|
64
|
+
'<td><span class="pr-date">' + escapeHtml((item.created || '').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
65
65
|
'<td style="white-space:nowrap;font-size:9px;color:var(--muted)">' +
|
|
66
66
|
(item.references && item.references.length ? '<span title="' + item.references.length + ' reference(s)" style="margin-right:4px">🔗' + item.references.length + '</span>' : '') +
|
|
67
67
|
(item.acceptanceCriteria && item.acceptanceCriteria.length ? '<span title="' + item.acceptanceCriteria.length + ' acceptance criteria">☑' + item.acceptanceCriteria.length + '</span>' : '') +
|
|
68
68
|
'</td>' +
|
|
69
69
|
'<td style="white-space:nowrap">' +
|
|
70
|
-
((item.status === 'pending' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' +
|
|
71
|
-
((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' +
|
|
72
|
-
((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' +
|
|
73
|
-
((item.status === 'pending' || item.status === 'dispatched' || item.status === 'queued' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--orange);border-color:var(--orange);margin-right:4px" onclick="event.stopPropagation();cancelWorkItem(\'' +
|
|
74
|
-
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' +
|
|
70
|
+
((item.status === 'pending' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Edit work item">✎</button>' : '') +
|
|
71
|
+
((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Archive work item">📦</button>' : '') +
|
|
72
|
+
((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Give feedback">👍👎</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '👍' : '👎') + '</span> ' : '')) +
|
|
73
|
+
((item.status === 'pending' || item.status === 'dispatched' || item.status === 'queued' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--orange);border-color:var(--orange);margin-right:4px" onclick="event.stopPropagation();cancelWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Cancel work item and kill agent">🚫</button>' : '') +
|
|
74
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button>' +
|
|
75
75
|
'</td>' +
|
|
76
76
|
'</tr>';
|
|
77
77
|
}
|
|
@@ -130,7 +130,7 @@ function editWorkItem(id, source) {
|
|
|
130
130
|
if (!item) return;
|
|
131
131
|
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'decompose', 'meeting', 'investigate', 'refactor', 'test', 'explore', 'ask', 'docs'];
|
|
132
132
|
const priorities = ['critical', 'high', 'medium', 'low'];
|
|
133
|
-
const agentOpts = (cmdAgents || []).map(a => '<option value="' +
|
|
133
|
+
const agentOpts = (cmdAgents || []).map(a => '<option value="' + escapeHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escapeHtml(a.name) + '</option>').join('');
|
|
134
134
|
const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
135
135
|
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((item.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
136
136
|
|
|
@@ -139,10 +139,10 @@ function editWorkItem(id, source) {
|
|
|
139
139
|
document.getElementById('modal-body').innerHTML =
|
|
140
140
|
'<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
141
141
|
'<label style="color:var(--text);font-size:var(--text-md)">Title' +
|
|
142
|
-
'<input id="wi-edit-title" value="' +
|
|
142
|
+
'<input id="wi-edit-title" value="' + escapeHtml(item.title || '') + '" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit">' +
|
|
143
143
|
'</label>' +
|
|
144
144
|
'<label style="color:var(--text);font-size:var(--text-md)">Description' +
|
|
145
|
-
'<textarea id="wi-edit-desc" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' +
|
|
145
|
+
'<textarea id="wi-edit-desc" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escapeHtml(item.description || '') + '</textarea>' +
|
|
146
146
|
'</label>' +
|
|
147
147
|
'<div style="display:flex;gap:12px">' +
|
|
148
148
|
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Type' +
|
|
@@ -156,14 +156,14 @@ function editWorkItem(id, source) {
|
|
|
156
156
|
'</label>' +
|
|
157
157
|
'</div>' +
|
|
158
158
|
'<label style="color:var(--text);font-size:var(--text-md)">References (one per line: url | title | type)' +
|
|
159
|
-
'<textarea id="wi-edit-refs" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' +
|
|
159
|
+
'<textarea id="wi-edit-refs" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escapeHtml((item.references || []).map(function(r) { return r.url + ' | ' + (r.title || '') + ' | ' + (r.type || 'link'); }).join('\n')) + '</textarea>' +
|
|
160
160
|
'</label>' +
|
|
161
161
|
'<label style="color:var(--text);font-size:var(--text-md)">Acceptance Criteria (one per line)' +
|
|
162
|
-
'<textarea id="wi-edit-ac" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' +
|
|
162
|
+
'<textarea id="wi-edit-ac" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escapeHtml((item.acceptanceCriteria || []).join('\n')) + '</textarea>' +
|
|
163
163
|
'</label>' +
|
|
164
164
|
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
|
|
165
165
|
'<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
|
|
166
|
-
'<button onclick="submitWorkItemEdit(\'' +
|
|
166
|
+
'<button onclick="submitWorkItemEdit(\'' + escapeHtml(id) + '\',\'' + escapeHtml(source || '') + '\',event)" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
|
|
167
167
|
'</div>' +
|
|
168
168
|
'</div>';
|
|
169
169
|
document.getElementById('modal').classList.add('open');
|
|
@@ -253,11 +253,11 @@ async function toggleWorkItemArchive() {
|
|
|
253
253
|
'<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Agent</th><th>Archived</th></tr></thead><tbody>' +
|
|
254
254
|
items.map(function(i) {
|
|
255
255
|
return '<tr style="opacity:0.6">' +
|
|
256
|
-
'<td><span class="pr-id">' +
|
|
257
|
-
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
|
|
258
|
-
'<td><span class="dispatch-type ' + (i.type || '') + '">' +
|
|
259
|
-
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' +
|
|
260
|
-
'<td>' +
|
|
256
|
+
'<td><span class="pr-id">' + escapeHtml(i.id || '') + '</span></td>' +
|
|
257
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(i.title || '') + '</td>' +
|
|
258
|
+
'<td><span class="dispatch-type ' + (i.type || '') + '">' + escapeHtml(i.type || '') + '</span></td>' +
|
|
259
|
+
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' + escapeHtml(i.status || '') + '</td>' +
|
|
260
|
+
'<td>' + escapeHtml(i.dispatched_to || '—') + '</td>' +
|
|
261
261
|
'<td class="pr-date">' + shortTime(i.archivedAt) + '</td>' +
|
|
262
262
|
'</tr>';
|
|
263
263
|
}).join('') + '</tbody></table></div>';
|
|
@@ -309,7 +309,7 @@ function feedbackWorkItem(id, source) {
|
|
|
309
309
|
'<textarea id="feedback-comment" rows="3" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-family:inherit;resize:vertical" placeholder="What was good or needs improvement?"></textarea>' +
|
|
310
310
|
'<div style="display:flex;justify-content:flex-end;gap:8px">' +
|
|
311
311
|
'<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
|
|
312
|
-
'<button id="fb-send" onclick="submitFeedback(\'' +
|
|
312
|
+
'<button id="fb-send" onclick="submitFeedback(\'' + escapeHtml(id) + '\',\'' + escapeHtml(source) + '\')" style="padding:6px 16px;background:var(--surface2);color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:not-allowed" disabled>Select rating first</button>' +
|
|
313
313
|
'</div>' +
|
|
314
314
|
'</div>';
|
|
315
315
|
document.getElementById('modal').classList.add('open');
|
|
@@ -352,10 +352,10 @@ function openCreateWorkItemModal() {
|
|
|
352
352
|
'<option value="' + p + '"' + (p === 'medium' ? ' selected' : '') + '>' + p + '</option>'
|
|
353
353
|
).join('');
|
|
354
354
|
const agentOpts = (typeof cmdAgents !== 'undefined' ? cmdAgents : []).map(a =>
|
|
355
|
-
'<option value="' +
|
|
355
|
+
'<option value="' + escapeHtml(a.id) + '">' + escapeHtml(a.name) + '</option>'
|
|
356
356
|
).join('');
|
|
357
357
|
const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p =>
|
|
358
|
-
'<option value="' +
|
|
358
|
+
'<option value="' + escapeHtml(p) + '">' + escapeHtml(p) + '</option>'
|
|
359
359
|
).join('');
|
|
360
360
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
361
361
|
|
|
@@ -443,41 +443,41 @@ function openWorkItemDetail(id) {
|
|
|
443
443
|
if (!item) return;
|
|
444
444
|
|
|
445
445
|
const field = (label, value) => value ? '<div style="margin-bottom:8px"><span style="color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:0.5px">' + label + '</span><div style="margin-top:2px">' + value + '</div></div>' : '';
|
|
446
|
-
const badge = (cls, text) => '<span class="pr-badge ' + cls + '">' +
|
|
446
|
+
const badge = (cls, text) => '<span class="pr-badge ' + cls + '">' + escapeHtml(text) + '</span>';
|
|
447
447
|
const statusCls = item.status === 'failed' ? 'rejected' : item.status === 'dispatched' ? 'building' : item.status === 'done' ? 'approved' : 'active';
|
|
448
448
|
|
|
449
449
|
let html = '<div style="display:flex;flex-direction:column;gap:4px;font-size:13px">';
|
|
450
450
|
html += '<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">' +
|
|
451
451
|
badge(statusCls, item.status || 'pending') +
|
|
452
452
|
(item._reopened ? ' <span style="font-size:9px;color:var(--purple);margin-left:4px" title="This item was reopened from a previously completed state">reopened</span>' : '') + ' ' +
|
|
453
|
-
'<span class="dispatch-type ' + (item.type || 'implement') + '">' +
|
|
454
|
-
'<span class="prd-item-priority ' + (item.priority || '') + '">' +
|
|
453
|
+
'<span class="dispatch-type ' + (item.type || 'implement') + '">' + escapeHtml(item.type || 'implement') + '</span>' +
|
|
454
|
+
'<span class="prd-item-priority ' + (item.priority || '') + '">' + escapeHtml(item.priority || 'medium') + '</span>' +
|
|
455
455
|
'</div>';
|
|
456
456
|
html += field('Description', '<div style="font-size:12px;max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + renderMd(item.description || item.title || '—') + '</div>');
|
|
457
|
-
html += field('Agent',
|
|
458
|
-
html += field('Source',
|
|
459
|
-
if (item.created) html += field('Created',
|
|
460
|
-
if (item.dispatched_at) html += field('Dispatched',
|
|
461
|
-
if (item.completedAt) html += field('Completed',
|
|
462
|
-
if (item.failReason) html += field('Failure Reason', '<span style="color:var(--red)">' +
|
|
463
|
-
if (item._pendingReason && item.status === 'pending') html += field('Pending Reason', item._pendingReason === 'already_dispatched' ? 'Queued — waiting for available agent slot' :
|
|
464
|
-
if (item._skipReason && item.status === 'pending') html += field('Dispatch Blocked', '<span style="color:var(--yellow)">' +
|
|
465
|
-
if (item.depends_on?.length) html += field('Depends On', item.depends_on.map(d => '<code>' +
|
|
466
|
-
if (item.acceptanceCriteria?.length) html += field('Acceptance Criteria', '<ul style="margin:0;padding-left:20px">' + item.acceptanceCriteria.map(c => '<li>' +
|
|
467
|
-
if (item.references?.length) html += field('References', item.references.map(r => '<a href="' +
|
|
468
|
-
if (item._humanFeedback) html += field('Human Feedback', (item._humanFeedback.rating === 'up' ? '👍' : '👎') + (item._humanFeedback.comment ? ' — ' +
|
|
469
|
-
if (item._pr) html += field('Pull Request', '<a href="' +
|
|
457
|
+
html += field('Agent', escapeHtml(item.dispatched_to || item.agent || 'Auto'));
|
|
458
|
+
html += field('Source', escapeHtml(item._source || 'central'));
|
|
459
|
+
if (item.created) html += field('Created', escapeHtml(new Date(item.created).toLocaleString()));
|
|
460
|
+
if (item.dispatched_at) html += field('Dispatched', escapeHtml(new Date(item.dispatched_at).toLocaleString()) + ' to ' + escapeHtml(item.dispatched_to || '?'));
|
|
461
|
+
if (item.completedAt) html += field('Completed', escapeHtml(new Date(item.completedAt).toLocaleString()));
|
|
462
|
+
if (item.failReason) html += field('Failure Reason', '<span style="color:var(--red)">' + escapeHtml(item.failReason) + '</span>');
|
|
463
|
+
if (item._pendingReason && item.status === 'pending') html += field('Pending Reason', item._pendingReason === 'already_dispatched' ? 'Queued — waiting for available agent slot' : escapeHtml(item._pendingReason.replace(/_/g, ' ')));
|
|
464
|
+
if (item._skipReason && item.status === 'pending') html += field('Dispatch Blocked', '<span style="color:var(--yellow)">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + '</span>' + (item._blockedBy ? ' — blocked by <strong>' + escapeHtml(item._blockedBy) + '</strong>' : ''));
|
|
465
|
+
if (item.depends_on?.length) html += field('Depends On', item.depends_on.map(d => '<code>' + escapeHtml(d) + '</code>').join(', '));
|
|
466
|
+
if (item.acceptanceCriteria?.length) html += field('Acceptance Criteria', '<ul style="margin:0;padding-left:20px">' + item.acceptanceCriteria.map(c => '<li>' + escapeHtml(c) + '</li>').join('') + '</ul>');
|
|
467
|
+
if (item.references?.length) html += field('References', item.references.map(r => '<a href="' + escapeHtml(r.url) + '" target="_blank" style="color:var(--blue)">' + escapeHtml(r.title || r.url) + '</a>' + (r.type ? ' <span style="color:var(--muted);font-size:10px">(' + escapeHtml(r.type) + ')</span>' : '')).join('<br>'));
|
|
468
|
+
if (item._humanFeedback) html += field('Human Feedback', (item._humanFeedback.rating === 'up' ? '👍' : '👎') + (item._humanFeedback.comment ? ' — ' + escapeHtml(item._humanFeedback.comment) : ''));
|
|
469
|
+
if (item._pr) html += field('Pull Request', '<a href="' + escapeHtml(item._prUrl || '#') + '" target="_blank" style="color:var(--blue)">' + escapeHtml(item._pr) + '</a>');
|
|
470
470
|
|
|
471
471
|
// Artifacts — output log, branch, skills, etc.
|
|
472
472
|
var arts = item._artifacts || {};
|
|
473
473
|
var artPills = '';
|
|
474
474
|
var pillStyle = 'display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:10px;cursor:pointer;background:var(--surface2);border:1px solid var(--border);color:var(--text)';
|
|
475
475
|
// Output log pill removed — raw stream-json output is not human-readable
|
|
476
|
-
var artBackFn = "pushModalBack(function(){openWorkItemDetail('" +
|
|
477
|
-
if (arts.branch) artPills += '<span style="' + pillStyle + ';cursor:default">🌿 ' +
|
|
478
|
-
if (arts.plan) artPills += '<span onclick="' + artBackFn + 'planView(\'' +
|
|
479
|
-
if (arts.prd) artPills += '<span onclick="' + artBackFn + 'planView(\'' +
|
|
480
|
-
if (arts.sourcePlan) artPills += '<span onclick="' + artBackFn + 'planView(\'' +
|
|
476
|
+
var artBackFn = "pushModalBack(function(){openWorkItemDetail('" + escapeHtml(item.id) + "')});";
|
|
477
|
+
if (arts.branch) artPills += '<span style="' + pillStyle + ';cursor:default">🌿 ' + escapeHtml(arts.branch) + '</span> ';
|
|
478
|
+
if (arts.plan) artPills += '<span onclick="' + artBackFn + 'planView(\'' + escapeHtml(arts.plan) + '\')" style="' + pillStyle + '">📋 Plan</span> ';
|
|
479
|
+
if (arts.prd) artPills += '<span onclick="' + artBackFn + 'planView(\'' + escapeHtml(arts.prd) + '\')" style="' + pillStyle + '">📄 PRD</span> ';
|
|
480
|
+
if (arts.sourcePlan) artPills += '<span onclick="' + artBackFn + 'planView(\'' + escapeHtml(arts.sourcePlan) + '\')" style="' + pillStyle + '">📋 Source Plan</span> ';
|
|
481
481
|
if (arts.notes && arts.notes.length > 0) {
|
|
482
482
|
arts.notes.forEach(function(n) {
|
|
483
483
|
var noteFile = (n && typeof n === 'object') ? (n.file || n) : String(n || '');
|
|
@@ -486,17 +486,17 @@ function openWorkItemDetail(id) {
|
|
|
486
486
|
var kbCat = kbParts[0];
|
|
487
487
|
var kbFile = kbParts.slice(1).join('/');
|
|
488
488
|
var kbLabel = kbFile.replace(/\.md$/, '').slice(0, 30);
|
|
489
|
-
artPills += '<span onclick="' + artBackFn + 'kbOpenItem(\'' +
|
|
489
|
+
artPills += '<span onclick="' + artBackFn + 'kbOpenItem(\'' + escapeHtml(kbCat) + '\',\'' + escapeHtml(kbFile) + '\')" style="' + pillStyle + '">📚 ' + escapeHtml(kbLabel) + '</span> ';
|
|
490
490
|
} else if (noteFile.startsWith('archive:')) {
|
|
491
491
|
var archLabel = noteFile.slice(8).replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '').slice(0, 30);
|
|
492
|
-
artPills += '<span onclick="' + artBackFn + 'openInboxNote(\'' +
|
|
492
|
+
artPills += '<span onclick="' + artBackFn + 'openInboxNote(\'' + escapeHtml(noteFile.slice(8)) + '\')" style="' + pillStyle + ';opacity:0.7">📄 ' + escapeHtml(archLabel) + ' <span style="font-size:8px">(archived)</span></span> ';
|
|
493
493
|
} else {
|
|
494
494
|
var noteLabel = noteFile.replace(/\.md$/, '').slice(0, 30);
|
|
495
|
-
artPills += '<span onclick="' + artBackFn + 'openInboxNote(\'' +
|
|
495
|
+
artPills += '<span onclick="' + artBackFn + 'openInboxNote(\'' + escapeHtml(noteFile) + '\')" style="' + pillStyle + '">📝 ' + escapeHtml(noteLabel) + '</span> ';
|
|
496
496
|
}
|
|
497
497
|
});
|
|
498
498
|
}
|
|
499
|
-
if (arts.skills && arts.skills.length > 0) arts.skills.forEach(function(s) { artPills += '<span onclick="openSkill(\'' +
|
|
499
|
+
if (arts.skills && arts.skills.length > 0) arts.skills.forEach(function(s) { artPills += '<span onclick="openSkill(\'' + escapeHtml(s) + '\',\'minions\',\'\')" style="' + pillStyle + '">⚙ ' + escapeHtml(s) + '</span> '; });
|
|
500
500
|
if (artPills) html += field('Artifacts', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + artPills + '</div>');
|
|
501
501
|
|
|
502
502
|
if (item._totalCostUsd != null) html += field('Cumulative Cost', '$' + Number(item._totalCostUsd).toFixed(4));
|
package/dashboard/js/utils.js
CHANGED
|
@@ -96,6 +96,25 @@ function toggleModalPin() {
|
|
|
96
96
|
else if (_modalFilePath.startsWith('knowledge/')) renderKnowledgeBase();
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Canonical HTML-escape helper (SEC-03). Use this in all new code and for any user-controlled
|
|
100
|
+
// field that reaches `.innerHTML` / a template literal. Escapes the 6 HTML metacharacters
|
|
101
|
+
// (& < > " ' /) — the `/` escape closes the `</script>` break-out path that a 5-char escape
|
|
102
|
+
// leaves open. Returns '' for null/undefined so missing fields never render the literal strings
|
|
103
|
+
// "null"/"undefined". Idempotent for non-metacharacter input (double-escaping only expands `&`).
|
|
104
|
+
function escapeHtml(str) {
|
|
105
|
+
if (str === null || str === undefined) return '';
|
|
106
|
+
return String(str)
|
|
107
|
+
.replace(/&/g, '&')
|
|
108
|
+
.replace(/</g, '<')
|
|
109
|
+
.replace(/>/g, '>')
|
|
110
|
+
.replace(/"/g, '"')
|
|
111
|
+
.replace(/'/g, ''')
|
|
112
|
+
.replace(/\//g, '/');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Legacy 5-char escape. Kept as a backward-compat alias so files not yet migrated under
|
|
116
|
+
// SEC-03 Phase A still compile. Prefer `escapeHtml` in new code — it adds the `/` escape
|
|
117
|
+
// and null/undefined handling. Phase B will remove this once the remaining renderers move.
|
|
99
118
|
function escHtml(s) {
|
|
100
119
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
101
120
|
}
|
|
@@ -482,4 +501,4 @@ async function submitBugReport() {
|
|
|
482
501
|
}
|
|
483
502
|
}
|
|
484
503
|
|
|
485
|
-
window.MinionsUtils = { wakeEngine, escHtml, renderMd, normalizePlanFile, timeAgo, statusColor, shouldIgnoreSelectionClick, llmCopyBtn, copyLlmText, openBugReport, submitBugReport };
|
|
504
|
+
window.MinionsUtils = { wakeEngine, escapeHtml, escHtml, renderMd, normalizePlanFile, timeAgo, statusColor, shouldIgnoreSelectionClick, llmCopyBtn, copyLlmText, openBugReport, submitBugReport };
|
package/dashboard.js
CHANGED
|
@@ -1292,7 +1292,20 @@ function readBody(req) {
|
|
|
1292
1292
|
reject(new Error('Request body timeout after 30s'));
|
|
1293
1293
|
}, 30000);
|
|
1294
1294
|
req.on('data', chunk => { body += chunk; if (body.length > 1e6) { clearTimeout(timeout); reject(new Error('Too large')); } });
|
|
1295
|
-
req.on('end', () => {
|
|
1295
|
+
req.on('end', () => {
|
|
1296
|
+
clearTimeout(timeout);
|
|
1297
|
+
let parsed;
|
|
1298
|
+
try { parsed = JSON.parse(body); } catch(e) { reject(e); return; }
|
|
1299
|
+
// Belt-and-braces: reject payloads containing prototype-pollution attack keys
|
|
1300
|
+
// before they reach any downstream Object.assign / spread / deep-merge.
|
|
1301
|
+
if (shared.hasDangerousKey(parsed)) {
|
|
1302
|
+
const err = new Error('Request body contains forbidden key (__proto__, constructor, or prototype)');
|
|
1303
|
+
err.statusCode = 400; // honoured by handler catch blocks so response is 400 regardless of handler
|
|
1304
|
+
reject(err);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
resolve(parsed);
|
|
1308
|
+
});
|
|
1296
1309
|
req.on('error', (e) => { clearTimeout(timeout); reject(e); });
|
|
1297
1310
|
});
|
|
1298
1311
|
}
|
|
@@ -1519,7 +1532,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1519
1532
|
}
|
|
1520
1533
|
}
|
|
1521
1534
|
return jsonReply(res, 200, { ok: true, message: 'Completion check ran but no verify task was needed' });
|
|
1522
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
1535
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
1523
1536
|
}
|
|
1524
1537
|
|
|
1525
1538
|
async function handleWorkItemsRetry(req, res) {
|
|
@@ -1847,7 +1860,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1847
1860
|
if (content) { try { allArchived.push(...JSON.parse(content).map(i => ({ ...i, _source: project.name }))); } catch {} }
|
|
1848
1861
|
}
|
|
1849
1862
|
return jsonReply(res, 200, allArchived);
|
|
1850
|
-
} catch (e) { console.error('Archive fetch error:', e.message); return jsonReply(res, 500, { error: e.message }); }
|
|
1863
|
+
} catch (e) { console.error('Archive fetch error:', e.message); return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
1851
1864
|
}
|
|
1852
1865
|
|
|
1853
1866
|
async function handleWorkItemsReopen(req, res) {
|
|
@@ -3472,7 +3485,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3472
3485
|
}
|
|
3473
3486
|
return jsonReply(res, 200, { ok: true, answer: answer + '\n\n(Read-only — changes not saved)', edited: false, actions });
|
|
3474
3487
|
} finally { _docAbort = null; docChatInFlight.delete(docKey); }
|
|
3475
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
3488
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
3476
3489
|
}
|
|
3477
3490
|
|
|
3478
3491
|
async function handleInboxPersist(req, res) {
|
|
@@ -3660,7 +3673,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3660
3673
|
}
|
|
3661
3674
|
if (!selectedPath) return jsonReply(res, 200, { cancelled: true });
|
|
3662
3675
|
return jsonReply(res, 200, { path: selectedPath.replace(/\\/g, '/') });
|
|
3663
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
3676
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
3664
3677
|
}
|
|
3665
3678
|
|
|
3666
3679
|
// ── Non-repo confirmation tokens (SEC-05) ───────────────────────────────
|
|
@@ -3857,7 +3870,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3857
3870
|
});
|
|
3858
3871
|
|
|
3859
3872
|
return jsonReply(res, 200, { repos: results });
|
|
3860
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
3873
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
3861
3874
|
}
|
|
3862
3875
|
|
|
3863
3876
|
async function handleFileBug(req, res) {
|
|
@@ -3992,7 +4005,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3992
4005
|
} finally {
|
|
3993
4006
|
_releaseCCTab(tabId);
|
|
3994
4007
|
}
|
|
3995
|
-
} catch (e) { _releaseCCTab(tabId); return jsonReply(res, 500, { error: e.message }); }
|
|
4008
|
+
} catch (e) { _releaseCCTab(tabId); return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
3996
4009
|
}
|
|
3997
4010
|
|
|
3998
4011
|
/** Build a lightweight input object for SSE tool events — keeps only the fields formatToolSummary needs, with truncated string values. */
|
|
@@ -4205,8 +4218,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4205
4218
|
} catch (e) {
|
|
4206
4219
|
stopCcHeartbeat();
|
|
4207
4220
|
_releaseCCTab(tabId);
|
|
4208
|
-
|
|
4209
|
-
|
|
4221
|
+
// If SSE headers haven't been sent yet (e.g. readBody guard fired), respond with the
|
|
4222
|
+
// intended HTTP status (400 for prototype-pollution rejection) instead of an SSE event.
|
|
4223
|
+
if (!res.headersSent) {
|
|
4224
|
+
res.statusCode = e.statusCode || 500;
|
|
4225
|
+
res.setHeader('Content-Type', 'application/json');
|
|
4226
|
+
try { res.end(JSON.stringify({ error: e.message })); } catch {}
|
|
4227
|
+
} else {
|
|
4228
|
+
writeCcEvent({ type: 'error', error: e.message });
|
|
4229
|
+
_ccStreamEnded = true; try { res.end(); } catch {}
|
|
4230
|
+
}
|
|
4210
4231
|
}
|
|
4211
4232
|
}
|
|
4212
4233
|
|
|
@@ -4360,7 +4381,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4360
4381
|
try {
|
|
4361
4382
|
const newPid = restartEngine();
|
|
4362
4383
|
return jsonReply(res, 200, { ok: true, pid: newPid });
|
|
4363
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
4384
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
4364
4385
|
}
|
|
4365
4386
|
|
|
4366
4387
|
async function handleSettingsRead(req, res) {
|
|
@@ -4384,7 +4405,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4384
4405
|
})),
|
|
4385
4406
|
routing,
|
|
4386
4407
|
});
|
|
4387
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
4408
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
4388
4409
|
}
|
|
4389
4410
|
|
|
4390
4411
|
async function handleSettingsUpdate(req, res) {
|
|
@@ -4526,7 +4547,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4526
4547
|
? 'Settings saved. Some values were adjusted: ' + _clamped.join('; ')
|
|
4527
4548
|
: 'Settings saved. Engine picks up changes on next tick.';
|
|
4528
4549
|
return jsonReply(res, 200, { ok: true, message: msg, clamped: _clamped });
|
|
4529
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
4550
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
4530
4551
|
}
|
|
4531
4552
|
|
|
4532
4553
|
async function handleSettingsRouting(req, res) {
|
|
@@ -4535,7 +4556,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4535
4556
|
if (!body.content) return jsonReply(res, 400, { error: 'content required' });
|
|
4536
4557
|
safeWrite(path.join(MINIONS_DIR, 'routing.md'), body.content);
|
|
4537
4558
|
return jsonReply(res, 200, { ok: true });
|
|
4538
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
4559
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
4539
4560
|
}
|
|
4540
4561
|
|
|
4541
4562
|
async function handleSettingsReset(req, res) {
|
|
@@ -4548,7 +4569,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4548
4569
|
reloadConfig();
|
|
4549
4570
|
invalidateStatusCache();
|
|
4550
4571
|
return jsonReply(res, 200, { ok: true });
|
|
4551
|
-
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
4572
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
4552
4573
|
}
|
|
4553
4574
|
|
|
4554
4575
|
async function handleHealth(req, res) {
|
package/engine/ado.js
CHANGED
|
@@ -449,7 +449,8 @@ async function pollPrStatus(config) {
|
|
|
449
449
|
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
450
450
|
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${project.repositoryId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
451
451
|
const buildsData = await adoFetch(buildsUrl, token);
|
|
452
|
-
const
|
|
452
|
+
const allBuilds = buildsData?.value || [];
|
|
453
|
+
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
453
454
|
|
|
454
455
|
if (prBuilds.length > 0) {
|
|
455
456
|
buildStatus = classifyBuildStatus(prBuilds);
|
|
@@ -462,6 +463,17 @@ async function pollPrStatus(config) {
|
|
|
462
463
|
_buildId: String(b.id),
|
|
463
464
|
}));
|
|
464
465
|
}
|
|
466
|
+
} else if (allBuilds.length > 0 && pr.buildStatus) {
|
|
467
|
+
// Stale merge-commit fallback — ADO returned builds for this PR's merge ref
|
|
468
|
+
// but none target the current `mergeCommitId`. Most likely the target branch
|
|
469
|
+
// moved, ADO recomputed the merge commit, but no new source-side changes
|
|
470
|
+
// triggered a rebuild. Preserve the previous `pr.buildStatus` so the tracker
|
|
471
|
+
// reflects the last known truth instead of flipping to a spurious 'none'.
|
|
472
|
+
// Also log a warn so stale states are detectable in engine logs. Issue #1233.
|
|
473
|
+
const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
|
|
474
|
+
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); preserving previous buildStatus '${pr.buildStatus}'`);
|
|
475
|
+
buildStatus = pr.buildStatus;
|
|
476
|
+
if (pr.buildFailReason) buildFailReason = pr.buildFailReason;
|
|
465
477
|
}
|
|
466
478
|
} catch (e) { log('warn', `ADO build query for ${pr.id}: ${e.message}`); }
|
|
467
479
|
}
|
package/engine/scheduler.js
CHANGED
|
@@ -24,10 +24,29 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./shared');
|
|
27
|
-
const { safeJson, safeWrite, mutateJsonFileLocked, ts, WI_STATUS } = shared;
|
|
27
|
+
const { safeJson, safeWrite, mutateJsonFileLocked, ts, dateStamp, WI_STATUS } = shared;
|
|
28
28
|
|
|
29
29
|
const SCHEDULE_RUNS_PATH = path.join(__dirname, 'schedule-runs.json');
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Substitute schedule-time template variables in a string.
|
|
33
|
+
* Currently supports:
|
|
34
|
+
* {{date}} — today's date as YYYY-MM-DD (UTC, via dateStamp())
|
|
35
|
+
*
|
|
36
|
+
* Downstream playbook rendering (engine/playbook.js) is a single-pass replace,
|
|
37
|
+
* so any {{date}} embedded in a schedule's title/description would survive
|
|
38
|
+
* substitution of {{task_description}} and surface as an "unresolved template
|
|
39
|
+
* variables: date" warning plus a literal "{{date}}" in agent filenames.
|
|
40
|
+
* Resolve these fields at schedule time so the work item carries a concrete
|
|
41
|
+
* date string from the moment it's created.
|
|
42
|
+
*
|
|
43
|
+
* Safe on undefined/null/empty/non-string inputs — returns the input unchanged.
|
|
44
|
+
*/
|
|
45
|
+
function resolveScheduleTemplateVars(str) {
|
|
46
|
+
if (typeof str !== 'string' || str.length === 0) return str;
|
|
47
|
+
return str.replace(/\{\{date\}\}/g, dateStamp());
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
// Parse a single cron field into a matcher function.
|
|
32
51
|
// field: e.g., "*", "5", "1,3,5", "*/15"
|
|
33
52
|
// min/max: valid range (0-59 for minute, 0-23 for hour, 0-6 for dow)
|
|
@@ -141,13 +160,16 @@ function discoverScheduledWork(config) {
|
|
|
141
160
|
const lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || null);
|
|
142
161
|
if (!shouldRunNow(sched, lastRun)) continue;
|
|
143
162
|
|
|
163
|
+
// Substitute schedule-time template vars (e.g. {{date}}) before the work
|
|
164
|
+
// item is written — single-pass playbook rendering can't reach placeholders
|
|
165
|
+
// embedded inside task_description, so they must be resolved up front.
|
|
144
166
|
const workItemId = `sched-${sched.id}-${Date.now()}`;
|
|
145
167
|
work.push({
|
|
146
168
|
id: workItemId,
|
|
147
|
-
title: sched.title,
|
|
169
|
+
title: resolveScheduleTemplateVars(sched.title),
|
|
148
170
|
type: sched.type || 'implement',
|
|
149
171
|
priority: sched.priority || 'medium',
|
|
150
|
-
description: sched.description || sched.title,
|
|
172
|
+
description: resolveScheduleTemplateVars(sched.description || sched.title),
|
|
151
173
|
status: WI_STATUS.PENDING,
|
|
152
174
|
created: ts(),
|
|
153
175
|
createdBy: 'scheduler',
|
|
@@ -170,4 +192,4 @@ function discoverScheduledWork(config) {
|
|
|
170
192
|
return work;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
|
-
module.exports = { parseCronExpr, parseCronField, shouldRunNow, discoverScheduledWork, SCHEDULE_RUNS_PATH };
|
|
195
|
+
module.exports = { parseCronExpr, parseCronField, shouldRunNow, discoverScheduledWork, resolveScheduleTemplateVars, SCHEDULE_RUNS_PATH };
|
package/engine/shared.js
CHANGED
|
@@ -1058,6 +1058,61 @@ function sanitizePath(file, baseDir) {
|
|
|
1058
1058
|
return resolved;
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
|
+
// ── Prototype Pollution Guard ────────────────────────────────────────────────
|
|
1062
|
+
|
|
1063
|
+
const _DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Detect the presence of prototype-pollution attack keys in a JSON-decoded payload.
|
|
1067
|
+
*
|
|
1068
|
+
* Belt-and-braces defence for endpoints that call `JSON.parse` on untrusted
|
|
1069
|
+
* request bodies and then feed the result into `Object.assign`, spread, or
|
|
1070
|
+
* deep-merge utilities. `JSON.parse` itself is safe — it installs `__proto__`
|
|
1071
|
+
* as a regular own data property and does not mutate the prototype chain —
|
|
1072
|
+
* but downstream code that shallow-merges the payload into a target object
|
|
1073
|
+
* CAN elevate it into a prototype write.
|
|
1074
|
+
*
|
|
1075
|
+
* Contract is **rejection, not sanitization**: we inspect the top level plus
|
|
1076
|
+
* one level deep and return a boolean. Deeper walks are intentionally skipped
|
|
1077
|
+
* to avoid their own DoS pathologies on adversarial inputs.
|
|
1078
|
+
*
|
|
1079
|
+
* - Null / undefined / primitives → false.
|
|
1080
|
+
* - Arrays are transparent: each element is checked at the same depth as the
|
|
1081
|
+
* array itself (an array does NOT consume a depth level).
|
|
1082
|
+
* - Max object nesting inspected: 1. Dangerous keys at object-depth 2+
|
|
1083
|
+
* are intentionally NOT flagged.
|
|
1084
|
+
* - Never mutates the input.
|
|
1085
|
+
*
|
|
1086
|
+
* @param {*} obj - any JSON-decoded value
|
|
1087
|
+
* @param {number} [_depth=0] - internal recursion counter; do not pass externally
|
|
1088
|
+
* @returns {boolean} true if any forbidden key is present at object-depth ≤ 1
|
|
1089
|
+
*/
|
|
1090
|
+
function hasDangerousKey(obj, _depth = 0) {
|
|
1091
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') return false;
|
|
1092
|
+
|
|
1093
|
+
// Arrays are transparent — preserve depth when recursing into elements.
|
|
1094
|
+
if (Array.isArray(obj)) {
|
|
1095
|
+
for (const elt of obj) {
|
|
1096
|
+
if (hasDangerousKey(elt, _depth)) return true;
|
|
1097
|
+
}
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Object: check own keys at the current depth.
|
|
1102
|
+
for (const key of Object.keys(obj)) {
|
|
1103
|
+
if (_DANGEROUS_KEYS.has(key)) return true;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Stop after one level of object nesting. Deeper recursion is an explicit
|
|
1107
|
+
// non-goal (see DoS note in the header).
|
|
1108
|
+
if (_depth >= 1) return false;
|
|
1109
|
+
|
|
1110
|
+
for (const v of Object.values(obj)) {
|
|
1111
|
+
if (hasDangerousKey(v, _depth + 1)) return true;
|
|
1112
|
+
}
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1061
1116
|
/**
|
|
1062
1117
|
* Validate that a PID value is a positive integer. Returns the numeric PID.
|
|
1063
1118
|
* Throws if the value could be used for command injection.
|
|
@@ -1754,6 +1809,7 @@ module.exports = {
|
|
|
1754
1809
|
getAdoOrgBase,
|
|
1755
1810
|
sanitizePath,
|
|
1756
1811
|
sanitizeBranch,
|
|
1812
|
+
hasDangerousKey,
|
|
1757
1813
|
validateProjectName,
|
|
1758
1814
|
validateProjectPath,
|
|
1759
1815
|
validatePid,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1157",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|