@yemi33/minions 0.1.327 → 0.1.328
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 +5 -0
- package/engine/lifecycle.js +118 -201
- package/engine.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/lifecycle.js
CHANGED
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const os = require('os');
|
|
9
8
|
const shared = require('./shared');
|
|
10
|
-
const { safeRead, safeJson, safeWrite,
|
|
11
|
-
log, ts, dateStamp
|
|
12
|
-
ENGINE_DEFAULTS } = shared;
|
|
9
|
+
const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
10
|
+
log, ts, dateStamp } = shared;
|
|
13
11
|
const { trackEngineUsage } = require('./llm');
|
|
14
12
|
const queries = require('./queries');
|
|
15
13
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
@@ -23,10 +21,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
23
21
|
const planPath = path.join(PRD_DIR, planFile);
|
|
24
22
|
const plan = safeJson(planPath);
|
|
25
23
|
if (!plan?.missing_features) return;
|
|
26
|
-
if (plan.status === 'completed')
|
|
27
|
-
if (plan._completionNotified) return;
|
|
28
|
-
// Crash recovery: status=completed but _completionNotified not set — fall through
|
|
29
|
-
}
|
|
24
|
+
if (plan.status === 'completed') return;
|
|
30
25
|
|
|
31
26
|
const projects = shared.getProjects(config);
|
|
32
27
|
|
|
@@ -58,7 +53,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
58
53
|
const unmaterialized = [...planFeatureIds].filter(id => {
|
|
59
54
|
if (workItemById[id]) return false;
|
|
60
55
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
61
|
-
return !(prdItem &&
|
|
56
|
+
return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
|
|
62
57
|
});
|
|
63
58
|
if (unmaterialized.length > 0) {
|
|
64
59
|
log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
|
|
@@ -68,17 +63,17 @@ function checkPlanCompletion(meta, config) {
|
|
|
68
63
|
// Check 2: every feature's work item must be done (or PRD item marked done externally)
|
|
69
64
|
const notDone = [...planFeatureIds].filter(id => {
|
|
70
65
|
const w = workItemById[id];
|
|
71
|
-
if (w &&
|
|
66
|
+
if (w && (w.status === 'done' || w.status === 'in-pr')) return false; // in-pr accepted for backward compat
|
|
72
67
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
73
|
-
return !(prdItem &&
|
|
68
|
+
return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
|
|
74
69
|
});
|
|
75
70
|
if (notDone.length > 0) {
|
|
76
71
|
log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
|
|
77
72
|
return;
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
const doneItems = planItems.filter(w =>
|
|
81
|
-
const failedItems = planItems.filter(w => w.status ===
|
|
75
|
+
const doneItems = planItems.filter(w => w.status === 'done' || w.status === 'in-pr');
|
|
76
|
+
const failedItems = planItems.filter(w => w.status === 'failed');
|
|
82
77
|
|
|
83
78
|
// 1. Mark plan as completed
|
|
84
79
|
plan.status = 'completed';
|
|
@@ -137,30 +132,15 @@ function checkPlanCompletion(meta, config) {
|
|
|
137
132
|
...uniquePrs.map(pr => `- ${pr.id}: ${pr.title || ''} ${pr.url || ''}`),
|
|
138
133
|
].filter(Boolean).join('\n');
|
|
139
134
|
|
|
140
|
-
// Write summary to notes/inbox
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Persist status and _completionNotified atomically BEFORE creating work items
|
|
146
|
-
// NOTE: Do NOT set plan._completionNotified in-memory before persist —
|
|
147
|
-
// if persist fails, the in-memory flag would prevent retry on next tick.
|
|
148
|
-
mutateJsonFileLocked(planPath, (data) => {
|
|
149
|
-
data.status = 'completed';
|
|
150
|
-
data.completedAt = plan.completedAt;
|
|
151
|
-
data._completionNotified = true;
|
|
152
|
-
if (plan._timing) data._timing = plan._timing;
|
|
153
|
-
return data;
|
|
154
|
-
});
|
|
135
|
+
// Write summary to notes/inbox
|
|
136
|
+
const summaryFile = `prd-completion-${planFile.replace('.json', '')}-${ts().slice(0, 10)}.md`;
|
|
137
|
+
shared.safeWrite(shared.uniquePath(path.join(MINIONS_DIR, 'notes', 'inbox', summaryFile)), summary);
|
|
138
|
+
log('info', `PRD completion summary written to notes/inbox/${summaryFile}`);
|
|
155
139
|
|
|
156
140
|
// Resolve the primary project for writing new work items (PR, verify)
|
|
157
141
|
const projectName = plan.project;
|
|
158
142
|
const primaryProject = projectName
|
|
159
|
-
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) :
|
|
160
|
-
if (!primaryProject) {
|
|
161
|
-
log('warn', `checkPlanCompletion: no project available (projects array ${projects.length === 0 ? 'empty' : 'no match for ' + projectName}) — skipping PR/verify creation for ${planFile}`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
143
|
+
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
|
|
164
144
|
const wiPath = primaryProject ? shared.projectWorkItemsPath(primaryProject) : null;
|
|
165
145
|
const workItems = wiPath ? (safeJson(wiPath) || []) : [];
|
|
166
146
|
|
|
@@ -197,7 +177,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
197
177
|
const prs = (safeJson(shared.projectPrPath(p)) || [])
|
|
198
178
|
.filter(pr => {
|
|
199
179
|
const linkedId = prLinks[pr.id];
|
|
200
|
-
return pr.status ===
|
|
180
|
+
return pr.status === 'active' && linkedId && doneItems.find(w => w.id === linkedId);
|
|
201
181
|
});
|
|
202
182
|
if (prs.length > 0) {
|
|
203
183
|
projectPrs[p.name] = { project: p, prs, mainBranch: p.mainBranch || 'main' };
|
|
@@ -206,12 +186,11 @@ function checkPlanCompletion(meta, config) {
|
|
|
206
186
|
|
|
207
187
|
// Build per-project checkout commands: one worktree, merge all PR branches into it
|
|
208
188
|
const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
|
|
209
|
-
const
|
|
210
|
-
const wtPath = `${localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
|
|
189
|
+
const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
|
|
211
190
|
const branches = prs.map(pr => pr.branch).filter(Boolean);
|
|
212
191
|
const lines = [
|
|
213
192
|
`# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
|
|
214
|
-
`cd "${localPath}"`,
|
|
193
|
+
`cd "${p.localPath}"`,
|
|
215
194
|
`git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
|
|
216
195
|
`git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
|
|
217
196
|
`cd "${wtPath}"`,
|
|
@@ -232,10 +211,9 @@ function checkPlanCompletion(meta, config) {
|
|
|
232
211
|
).join('\n');
|
|
233
212
|
|
|
234
213
|
// List projects and their worktree paths for the agent
|
|
235
|
-
const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}).join('\n');
|
|
214
|
+
const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
|
|
215
|
+
`- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
|
|
216
|
+
).join('\n');
|
|
239
217
|
|
|
240
218
|
const description = [
|
|
241
219
|
`Verification task for completed plan \`${planFile}\`.`,
|
|
@@ -280,75 +258,46 @@ function checkPlanCompletion(meta, config) {
|
|
|
280
258
|
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
281
259
|
}
|
|
282
260
|
|
|
283
|
-
// 5. Archive
|
|
284
|
-
// Plan stays active until verification finishes so artifacts are visible.
|
|
285
|
-
|
|
286
|
-
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ─── Plan Archiving (called after verify completes) ─────────────────────────
|
|
290
|
-
|
|
291
|
-
function archivePlan(planFile, plan, projects, config) {
|
|
292
|
-
const planPath = path.join(PRD_DIR, planFile);
|
|
293
|
-
const projectName = plan.project || '';
|
|
294
|
-
|
|
295
|
-
// Archive PRD .json to prd/archive/
|
|
261
|
+
// 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
|
|
296
262
|
const prdArchiveDir = path.join(PRD_DIR, 'archive');
|
|
297
263
|
if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
|
|
298
|
-
|
|
264
|
+
shared.safeWrite(planPath, plan); // save completed status first
|
|
265
|
+
try {
|
|
266
|
+
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
267
|
+
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
299
270
|
shared.safeWrite(planPath, plan);
|
|
300
|
-
try {
|
|
301
|
-
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
302
|
-
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
303
|
-
} catch (err) { log('warn', `Failed to archive PRD ${planFile}: ${err.message}`); }
|
|
304
271
|
}
|
|
305
272
|
|
|
306
|
-
//
|
|
273
|
+
// Also archive the source .md plan if it exists
|
|
307
274
|
const planArchiveDir = path.join(PLANS_DIR, 'archive');
|
|
308
275
|
if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
|
|
309
|
-
if (plan.source_plan) {
|
|
310
|
-
const mdPath = path.join(PLANS_DIR, plan.source_plan);
|
|
311
|
-
if (fs.existsSync(mdPath)) {
|
|
312
|
-
try {
|
|
313
|
-
fs.renameSync(mdPath, path.join(planArchiveDir, plan.source_plan));
|
|
314
|
-
log('info', `Archived source plan: plans/archive/${plan.source_plan}`);
|
|
315
|
-
} catch (err) { log('warn', `Failed to archive source plan ${plan.source_plan}: ${err.message}`); }
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
try {
|
|
319
|
-
const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
320
|
-
for (const md of mdFiles) {
|
|
321
|
-
const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
|
|
322
|
-
if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
323
|
-
try {
|
|
324
|
-
fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
|
|
325
|
-
log('info', `Archived source plan: plans/archive/${md}`);
|
|
326
|
-
} catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
334
276
|
try {
|
|
335
|
-
|
|
336
|
-
for (const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
277
|
+
const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
278
|
+
for (const md of mdFiles) {
|
|
279
|
+
const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
|
|
280
|
+
// Match by project name or plan summary appearing in the .md content
|
|
281
|
+
if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
282
|
+
try {
|
|
283
|
+
fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
|
|
284
|
+
log('info', `Archived source plan: plans/archive/${md}`);
|
|
285
|
+
} catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
343
288
|
}
|
|
289
|
+
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
344
290
|
|
|
291
|
+
// 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
292
|
+
try {
|
|
293
|
+
// Collect all branch slugs: shared-branch + per-item branches + item IDs
|
|
345
294
|
const branchSlugs = new Set();
|
|
346
295
|
if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
347
|
-
for (const w of
|
|
296
|
+
for (const w of doneItems) {
|
|
348
297
|
if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
|
|
349
298
|
if (w.id) branchSlugs.add(w.id.toLowerCase());
|
|
350
299
|
}
|
|
351
|
-
for (const pr of
|
|
300
|
+
for (const pr of uniquePrs) {
|
|
352
301
|
if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
353
302
|
}
|
|
354
303
|
|
|
@@ -370,8 +319,10 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
370
319
|
}
|
|
371
320
|
}
|
|
372
321
|
}
|
|
373
|
-
if (cleanedWt > 0) log('info', `Plan
|
|
322
|
+
if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
|
|
374
323
|
} catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
|
|
324
|
+
|
|
325
|
+
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
375
326
|
}
|
|
376
327
|
|
|
377
328
|
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
@@ -430,10 +381,6 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
430
381
|
|
|
431
382
|
const projectName = meta?.item?.project || meta?.project?.name;
|
|
432
383
|
const projects = shared.getProjects(config);
|
|
433
|
-
if (projects.length === 0) {
|
|
434
|
-
log('error', 'Plan chaining: no projects configured — cannot chain plan to PRD');
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
384
|
const targetProject = projectName
|
|
438
385
|
? projects.find(p => p.name === projectName) || projects[0]
|
|
439
386
|
: projects[0];
|
|
@@ -446,10 +393,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
446
393
|
log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
|
|
447
394
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
448
395
|
let items = [];
|
|
449
|
-
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch
|
|
450
|
-
log('warn', `Failed to parse ${wiPath}: ${err.message} — creating .bak and starting fresh`);
|
|
451
|
-
try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch {}
|
|
452
|
-
}
|
|
396
|
+
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
|
|
453
397
|
items.push({
|
|
454
398
|
id: 'W-' + shared.uid(),
|
|
455
399
|
title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
|
|
@@ -491,20 +435,20 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
491
435
|
target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
|
|
492
436
|
|
|
493
437
|
const results = Object.values(target.agentResults);
|
|
494
|
-
const anySuccess = results.some(r => r.status ===
|
|
438
|
+
const anySuccess = results.some(r => r.status === 'done');
|
|
495
439
|
const allDone = Array.isArray(target.fanOutAgents) && target.fanOutAgents.length > 0 ? results.length >= target.fanOutAgents.length : false;
|
|
496
440
|
const dispatchAge = target.dispatched_at ? Date.now() - new Date(target.dispatched_at).getTime() : 0;
|
|
497
441
|
const timedOut = !allDone && dispatchAge > 6 * 60 * 60 * 1000 && results.length > 0;
|
|
498
442
|
|
|
499
443
|
if (anySuccess) {
|
|
500
|
-
target.status =
|
|
444
|
+
target.status = 'done';
|
|
501
445
|
delete target.failReason;
|
|
502
446
|
delete target.failedAt;
|
|
503
447
|
target.completedAgents = Object.entries(target.agentResults)
|
|
504
|
-
.filter(([, r]) => r.status ===
|
|
448
|
+
.filter(([, r]) => r.status === 'done')
|
|
505
449
|
.map(([a]) => a);
|
|
506
450
|
} else if (allDone || timedOut) {
|
|
507
|
-
target.status =
|
|
451
|
+
target.status = 'failed';
|
|
508
452
|
target.failReason = timedOut
|
|
509
453
|
? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
|
|
510
454
|
: 'All fan-out agents failed';
|
|
@@ -512,11 +456,11 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
512
456
|
}
|
|
513
457
|
} else {
|
|
514
458
|
target.status = status;
|
|
515
|
-
if (status ===
|
|
459
|
+
if (status === 'done') {
|
|
516
460
|
delete target.failReason;
|
|
517
461
|
delete target.failedAt;
|
|
518
462
|
target.completedAt = ts();
|
|
519
|
-
} else if (status ===
|
|
463
|
+
} else if (status === 'failed') {
|
|
520
464
|
if (reason) target.failReason = reason;
|
|
521
465
|
target.failedAt = ts();
|
|
522
466
|
}
|
|
@@ -594,11 +538,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
594
538
|
if (prMatches.size === 0) return 0;
|
|
595
539
|
|
|
596
540
|
const projects = shared.getProjects(config);
|
|
597
|
-
|
|
598
|
-
log('warn', `syncPrsFromOutput: no projects configured and no project in meta — cannot sync PRs`);
|
|
599
|
-
return 0;
|
|
600
|
-
}
|
|
601
|
-
const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || (projects[0] || null);
|
|
541
|
+
const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || projects[0];
|
|
602
542
|
const useCentral = !defaultProject;
|
|
603
543
|
|
|
604
544
|
// Match each PR to its correct project by finding which repo URL appears near the PR number in output
|
|
@@ -626,54 +566,47 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
626
566
|
const agentName = config.agents?.[agentId]?.name || agentId;
|
|
627
567
|
let added = 0;
|
|
628
568
|
const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
569
|
+
// Track which PR files need writing — keyed by target name
|
|
570
|
+
const dirtyTargets = new Map(); // name -> { prs, prPath }
|
|
629
571
|
|
|
630
|
-
// Group PR matches by target file so we take one lock per target
|
|
631
|
-
const targetPrIds = new Map(); // targetName -> { prPath, prIds: [{ prId, fullId }] }
|
|
632
572
|
for (const prId of prMatches) {
|
|
633
573
|
const fullId = `PR-${prId}`;
|
|
634
574
|
const targetProject = useCentral ? null : resolveProjectForPr(prId);
|
|
635
575
|
const targetName = targetProject ? targetProject.name : '_central';
|
|
636
576
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
|
|
637
|
-
|
|
638
|
-
|
|
577
|
+
|
|
578
|
+
// Load PRs for this target (cache per target)
|
|
579
|
+
if (!dirtyTargets.has(targetName)) {
|
|
580
|
+
dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
|
|
581
|
+
}
|
|
582
|
+
const entry = dirtyTargets.get(targetName);
|
|
583
|
+
if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
|
|
584
|
+
|
|
585
|
+
let title = meta?.item?.title || '';
|
|
586
|
+
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
587
|
+
if (titleMatch) title = titleMatch[1].trim();
|
|
588
|
+
if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
|
|
589
|
+
title = meta?.item?.title || '';
|
|
639
590
|
}
|
|
640
|
-
|
|
591
|
+
entry.prs.push({
|
|
592
|
+
id: fullId,
|
|
593
|
+
title: (title || `PR created by ${agentName}`).slice(0, 120),
|
|
594
|
+
agent: agentName,
|
|
595
|
+
branch: meta?.branch || '',
|
|
596
|
+
reviewStatus: 'pending',
|
|
597
|
+
status: 'active',
|
|
598
|
+
created: dateStamp(),
|
|
599
|
+
url: extractPrUrl(prId),
|
|
600
|
+
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
601
|
+
sourcePlan: meta?.item?.sourcePlan || '',
|
|
602
|
+
itemType: meta?.item?.itemType || ''
|
|
603
|
+
});
|
|
604
|
+
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
605
|
+
added++;
|
|
641
606
|
}
|
|
642
607
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
mutateJsonFileLocked(prPath, (prs) => {
|
|
646
|
-
if (!Array.isArray(prs)) prs = [];
|
|
647
|
-
// Deduplicate any existing entries with same id (case-insensitive agent name race)
|
|
648
|
-
const seen = new Set();
|
|
649
|
-
prs = prs.filter(p => { const k = String(p.id); if (seen.has(k)) return false; seen.add(k); return true; });
|
|
650
|
-
for (const { prId, fullId } of prIds) {
|
|
651
|
-
if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
|
|
652
|
-
|
|
653
|
-
let title = meta?.item?.title || '';
|
|
654
|
-
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
655
|
-
if (titleMatch) title = titleMatch[1].trim();
|
|
656
|
-
if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
|
|
657
|
-
title = meta?.item?.title || '';
|
|
658
|
-
}
|
|
659
|
-
prs.push({
|
|
660
|
-
id: fullId,
|
|
661
|
-
title: (title || `PR created by ${agentName}`).slice(0, 120),
|
|
662
|
-
agent: agentName,
|
|
663
|
-
branch: meta?.branch || '',
|
|
664
|
-
reviewStatus: 'pending',
|
|
665
|
-
status: 'active',
|
|
666
|
-
created: dateStamp(),
|
|
667
|
-
url: extractPrUrl(prId),
|
|
668
|
-
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
669
|
-
sourcePlan: meta?.item?.sourcePlan || '',
|
|
670
|
-
itemType: meta?.item?.itemType || ''
|
|
671
|
-
});
|
|
672
|
-
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
673
|
-
added++;
|
|
674
|
-
}
|
|
675
|
-
return prs;
|
|
676
|
-
}, { defaultValue: [] });
|
|
608
|
+
for (const [name, entry] of dirtyTargets) {
|
|
609
|
+
shared.safeWrite(entry.prPath, entry.prs);
|
|
677
610
|
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
|
|
678
611
|
}
|
|
679
612
|
return added;
|
|
@@ -693,7 +626,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
693
626
|
// Record the reviewer — actual verdict comes from ADO/GitHub votes via pollPrStatus.
|
|
694
627
|
// Set to 'waiting' so pollPrStatus updates it with the real vote on next cycle.
|
|
695
628
|
const dispatch = getDispatch();
|
|
696
|
-
const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type ===
|
|
629
|
+
const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === 'review');
|
|
697
630
|
|
|
698
631
|
// Set reviewStatus to 'waiting' (single source of truth — synced from ADO/GitHub votes on next poll)
|
|
699
632
|
target.reviewStatus = 'waiting';
|
|
@@ -718,7 +651,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
718
651
|
}
|
|
719
652
|
|
|
720
653
|
shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
|
|
721
|
-
log('info', `Updated ${pr.id} → minions review: ${
|
|
654
|
+
log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
|
|
722
655
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
723
656
|
}
|
|
724
657
|
|
|
@@ -789,13 +722,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
789
722
|
const plan = safeJson(path.join(prdDir, pf));
|
|
790
723
|
if (!plan?.missing_features) continue;
|
|
791
724
|
const feature = plan.missing_features.find(f => f.id === mergedItemId);
|
|
792
|
-
if (feature && feature.status !==
|
|
793
|
-
feature.status =
|
|
725
|
+
if (feature && feature.status !== 'implemented') {
|
|
726
|
+
feature.status = 'implemented';
|
|
794
727
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
795
728
|
updated++;
|
|
796
729
|
}
|
|
797
730
|
}
|
|
798
|
-
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as
|
|
731
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as implemented for ${pr.id}`);
|
|
799
732
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
800
733
|
|
|
801
734
|
// Mark work item as done
|
|
@@ -806,10 +739,10 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
806
739
|
const items = safeJson(wiPath);
|
|
807
740
|
if (!items) continue;
|
|
808
741
|
const item = items.find(i => i.id === mergedItemId);
|
|
809
|
-
if (item && item.status !==
|
|
742
|
+
if (item && item.status !== 'done') {
|
|
810
743
|
log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
|
|
811
|
-
item.status =
|
|
812
|
-
item.completedAt = ts();
|
|
744
|
+
item.status = 'done';
|
|
745
|
+
item.completedAt = e.ts();
|
|
813
746
|
item._mergedVia = pr.id;
|
|
814
747
|
shared.safeWrite(wiPath, items);
|
|
815
748
|
break;
|
|
@@ -906,7 +839,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
906
839
|
}
|
|
907
840
|
} else {
|
|
908
841
|
// Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
|
|
909
|
-
const claudeSkillsDir = path.join(
|
|
842
|
+
const claudeSkillsDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'skills');
|
|
910
843
|
const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
|
|
911
844
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
912
845
|
if (!fs.existsSync(skillPath)) {
|
|
@@ -985,10 +918,10 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
985
918
|
m.lastTask = dispatchItem.task;
|
|
986
919
|
m.lastCompleted = ts();
|
|
987
920
|
if (model) m.model = model;
|
|
988
|
-
if (result ===
|
|
921
|
+
if (result === 'success') {
|
|
989
922
|
m.tasksCompleted++;
|
|
990
923
|
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
991
|
-
if (dispatchItem.type ===
|
|
924
|
+
if (dispatchItem.type === 'review') m.reviewsDone++;
|
|
992
925
|
} else if (result === 'retry') {
|
|
993
926
|
// Auto-retry: count cost but not as a final outcome
|
|
994
927
|
m.tasksRetried = (m.tasksRetried || 0) + 1;
|
|
@@ -1070,7 +1003,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1070
1003
|
if (!parent) continue;
|
|
1071
1004
|
|
|
1072
1005
|
// Mark parent as decomposed
|
|
1073
|
-
parent.status =
|
|
1006
|
+
parent.status = 'decomposed';
|
|
1074
1007
|
parent._decomposed = true;
|
|
1075
1008
|
delete parent._decomposing;
|
|
1076
1009
|
parent._subItemIds = subItems.map(s => s.id);
|
|
@@ -1109,7 +1042,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1109
1042
|
const type = dispatchItem.type;
|
|
1110
1043
|
const meta = dispatchItem.meta;
|
|
1111
1044
|
const isSuccess = code === 0;
|
|
1112
|
-
const result = isSuccess ?
|
|
1045
|
+
const result = isSuccess ? 'success' : 'error';
|
|
1113
1046
|
const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout);
|
|
1114
1047
|
|
|
1115
1048
|
// Save session for potential resume on next dispatch
|
|
@@ -1145,10 +1078,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1145
1078
|
}
|
|
1146
1079
|
} catch { /* optional */ }
|
|
1147
1080
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
|
|
1081
|
+
if (retries < 3) {
|
|
1082
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1083
|
+
updateWorkItemStatus(meta, 'pending', '');
|
|
1152
1084
|
try {
|
|
1153
1085
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1154
1086
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1157,14 +1089,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1157
1089
|
const items = safeJson(wiPath) || [];
|
|
1158
1090
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1159
1091
|
if (wi) {
|
|
1160
|
-
wi._retryCount = retries + 1; wi.status =
|
|
1161
|
-
if (type ===
|
|
1092
|
+
wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
|
|
1093
|
+
if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
|
|
1162
1094
|
shared.safeWrite(wiPath, items);
|
|
1163
1095
|
}
|
|
1164
1096
|
}
|
|
1165
1097
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1166
1098
|
} else {
|
|
1167
|
-
updateWorkItemStatus(meta,
|
|
1099
|
+
updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
|
|
1168
1100
|
}
|
|
1169
1101
|
// Clear _decomposing flag on failure so item doesn't get permanently stuck
|
|
1170
1102
|
if (type === 'decompose') {
|
|
@@ -1194,19 +1126,6 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1194
1126
|
let prsCreatedCount = 0;
|
|
1195
1127
|
if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
1196
1128
|
|
|
1197
|
-
// Archive plan after verify task completes (AFTER PR sync so E2E PR is linked)
|
|
1198
|
-
if (meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
|
|
1199
|
-
try {
|
|
1200
|
-
const vPlanFile = meta.item.sourcePlan;
|
|
1201
|
-
const vPlanPath = path.join(PRD_DIR, vPlanFile);
|
|
1202
|
-
const vPlan = safeJson(vPlanPath);
|
|
1203
|
-
if (vPlan) {
|
|
1204
|
-
const vProjects = shared.getProjects(config);
|
|
1205
|
-
archivePlan(vPlanFile, vPlan, vProjects, config);
|
|
1206
|
-
}
|
|
1207
|
-
} catch (err) { log('warn', `Verify archive: ${err.message}`); }
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
1129
|
// Clean up worktree for non-shared-branch tasks after completion
|
|
1211
1130
|
if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
|
|
1212
1131
|
try {
|
|
@@ -1221,7 +1140,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1221
1140
|
return d.includes(branchSlug) && fs.statSync(path.join(worktreeRoot, d)).isDirectory();
|
|
1222
1141
|
});
|
|
1223
1142
|
// Only remove if no other active dispatch uses this branch
|
|
1224
|
-
const dispatch = getDispatch();
|
|
1143
|
+
const dispatch = e.getDispatch();
|
|
1225
1144
|
const otherActive = ((dispatch.active || []).concat(dispatch.pending || [])).some(d =>
|
|
1226
1145
|
d.id !== dispatchItem.id && d.meta?.branch && shared.sanitizeBranch && shared.sanitizeBranch(d.meta.branch) === branchSlug
|
|
1227
1146
|
);
|
|
@@ -1229,7 +1148,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1229
1148
|
for (const dir of dirs) {
|
|
1230
1149
|
const wtPath = path.join(worktreeRoot, dir);
|
|
1231
1150
|
try {
|
|
1232
|
-
|
|
1151
|
+
shared.exec(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
1233
1152
|
log('info', `Post-completion: removed worktree ${dir}`);
|
|
1234
1153
|
} catch (err) {
|
|
1235
1154
|
log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
|
|
@@ -1243,7 +1162,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1243
1162
|
}
|
|
1244
1163
|
|
|
1245
1164
|
// Detect implement tasks that completed without creating a PR
|
|
1246
|
-
if (isSuccess && (type ===
|
|
1165
|
+
if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id) {
|
|
1247
1166
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1248
1167
|
const projects = shared.getProjects(config);
|
|
1249
1168
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
@@ -1263,16 +1182,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1263
1182
|
wi.noPr = true;
|
|
1264
1183
|
wi.failReason = 'Completed without creating a pull request';
|
|
1265
1184
|
const retries = wi._retryCount || 0;
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
wi.status = WI_STATUS.PENDING;
|
|
1185
|
+
if (retries < 3) {
|
|
1186
|
+
wi.status = 'pending';
|
|
1269
1187
|
wi._retryCount = retries + 1;
|
|
1270
1188
|
delete wi.dispatched_at;
|
|
1271
1189
|
delete wi.dispatched_to;
|
|
1272
|
-
log('info', `Auto-retry ${retries + 1}
|
|
1190
|
+
e.log('info', `Auto-retry ${retries + 1}/3 for ${meta.item.id} (no PR created)`);
|
|
1273
1191
|
} else {
|
|
1274
|
-
wi.status =
|
|
1275
|
-
log('warn', `${meta.item.id} failed after
|
|
1192
|
+
wi.status = 'failed';
|
|
1193
|
+
e.log('warn', `${meta.item.id} failed after 3 retries — no PR created`);
|
|
1276
1194
|
}
|
|
1277
1195
|
shared.safeWrite(wiPath, items);
|
|
1278
1196
|
}
|
|
@@ -1280,8 +1198,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1280
1198
|
}
|
|
1281
1199
|
}
|
|
1282
1200
|
|
|
1283
|
-
if (type ===
|
|
1284
|
-
if (type ===
|
|
1201
|
+
if (type === 'review') updatePrAfterReview(agentId, meta?.pr, meta?.project);
|
|
1202
|
+
if (type === 'fix') updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
|
|
1285
1203
|
checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
|
|
1286
1204
|
if (isSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
|
|
1287
1205
|
updateAgentHistory(agentId, dispatchItem, result);
|
|
@@ -1311,14 +1229,14 @@ function syncPrdFromPrs(config) {
|
|
|
1311
1229
|
for (const project of allProjects) {
|
|
1312
1230
|
const wiPath = projectWorkItemsPath(project);
|
|
1313
1231
|
const items = safeJson(wiPath) || [];
|
|
1314
|
-
const hasPending = items.some(wi => wi.status ===
|
|
1232
|
+
const hasPending = items.some(wi => wi.status === 'pending' && !wi._pr);
|
|
1315
1233
|
if (!hasPending) continue;
|
|
1316
1234
|
const reconciled = reconcileItemsWithPrs(items, allPrs);
|
|
1317
1235
|
if (reconciled > 0) {
|
|
1318
1236
|
safeWrite(wiPath, items);
|
|
1319
1237
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
1320
1238
|
for (const wi of items) {
|
|
1321
|
-
if (wi.status ===
|
|
1239
|
+
if (wi.status === 'done') syncPrdItemStatus(wi.id, 'done', wi.sourcePlan);
|
|
1322
1240
|
}
|
|
1323
1241
|
totalReconciled += reconciled;
|
|
1324
1242
|
}
|
|
@@ -1334,7 +1252,6 @@ function syncPrdFromPrs(config) {
|
|
|
1334
1252
|
|
|
1335
1253
|
module.exports = {
|
|
1336
1254
|
checkPlanCompletion,
|
|
1337
|
-
archivePlan,
|
|
1338
1255
|
updateWorkItemStatus,
|
|
1339
1256
|
syncPrdItemStatus,
|
|
1340
1257
|
syncPrsFromOutput,
|
package/engine.js
CHANGED
|
@@ -25,7 +25,7 @@ const fs = require('fs');
|
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./engine/shared');
|
|
27
27
|
const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS,
|
|
28
|
-
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, DISPATCH_RESULT } = shared;
|
|
28
|
+
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
29
29
|
const queries = require('./engine/queries');
|
|
30
30
|
|
|
31
31
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.328",
|
|
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"
|