@yemi33/minions 0.1.85 → 0.1.87
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 +21 -0
- package/dashboard/js/render-prd.js +50 -29
- package/engine/consolidation.js +28 -35
- package/engine/dispatch.js +1 -8
- package/engine/lifecycle.js +95 -101
- package/engine/meeting.js +72 -8
- package/engine/playbook.js +21 -11
- package/engine/shared.js +36 -5
- package/engine.js +8 -16
- package/package.json +1 -1
- package/tools/generate-pixel-art.js +134 -0
- package/tools/pixel-robot.bmp +0 -0
package/engine/lifecycle.js
CHANGED
|
@@ -6,22 +6,16 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
|
-
const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink
|
|
9
|
+
const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
10
|
+
log, ts, dateStamp } = shared;
|
|
10
11
|
const { trackEngineUsage } = require('./llm');
|
|
11
12
|
const queries = require('./queries');
|
|
12
13
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
13
14
|
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, PRD_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
14
15
|
|
|
15
|
-
// Lazy require — only for log(), ts(), dateStamp() and engine-specific functions
|
|
16
|
-
let _engine = null;
|
|
17
|
-
function engine() {
|
|
18
|
-
if (!_engine) _engine = require('../engine');
|
|
19
|
-
return _engine;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
16
|
// ─── Plan Completion Detection ───────────────────────────────────────────────
|
|
23
17
|
function checkPlanCompletion(meta, config) {
|
|
24
|
-
|
|
18
|
+
|
|
25
19
|
const planFile = meta.item?.sourcePlan;
|
|
26
20
|
if (!planFile) return;
|
|
27
21
|
const planPath = path.join(PRD_DIR, planFile);
|
|
@@ -62,7 +56,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
62
56
|
return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
|
|
63
57
|
});
|
|
64
58
|
if (unmaterialized.length > 0) {
|
|
65
|
-
|
|
59
|
+
log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
|
|
66
60
|
return;
|
|
67
61
|
}
|
|
68
62
|
|
|
@@ -74,7 +68,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
74
68
|
return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
|
|
75
69
|
});
|
|
76
70
|
if (notDone.length > 0) {
|
|
77
|
-
|
|
71
|
+
log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
|
|
78
72
|
return;
|
|
79
73
|
}
|
|
80
74
|
|
|
@@ -83,7 +77,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
83
77
|
|
|
84
78
|
// 1. Mark plan as completed
|
|
85
79
|
plan.status = 'completed';
|
|
86
|
-
plan.completedAt =
|
|
80
|
+
plan.completedAt = ts();
|
|
87
81
|
|
|
88
82
|
// Compute timing
|
|
89
83
|
let firstDispatched = null, lastCompleted = null;
|
|
@@ -139,9 +133,9 @@ function checkPlanCompletion(meta, config) {
|
|
|
139
133
|
].filter(Boolean).join('\n');
|
|
140
134
|
|
|
141
135
|
// Write summary to notes/inbox
|
|
142
|
-
const summaryFile = `prd-completion-${planFile.replace('.json', '')}-${
|
|
136
|
+
const summaryFile = `prd-completion-${planFile.replace('.json', '')}-${ts().slice(0, 10)}.md`;
|
|
143
137
|
shared.safeWrite(shared.uniquePath(path.join(MINIONS_DIR, 'notes', 'inbox', summaryFile)), summary);
|
|
144
|
-
|
|
138
|
+
log('info', `PRD completion summary written to notes/inbox/${summaryFile}`);
|
|
145
139
|
|
|
146
140
|
// Resolve the primary project for writing new work items (PR, verify)
|
|
147
141
|
const projectName = plan.project;
|
|
@@ -162,7 +156,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
162
156
|
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
163
157
|
type: 'implement', priority: 'high',
|
|
164
158
|
description: `All plan items from \`${planFile}\` are complete on branch \`${featureBranch}\`.\n\n**Branch:** \`${featureBranch}\`\n**Target:** \`${mainBranch}\`\n\n## Completed Items\n${itemSummary}`,
|
|
165
|
-
status: 'pending', created:
|
|
159
|
+
status: 'pending', created: ts(), createdBy: 'engine:plan-completion',
|
|
166
160
|
sourcePlan: planFile, itemType: 'pr',
|
|
167
161
|
branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
|
|
168
162
|
});
|
|
@@ -254,14 +248,14 @@ function checkPlanCompletion(meta, config) {
|
|
|
254
248
|
priority: 'high',
|
|
255
249
|
description,
|
|
256
250
|
status: 'pending',
|
|
257
|
-
created:
|
|
251
|
+
created: ts(),
|
|
258
252
|
createdBy: 'engine:plan-verification',
|
|
259
253
|
sourcePlan: planFile,
|
|
260
254
|
itemType: 'verify',
|
|
261
255
|
project: projectName,
|
|
262
256
|
});
|
|
263
257
|
shared.safeWrite(wiPath, workItems);
|
|
264
|
-
|
|
258
|
+
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
265
259
|
}
|
|
266
260
|
|
|
267
261
|
// 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
|
|
@@ -270,9 +264,9 @@ function checkPlanCompletion(meta, config) {
|
|
|
270
264
|
shared.safeWrite(planPath, plan); // save completed status first
|
|
271
265
|
try {
|
|
272
266
|
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
273
|
-
|
|
267
|
+
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
274
268
|
} catch (err) {
|
|
275
|
-
|
|
269
|
+
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
276
270
|
shared.safeWrite(planPath, plan);
|
|
277
271
|
}
|
|
278
272
|
|
|
@@ -287,12 +281,12 @@ function checkPlanCompletion(meta, config) {
|
|
|
287
281
|
if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
288
282
|
try {
|
|
289
283
|
fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
|
|
290
|
-
|
|
291
|
-
} catch (err) {
|
|
284
|
+
log('info', `Archived source plan: plans/archive/${md}`);
|
|
285
|
+
} catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
|
|
292
286
|
break;
|
|
293
287
|
}
|
|
294
288
|
}
|
|
295
|
-
} catch (err) {
|
|
289
|
+
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
296
290
|
|
|
297
291
|
// 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
298
292
|
try {
|
|
@@ -321,19 +315,19 @@ function checkPlanCompletion(meta, config) {
|
|
|
321
315
|
try {
|
|
322
316
|
execSilent(`git worktree remove "${wtPath}" --force`, { cwd: root, stdio: 'pipe', timeout: 15000 });
|
|
323
317
|
cleanedWt++;
|
|
324
|
-
} catch (err) {
|
|
318
|
+
} catch (err) { log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
|
|
325
319
|
}
|
|
326
320
|
}
|
|
327
321
|
}
|
|
328
|
-
if (cleanedWt > 0)
|
|
329
|
-
} catch (err) {
|
|
322
|
+
if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
|
|
323
|
+
} catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
|
|
330
324
|
|
|
331
|
-
|
|
325
|
+
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
332
326
|
}
|
|
333
327
|
|
|
334
328
|
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
335
329
|
function chainPlanToPrd(dispatchItem, meta, config) {
|
|
336
|
-
|
|
330
|
+
|
|
337
331
|
const planDir = path.join(MINIONS_DIR, 'plans');
|
|
338
332
|
if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
|
|
339
333
|
|
|
@@ -347,10 +341,10 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
347
341
|
.sort((a, b) => b.mtime - a.mtime);
|
|
348
342
|
planFileName = planFiles[0]?.name;
|
|
349
343
|
if (!planFileName) {
|
|
350
|
-
|
|
344
|
+
log('warn', `Plan chaining: no plan files found in plans/ after task ${dispatchItem.id}`);
|
|
351
345
|
return;
|
|
352
346
|
}
|
|
353
|
-
|
|
347
|
+
log('info', `Plan chaining: using mtime fallback — found ${planFileName}`);
|
|
354
348
|
}
|
|
355
349
|
|
|
356
350
|
if (planFileName.endsWith('.json')) {
|
|
@@ -366,14 +360,14 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
366
360
|
if (!parsed.missing_features) {
|
|
367
361
|
fs.renameSync(jsonPath, mdPath);
|
|
368
362
|
planFileName = mdName;
|
|
369
|
-
|
|
363
|
+
log('info', `Plan chaining: renamed ${planFileName} → ${mdName} (plans must be .md)`);
|
|
370
364
|
}
|
|
371
365
|
} catch {
|
|
372
366
|
try {
|
|
373
367
|
if (fs.existsSync(jsonPath)) fs.renameSync(jsonPath, path.join(planDir, mdName));
|
|
374
368
|
planFileName = mdName;
|
|
375
|
-
|
|
376
|
-
} catch (err) {
|
|
369
|
+
log('info', `Plan chaining: renamed to .md (not valid JSON)`);
|
|
370
|
+
} catch (err) { log('warn', `Plan rename fallback: ${err.message}`); }
|
|
377
371
|
}
|
|
378
372
|
}
|
|
379
373
|
|
|
@@ -381,7 +375,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
381
375
|
const planPath = path.join(planDir, planFileName);
|
|
382
376
|
let planContent;
|
|
383
377
|
try { planContent = fs.readFileSync(planPath, 'utf8'); } catch (err) {
|
|
384
|
-
|
|
378
|
+
log('error', `Plan chaining: failed to read plan file ${planFile.name}: ${err.message}`);
|
|
385
379
|
return;
|
|
386
380
|
}
|
|
387
381
|
|
|
@@ -392,11 +386,11 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
392
386
|
: projects[0];
|
|
393
387
|
|
|
394
388
|
if (!targetProject) {
|
|
395
|
-
|
|
389
|
+
log('error', 'Plan chaining: no target project available');
|
|
396
390
|
return;
|
|
397
391
|
}
|
|
398
392
|
|
|
399
|
-
|
|
393
|
+
log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
|
|
400
394
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
401
395
|
let items = [];
|
|
402
396
|
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
|
|
@@ -407,7 +401,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
407
401
|
priority: meta?.item?.priority || 'high',
|
|
408
402
|
description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
|
|
409
403
|
status: 'pending',
|
|
410
|
-
created:
|
|
404
|
+
created: ts(),
|
|
411
405
|
createdBy: 'engine:chain',
|
|
412
406
|
project: targetProject.name,
|
|
413
407
|
planFile: planFile.name,
|
|
@@ -417,7 +411,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
417
411
|
|
|
418
412
|
// ─── Work Item Status ────────────────────────────────────────────────────────
|
|
419
413
|
function updateWorkItemStatus(meta, status, reason) {
|
|
420
|
-
|
|
414
|
+
|
|
421
415
|
const itemId = meta.item?.id;
|
|
422
416
|
if (!itemId) return;
|
|
423
417
|
|
|
@@ -438,7 +432,7 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
438
432
|
if (!target.agentResults) target.agentResults = {};
|
|
439
433
|
const parts = (meta.dispatchKey || '').split('-');
|
|
440
434
|
const agent = parts[parts.length - 1] || 'unknown';
|
|
441
|
-
target.agentResults[agent] = { status, completedAt:
|
|
435
|
+
target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
|
|
442
436
|
|
|
443
437
|
const results = Object.values(target.agentResults);
|
|
444
438
|
const anySuccess = results.some(r => r.status === 'done');
|
|
@@ -458,22 +452,22 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
458
452
|
target.failReason = timedOut
|
|
459
453
|
? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
|
|
460
454
|
: 'All fan-out agents failed';
|
|
461
|
-
target.failedAt =
|
|
455
|
+
target.failedAt = ts();
|
|
462
456
|
}
|
|
463
457
|
} else {
|
|
464
458
|
target.status = status;
|
|
465
459
|
if (status === 'done') {
|
|
466
460
|
delete target.failReason;
|
|
467
461
|
delete target.failedAt;
|
|
468
|
-
target.completedAt =
|
|
462
|
+
target.completedAt = ts();
|
|
469
463
|
} else if (status === 'failed') {
|
|
470
464
|
if (reason) target.failReason = reason;
|
|
471
|
-
target.failedAt =
|
|
465
|
+
target.failedAt = ts();
|
|
472
466
|
}
|
|
473
467
|
}
|
|
474
468
|
|
|
475
469
|
shared.safeWrite(wiPath, items);
|
|
476
|
-
|
|
470
|
+
log('info', `Work item ${itemId} → ${status}${reason ? ': ' + reason : ''}`);
|
|
477
471
|
|
|
478
472
|
// Sync status to PRD JSON so the two share the same value (work item is source of truth)
|
|
479
473
|
syncPrdItemStatus(itemId, status, meta.item?.sourcePlan);
|
|
@@ -496,13 +490,13 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
496
490
|
return;
|
|
497
491
|
}
|
|
498
492
|
}
|
|
499
|
-
} catch (err) {
|
|
493
|
+
} catch (err) { log('warn', `PRD status sync: ${err.message}`); }
|
|
500
494
|
}
|
|
501
495
|
|
|
502
496
|
// ─── PR Sync from Output ─────────────────────────────────────────────────────
|
|
503
497
|
|
|
504
498
|
function syncPrsFromOutput(output, agentId, meta, config) {
|
|
505
|
-
|
|
499
|
+
|
|
506
500
|
const prMatches = new Set();
|
|
507
501
|
const urlPattern = /(?:visualstudio\.com|dev\.azure\.com)[^\s"]*?pullrequest\/(\d+)|github\.com\/[^\s"]*?\/pull\/(\d+)/g;
|
|
508
502
|
let match;
|
|
@@ -533,7 +527,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
533
527
|
}
|
|
534
528
|
} catch {}
|
|
535
529
|
|
|
536
|
-
const today =
|
|
530
|
+
const today = dateStamp();
|
|
537
531
|
const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
|
|
538
532
|
for (const f of inboxFiles) {
|
|
539
533
|
const content = safeRead(path.join(INBOX_DIR, f));
|
|
@@ -601,7 +595,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
601
595
|
branch: meta?.branch || '',
|
|
602
596
|
reviewStatus: 'pending',
|
|
603
597
|
status: 'active',
|
|
604
|
-
created:
|
|
598
|
+
created: dateStamp(),
|
|
605
599
|
url: extractPrUrl(prId),
|
|
606
600
|
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
607
601
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
@@ -613,7 +607,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
613
607
|
|
|
614
608
|
for (const [name, entry] of dirtyTargets) {
|
|
615
609
|
shared.safeWrite(entry.prPath, entry.prs);
|
|
616
|
-
|
|
610
|
+
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
|
|
617
611
|
}
|
|
618
612
|
return added;
|
|
619
613
|
}
|
|
@@ -621,7 +615,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
621
615
|
// ─── Post-Completion Hooks ──────────────────────────────────────────────────
|
|
622
616
|
|
|
623
617
|
function updatePrAfterReview(agentId, pr, project) {
|
|
624
|
-
|
|
618
|
+
|
|
625
619
|
if (!pr?.id) return;
|
|
626
620
|
const prs = getPrs(project);
|
|
627
621
|
const target = prs.find(p => p.id === pr.id);
|
|
@@ -638,7 +632,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
638
632
|
target.reviewStatus = 'waiting';
|
|
639
633
|
target.minionsReview = {
|
|
640
634
|
reviewer: reviewerName,
|
|
641
|
-
reviewedAt:
|
|
635
|
+
reviewedAt: ts(),
|
|
642
636
|
note: completedEntry?.task || ''
|
|
643
637
|
};
|
|
644
638
|
// Metrics update: don't track 'waiting' as a verdict — metrics are updated
|
|
@@ -657,12 +651,12 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
657
651
|
}
|
|
658
652
|
|
|
659
653
|
shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
|
|
660
|
-
|
|
654
|
+
log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
|
|
661
655
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
662
656
|
}
|
|
663
657
|
|
|
664
658
|
function updatePrAfterFix(pr, project, source) {
|
|
665
|
-
|
|
659
|
+
|
|
666
660
|
if (!pr?.id) return;
|
|
667
661
|
const prs = getPrs(project);
|
|
668
662
|
const target = prs.find(p => p.id === pr.id);
|
|
@@ -672,11 +666,11 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
672
666
|
target.reviewStatus = 'waiting';
|
|
673
667
|
if (source === 'pr-human-feedback') {
|
|
674
668
|
if (target.humanFeedback) target.humanFeedback.pendingFix = false;
|
|
675
|
-
target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt:
|
|
676
|
-
|
|
669
|
+
target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt: ts() };
|
|
670
|
+
log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
|
|
677
671
|
} else {
|
|
678
|
-
target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt:
|
|
679
|
-
|
|
672
|
+
target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt: ts() };
|
|
673
|
+
log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
680
674
|
}
|
|
681
675
|
|
|
682
676
|
shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
|
|
@@ -685,7 +679,7 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
685
679
|
// ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
|
|
686
680
|
|
|
687
681
|
async function handlePostMerge(pr, project, config, newStatus) {
|
|
688
|
-
|
|
682
|
+
|
|
689
683
|
const prNum = (pr.id || '').replace('PR-', '');
|
|
690
684
|
|
|
691
685
|
if (pr.branch) {
|
|
@@ -702,11 +696,11 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
702
696
|
try {
|
|
703
697
|
if (!require('fs').statSync(wtPath).isDirectory()) continue;
|
|
704
698
|
execSilent(`git worktree remove "${wtPath}" --force`, { cwd: root, stdio: 'pipe', timeout: 15000 });
|
|
705
|
-
|
|
706
|
-
} catch (err) {
|
|
699
|
+
log('info', `Post-merge cleanup: removed worktree ${dir}`);
|
|
700
|
+
} catch (err) { log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
|
|
707
701
|
}
|
|
708
702
|
}
|
|
709
|
-
} catch (err) {
|
|
703
|
+
} catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
|
|
710
704
|
}
|
|
711
705
|
|
|
712
706
|
if (newStatus !== 'merged') return;
|
|
@@ -727,8 +721,8 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
727
721
|
updated++;
|
|
728
722
|
}
|
|
729
723
|
}
|
|
730
|
-
if (updated > 0)
|
|
731
|
-
} catch (err) {
|
|
724
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as implemented for ${pr.id}`);
|
|
725
|
+
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
732
726
|
}
|
|
733
727
|
|
|
734
728
|
const agentId = (pr.agent || '').toLowerCase();
|
|
@@ -748,26 +742,26 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
748
742
|
headers: { 'Content-Type': 'application/json' },
|
|
749
743
|
body: JSON.stringify({ text: `PR ${pr.id} merged: ${pr.title} (${project.name}) by ${pr.agent || 'unknown'}` })
|
|
750
744
|
});
|
|
751
|
-
} catch (err) {
|
|
745
|
+
} catch (err) { log('warn', `Teams post-merge notify failed: ${err.message}`); }
|
|
752
746
|
}
|
|
753
747
|
|
|
754
|
-
|
|
748
|
+
log('info', `Post-merge hooks completed for ${pr.id}`);
|
|
755
749
|
}
|
|
756
750
|
|
|
757
751
|
function checkForLearnings(agentId, agentInfo, taskDesc) {
|
|
758
|
-
|
|
759
|
-
const today =
|
|
752
|
+
|
|
753
|
+
const today = dateStamp();
|
|
760
754
|
const inboxFiles = getInboxFiles();
|
|
761
755
|
const agentFiles = inboxFiles.filter(f => f.includes(agentId) && f.includes(today));
|
|
762
756
|
if (agentFiles.length > 0) {
|
|
763
|
-
|
|
757
|
+
log('info', `${agentInfo?.name || agentId} wrote ${agentFiles.length} finding(s) to inbox`);
|
|
764
758
|
return;
|
|
765
759
|
}
|
|
766
|
-
|
|
760
|
+
log('warn', `${agentInfo?.name || agentId} didn't write learnings — no follow-up queued`);
|
|
767
761
|
}
|
|
768
762
|
|
|
769
763
|
function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
770
|
-
|
|
764
|
+
|
|
771
765
|
if (!output) return;
|
|
772
766
|
let fullText = '';
|
|
773
767
|
for (const line of output.split('\n')) {
|
|
@@ -791,16 +785,16 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
791
785
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
792
786
|
for (const block of skillBlocks) {
|
|
793
787
|
const fmMatch = block.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
794
|
-
if (!fmMatch) {
|
|
788
|
+
if (!fmMatch) { log('warn', `Skill block from ${agentName} has no frontmatter, skipping`); continue; }
|
|
795
789
|
const fm = fmMatch[1];
|
|
796
790
|
const m = (key) => { const r = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm')); return r ? r[1].trim() : ''; };
|
|
797
791
|
const name = m('name');
|
|
798
|
-
if (!name) {
|
|
792
|
+
if (!name) { log('warn', `Skill block from ${agentName} has no name, skipping`); continue; }
|
|
799
793
|
const scope = m('scope') || 'minions';
|
|
800
794
|
const project = m('project');
|
|
801
795
|
let enrichedBlock = block;
|
|
802
796
|
if (!m('author')) enrichedBlock = enrichedBlock.replace('---\n', `---\nauthor: ${agentName}\n`);
|
|
803
|
-
if (!m('created')) enrichedBlock = enrichedBlock.replace('---\n', `---\ncreated: ${
|
|
797
|
+
if (!m('created')) enrichedBlock = enrichedBlock.replace('---\n', `---\ncreated: ${dateStamp()}\n`);
|
|
804
798
|
const filename = name.replace(/[^a-z0-9-]/g, '-') + '.md';
|
|
805
799
|
if (scope === 'project' && project) {
|
|
806
800
|
const proj = shared.getProjects(config).find(p => p.name === project);
|
|
@@ -812,9 +806,9 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
812
806
|
const skillId = `SK${String(items.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
813
807
|
items.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
|
|
814
808
|
description: `Create project-level skill \`${filename}\` in ${project}.\n\nWrite this file to \`${proj.localPath}/.claude/skills/${filename}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
815
|
-
priority: 'low', status: 'queued', created:
|
|
809
|
+
priority: 'low', status: 'queued', created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
816
810
|
shared.safeWrite(centralPath, items);
|
|
817
|
-
|
|
811
|
+
log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
|
|
818
812
|
}
|
|
819
813
|
}
|
|
820
814
|
} else {
|
|
@@ -829,9 +823,9 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
829
823
|
const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${body.trim()}\n`;
|
|
830
824
|
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
831
825
|
shared.safeWrite(skillPath, ccContent);
|
|
832
|
-
|
|
826
|
+
log('info', `Extracted skill "${name}" from ${agentName} → ~/.claude/skills/${name.replace(/[^a-z0-9-]/g, '-')}/SKILL.md`);
|
|
833
827
|
} else {
|
|
834
|
-
|
|
828
|
+
log('info', `Skill "${name}" already exists, skipping`);
|
|
835
829
|
}
|
|
836
830
|
|
|
837
831
|
}
|
|
@@ -839,10 +833,10 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
839
833
|
}
|
|
840
834
|
|
|
841
835
|
function updateAgentHistory(agentId, dispatchItem, result) {
|
|
842
|
-
|
|
836
|
+
|
|
843
837
|
const historyPath = path.join(AGENTS_DIR, agentId, 'history.md');
|
|
844
838
|
let history = safeRead(historyPath) || '# Agent History\n\n';
|
|
845
|
-
const entry = `### ${
|
|
839
|
+
const entry = `### ${ts()} — ${result}\n` +
|
|
846
840
|
`- **Task:** ${dispatchItem.task}\n` +
|
|
847
841
|
`- **Type:** ${dispatchItem.type}\n` +
|
|
848
842
|
`- **Project:** ${dispatchItem.meta?.project?.name || 'central'}\n` +
|
|
@@ -859,15 +853,15 @@ function updateAgentHistory(agentId, dispatchItem, result) {
|
|
|
859
853
|
const trimmed = entries.slice(0, 20);
|
|
860
854
|
history = header + trimmed.map(e => '### ' + e).join('');
|
|
861
855
|
shared.safeWrite(historyPath, history);
|
|
862
|
-
|
|
856
|
+
log('info', `Updated history for ${agentId}`);
|
|
863
857
|
}
|
|
864
858
|
|
|
865
859
|
function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
866
|
-
|
|
860
|
+
|
|
867
861
|
if (!pr?.id || !pr?.agent) return;
|
|
868
862
|
const authorAgentId = pr.agent.toLowerCase();
|
|
869
863
|
if (!config.agents[authorAgentId]) return;
|
|
870
|
-
const today =
|
|
864
|
+
const today = dateStamp();
|
|
871
865
|
const inboxFiles = getInboxFiles();
|
|
872
866
|
const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
|
|
873
867
|
if (reviewFiles.length === 0) return;
|
|
@@ -883,11 +877,11 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
883
877
|
`avoid the patterns flagged here. If you are assigned to fix this PR, ` +
|
|
884
878
|
`address every point raised above.\n`;
|
|
885
879
|
shared.safeWrite(feedbackPath, content);
|
|
886
|
-
|
|
880
|
+
log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
|
|
887
881
|
}
|
|
888
882
|
|
|
889
883
|
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount) {
|
|
890
|
-
|
|
884
|
+
|
|
891
885
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
892
886
|
const metrics = safeJson(metricsPath) || {};
|
|
893
887
|
if (!metrics[agentId]) {
|
|
@@ -896,7 +890,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
896
890
|
}
|
|
897
891
|
const m = metrics[agentId];
|
|
898
892
|
m.lastTask = dispatchItem.task;
|
|
899
|
-
m.lastCompleted =
|
|
893
|
+
m.lastCompleted = ts();
|
|
900
894
|
if (result === 'success') {
|
|
901
895
|
m.tasksCompleted++;
|
|
902
896
|
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
@@ -913,7 +907,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
913
907
|
m.totalOutputTokens = (m.totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
|
|
914
908
|
m.totalCacheRead = (m.totalCacheRead || 0) + (taskUsage.cacheRead || 0);
|
|
915
909
|
}
|
|
916
|
-
const today =
|
|
910
|
+
const today = dateStamp();
|
|
917
911
|
if (!metrics._daily) metrics._daily = {};
|
|
918
912
|
if (!metrics._daily[today]) metrics._daily[today] = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, tasks: 0 };
|
|
919
913
|
const daily = metrics._daily[today];
|
|
@@ -945,7 +939,7 @@ function parseAgentOutput(stdout) {
|
|
|
945
939
|
* Called from runPostCompletionHooks when type === 'decompose'.
|
|
946
940
|
*/
|
|
947
941
|
function handleDecompositionResult(stdout, meta, config) {
|
|
948
|
-
|
|
942
|
+
|
|
949
943
|
const parentId = meta?.item?.id;
|
|
950
944
|
if (!parentId) return 0;
|
|
951
945
|
|
|
@@ -953,7 +947,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
953
947
|
const { text } = shared.parseStreamJsonOutput(stdout);
|
|
954
948
|
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)```/);
|
|
955
949
|
if (!jsonMatch) {
|
|
956
|
-
|
|
950
|
+
log('warn', `Decomposition for ${parentId}: no JSON block found in output`);
|
|
957
951
|
return 0;
|
|
958
952
|
}
|
|
959
953
|
|
|
@@ -961,13 +955,13 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
961
955
|
try {
|
|
962
956
|
decomposition = JSON.parse(jsonMatch[1]);
|
|
963
957
|
} catch (err) {
|
|
964
|
-
|
|
958
|
+
log('warn', `Decomposition for ${parentId}: invalid JSON — ${err.message}`);
|
|
965
959
|
return 0;
|
|
966
960
|
}
|
|
967
961
|
|
|
968
962
|
const subItems = decomposition.sub_items || decomposition.subItems || [];
|
|
969
963
|
if (subItems.length === 0) {
|
|
970
|
-
|
|
964
|
+
log('warn', `Decomposition for ${parentId}: no sub-items produced`);
|
|
971
965
|
return 0;
|
|
972
966
|
}
|
|
973
967
|
|
|
@@ -1009,7 +1003,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1009
1003
|
}
|
|
1010
1004
|
|
|
1011
1005
|
safeWrite(wiPath, items);
|
|
1012
|
-
|
|
1006
|
+
log('info', `Decomposition: ${parentId} → ${subItems.length} sub-items: ${subItems.map(s => s.id).join(', ')}`);
|
|
1013
1007
|
return subItems.length;
|
|
1014
1008
|
}
|
|
1015
1009
|
|
|
@@ -1017,7 +1011,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1017
1011
|
}
|
|
1018
1012
|
|
|
1019
1013
|
function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
1020
|
-
|
|
1014
|
+
|
|
1021
1015
|
const type = dispatchItem.type;
|
|
1022
1016
|
const meta = dispatchItem.meta;
|
|
1023
1017
|
const isSuccess = code === 0;
|
|
@@ -1031,7 +1025,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1031
1025
|
sessionId, dispatchId: dispatchItem.id, savedAt: new Date().toISOString(),
|
|
1032
1026
|
branch: dispatchItem.meta?.branch || null,
|
|
1033
1027
|
});
|
|
1034
|
-
} catch (err) {
|
|
1028
|
+
} catch (err) { log('warn', `Session save: ${err.message}`); }
|
|
1035
1029
|
}
|
|
1036
1030
|
|
|
1037
1031
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
@@ -1058,7 +1052,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1058
1052
|
} catch { /* optional */ }
|
|
1059
1053
|
|
|
1060
1054
|
if (retries < 3) {
|
|
1061
|
-
|
|
1055
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1062
1056
|
updateWorkItemStatus(meta, 'pending', '');
|
|
1063
1057
|
try {
|
|
1064
1058
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
@@ -1073,7 +1067,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1073
1067
|
shared.safeWrite(wiPath, items);
|
|
1074
1068
|
}
|
|
1075
1069
|
}
|
|
1076
|
-
} catch (err) {
|
|
1070
|
+
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1077
1071
|
} else {
|
|
1078
1072
|
updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
|
|
1079
1073
|
}
|
|
@@ -1088,7 +1082,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1088
1082
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1089
1083
|
if (wi) { delete wi._decomposing; shared.safeWrite(wiPath, items); }
|
|
1090
1084
|
}
|
|
1091
|
-
} catch (err) {
|
|
1085
|
+
} catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
|
|
1092
1086
|
}
|
|
1093
1087
|
}
|
|
1094
1088
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
@@ -1096,7 +1090,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1096
1090
|
try {
|
|
1097
1091
|
const { collectMeetingFindings } = require('./meeting');
|
|
1098
1092
|
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
|
|
1099
|
-
} catch (err) {
|
|
1093
|
+
} catch (err) { log('warn', `Meeting collect: ${err.message}`); }
|
|
1100
1094
|
}
|
|
1101
1095
|
|
|
1102
1096
|
// Plan chaining removed — user must explicitly execute plan-to-prd after reviewing the plan
|
|
@@ -1128,15 +1122,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1128
1122
|
const wtPath = path.join(worktreeRoot, dir);
|
|
1129
1123
|
try {
|
|
1130
1124
|
shared.exec(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
1131
|
-
|
|
1125
|
+
log('info', `Post-completion: removed worktree ${dir}`);
|
|
1132
1126
|
} catch (err) {
|
|
1133
|
-
|
|
1127
|
+
log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
|
|
1134
1128
|
}
|
|
1135
1129
|
}
|
|
1136
1130
|
}
|
|
1137
1131
|
}
|
|
1138
1132
|
} catch (err) {
|
|
1139
|
-
|
|
1133
|
+
log('warn', `Post-completion worktree cleanup error: ${err.message}`);
|
|
1140
1134
|
}
|
|
1141
1135
|
}
|
|
1142
1136
|
|
|
@@ -1146,7 +1140,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1146
1140
|
const projects = shared.getProjects(config);
|
|
1147
1141
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
1148
1142
|
if (!existingPrFound) {
|
|
1149
|
-
|
|
1143
|
+
log('warn', `Agent completed implement task ${meta.item.id} but no PR was created — reverting to failed for retry`);
|
|
1150
1144
|
// Revert to failed so auto-retry can re-attempt with PR creation
|
|
1151
1145
|
let wiPath;
|
|
1152
1146
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
@@ -1221,11 +1215,11 @@ function syncPrdFromPrs(config) {
|
|
|
1221
1215
|
}
|
|
1222
1216
|
}
|
|
1223
1217
|
if (totalReconciled > 0) {
|
|
1224
|
-
|
|
1218
|
+
log('info', `PR sync: reconciled ${totalReconciled} pending work item(s) to done`);
|
|
1225
1219
|
}
|
|
1226
1220
|
} catch (err) {
|
|
1227
1221
|
// Non-fatal — log and continue
|
|
1228
|
-
try {
|
|
1222
|
+
try { log('warn', `syncPrdFromPrs error: ${err?.message || err}`); } catch { /* engine not available */ }
|
|
1229
1223
|
}
|
|
1230
1224
|
}
|
|
1231
1225
|
|