@yemi33/minions 0.1.176 → 0.1.177
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 +8 -0
- package/dashboard.js +130 -108
- package/engine/cli.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -667,7 +667,7 @@ function readBody(req) {
|
|
|
667
667
|
return new Promise((resolve, reject) => {
|
|
668
668
|
let body = '';
|
|
669
669
|
req.on('data', chunk => { body += chunk; if (body.length > 1e6) reject(new Error('Too large')); });
|
|
670
|
-
req.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
|
|
670
|
+
req.on('end', () => { try { resolve(body ? JSON.parse(body) : {}); } catch(e) { reject(e); } });
|
|
671
671
|
});
|
|
672
672
|
}
|
|
673
673
|
|
|
@@ -814,7 +814,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
814
814
|
// If archived, temporarily restore to active so checkPlanCompletion can find it
|
|
815
815
|
const activePath = path.join(prdDir, body.file);
|
|
816
816
|
if (fromArchive) {
|
|
817
|
-
const plan = JSON.parse(safeRead(prdPath));
|
|
817
|
+
const plan = JSON.parse(safeRead(prdPath) || '{}');
|
|
818
818
|
plan.status = 'approved';
|
|
819
819
|
delete plan.completedAt;
|
|
820
820
|
safeWrite(activePath, plan);
|
|
@@ -860,18 +860,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
860
860
|
}
|
|
861
861
|
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
862
862
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
863
|
+
let found = false;
|
|
864
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
865
|
+
if (!Array.isArray(items)) items = [];
|
|
866
|
+
const item = items.find(i => i.id === id);
|
|
867
|
+
if (!item) return items;
|
|
868
|
+
found = true;
|
|
869
|
+
item.status = 'pending';
|
|
870
|
+
item._retryCount = 0;
|
|
871
|
+
delete item.dispatched_at;
|
|
872
|
+
delete item.dispatched_to;
|
|
873
|
+
delete item.failReason;
|
|
874
|
+
delete item.failedAt;
|
|
875
|
+
delete item.fanOutAgents;
|
|
876
|
+
return items;
|
|
877
|
+
}, { defaultValue: [] });
|
|
878
|
+
if (!found) return jsonReply(res, 404, { error: 'item not found' });
|
|
875
879
|
|
|
876
880
|
// Clear completed dispatch entries so the engine doesn't dedup this item
|
|
877
881
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
@@ -889,11 +893,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
889
893
|
// Clear cooldown so item isn't blocked by exponential backoff
|
|
890
894
|
try {
|
|
891
895
|
const cooldownPath = path.join(MINIONS_DIR, 'engine', 'cooldowns.json');
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
896
|
+
mutateJsonFileLocked(cooldownPath, (cooldowns) => {
|
|
897
|
+
if (cooldowns[dispatchKey]) delete cooldowns[dispatchKey];
|
|
898
|
+
return cooldowns;
|
|
899
|
+
});
|
|
897
900
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
898
901
|
|
|
899
902
|
return jsonReply(res, 200, { ok: true, id });
|
|
@@ -918,15 +921,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
918
921
|
}
|
|
919
922
|
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
920
923
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
924
|
+
let found = false;
|
|
925
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
926
|
+
if (!Array.isArray(items)) items = [];
|
|
927
|
+
const idx = items.findIndex(i => i.id === id);
|
|
928
|
+
if (idx === -1) return items;
|
|
929
|
+
found = true;
|
|
930
|
+
items.splice(idx, 1);
|
|
931
|
+
return items;
|
|
932
|
+
}, { defaultValue: [] });
|
|
933
|
+
if (!found) return jsonReply(res, 404, { error: 'item not found' });
|
|
930
934
|
|
|
931
935
|
// Clean dispatch entries + kill running agent
|
|
932
936
|
const dispatchRemoved = cleanDispatchEntries(d =>
|
|
@@ -937,12 +941,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
937
941
|
// Clean cooldown entries so item can be re-created immediately
|
|
938
942
|
try {
|
|
939
943
|
const cooldownPath = path.join(MINIONS_DIR, 'engine', 'cooldowns.json');
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
944
|
+
mutateJsonFileLocked(cooldownPath, (cooldowns) => {
|
|
945
|
+
for (const key of Object.keys(cooldowns)) {
|
|
946
|
+
if (key.includes(id)) delete cooldowns[key];
|
|
947
|
+
}
|
|
948
|
+
return cooldowns;
|
|
949
|
+
});
|
|
946
950
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
947
951
|
|
|
948
952
|
invalidateStatusCache();
|
|
@@ -1024,9 +1028,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1024
1028
|
// Write to central queue — agent decides which project
|
|
1025
1029
|
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
1026
1030
|
}
|
|
1027
|
-
let items = [];
|
|
1028
|
-
const existing = safeRead(wiPath);
|
|
1029
|
-
if (existing) { try { items = JSON.parse(existing); } catch {} }
|
|
1030
1031
|
const id = 'W-' + shared.uid();
|
|
1031
1032
|
const item = {
|
|
1032
1033
|
id, title: body.title, type: body.type || 'implement',
|
|
@@ -1039,8 +1040,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1039
1040
|
if (body.references) item.references = body.references;
|
|
1040
1041
|
if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
1041
1042
|
if (body.skipPr === true) item.skipPr = true;
|
|
1042
|
-
items
|
|
1043
|
-
|
|
1043
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
1044
|
+
if (!Array.isArray(items)) items = [];
|
|
1045
|
+
items.push(item);
|
|
1046
|
+
return items;
|
|
1047
|
+
}, { defaultValue: [] });
|
|
1044
1048
|
return jsonReply(res, 200, { ok: true, id });
|
|
1045
1049
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1046
1050
|
}
|
|
@@ -1062,26 +1066,27 @@ const server = http.createServer(async (req, res) => {
|
|
|
1062
1066
|
}
|
|
1063
1067
|
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
1064
1068
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
return jsonReply(res,
|
|
1069
|
+
let result = null;
|
|
1070
|
+
let error = null;
|
|
1071
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
1072
|
+
if (!Array.isArray(items)) items = [];
|
|
1073
|
+
const item = items.find(i => i.id === id);
|
|
1074
|
+
if (!item) { error = 'item not found'; return items; }
|
|
1075
|
+
if (item.status === 'dispatched') { error = 'Cannot edit dispatched items'; return items; }
|
|
1076
|
+
if (title !== undefined) item.title = title;
|
|
1077
|
+
if (description !== undefined) item.description = description;
|
|
1078
|
+
if (type !== undefined) item.type = type;
|
|
1079
|
+
if (priority !== undefined) item.priority = priority;
|
|
1080
|
+
if (agent !== undefined) item.agent = agent || null;
|
|
1081
|
+
if (body.references !== undefined) item.references = body.references;
|
|
1082
|
+
if (body.acceptanceCriteria !== undefined) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
1083
|
+
if (body.skipPr !== undefined) item.skipPr = body.skipPr === true;
|
|
1084
|
+
item.updatedAt = new Date().toISOString();
|
|
1085
|
+
result = { ...item };
|
|
1086
|
+
return items;
|
|
1087
|
+
}, { defaultValue: [] });
|
|
1088
|
+
if (error) return jsonReply(res, error === 'item not found' ? 404 : 400, { error });
|
|
1089
|
+
return jsonReply(res, 200, { ok: true, item: result });
|
|
1085
1090
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1086
1091
|
}
|
|
1087
1092
|
|
|
@@ -1165,24 +1170,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1165
1170
|
const item = (plan.missing_features || []).find(f => f.id === body.itemId);
|
|
1166
1171
|
if (!item) return jsonReply(res, 404, { error: 'item not found in plan' });
|
|
1167
1172
|
|
|
1168
|
-
// Update
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (body.description !== undefined) freshItem.description = body.description;
|
|
1181
|
-
if (body.priority !== undefined) freshItem.priority = body.priority;
|
|
1182
|
-
if (body.estimated_complexity !== undefined) freshItem.estimated_complexity = body.estimated_complexity;
|
|
1183
|
-
if (body.status !== undefined) freshItem.status = body.status;
|
|
1184
|
-
}
|
|
1185
|
-
safeWrite(planPath, freshPlan);
|
|
1173
|
+
// Update plan item under lock
|
|
1174
|
+
mutateJsonFileLocked(planPath, (freshPlan) => {
|
|
1175
|
+
const freshItem = (freshPlan.missing_features || []).find(f => f.id === body.itemId);
|
|
1176
|
+
if (freshItem) {
|
|
1177
|
+
if (body.name !== undefined) freshItem.name = body.name;
|
|
1178
|
+
if (body.description !== undefined) freshItem.description = body.description;
|
|
1179
|
+
if (body.priority !== undefined) freshItem.priority = body.priority;
|
|
1180
|
+
if (body.estimated_complexity !== undefined) freshItem.estimated_complexity = body.estimated_complexity;
|
|
1181
|
+
if (body.status !== undefined) freshItem.status = body.status;
|
|
1182
|
+
}
|
|
1183
|
+
return freshPlan;
|
|
1184
|
+
});
|
|
1186
1185
|
|
|
1187
1186
|
// Feature 3: Sync edits to materialized work item if still pending
|
|
1188
1187
|
let workItemSynced = false;
|
|
@@ -1192,18 +1191,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
1192
1191
|
}
|
|
1193
1192
|
for (const wiPath of wiSyncPaths) {
|
|
1194
1193
|
try {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1194
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
1195
|
+
if (!Array.isArray(items)) return items;
|
|
1196
|
+
const wi = items.find(w => w.sourcePlan === body.source && w.id === body.itemId);
|
|
1197
|
+
if (wi && wi.status === 'pending') {
|
|
1198
|
+
if (body.name !== undefined) wi.title = 'Implement: ' + body.name;
|
|
1199
|
+
if (body.description !== undefined) wi.description = body.description;
|
|
1200
|
+
if (body.priority !== undefined) wi.priority = body.priority;
|
|
1201
|
+
if (body.estimated_complexity !== undefined) {
|
|
1202
|
+
wi.type = body.estimated_complexity === 'large' ? 'implement:large' : 'implement';
|
|
1203
|
+
}
|
|
1204
|
+
workItemSynced = true;
|
|
1203
1205
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1206
|
+
return items;
|
|
1207
|
+
}, { defaultValue: [] });
|
|
1207
1208
|
} catch (e) { console.error('work item sync:', e.message); }
|
|
1208
1209
|
}
|
|
1209
1210
|
|
|
@@ -1338,6 +1339,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1338
1339
|
|
|
1339
1340
|
// Watch for changes using fs.watchFile (cross-platform, works on Windows)
|
|
1340
1341
|
const watcher = () => {
|
|
1342
|
+
if (res.writableEnded) return;
|
|
1341
1343
|
try {
|
|
1342
1344
|
const stat = fs.statSync(liveLogPath);
|
|
1343
1345
|
if (stat.size > offset) {
|
|
@@ -1356,14 +1358,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
1356
1358
|
|
|
1357
1359
|
// Check if agent is still active (poll every 5s)
|
|
1358
1360
|
const doneCheck = setInterval(() => {
|
|
1361
|
+
if (res.writableEnded) { clearInterval(doneCheck); return; }
|
|
1359
1362
|
const dispatch = getDispatchQueue();
|
|
1360
1363
|
const isActive = (dispatch.active || []).some(d => d.agent === agentId);
|
|
1361
1364
|
if (!isActive) {
|
|
1362
1365
|
watcher(); // flush final content
|
|
1363
|
-
res.write(`event: done\ndata: complete\n\n`);
|
|
1364
1366
|
clearInterval(doneCheck);
|
|
1365
1367
|
fs.unwatchFile(liveLogPath, watcher);
|
|
1366
|
-
res.
|
|
1368
|
+
if (!res.writableEnded) {
|
|
1369
|
+
res.write(`event: done\ndata: complete\n\n`);
|
|
1370
|
+
res.end();
|
|
1371
|
+
}
|
|
1367
1372
|
}
|
|
1368
1373
|
}, 5000);
|
|
1369
1374
|
|
|
@@ -1387,7 +1392,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1387
1392
|
} else {
|
|
1388
1393
|
// Return last N bytes via ?tail=N param (default last 8KB)
|
|
1389
1394
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
1390
|
-
const tailBytes = parseInt(params.get('tail')) || 8192;
|
|
1395
|
+
const tailBytes = Math.min(parseInt(params.get('tail')) || 8192, 1024 * 1024);
|
|
1391
1396
|
res.end(content.length > tailBytes ? content.slice(-tailBytes) : content);
|
|
1392
1397
|
}
|
|
1393
1398
|
return;
|
|
@@ -3072,24 +3077,34 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3072
3077
|
const projects = shared.getProjects(CONFIG);
|
|
3073
3078
|
const paths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
3074
3079
|
for (const p of projects) paths.push(shared.projectWorkItemsPath(p));
|
|
3080
|
+
let found = false;
|
|
3081
|
+
let feedbackAgent = 'unknown';
|
|
3082
|
+
let feedbackTitle = id;
|
|
3075
3083
|
for (const wiPath of paths) {
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3084
|
+
try {
|
|
3085
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
3086
|
+
if (!Array.isArray(items)) return items;
|
|
3087
|
+
const item = items.find(i => i.id === id);
|
|
3088
|
+
if (!item) return items;
|
|
3089
|
+
found = true;
|
|
3090
|
+
item._humanFeedback = { rating, comment: comment || '', at: new Date().toISOString() };
|
|
3091
|
+
feedbackAgent = item.dispatched_to || item.agent || 'unknown';
|
|
3092
|
+
feedbackTitle = item.title || id;
|
|
3093
|
+
return items;
|
|
3094
|
+
}, { defaultValue: [] });
|
|
3095
|
+
} catch { /* optional */ }
|
|
3096
|
+
if (found) break;
|
|
3097
|
+
}
|
|
3098
|
+
if (!found) return jsonReply(res, 404, { error: 'Work item not found' });
|
|
3099
|
+
const feedbackNote = '# Human Feedback on ' + id + '\n\n' +
|
|
3100
|
+
'**Rating:** ' + (rating === 'up' ? '👍 Good' : '👎 Needs improvement') + '\n' +
|
|
3101
|
+
'**Item:** ' + feedbackTitle + '\n' +
|
|
3102
|
+
'**Agent:** ' + feedbackAgent + '\n' +
|
|
3103
|
+
(comment ? '**Feedback:** ' + comment + '\n' : '');
|
|
3104
|
+
const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', feedbackAgent + '-feedback-' + new Date().toISOString().slice(0, 10) + '-' + shared.uid().slice(0, 4) + '.md');
|
|
3105
|
+
safeWrite(inboxPath, feedbackNote);
|
|
3106
|
+
invalidateStatusCache();
|
|
3107
|
+
return jsonReply(res, 200, { ok: true });
|
|
3093
3108
|
}},
|
|
3094
3109
|
|
|
3095
3110
|
// Pinned notes
|
|
@@ -3461,6 +3476,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3461
3476
|
|
|
3462
3477
|
const pathname = req.url.split('?')[0];
|
|
3463
3478
|
const _reqStart = Date.now();
|
|
3479
|
+
try {
|
|
3464
3480
|
for (const route of ROUTES) {
|
|
3465
3481
|
if (route.method !== req.method) continue;
|
|
3466
3482
|
if (typeof route.path === 'string') {
|
|
@@ -3506,6 +3522,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3506
3522
|
} else {
|
|
3507
3523
|
res.end(HTML);
|
|
3508
3524
|
}
|
|
3525
|
+
} catch (err) {
|
|
3526
|
+
console.error(`[ERROR] ${req.method} ${req.url}: ${err.message}`);
|
|
3527
|
+
if (!res.headersSent) {
|
|
3528
|
+
try { jsonReply(res, 500, { error: 'Internal server error' }); } catch { res.end(); }
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3509
3531
|
});
|
|
3510
3532
|
|
|
3511
3533
|
server.listen(PORT, '127.0.0.1', () => {
|
package/engine/cli.js
CHANGED
|
@@ -312,7 +312,7 @@ const commands = {
|
|
|
312
312
|
const tickTimer = setInterval(() => e.tick(), interval);
|
|
313
313
|
|
|
314
314
|
// Fast poll for immediate wakeup signals (checks control.json every 2s)
|
|
315
|
-
setInterval(() => {
|
|
315
|
+
const wakeupTimer = setInterval(() => {
|
|
316
316
|
const ctrl = getControl();
|
|
317
317
|
if (ctrl._wakeupAt && Date.now() - ctrl._wakeupAt < 5000) {
|
|
318
318
|
delete ctrl._wakeupAt;
|
|
@@ -370,6 +370,7 @@ const commands = {
|
|
|
370
370
|
shuttingDown = true;
|
|
371
371
|
console.log(`\n${signal} received — initiating graceful shutdown...`);
|
|
372
372
|
clearInterval(tickTimer);
|
|
373
|
+
clearInterval(wakeupTimer);
|
|
373
374
|
for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
|
|
374
375
|
safeWrite(CONTROL_PATH, { state: 'stopping', pid: process.pid, stopping_at: e.ts() });
|
|
375
376
|
e.log('info', `Graceful shutdown initiated (${signal})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.177",
|
|
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"
|