@yemi33/minions 0.1.208 → 0.1.210
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 +11 -0
- package/dashboard/js/settings.js +20 -4
- package/dashboard.js +267 -166
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard/js/settings.js
CHANGED
|
@@ -57,11 +57,13 @@ async function openSettings() {
|
|
|
57
57
|
'<div style="margin-bottom:16px">' +
|
|
58
58
|
'<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Permission Mode <span style="opacity:0.6">(how agents handle tool approvals)</span></label>' +
|
|
59
59
|
'<select id="set-permissionMode" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
|
|
60
|
-
'<option value="bypassPermissions"' + ((c.permissionMode || 'bypassPermissions') === 'bypassPermissions' ? ' selected' : '') + '>Bypass (recommended) — agents run without permission prompts</option>' +
|
|
61
|
-
'<option value="auto"' + ((c.permissionMode) === 'auto' ? ' selected' : '') + '>Auto —
|
|
62
|
-
'<option value="default"' + ((c.permissionMode) === 'default' ? ' selected' : '') + '>Default —
|
|
60
|
+
'<option value="bypassPermissions"' + ((c.permissionMode || 'bypassPermissions') === 'bypassPermissions' ? ' selected' : '') + '>Bypass (recommended) — agents run autonomously without permission prompts</option>' +
|
|
61
|
+
'<option value="auto"' + ((c.permissionMode) === 'auto' ? ' selected' : '') + '>Auto — auto-approve safe tools, prompt for risky ones (agents may hang on risky tools)</option>' +
|
|
62
|
+
'<option value="default"' + ((c.permissionMode) === 'default' ? ' selected' : '') + '>Default — prompt for every tool (agents WILL hang — not recommended)</option>' +
|
|
63
63
|
'</select>' +
|
|
64
|
-
'<div style="font-size:9px;
|
|
64
|
+
'<div id="set-permissionMode-warn" style="font-size:9px;margin-top:4px;padding:4px 8px;border-radius:4px;' + ((c.permissionMode && c.permissionMode !== 'bypassPermissions') ? 'display:block;background:rgba(248,81,73,0.1);color:var(--red)' : 'display:none') + '">' +
|
|
65
|
+
'\u26A0 Tools listed in Allowed Tools above are auto-approved even in non-bypass modes. Agents will only hang if they try to use a tool NOT on that list (e.g. MCP tools). In bypass mode, all tools are approved automatically.' +
|
|
66
|
+
'</div>' +
|
|
65
67
|
'</div>' +
|
|
66
68
|
|
|
67
69
|
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Agents</h3>' +
|
|
@@ -96,6 +98,20 @@ async function openSettings() {
|
|
|
96
98
|
document.getElementById('modal-body').style.fontFamily = '';
|
|
97
99
|
document.getElementById('modal-body').style.whiteSpace = '';
|
|
98
100
|
document.getElementById('modal').classList.add('open');
|
|
101
|
+
|
|
102
|
+
// Wire permission mode warning toggle
|
|
103
|
+
var pmSelect = document.getElementById('set-permissionMode');
|
|
104
|
+
if (pmSelect) pmSelect.addEventListener('change', function() {
|
|
105
|
+
var warn = document.getElementById('set-permissionMode-warn');
|
|
106
|
+
if (!warn) return;
|
|
107
|
+
if (this.value !== 'bypassPermissions') {
|
|
108
|
+
warn.style.display = 'block';
|
|
109
|
+
warn.style.background = 'rgba(248,81,73,0.1)';
|
|
110
|
+
warn.style.color = 'var(--red)';
|
|
111
|
+
} else {
|
|
112
|
+
warn.style.display = 'none';
|
|
113
|
+
}
|
|
114
|
+
});
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
function settingsToggle(label, id, checked, hint) {
|
package/dashboard.js
CHANGED
|
@@ -370,16 +370,7 @@ I'll save that as a note and dispatch dallas to fix the bug.
|
|
|
370
370
|
If no actions are needed (just answering a question, or you handled it directly), do NOT include the ===ACTIONS=== line.
|
|
371
371
|
|
|
372
372
|
Available action types:
|
|
373
|
-
- **dispatch**: Create a work item for an agent. Fields: title, workType, priority (low/medium/high), agents (array of IDs, optional), project, description.
|
|
374
|
-
workType values — choose carefully, this determines the playbook and whether a PR is expected:
|
|
375
|
-
- \`explore\` — research, investigate, read code, gather information, write findings (NO PR expected)
|
|
376
|
-
- \`ask\` — answer a question, analyze something, produce a report (NO PR expected)
|
|
377
|
-
- \`implement\` — write new code, add a feature, create something (PR expected)
|
|
378
|
-
- \`fix\` — fix a bug, address review feedback on an existing PR (PR expected)
|
|
379
|
-
- \`review\` — code review, quality assessment, evaluate completed work, verify acceptance criteria (NO PR expected)
|
|
380
|
-
- \`test\` — run tests, write test cases (PR expected if new tests written)
|
|
381
|
-
- \`verify\` — build PRs locally, merge branches, start dev server, get localhost URL to test (NO PR expected)
|
|
382
|
-
If unsure, prefer \`explore\` for read-only tasks and \`implement\` for write tasks.
|
|
373
|
+
- **dispatch**: Create a work item for an agent. Fields: title, workType (ask/explore/fix/review/test/implement/verify), priority (low/medium/high), agents (array of IDs, optional), project, description. Use \`verify\` when the user wants to build PRs locally, merge branches together, start a dev server, and get a localhost URL to test.
|
|
383
374
|
- **note**: Save a note/decision. Fields: title, content
|
|
384
375
|
- **plan**: Create a multi-step plan. Fields: title, description, project, branchStrategy (parallel/shared-branch)
|
|
385
376
|
- **cancel**: Cancel a running agent. Fields: agent (agent ID), reason
|
|
@@ -676,7 +667,7 @@ function readBody(req) {
|
|
|
676
667
|
return new Promise((resolve, reject) => {
|
|
677
668
|
let body = '';
|
|
678
669
|
req.on('data', chunk => { body += chunk; if (body.length > 1e6) reject(new Error('Too large')); });
|
|
679
|
-
req.on('end', () => { try { resolve(
|
|
670
|
+
req.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
|
|
680
671
|
});
|
|
681
672
|
}
|
|
682
673
|
|
|
@@ -823,7 +814,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
823
814
|
// If archived, temporarily restore to active so checkPlanCompletion can find it
|
|
824
815
|
const activePath = path.join(prdDir, body.file);
|
|
825
816
|
if (fromArchive) {
|
|
826
|
-
const plan = JSON.parse(safeRead(prdPath)
|
|
817
|
+
const plan = JSON.parse(safeRead(prdPath));
|
|
827
818
|
plan.status = 'approved';
|
|
828
819
|
delete plan.completedAt;
|
|
829
820
|
safeWrite(activePath, plan);
|
|
@@ -869,22 +860,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
869
860
|
}
|
|
870
861
|
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
871
862
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
delete item.fanOutAgents;
|
|
885
|
-
return items;
|
|
886
|
-
}, { defaultValue: [] });
|
|
887
|
-
if (!found) return jsonReply(res, 404, { error: 'item not found' });
|
|
863
|
+
const items = JSON.parse(safeRead(wiPath) || '[]');
|
|
864
|
+
const item = items.find(i => i.id === id);
|
|
865
|
+
if (!item) return jsonReply(res, 404, { error: 'item not found' });
|
|
866
|
+
|
|
867
|
+
item.status = 'pending';
|
|
868
|
+
item._retryCount = 0; // Reset retry counter on manual retry
|
|
869
|
+
delete item.dispatched_at;
|
|
870
|
+
delete item.dispatched_to;
|
|
871
|
+
delete item.failReason;
|
|
872
|
+
delete item.failedAt;
|
|
873
|
+
delete item.fanOutAgents;
|
|
874
|
+
safeWrite(wiPath, items);
|
|
888
875
|
|
|
889
876
|
// Clear completed dispatch entries so the engine doesn't dedup this item
|
|
890
877
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
@@ -902,10 +889,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
902
889
|
// Clear cooldown so item isn't blocked by exponential backoff
|
|
903
890
|
try {
|
|
904
891
|
const cooldownPath = path.join(MINIONS_DIR, 'engine', 'cooldowns.json');
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
892
|
+
const cooldowns = JSON.parse(safeRead(cooldownPath) || '{}');
|
|
893
|
+
if (cooldowns[dispatchKey]) {
|
|
894
|
+
delete cooldowns[dispatchKey];
|
|
895
|
+
safeWrite(cooldownPath, cooldowns);
|
|
896
|
+
}
|
|
909
897
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
910
898
|
|
|
911
899
|
return jsonReply(res, 200, { ok: true, id });
|
|
@@ -930,16 +918,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
930
918
|
}
|
|
931
919
|
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
932
920
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (!found) return jsonReply(res, 404, { error: 'item not found' });
|
|
921
|
+
const items = JSON.parse(safeRead(wiPath) || '[]');
|
|
922
|
+
const idx = items.findIndex(i => i.id === id);
|
|
923
|
+
if (idx === -1) return jsonReply(res, 404, { error: 'item not found' });
|
|
924
|
+
|
|
925
|
+
const item = items[idx];
|
|
926
|
+
|
|
927
|
+
// Remove item from work-items file
|
|
928
|
+
items.splice(idx, 1);
|
|
929
|
+
safeWrite(wiPath, items);
|
|
943
930
|
|
|
944
931
|
// Clean dispatch entries + kill running agent
|
|
945
932
|
const dispatchRemoved = cleanDispatchEntries(d =>
|
|
@@ -950,12 +937,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
950
937
|
// Clean cooldown entries so item can be re-created immediately
|
|
951
938
|
try {
|
|
952
939
|
const cooldownPath = path.join(MINIONS_DIR, 'engine', 'cooldowns.json');
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
940
|
+
const cooldowns = JSON.parse(safeRead(cooldownPath) || '{}');
|
|
941
|
+
let cleaned = false;
|
|
942
|
+
for (const key of Object.keys(cooldowns)) {
|
|
943
|
+
if (key.includes(id)) { delete cooldowns[key]; cleaned = true; }
|
|
944
|
+
}
|
|
945
|
+
if (cleaned) safeWrite(cooldownPath, cooldowns);
|
|
959
946
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
960
947
|
|
|
961
948
|
invalidateStatusCache();
|
|
@@ -1037,6 +1024,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1037
1024
|
// Write to central queue — agent decides which project
|
|
1038
1025
|
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
1039
1026
|
}
|
|
1027
|
+
let items = [];
|
|
1028
|
+
const existing = safeRead(wiPath);
|
|
1029
|
+
if (existing) { try { items = JSON.parse(existing); } catch {} }
|
|
1040
1030
|
const id = 'W-' + shared.uid();
|
|
1041
1031
|
const item = {
|
|
1042
1032
|
id, title: body.title, type: body.type || 'implement',
|
|
@@ -1048,12 +1038,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1048
1038
|
if (body.agents) item.agents = body.agents;
|
|
1049
1039
|
if (body.references) item.references = body.references;
|
|
1050
1040
|
if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (!Array.isArray(items)) items = [];
|
|
1054
|
-
items.push(item);
|
|
1055
|
-
return items;
|
|
1056
|
-
}, { defaultValue: [] });
|
|
1041
|
+
items.push(item);
|
|
1042
|
+
safeWrite(wiPath, items);
|
|
1057
1043
|
return jsonReply(res, 200, { ok: true, id });
|
|
1058
1044
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1059
1045
|
}
|
|
@@ -1075,27 +1061,29 @@ const server = http.createServer(async (req, res) => {
|
|
|
1075
1061
|
}
|
|
1076
1062
|
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
1077
1063
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
item
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1064
|
+
const items = JSON.parse(safeRead(wiPath) || '[]');
|
|
1065
|
+
const item = items.find(i => i.id === id);
|
|
1066
|
+
if (!item) return jsonReply(res, 404, { error: 'item not found' });
|
|
1067
|
+
|
|
1068
|
+
if (item.status === 'dispatched') {
|
|
1069
|
+
return jsonReply(res, 400, { error: 'Cannot edit dispatched items' });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (title !== undefined) item.title = title;
|
|
1073
|
+
if (description !== undefined) item.description = description;
|
|
1074
|
+
if (type !== undefined) item.type = type;
|
|
1075
|
+
if (priority !== undefined) item.priority = priority;
|
|
1076
|
+
if (agent !== undefined) {
|
|
1077
|
+
item.agent = agent || null;
|
|
1078
|
+
// Clear stale pending dispatch entries so the engine re-queues with the new agent
|
|
1079
|
+
cleanDispatchEntries(d => d.meta?.item?.id === id);
|
|
1080
|
+
}
|
|
1081
|
+
if (body.references !== undefined) item.references = body.references;
|
|
1082
|
+
if (body.acceptanceCriteria !== undefined) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
1083
|
+
item.updatedAt = new Date().toISOString();
|
|
1084
|
+
|
|
1085
|
+
safeWrite(wiPath, items);
|
|
1086
|
+
return jsonReply(res, 200, { ok: true, item });
|
|
1099
1087
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1100
1088
|
}
|
|
1101
1089
|
|
|
@@ -1179,18 +1167,24 @@ const server = http.createServer(async (req, res) => {
|
|
|
1179
1167
|
const item = (plan.missing_features || []).find(f => f.id === body.itemId);
|
|
1180
1168
|
if (!item) return jsonReply(res, 404, { error: 'item not found in plan' });
|
|
1181
1169
|
|
|
1182
|
-
// Update
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1170
|
+
// Update allowed fields
|
|
1171
|
+
if (body.name !== undefined) item.name = body.name;
|
|
1172
|
+
if (body.description !== undefined) item.description = body.description;
|
|
1173
|
+
if (body.priority !== undefined) item.priority = body.priority;
|
|
1174
|
+
if (body.estimated_complexity !== undefined) item.estimated_complexity = body.estimated_complexity;
|
|
1175
|
+
if (body.status !== undefined) item.status = body.status;
|
|
1176
|
+
|
|
1177
|
+
// Re-read plan before writing to minimize race window with engine
|
|
1178
|
+
const freshPlan = safeJson(planPath) || plan;
|
|
1179
|
+
const freshItem = (freshPlan.missing_features || []).find(f => f.id === body.itemId);
|
|
1180
|
+
if (freshItem) {
|
|
1181
|
+
if (body.name !== undefined) freshItem.name = body.name;
|
|
1182
|
+
if (body.description !== undefined) freshItem.description = body.description;
|
|
1183
|
+
if (body.priority !== undefined) freshItem.priority = body.priority;
|
|
1184
|
+
if (body.estimated_complexity !== undefined) freshItem.estimated_complexity = body.estimated_complexity;
|
|
1185
|
+
if (body.status !== undefined) freshItem.status = body.status;
|
|
1186
|
+
}
|
|
1187
|
+
safeWrite(planPath, freshPlan);
|
|
1194
1188
|
|
|
1195
1189
|
// Feature 3: Sync edits to materialized work item if still pending
|
|
1196
1190
|
let workItemSynced = false;
|
|
@@ -1200,20 +1194,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1200
1194
|
}
|
|
1201
1195
|
for (const wiPath of wiSyncPaths) {
|
|
1202
1196
|
try {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
wi.type = body.estimated_complexity === 'large' ? 'implement:large' : 'implement';
|
|
1212
|
-
}
|
|
1213
|
-
workItemSynced = true;
|
|
1197
|
+
const items = safeJson(wiPath);
|
|
1198
|
+
const wi = items.find(w => w.sourcePlan === body.source && w.id === body.itemId);
|
|
1199
|
+
if (wi && wi.status === 'pending') {
|
|
1200
|
+
if (body.name !== undefined) wi.title = 'Implement: ' + body.name;
|
|
1201
|
+
if (body.description !== undefined) wi.description = body.description;
|
|
1202
|
+
if (body.priority !== undefined) wi.priority = body.priority;
|
|
1203
|
+
if (body.estimated_complexity !== undefined) {
|
|
1204
|
+
wi.type = body.estimated_complexity === 'large' ? 'implement:large' : 'implement';
|
|
1214
1205
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1206
|
+
safeWrite(wiPath, items);
|
|
1207
|
+
workItemSynced = true;
|
|
1208
|
+
}
|
|
1217
1209
|
} catch (e) { console.error('work item sync:', e.message); }
|
|
1218
1210
|
}
|
|
1219
1211
|
|
|
@@ -1348,7 +1340,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1348
1340
|
|
|
1349
1341
|
// Watch for changes using fs.watchFile (cross-platform, works on Windows)
|
|
1350
1342
|
const watcher = () => {
|
|
1351
|
-
if (res.writableEnded) return;
|
|
1352
1343
|
try {
|
|
1353
1344
|
const stat = fs.statSync(liveLogPath);
|
|
1354
1345
|
if (stat.size > offset) {
|
|
@@ -1367,17 +1358,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1367
1358
|
|
|
1368
1359
|
// Check if agent is still active (poll every 5s)
|
|
1369
1360
|
const doneCheck = setInterval(() => {
|
|
1370
|
-
if (res.writableEnded) { clearInterval(doneCheck); return; }
|
|
1371
1361
|
const dispatch = getDispatchQueue();
|
|
1372
1362
|
const isActive = (dispatch.active || []).some(d => d.agent === agentId);
|
|
1373
1363
|
if (!isActive) {
|
|
1374
1364
|
watcher(); // flush final content
|
|
1365
|
+
res.write(`event: done\ndata: complete\n\n`);
|
|
1375
1366
|
clearInterval(doneCheck);
|
|
1376
1367
|
fs.unwatchFile(liveLogPath, watcher);
|
|
1377
|
-
|
|
1378
|
-
res.write(`event: done\ndata: complete\n\n`);
|
|
1379
|
-
res.end();
|
|
1380
|
-
}
|
|
1368
|
+
res.end();
|
|
1381
1369
|
}
|
|
1382
1370
|
}, 5000);
|
|
1383
1371
|
|
|
@@ -1401,7 +1389,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1401
1389
|
} else {
|
|
1402
1390
|
// Return last N bytes via ?tail=N param (default last 8KB)
|
|
1403
1391
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
1404
|
-
const tailBytes =
|
|
1392
|
+
const tailBytes = parseInt(params.get('tail')) || 8192;
|
|
1405
1393
|
res.end(content.length > tailBytes ? content.slice(-tailBytes) : content);
|
|
1406
1394
|
}
|
|
1407
1395
|
return;
|
|
@@ -1659,13 +1647,12 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1659
1647
|
file: f, format: 'draft', archived,
|
|
1660
1648
|
project: projectMatch ? projectMatch[1].trim() : '',
|
|
1661
1649
|
summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
|
|
1662
|
-
status: archived ? 'completed' : completedPrdFiles.has(f) ? '
|
|
1650
|
+
status: archived ? 'completed' : completedPrdFiles.has(f) ? 'converted' : 'draft',
|
|
1663
1651
|
branchStrategy: '',
|
|
1664
1652
|
featureBranch: '',
|
|
1665
1653
|
itemCount: (content.match(/^\d+\.\s+\*\*/gm) || []).length,
|
|
1666
1654
|
generatedBy: authorMatch ? authorMatch[1].trim() : '',
|
|
1667
1655
|
generatedAt: dateMatch ? dateMatch[1].trim() : '',
|
|
1668
|
-
completedAt: archived ? updatedAt : '',
|
|
1669
1656
|
updatedAt,
|
|
1670
1657
|
requiresApproval: false,
|
|
1671
1658
|
revisionFeedback: null,
|
|
@@ -1874,7 +1861,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1874
1861
|
for (const w of items) {
|
|
1875
1862
|
if (w.sourcePlan !== body.file) continue;
|
|
1876
1863
|
// Keep completed items as-is, reset everything else to pending.
|
|
1877
|
-
if (w.status === 'done') continue;
|
|
1864
|
+
if (w.status === 'done' || w.status === 'implemented' || w.status === 'complete' || w.status === 'in-pr') continue;
|
|
1878
1865
|
|
|
1879
1866
|
if (w.status === 'dispatched') {
|
|
1880
1867
|
// Kill the agent working on this item, if any.
|
|
@@ -2276,6 +2263,148 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2276
2263
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2277
2264
|
}
|
|
2278
2265
|
|
|
2266
|
+
// POST /api/plans/revise-and-regenerate — REMOVED: plan versioning now handled by /api/doc-chat
|
|
2267
|
+
// The "Replace old PRD" flow uses qaReplacePrd (frontend) which calls /api/plans/pause + /api/plans/regenerate + planExecute
|
|
2268
|
+
async function handlePlansReviseAndRegenerate(req, res) {
|
|
2269
|
+
try {
|
|
2270
|
+
const body = await readBody(req);
|
|
2271
|
+
if (!body.source || !body.instruction) return jsonReply(res, 400, { error: 'source and instruction required' });
|
|
2272
|
+
|
|
2273
|
+
// Find the source plan .md file for this PRD
|
|
2274
|
+
// Convention: PRD JSON references plan via plan_summary containing the work item ID,
|
|
2275
|
+
// or the .md file has a matching name prefix
|
|
2276
|
+
const prdPath = path.join(PRD_DIR, body.source);
|
|
2277
|
+
if (!fs.existsSync(prdPath)) return jsonReply(res, 404, { error: 'PRD file not found' });
|
|
2278
|
+
|
|
2279
|
+
// Look for corresponding .md plan file
|
|
2280
|
+
let sourcePlanFile = null;
|
|
2281
|
+
const planFiles = safeReadDir(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
2282
|
+
if (body.sourcePlan) {
|
|
2283
|
+
// Explicit source plan provided
|
|
2284
|
+
sourcePlanFile = body.sourcePlan;
|
|
2285
|
+
} else {
|
|
2286
|
+
// Heuristic: find .md plan by matching prefix or by reading PRD's generated_from field
|
|
2287
|
+
const prd = JSON.parse(safeRead(prdPath) || '{}');
|
|
2288
|
+
if (prd.source_plan) {
|
|
2289
|
+
sourcePlanFile = prd.source_plan;
|
|
2290
|
+
} else {
|
|
2291
|
+
// Match by prefix: officeagent-2026-03-15.json → plan-*officeagent* or plan-w025*.md
|
|
2292
|
+
const prdBase = body.source.replace('.json', '');
|
|
2293
|
+
for (const f of planFiles) {
|
|
2294
|
+
// Check if plan file mentions the same project or was created around same time
|
|
2295
|
+
const content = safeRead(path.join(PLANS_DIR, f)) || '';
|
|
2296
|
+
if (content.includes(prd.project || '___nomatch___') || content.includes(prd.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
2297
|
+
sourcePlanFile = f;
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
// Last resort: most recent .md plan
|
|
2302
|
+
if (!sourcePlanFile && planFiles.length > 0) {
|
|
2303
|
+
sourcePlanFile = planFiles.sort((a, b) => {
|
|
2304
|
+
try { return fs.statSync(path.join(PLANS_DIR, b)).mtimeMs - fs.statSync(path.join(PLANS_DIR, a)).mtimeMs; } catch { return 0; }
|
|
2305
|
+
})[0];
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (!sourcePlanFile) {
|
|
2311
|
+
return jsonReply(res, 404, { error: 'No source plan (.md) found for this PRD. You can edit the PRD JSON directly using "Edit Plan".' });
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const sourcePlanPath = path.join(PLANS_DIR, sourcePlanFile);
|
|
2315
|
+
const planContent = safeRead(sourcePlanPath);
|
|
2316
|
+
if (!planContent) return jsonReply(res, 404, { error: 'Source plan file not readable: ' + sourcePlanFile });
|
|
2317
|
+
|
|
2318
|
+
// Step 1: Steer the source plan with the user's instruction via CC
|
|
2319
|
+
const result = await ccDocCall({
|
|
2320
|
+
message: body.instruction,
|
|
2321
|
+
document: planContent,
|
|
2322
|
+
title: sourcePlanFile,
|
|
2323
|
+
filePath: 'plans/' + sourcePlanFile,
|
|
2324
|
+
selection: body.selection || '',
|
|
2325
|
+
canEdit: true,
|
|
2326
|
+
isJson: false,
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
if (!result.content) {
|
|
2330
|
+
return jsonReply(res, 200, { ok: true, answer: result.answer, updated: false });
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// Save the revised plan
|
|
2334
|
+
safeWrite(sourcePlanPath, result.content);
|
|
2335
|
+
|
|
2336
|
+
// Step 2: Pause the old PRD so it stops materializing items
|
|
2337
|
+
const prd = JSON.parse(safeRead(prdPath) || '{}');
|
|
2338
|
+
prd.status = 'revision-requested';
|
|
2339
|
+
prd.revision_feedback = body.instruction;
|
|
2340
|
+
prd.revisionRequestedAt = new Date().toISOString();
|
|
2341
|
+
safeWrite(prdPath, prd);
|
|
2342
|
+
|
|
2343
|
+
// Step 3: Clean up pending/failed work items from old PRD
|
|
2344
|
+
let reset = 0, kept = 0;
|
|
2345
|
+
const wiPaths = [{ path: path.join(MINIONS_DIR, 'work-items.json'), label: 'central' }];
|
|
2346
|
+
for (const proj of PROJECTS) {
|
|
2347
|
+
wiPaths.push({ path: shared.projectWorkItemsPath(proj), label: proj.name });
|
|
2348
|
+
}
|
|
2349
|
+
const deletedItemIds = [];
|
|
2350
|
+
for (const wiInfo of wiPaths) {
|
|
2351
|
+
try {
|
|
2352
|
+
const items = safeJson(wiInfo.path);
|
|
2353
|
+
const filtered = [];
|
|
2354
|
+
for (const w of items) {
|
|
2355
|
+
if (w.sourcePlan === body.source) {
|
|
2356
|
+
if (w.status === 'pending' || w.status === 'failed') {
|
|
2357
|
+
reset++;
|
|
2358
|
+
deletedItemIds.push(w.id);
|
|
2359
|
+
} else {
|
|
2360
|
+
kept++;
|
|
2361
|
+
filtered.push(w);
|
|
2362
|
+
}
|
|
2363
|
+
} else {
|
|
2364
|
+
filtered.push(w);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (filtered.length < items.length) safeWrite(wiInfo.path, filtered);
|
|
2368
|
+
} catch (e) { console.error('work item deletion:', e.message); }
|
|
2369
|
+
}
|
|
2370
|
+
for (const itemId of deletedItemIds) {
|
|
2371
|
+
cleanDispatchEntries(d =>
|
|
2372
|
+
d.meta?.item?.sourcePlan === body.source && d.meta?.item?.id === itemId
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// Step 4: Dispatch plan-to-prd to regenerate PRD from revised plan
|
|
2377
|
+
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
2378
|
+
let centralItems = [];
|
|
2379
|
+
try { centralItems = JSON.parse(safeRead(centralWiPath) || '[]'); } catch {}
|
|
2380
|
+
const wiId = 'W-' + shared.uid();
|
|
2381
|
+
centralItems.push({
|
|
2382
|
+
id: wiId,
|
|
2383
|
+
title: 'Regenerate PRD from revised plan: ' + sourcePlanFile,
|
|
2384
|
+
type: 'plan-to-prd',
|
|
2385
|
+
priority: 'high',
|
|
2386
|
+
description: `The source plan \`${sourcePlanFile}\` has been revised. Convert it into a fresh PRD JSON.\n\nRevision instruction: ${body.instruction}\n\nRead the revised plan, generate updated PRD items (missing_features), and write to \`prd/${body.source}\`. Set status to "approved". Include \`"source_plan": "${sourcePlanFile}"\` in the JSON root.\n\nPreserve items that are already done (status "implemented" or "complete"). Reset or replace items that were pending/failed.`,
|
|
2387
|
+
status: 'pending',
|
|
2388
|
+
created: new Date().toISOString(),
|
|
2389
|
+
createdBy: 'dashboard:revise-and-regenerate',
|
|
2390
|
+
project: prd.project || '',
|
|
2391
|
+
planFile: sourcePlanFile,
|
|
2392
|
+
});
|
|
2393
|
+
safeWrite(centralWiPath, centralItems);
|
|
2394
|
+
|
|
2395
|
+
return jsonReply(res, 200, {
|
|
2396
|
+
ok: true,
|
|
2397
|
+
answer: result.answer,
|
|
2398
|
+
updated: true,
|
|
2399
|
+
sourcePlan: sourcePlanFile,
|
|
2400
|
+
prdPaused: true,
|
|
2401
|
+
reset,
|
|
2402
|
+
kept,
|
|
2403
|
+
workItemId: wiId,
|
|
2404
|
+
});
|
|
2405
|
+
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2279
2408
|
async function handlePlansDiscuss(req, res) {
|
|
2280
2409
|
try {
|
|
2281
2410
|
const body = await readBody(req);
|
|
@@ -2445,7 +2574,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2445
2574
|
let changed = false;
|
|
2446
2575
|
for (const w of items) {
|
|
2447
2576
|
if (w.sourcePlan !== f) continue;
|
|
2448
|
-
if (w.status === 'done') continue;
|
|
2577
|
+
if (w.status === 'done' || w.status === 'implemented' || w.status === 'complete' || w.status === 'in-pr') continue;
|
|
2449
2578
|
if (w.status === 'dispatched') {
|
|
2450
2579
|
const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
|
|
2451
2580
|
if (activeEntry) {
|
|
@@ -2711,12 +2840,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2711
2840
|
const { execSync: ex } = require('child_process');
|
|
2712
2841
|
const detected = { name: path.basename(target), _found: [] };
|
|
2713
2842
|
try {
|
|
2714
|
-
|
|
2715
|
-
try {
|
|
2716
|
-
head = ex('git symbolic-ref refs/remotes/origin/HEAD', { cwd: target, encoding: 'utf8', timeout: 5000 }).trim();
|
|
2717
|
-
} catch {
|
|
2718
|
-
head = ex('git symbolic-ref HEAD', { cwd: target, encoding: 'utf8', timeout: 5000 }).trim();
|
|
2719
|
-
}
|
|
2843
|
+
const head = ex('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || git symbolic-ref HEAD', { cwd: target, encoding: 'utf8', timeout: 5000 }).trim();
|
|
2720
2844
|
detected.mainBranch = head.replace('refs/remotes/origin/', '').replace('refs/heads/', '');
|
|
2721
2845
|
} catch { detected.mainBranch = 'main'; }
|
|
2722
2846
|
try {
|
|
@@ -2795,7 +2919,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2795
2919
|
|
|
2796
2920
|
// Concurrency guard — only one CC call at a time, with auto-release for stuck requests
|
|
2797
2921
|
if (ccInFlight && (Date.now() - ccInFlightSince) < CC_INFLIGHT_TIMEOUT_MS) {
|
|
2798
|
-
return jsonReply(res, 429, { error: 'Command Center is busy — wait for the current request to finish.' });
|
|
2922
|
+
return jsonReply(res, 429, { error: 'Command Center is busy — wait for the current request to finish, or click "New Session" to reset.' });
|
|
2799
2923
|
}
|
|
2800
2924
|
if (ccInFlight) console.log('[CC] Auto-releasing stuck in-flight guard after timeout');
|
|
2801
2925
|
ccInFlight = true;
|
|
@@ -2960,6 +3084,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2960
3084
|
if (body.engine) {
|
|
2961
3085
|
const e = body.engine;
|
|
2962
3086
|
const D = shared.ENGINE_DEFAULTS;
|
|
3087
|
+
// Numeric fields: { key: [min, max?] }
|
|
2963
3088
|
const numericFields = {
|
|
2964
3089
|
tickInterval: [10000], maxConcurrent: [1, 10], inboxConsolidateThreshold: [1],
|
|
2965
3090
|
agentTimeout: [60000], maxTurns: [5, 500], heartbeatTimeout: [60000],
|
|
@@ -2975,22 +3100,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2975
3100
|
config.engine[key] = val;
|
|
2976
3101
|
}
|
|
2977
3102
|
}
|
|
3103
|
+
// String fields
|
|
2978
3104
|
if (e.worktreeRoot !== undefined) config.engine.worktreeRoot = String(e.worktreeRoot || D.worktreeRoot);
|
|
2979
|
-
|
|
3105
|
+
// Boolean fields
|
|
3106
|
+
for (const key of ['autoApprovePlans', 'autoReview', 'autoDecompose', 'allowTempAgents']) {
|
|
2980
3107
|
if (e[key] !== undefined) config.engine[key] = !!e[key];
|
|
2981
3108
|
}
|
|
2982
|
-
if (e.evalMaxIterations !== undefined) config.engine.evalMaxIterations = Math.max(1, Math.min(10, Number(e.evalMaxIterations) || D.evalMaxIterations));
|
|
2983
|
-
if (e.evalMaxCost !== undefined) config.engine.evalMaxCost = e.evalMaxCost === null || e.evalMaxCost === '' ? null : Math.max(0, Number(e.evalMaxCost) || 0);
|
|
2984
3109
|
}
|
|
2985
3110
|
|
|
2986
3111
|
if (body.claude) {
|
|
2987
3112
|
for (const key of ['allowedTools', 'outputFormat']) {
|
|
2988
3113
|
if (body.claude[key] !== undefined) config.claude[key] = String(body.claude[key]);
|
|
2989
3114
|
}
|
|
2990
|
-
if (body.claude.permissionMode !== undefined) {
|
|
2991
|
-
const valid = ['bypassPermissions', 'auto', 'default'];
|
|
2992
|
-
config.claude.permissionMode = valid.includes(body.claude.permissionMode) ? body.claude.permissionMode : 'bypassPermissions';
|
|
2993
|
-
}
|
|
2994
3115
|
}
|
|
2995
3116
|
|
|
2996
3117
|
if (body.agents) {
|
|
@@ -2998,11 +3119,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2998
3119
|
if (!config.agents[id]) continue;
|
|
2999
3120
|
if (updates.role !== undefined) config.agents[id].role = String(updates.role);
|
|
3000
3121
|
if (updates.skills !== undefined) config.agents[id].skills = Array.isArray(updates.skills) ? updates.skills : String(updates.skills).split(',').map(s => s.trim()).filter(Boolean);
|
|
3001
|
-
if (updates.monthlyBudgetUsd !== undefined) {
|
|
3002
|
-
const val = updates.monthlyBudgetUsd === '' || updates.monthlyBudgetUsd === null ? undefined : Number(updates.monthlyBudgetUsd);
|
|
3003
|
-
if (val === undefined || isNaN(val)) delete config.agents[id].monthlyBudgetUsd;
|
|
3004
|
-
else config.agents[id].monthlyBudgetUsd = Math.max(0, val);
|
|
3005
|
-
}
|
|
3006
3122
|
}
|
|
3007
3123
|
}
|
|
3008
3124
|
|
|
@@ -3090,7 +3206,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3090
3206
|
}},
|
|
3091
3207
|
|
|
3092
3208
|
// Work items
|
|
3093
|
-
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria
|
|
3209
|
+
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?', handler: handleWorkItemsCreate },
|
|
3094
3210
|
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, references?, acceptanceCriteria?', handler: handleWorkItemsUpdate },
|
|
3095
3211
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
3096
3212
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
|
@@ -3103,34 +3219,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3103
3219
|
const projects = shared.getProjects(CONFIG);
|
|
3104
3220
|
const paths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
3105
3221
|
for (const p of projects) paths.push(shared.projectWorkItemsPath(p));
|
|
3106
|
-
let found = false;
|
|
3107
|
-
let feedbackAgent = 'unknown';
|
|
3108
|
-
let feedbackTitle = id;
|
|
3109
3222
|
for (const wiPath of paths) {
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
'**Item:** ' + feedbackTitle + '\n' +
|
|
3128
|
-
'**Agent:** ' + feedbackAgent + '\n' +
|
|
3129
|
-
(comment ? '**Feedback:** ' + comment + '\n' : '');
|
|
3130
|
-
const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', feedbackAgent + '-feedback-' + new Date().toISOString().slice(0, 10) + '-' + shared.uid().slice(0, 4) + '.md');
|
|
3131
|
-
safeWrite(inboxPath, feedbackNote);
|
|
3132
|
-
invalidateStatusCache();
|
|
3133
|
-
return jsonReply(res, 200, { ok: true });
|
|
3223
|
+
const items = JSON.parse(safeRead(wiPath) || '[]');
|
|
3224
|
+
const item = items.find(i => i.id === id);
|
|
3225
|
+
if (!item) continue;
|
|
3226
|
+
item._humanFeedback = { rating, comment: comment || '', at: new Date().toISOString() };
|
|
3227
|
+
safeWrite(wiPath, items);
|
|
3228
|
+
const agent = item.dispatched_to || item.agent || 'unknown';
|
|
3229
|
+
const feedbackNote = '# Human Feedback on ' + id + '\n\n' +
|
|
3230
|
+
'**Rating:** ' + (rating === 'up' ? '👍 Good' : '👎 Needs improvement') + '\n' +
|
|
3231
|
+
'**Item:** ' + (item.title || id) + '\n' +
|
|
3232
|
+
'**Agent:** ' + agent + '\n' +
|
|
3233
|
+
(comment ? '**Feedback:** ' + comment + '\n' : '');
|
|
3234
|
+
const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', agent + '-feedback-' + new Date().toISOString().slice(0, 10) + '-' + shared.uid().slice(0, 4) + '.md');
|
|
3235
|
+
safeWrite(inboxPath, feedbackNote);
|
|
3236
|
+
invalidateStatusCache();
|
|
3237
|
+
return jsonReply(res, 200, { ok: true });
|
|
3238
|
+
}
|
|
3239
|
+
return jsonReply(res, 404, { error: 'Work item not found' });
|
|
3134
3240
|
}},
|
|
3135
3241
|
|
|
3136
3242
|
// Pinned notes
|
|
@@ -3179,10 +3285,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3179
3285
|
{ method: 'POST', path: '/api/plans/reject', desc: 'Reject a plan', params: 'file, rejectedBy?, reason?', handler: handlePlansReject },
|
|
3180
3286
|
{ method: 'POST', path: '/api/plans/regenerate', desc: 'Reset pending/failed work items for a plan so they re-materialize', params: 'source', handler: handlePlansRegenerate },
|
|
3181
3287
|
{ method: 'POST', path: '/api/plans/delete', desc: 'Delete a plan file and clean up work items', params: 'file', handler: handlePlansDelete },
|
|
3182
|
-
{ method: 'POST', path: '/api/plans/archive', desc: '
|
|
3288
|
+
{ method: 'POST', path: '/api/plans/archive', desc: 'Move a plan/PRD to archive (preserves work items)', params: 'file', handler: handlePlansArchive },
|
|
3183
3289
|
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
|
|
3184
3290
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
3185
3291
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
3292
|
+
{ method: 'POST', path: '/api/plans/archive', desc: 'Archive a plan/PRD (move to archive folder)', params: 'file', handler: handlePlansArchiveMove },
|
|
3293
|
+
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Unarchive a plan/PRD (restore from archive folder)', params: 'file', handler: handlePlansUnarchive },
|
|
3186
3294
|
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
|
3187
3295
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
3188
3296
|
|
|
@@ -3502,7 +3610,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3502
3610
|
|
|
3503
3611
|
const pathname = req.url.split('?')[0];
|
|
3504
3612
|
const _reqStart = Date.now();
|
|
3505
|
-
try {
|
|
3506
3613
|
for (const route of ROUTES) {
|
|
3507
3614
|
if (route.method !== req.method) continue;
|
|
3508
3615
|
if (typeof route.path === 'string') {
|
|
@@ -3548,12 +3655,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3548
3655
|
} else {
|
|
3549
3656
|
res.end(HTML);
|
|
3550
3657
|
}
|
|
3551
|
-
} catch (err) {
|
|
3552
|
-
console.error(`[ERROR] ${req.method} ${req.url}: ${err.message}`);
|
|
3553
|
-
if (!res.headersSent) {
|
|
3554
|
-
try { jsonReply(res, 500, { error: 'Internal server error' }); } catch { res.end(); }
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
3658
|
});
|
|
3558
3659
|
|
|
3559
3660
|
server.listen(PORT, '127.0.0.1', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.210",
|
|
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"
|