@yemi33/minions 0.1.57 → 0.1.59
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 +39 -0
- package/dashboard/js/command-center.js +1 -1
- package/dashboard/js/live-stream.js +3 -3
- package/dashboard/js/modal-qa.js +4 -4
- package/dashboard/js/refresh.js +2 -2
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-kb.js +1 -1
- package/dashboard/js/render-plans.js +3 -3
- package/dashboard/js/render-prs.js +2 -2
- package/dashboard/js/render-work-items.js +2 -2
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +87 -58
- package/engine/ado.js +2 -2
- package/engine/cli.js +16 -16
- package/engine/consolidation.js +4 -4
- package/engine/github.js +3 -3
- package/engine/lifecycle.js +14 -14
- package/engine/llm.js +2 -2
- package/engine/preflight.js +2 -2
- package/engine/queries.js +13 -13
- package/engine/shared.js +6 -6
- package/engine/spawn-agent.js +3 -3
- package/engine.js +101 -101
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -124,11 +124,11 @@ if (fs.existsSync(dashDir)) {
|
|
|
124
124
|
_reloadTimer = setTimeout(rebuildDashboardHtml, 300); // debounce 300ms
|
|
125
125
|
};
|
|
126
126
|
// Watch top-level files (styles.css, layout.html)
|
|
127
|
-
try { fs.watch(dashDir, scheduleReload); } catch {}
|
|
127
|
+
try { fs.watch(dashDir, scheduleReload); } catch { /* optional */ }
|
|
128
128
|
// Watch subdirectories (pages/, js/)
|
|
129
129
|
for (const sub of ['pages', 'js']) {
|
|
130
130
|
const subDir = path.join(dashDir, sub);
|
|
131
|
-
if (fs.existsSync(subDir)) try { fs.watch(subDir, scheduleReload); } catch {}
|
|
131
|
+
if (fs.existsSync(subDir)) try { fs.watch(subDir, scheduleReload); } catch { /* optional */ }
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -145,7 +145,7 @@ function getVerifyGuides() {
|
|
|
145
145
|
const planFile = planSlug + '.json';
|
|
146
146
|
guides.push({ file: f, planFile });
|
|
147
147
|
}
|
|
148
|
-
} catch {}
|
|
148
|
+
} catch (e) { console.error('getVerifyGuides:', e.message); }
|
|
149
149
|
return guides;
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -273,7 +273,7 @@ try {
|
|
|
273
273
|
const age = Date.now() - new Date(saved.lastActiveAt || 0).getTime();
|
|
274
274
|
if (age < CC_SESSION_EXPIRY_MS) ccSession = saved;
|
|
275
275
|
}
|
|
276
|
-
} catch {}
|
|
276
|
+
} catch { /* optional */ }
|
|
277
277
|
|
|
278
278
|
// Static system prompt — baked into session on creation, never changes
|
|
279
279
|
const CC_STATIC_SYSTEM_PROMPT = `You are the Command Center AI for a software engineering minions called "Minions."
|
|
@@ -459,7 +459,7 @@ try {
|
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
|
-
} catch {}
|
|
462
|
+
} catch { /* optional */ }
|
|
463
463
|
|
|
464
464
|
function persistDocSessions() {
|
|
465
465
|
const obj = {};
|
|
@@ -635,6 +635,17 @@ function readBody(req) {
|
|
|
635
635
|
});
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
const _rateLimits = new Map();
|
|
639
|
+
function checkRateLimit(key, maxPerMinute) {
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const entries = _rateLimits.get(key) || [];
|
|
642
|
+
const recent = entries.filter(t => now - t < 60000);
|
|
643
|
+
if (recent.length >= maxPerMinute) return true;
|
|
644
|
+
recent.push(now);
|
|
645
|
+
_rateLimits.set(key, recent);
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
638
649
|
function jsonReply(res, code, data, req) {
|
|
639
650
|
res.setHeader('Content-Type', 'application/json');
|
|
640
651
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -676,12 +687,12 @@ function cleanDispatchEntries(matchFn) {
|
|
|
676
687
|
try {
|
|
677
688
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
|
|
678
689
|
if (pid) process.kill(pid, 'SIGTERM');
|
|
679
|
-
} catch {}
|
|
680
|
-
try { fs.unlinkSync(pidFile); } catch {}
|
|
690
|
+
} catch { /* process may be dead */ }
|
|
691
|
+
try { fs.unlinkSync(pidFile); } catch { /* cleanup */ }
|
|
681
692
|
// Clean up temp prompt files
|
|
682
|
-
try { fs.unlinkSync(path.join(engineDir, 'tmp', `prompt-${d.id}.md`)); } catch {}
|
|
683
|
-
try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md`)); } catch {}
|
|
684
|
-
try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md.tmp`)); } catch {}
|
|
693
|
+
try { fs.unlinkSync(path.join(engineDir, 'tmp', `prompt-${d.id}.md`)); } catch { /* cleanup */ }
|
|
694
|
+
try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md`)); } catch { /* cleanup */ }
|
|
695
|
+
try { fs.unlinkSync(path.join(engineDir, 'tmp', `sysprompt-${d.id}.md.tmp`)); } catch { /* cleanup */ }
|
|
685
696
|
}
|
|
686
697
|
}
|
|
687
698
|
dispatch[queue] = dispatch[queue].filter(d => !matchFn(d));
|
|
@@ -718,7 +729,7 @@ function killEnginePid(pid) {
|
|
|
718
729
|
} else {
|
|
719
730
|
process.kill(pid, 'SIGKILL');
|
|
720
731
|
}
|
|
721
|
-
} catch {}
|
|
732
|
+
} catch { /* process may be dead */ }
|
|
722
733
|
}
|
|
723
734
|
|
|
724
735
|
function restartEngine() {
|
|
@@ -835,7 +846,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
835
846
|
dispatch.completed = dispatch.completed.filter(d => !d.meta?.parentKey || d.meta.parentKey !== dispatchKey);
|
|
836
847
|
return dispatch;
|
|
837
848
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
838
|
-
} catch {}
|
|
849
|
+
} catch (e) { console.error('dispatch cleanup:', e.message); }
|
|
839
850
|
|
|
840
851
|
// Clear cooldown so item isn't blocked by exponential backoff
|
|
841
852
|
try {
|
|
@@ -845,7 +856,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
845
856
|
delete cooldowns[dispatchKey];
|
|
846
857
|
safeWrite(cooldownPath, cooldowns);
|
|
847
858
|
}
|
|
848
|
-
} catch {}
|
|
859
|
+
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
849
860
|
|
|
850
861
|
return jsonReply(res, 200, { ok: true, id });
|
|
851
862
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
@@ -894,7 +905,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
894
905
|
if (key.includes(id)) { delete cooldowns[key]; cleaned = true; }
|
|
895
906
|
}
|
|
896
907
|
if (cleaned) safeWrite(cooldownPath, cooldowns);
|
|
897
|
-
} catch {}
|
|
908
|
+
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
898
909
|
|
|
899
910
|
invalidateStatusCache();
|
|
900
911
|
return jsonReply(res, 200, { ok: true, id, dispatchRemoved });
|
|
@@ -1153,7 +1164,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1153
1164
|
safeWrite(wiPath, items);
|
|
1154
1165
|
workItemSynced = true;
|
|
1155
1166
|
}
|
|
1156
|
-
} catch {}
|
|
1167
|
+
} catch (e) { console.error('work item sync:', e.message); }
|
|
1157
1168
|
}
|
|
1158
1169
|
|
|
1159
1170
|
return jsonReply(res, 200, { ok: true, item, workItemSynced });
|
|
@@ -1185,7 +1196,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1185
1196
|
safeWrite(wiPath, filtered);
|
|
1186
1197
|
cancelled = true;
|
|
1187
1198
|
}
|
|
1188
|
-
} catch {}
|
|
1199
|
+
} catch (e) { console.error('work item cleanup:', e.message); }
|
|
1189
1200
|
}
|
|
1190
1201
|
// Also check central work-items
|
|
1191
1202
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -1194,7 +1205,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1194
1205
|
const before = items.length;
|
|
1195
1206
|
const filtered = items.filter(w => !(w.sourcePlan === body.source && w.id === body.itemId));
|
|
1196
1207
|
if (filtered.length < before) { safeWrite(centralPath, filtered); cancelled = true; }
|
|
1197
|
-
} catch {}
|
|
1208
|
+
} catch (e) { console.error('central work item cleanup:', e.message); }
|
|
1198
1209
|
|
|
1199
1210
|
// Clean dispatch entries for this item
|
|
1200
1211
|
cleanDispatchEntries(d =>
|
|
@@ -1224,16 +1235,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
1224
1235
|
const status = JSON.parse(safeRead(statusPath) || '{}');
|
|
1225
1236
|
if (status.pid) {
|
|
1226
1237
|
if (process.platform === 'win32') {
|
|
1227
|
-
try { require('child_process').execSync('taskkill /PID ' + status.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch {}
|
|
1238
|
+
try { require('child_process').execSync('taskkill /PID ' + status.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch { /* process may be dead */ }
|
|
1228
1239
|
} else {
|
|
1229
|
-
try { process.kill(status.pid, 'SIGTERM'); } catch {}
|
|
1240
|
+
try { process.kill(status.pid, 'SIGTERM'); } catch { /* process may be dead */ }
|
|
1230
1241
|
}
|
|
1231
1242
|
}
|
|
1232
1243
|
status.status = 'idle';
|
|
1233
1244
|
delete status.currentTask;
|
|
1234
1245
|
delete status.dispatched;
|
|
1235
1246
|
safeWrite(statusPath, status);
|
|
1236
|
-
} catch {}
|
|
1247
|
+
} catch (e) { console.error('agent cancel:', e.message); }
|
|
1237
1248
|
|
|
1238
1249
|
cancelled.push({ agent: d.agent, task: d.task });
|
|
1239
1250
|
}
|
|
@@ -1270,7 +1281,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1270
1281
|
'Content-Type': 'text/event-stream',
|
|
1271
1282
|
'Cache-Control': 'no-cache',
|
|
1272
1283
|
'Connection': 'keep-alive',
|
|
1273
|
-
'Access-Control-Allow-Origin': '*',
|
|
1274
1284
|
});
|
|
1275
1285
|
|
|
1276
1286
|
// Send initial content
|
|
@@ -1281,7 +1291,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1281
1291
|
res.write(`data: ${JSON.stringify(content)}\n\n`);
|
|
1282
1292
|
offset = Buffer.byteLength(content, 'utf8');
|
|
1283
1293
|
}
|
|
1284
|
-
} catch {}
|
|
1294
|
+
} catch { /* optional */ }
|
|
1285
1295
|
|
|
1286
1296
|
// Watch for changes using fs.watchFile (cross-platform, works on Windows)
|
|
1287
1297
|
const watcher = () => {
|
|
@@ -1296,7 +1306,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1296
1306
|
const chunk = buf.toString('utf8');
|
|
1297
1307
|
if (chunk) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
1298
1308
|
}
|
|
1299
|
-
} catch {}
|
|
1309
|
+
} catch { /* optional */ }
|
|
1300
1310
|
};
|
|
1301
1311
|
|
|
1302
1312
|
fs.watchFile(liveLogPath, { interval: 500 }, watcher);
|
|
@@ -1387,7 +1397,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1387
1397
|
const cat = match[1];
|
|
1388
1398
|
const file = decodeURIComponent(match[2]);
|
|
1389
1399
|
// Prevent path traversal
|
|
1390
|
-
if (file.includes('..') || file.includes('/') || file.includes('\\')) {
|
|
1400
|
+
if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) {
|
|
1391
1401
|
return jsonReply(res, 400, { error: 'invalid file name' });
|
|
1392
1402
|
}
|
|
1393
1403
|
const content = safeRead(path.join(MINIONS_DIR, 'knowledge', cat, file));
|
|
@@ -1487,7 +1497,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1487
1497
|
const meta = `<!-- swept: ${new Date().toISOString()} | reason: ${reason} -->\n`;
|
|
1488
1498
|
safeWrite(destPath, meta + content);
|
|
1489
1499
|
safeUnlink(filePath);
|
|
1490
|
-
} catch {}
|
|
1500
|
+
} catch (e) { console.error('kb archive:', e.message); }
|
|
1491
1501
|
}
|
|
1492
1502
|
|
|
1493
1503
|
// Process removals (stale/empty) — archive, not delete
|
|
@@ -1524,7 +1534,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1524
1534
|
safeWrite(path.join(destDir, entry.file), updated);
|
|
1525
1535
|
safeUnlink(srcPath);
|
|
1526
1536
|
reclassified++;
|
|
1527
|
-
} catch {}
|
|
1537
|
+
} catch (e) { console.error('kb reclassify:', e.message); }
|
|
1528
1538
|
}
|
|
1529
1539
|
|
|
1530
1540
|
// Prune swept files older than 30 days
|
|
@@ -1535,9 +1545,9 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1535
1545
|
const fp = path.join(kbArchiveDir, f);
|
|
1536
1546
|
try {
|
|
1537
1547
|
if (Date.now() - fs.statSync(fp).mtimeMs > SWEPT_RETENTION_MS) { safeUnlink(fp); pruned++; }
|
|
1538
|
-
} catch {}
|
|
1548
|
+
} catch { /* cleanup */ }
|
|
1539
1549
|
}
|
|
1540
|
-
} catch {}
|
|
1550
|
+
} catch { /* optional */ }
|
|
1541
1551
|
|
|
1542
1552
|
const summary = `${merged} duplicates merged, ${removed} stale removed, ${reclassified} reclassified${pruned ? ', ' + pruned + ' old swept files pruned' : ''}`;
|
|
1543
1553
|
safeWrite(path.join(ENGINE_DIR, 'kb-swept.json'), JSON.stringify({ timestamp: new Date().toISOString(), summary }));
|
|
@@ -1565,7 +1575,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1565
1575
|
const filePath = path.join(dir, f);
|
|
1566
1576
|
const content = safeRead(filePath) || '';
|
|
1567
1577
|
let updatedAt = '';
|
|
1568
|
-
try { updatedAt = new Date(fs.statSync(filePath).mtimeMs).toISOString(); } catch {}
|
|
1578
|
+
try { updatedAt = new Date(fs.statSync(filePath).mtimeMs).toISOString(); } catch { /* optional */ }
|
|
1569
1579
|
const isJson = f.endsWith('.json');
|
|
1570
1580
|
if (isJson) {
|
|
1571
1581
|
try {
|
|
@@ -1587,7 +1597,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1587
1597
|
revisionFeedback: plan.revision_feedback || null,
|
|
1588
1598
|
sourcePlan: plan.source_plan || null,
|
|
1589
1599
|
});
|
|
1590
|
-
} catch {}
|
|
1600
|
+
} catch { /* JSON parse fallback */ }
|
|
1591
1601
|
} else {
|
|
1592
1602
|
const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
|
|
1593
1603
|
const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
|
|
@@ -1618,7 +1628,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1618
1628
|
|
|
1619
1629
|
async function handlePlansArchiveRead(req, res, match) {
|
|
1620
1630
|
const file = decodeURIComponent(match[1]);
|
|
1621
|
-
if (file.includes('..')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1631
|
+
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1622
1632
|
// Check prd/archive/ first for .json, then plans/archive/ for .md
|
|
1623
1633
|
const archiveDir = file.endsWith('.json') ? path.join(PRD_DIR, 'archive') : path.join(PLANS_DIR, 'archive');
|
|
1624
1634
|
let content = safeRead(path.join(archiveDir, file));
|
|
@@ -1634,7 +1644,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1634
1644
|
|
|
1635
1645
|
async function handlePlansRead(req, res, match) {
|
|
1636
1646
|
const file = decodeURIComponent(match[1]);
|
|
1637
|
-
if (file.includes('..') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1647
|
+
if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1638
1648
|
let content = safeRead(resolvePlanPath(file));
|
|
1639
1649
|
// Fallback: check all directories (prd/, plans/, guides/, archives)
|
|
1640
1650
|
if (!content) content = safeRead(path.join(PRD_DIR, file));
|
|
@@ -1645,7 +1655,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1645
1655
|
if (!content) return jsonReply(res, 404, { error: 'not found' });
|
|
1646
1656
|
// Find the actual file path for Last-Modified header + expose resolved relative path
|
|
1647
1657
|
const planCandidates = [resolvePlanPath(file), path.join(PRD_DIR, file), path.join(PRD_DIR, 'guides', file), path.join(PLANS_DIR, file), path.join(PRD_DIR, 'archive', file), path.join(PLANS_DIR, 'archive', file)];
|
|
1648
|
-
for (const p of planCandidates) { try { const st = fs.statSync(p); if (st) { res.setHeader('Last-Modified', st.mtime.toISOString()); res.setHeader('X-Resolved-Path', path.relative(MINIONS_DIR, p).replace(/\\/g, '/')); break; } } catch {} }
|
|
1658
|
+
for (const p of planCandidates) { try { const st = fs.statSync(p); if (st) { res.setHeader('Last-Modified', st.mtime.toISOString()); res.setHeader('X-Resolved-Path', path.relative(MINIONS_DIR, p).replace(/\\/g, '/')); break; } } catch { /* optional */ } }
|
|
1649
1659
|
const contentType = file.endsWith('.json') ? 'application/json' : 'text/plain';
|
|
1650
1660
|
res.setHeader('Content-Type', contentType + '; charset=utf-8');
|
|
1651
1661
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1689,7 +1699,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1689
1699
|
}
|
|
1690
1700
|
}
|
|
1691
1701
|
if (changed) safeWrite(wiPath, items);
|
|
1692
|
-
} catch {}
|
|
1702
|
+
} catch (e) { console.error('resume work items:', e.message); }
|
|
1693
1703
|
}
|
|
1694
1704
|
|
|
1695
1705
|
// Clear dispatch completed entries for resumed items so they aren't dedup-blocked
|
|
@@ -1749,16 +1759,16 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1749
1759
|
const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
|
|
1750
1760
|
if (agentStatus.pid) {
|
|
1751
1761
|
if (process.platform === 'win32') {
|
|
1752
|
-
try { require('child_process').execSync('taskkill /PID ' + agentStatus.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch {}
|
|
1762
|
+
try { require('child_process').execSync('taskkill /PID ' + agentStatus.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch { /* process may be dead */ }
|
|
1753
1763
|
} else {
|
|
1754
|
-
try { process.kill(agentStatus.pid, 'SIGTERM'); } catch {}
|
|
1764
|
+
try { process.kill(agentStatus.pid, 'SIGTERM'); } catch { /* process may be dead */ }
|
|
1755
1765
|
}
|
|
1756
1766
|
}
|
|
1757
1767
|
agentStatus.status = 'idle';
|
|
1758
1768
|
delete agentStatus.currentTask;
|
|
1759
1769
|
delete agentStatus.dispatched;
|
|
1760
1770
|
safeWrite(statusPath, agentStatus);
|
|
1761
|
-
} catch {}
|
|
1771
|
+
} catch (e) { console.error('agent reset:', e.message); }
|
|
1762
1772
|
killedAgents.add(activeEntry.agent);
|
|
1763
1773
|
}
|
|
1764
1774
|
}
|
|
@@ -1775,7 +1785,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1775
1785
|
if (w.id) resetItemIds.add(w.id);
|
|
1776
1786
|
}
|
|
1777
1787
|
if (changed) safeWrite(wiPath, items);
|
|
1778
|
-
} catch {}
|
|
1788
|
+
} catch (e) { console.error('reset work items:', e.message); }
|
|
1779
1789
|
}
|
|
1780
1790
|
|
|
1781
1791
|
// Remove dispatch active entries for reset items or killed agents.
|
|
@@ -1801,7 +1811,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1801
1811
|
try {
|
|
1802
1812
|
const body = await readBody(req);
|
|
1803
1813
|
if (!body.file) return jsonReply(res, 400, { error: 'file is required' });
|
|
1804
|
-
if (body.file.includes('..')) return jsonReply(res, 400, { error: 'invalid file path' });
|
|
1814
|
+
if (body.file.includes('..') || body.file.includes('\0')) return jsonReply(res, 400, { error: 'invalid file path' });
|
|
1805
1815
|
|
|
1806
1816
|
const prdPath = path.join(PRD_DIR, body.file);
|
|
1807
1817
|
const plan = safeJson(prdPath);
|
|
@@ -1832,7 +1842,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1832
1842
|
}
|
|
1833
1843
|
|
|
1834
1844
|
// Delete old PRD — agent will write replacement at same path
|
|
1835
|
-
try { fs.unlinkSync(prdPath); } catch {}
|
|
1845
|
+
try { fs.unlinkSync(prdPath); } catch { /* cleanup */ }
|
|
1836
1846
|
|
|
1837
1847
|
// Queue plan-to-prd regeneration with instructions to preserve completed items
|
|
1838
1848
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -1865,6 +1875,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1865
1875
|
}
|
|
1866
1876
|
|
|
1867
1877
|
async function handlePlansExecute(req, res) {
|
|
1878
|
+
if (checkRateLimit('plans-execute', 5)) return jsonReply(res, 429, { error: 'Rate limited — max 5 requests/minute' });
|
|
1868
1879
|
try {
|
|
1869
1880
|
const body = await readBody(req);
|
|
1870
1881
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
@@ -1952,7 +1963,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1952
1963
|
if (filtered.length < items.length) {
|
|
1953
1964
|
safeWrite(wiInfo.path, filtered);
|
|
1954
1965
|
}
|
|
1955
|
-
} catch {}
|
|
1966
|
+
} catch (e) { console.error('work item sync:', e.message); }
|
|
1956
1967
|
}
|
|
1957
1968
|
|
|
1958
1969
|
// Count plan items that have no work item yet (will auto-materialize)
|
|
@@ -1975,7 +1986,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1975
1986
|
try {
|
|
1976
1987
|
const body = await readBody(req);
|
|
1977
1988
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1978
|
-
if (body.file.includes('..') || body.file.includes('/') || body.file.includes('\\')) {
|
|
1989
|
+
if (body.file.includes('..') || body.file.includes('\0') || body.file.includes('/') || body.file.includes('\\')) {
|
|
1979
1990
|
return jsonReply(res, 400, { error: 'invalid filename' });
|
|
1980
1991
|
}
|
|
1981
1992
|
const planPath = resolvePlanPath(body.file);
|
|
@@ -2001,7 +2012,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2001
2012
|
cleaned += items.length - filtered.length;
|
|
2002
2013
|
safeWrite(wiPath, filtered);
|
|
2003
2014
|
}
|
|
2004
|
-
} catch {}
|
|
2015
|
+
} catch (e) { console.error('plan cleanup:', e.message); }
|
|
2005
2016
|
}
|
|
2006
2017
|
|
|
2007
2018
|
// Clean up dispatch entries for this plan's items
|
|
@@ -2025,7 +2036,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2025
2036
|
}
|
|
2026
2037
|
}
|
|
2027
2038
|
if (changed) safeWrite(centralPath, centralItems);
|
|
2028
|
-
} catch {}
|
|
2039
|
+
} catch (e) { console.error('plan-to-prd cleanup:', e.message); }
|
|
2029
2040
|
}
|
|
2030
2041
|
|
|
2031
2042
|
invalidateStatusCache();
|
|
@@ -2166,7 +2177,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2166
2177
|
}
|
|
2167
2178
|
}
|
|
2168
2179
|
if (filtered.length < items.length) safeWrite(wiInfo.path, filtered);
|
|
2169
|
-
} catch {}
|
|
2180
|
+
} catch (e) { console.error('work item deletion:', e.message); }
|
|
2170
2181
|
}
|
|
2171
2182
|
for (const itemId of deletedItemIds) {
|
|
2172
2183
|
cleanDispatchEntries(d =>
|
|
@@ -2345,6 +2356,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2345
2356
|
const body = await readBody(req);
|
|
2346
2357
|
const { name } = body;
|
|
2347
2358
|
if (!name) return jsonReply(res, 400, { error: 'name required' });
|
|
2359
|
+
if (name.includes('..') || name.includes('\0')) return jsonReply(res, 400, { error: 'Invalid file name' });
|
|
2348
2360
|
|
|
2349
2361
|
const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', name);
|
|
2350
2362
|
const content = safeRead(inboxPath);
|
|
@@ -2373,7 +2385,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2373
2385
|
// Move to archive
|
|
2374
2386
|
const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
2375
2387
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
2376
|
-
try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `persisted-${name}`), _c); safeUnlink(inboxPath); } catch {}
|
|
2388
|
+
try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `persisted-${name}`), _c); safeUnlink(inboxPath); } catch (e) { console.error('inbox archive:', e.message); }
|
|
2377
2389
|
|
|
2378
2390
|
return jsonReply(res, 200, { ok: true, title });
|
|
2379
2391
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
@@ -2384,6 +2396,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2384
2396
|
const body = await readBody(req);
|
|
2385
2397
|
const { name, category } = body;
|
|
2386
2398
|
if (!name) return jsonReply(res, 400, { error: 'name required' });
|
|
2399
|
+
if (name.includes('..') || name.includes('\0')) return jsonReply(res, 400, { error: 'Invalid file name' });
|
|
2387
2400
|
if (!category || !shared.KB_CATEGORIES.includes(category)) {
|
|
2388
2401
|
return jsonReply(res, 400, { error: 'category required: ' + shared.KB_CATEGORIES.join(', ') });
|
|
2389
2402
|
}
|
|
@@ -2410,7 +2423,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2410
2423
|
// Move inbox item to archive
|
|
2411
2424
|
const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
2412
2425
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
2413
|
-
try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `kb-${category}-${name}`), _c); safeUnlink(inboxPath); } catch {}
|
|
2426
|
+
try { const _c = safeRead(inboxPath); safeWrite(path.join(archiveDir, `kb-${category}-${name}`), _c); safeUnlink(inboxPath); } catch (e) { console.error('inbox archive:', e.message); }
|
|
2414
2427
|
|
|
2415
2428
|
return jsonReply(res, 200, { ok: true, category, file: name });
|
|
2416
2429
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
@@ -2420,7 +2433,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2420
2433
|
try {
|
|
2421
2434
|
const body = await readBody(req);
|
|
2422
2435
|
const { name } = body;
|
|
2423
|
-
if (!name || name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
2436
|
+
if (!name || name.includes('..') || name.includes('\0') || name.includes('/') || name.includes('\\')) {
|
|
2424
2437
|
return jsonReply(res, 400, { error: 'invalid name' });
|
|
2425
2438
|
}
|
|
2426
2439
|
const filePath = path.join(MINIONS_DIR, 'notes', 'inbox', name);
|
|
@@ -2446,7 +2459,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2446
2459
|
try {
|
|
2447
2460
|
const body = await readBody(req);
|
|
2448
2461
|
const { name } = body;
|
|
2449
|
-
if (!name || name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
2462
|
+
if (!name || name.includes('..') || name.includes('\0') || name.includes('/') || name.includes('\\')) {
|
|
2450
2463
|
return jsonReply(res, 400, { error: 'invalid name' });
|
|
2451
2464
|
}
|
|
2452
2465
|
const filePath = path.join(MINIONS_DIR, 'notes', 'inbox', name);
|
|
@@ -2460,7 +2473,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2460
2473
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
2461
2474
|
const file = params.get('file');
|
|
2462
2475
|
const dir = params.get('dir');
|
|
2463
|
-
if (!file || file.includes('..')) { res.statusCode = 400; res.end('Invalid file'); return; }
|
|
2476
|
+
if (!file || file.includes('..') || file.includes('\0')) { res.statusCode = 400; res.end('Invalid file'); return; }
|
|
2464
2477
|
|
|
2465
2478
|
let content = '';
|
|
2466
2479
|
if (dir) {
|
|
@@ -2513,7 +2526,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2513
2526
|
fs.writeFileSync(psPath, psScript);
|
|
2514
2527
|
try {
|
|
2515
2528
|
selectedPath = execSync(`powershell -STA -NoProfile -ExecutionPolicy Bypass -File "${psPath}"`, { encoding: 'utf8', timeout: 120000 }).trim();
|
|
2516
|
-
} finally { try { fs.unlinkSync(psPath); } catch {} }
|
|
2529
|
+
} finally { try { fs.unlinkSync(psPath); } catch { /* cleanup */ } }
|
|
2517
2530
|
} else if (process.platform === 'darwin') {
|
|
2518
2531
|
selectedPath = execSync(`osascript -e 'POSIX path of (choose folder with prompt "Select project folder")'`, { encoding: 'utf8', timeout: 120000 }).trim();
|
|
2519
2532
|
} else {
|
|
@@ -2559,14 +2572,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2559
2572
|
remoteUrl.match(/https:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/);
|
|
2560
2573
|
if (m) { detected.org = m[1]; detected.project = m[2]; detected.repoName = m[3]; }
|
|
2561
2574
|
}
|
|
2562
|
-
} catch {}
|
|
2575
|
+
} catch (e) { console.error('git remote detection:', e.message); }
|
|
2563
2576
|
try {
|
|
2564
2577
|
const pkgPath = path.join(target, 'package.json');
|
|
2565
2578
|
if (fs.existsSync(pkgPath)) {
|
|
2566
2579
|
const pkg = safeJson(pkgPath);
|
|
2567
2580
|
if (pkg.name) detected.name = pkg.name.replace(/^@[^/]+\//, '');
|
|
2568
2581
|
}
|
|
2569
|
-
} catch {}
|
|
2582
|
+
} catch { /* optional */ }
|
|
2570
2583
|
let description = '';
|
|
2571
2584
|
try {
|
|
2572
2585
|
const claudeMd = path.join(target, 'CLAUDE.md');
|
|
@@ -2574,7 +2587,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2574
2587
|
const lines = (safeRead(claudeMd) || '').split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
2575
2588
|
if (lines[0] && lines[0].length < 200) description = lines[0].trim();
|
|
2576
2589
|
}
|
|
2577
|
-
} catch {}
|
|
2590
|
+
} catch { /* optional */ }
|
|
2578
2591
|
|
|
2579
2592
|
const name = body.name || detected.name;
|
|
2580
2593
|
const prUrlBase = detected.repoHost === 'github'
|
|
@@ -2616,6 +2629,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2616
2629
|
}
|
|
2617
2630
|
|
|
2618
2631
|
async function handleCommandCenter(req, res) {
|
|
2632
|
+
if (checkRateLimit('command-center', 10)) return jsonReply(res, 429, { error: 'Rate limited — max 10 requests/minute' });
|
|
2619
2633
|
try {
|
|
2620
2634
|
const body = await readBody(req);
|
|
2621
2635
|
if (!body.message) return jsonReply(res, 400, { error: 'message required' });
|
|
@@ -3028,7 +3042,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3028
3042
|
|
|
3029
3043
|
// Also append to live-output.log so it shows in the chat view
|
|
3030
3044
|
const liveLogPath = path.join(agentDir, 'live-output.log');
|
|
3031
|
-
try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + message + '\n'); } catch {}
|
|
3045
|
+
try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + message + '\n'); } catch { /* optional */ }
|
|
3032
3046
|
|
|
3033
3047
|
return jsonReply(res, 200, { ok: true, message: 'Steering message sent' });
|
|
3034
3048
|
}},
|
|
@@ -3103,19 +3117,34 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3103
3117
|
// ── Route Dispatcher ────────────────────────────────────────────────────────
|
|
3104
3118
|
|
|
3105
3119
|
const pathname = req.url.split('?')[0];
|
|
3120
|
+
const _reqStart = Date.now();
|
|
3106
3121
|
for (const route of ROUTES) {
|
|
3107
3122
|
if (route.method !== req.method) continue;
|
|
3108
3123
|
if (typeof route.path === 'string') {
|
|
3109
3124
|
// For /api/skill, match with query string prefix since it has no fixed path variant
|
|
3110
3125
|
if (route.path === '/api/skill') {
|
|
3111
3126
|
if (!req.url.startsWith('/api/skill?') && req.url !== '/api/skill') continue;
|
|
3112
|
-
|
|
3127
|
+
const _result = await route.handler(req, res, {});
|
|
3128
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
3129
|
+
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
3130
|
+
}
|
|
3131
|
+
return _result;
|
|
3113
3132
|
}
|
|
3114
3133
|
if (pathname !== route.path) continue;
|
|
3115
|
-
|
|
3134
|
+
const _result = await route.handler(req, res, {});
|
|
3135
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
3136
|
+
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
3137
|
+
}
|
|
3138
|
+
return _result;
|
|
3116
3139
|
}
|
|
3117
3140
|
const m = pathname.match(route.path);
|
|
3118
|
-
if (m)
|
|
3141
|
+
if (m) {
|
|
3142
|
+
const _result = await route.handler(req, res, m);
|
|
3143
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
3144
|
+
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
3145
|
+
}
|
|
3146
|
+
return _result;
|
|
3147
|
+
}
|
|
3119
3148
|
}
|
|
3120
3149
|
|
|
3121
3150
|
// Serve dashboard HTML with gzip + caching
|
package/engine/ado.js
CHANGED
|
@@ -91,7 +91,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
91
91
|
const updated = await callback(project, pr, prNum, orgBase);
|
|
92
92
|
if (updated) projectUpdated++;
|
|
93
93
|
} catch (err) {
|
|
94
|
-
try { engine().log('warn', `Failed to poll status for ${pr.id}: ${err.message}`); } catch {}
|
|
94
|
+
try { engine().log('warn', `Failed to poll status for ${pr.id}: ${err.message}`); } catch { /* engine not available */ }
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -160,7 +160,7 @@ async function pollPrStatus(config) {
|
|
|
160
160
|
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
161
161
|
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
162
162
|
shared.safeWrite(metricsPath, metrics);
|
|
163
|
-
} catch {}
|
|
163
|
+
} catch (err) { try { engine().log('warn', `Metrics update: ${err.message}`); } catch { /* engine not available */ } }
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
}
|