@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.
@@ -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 } = shared;
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
- const e = engine();
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
- e.log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
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
- e.log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
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 = e.ts();
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', '')}-${e.ts().slice(0, 10)}.md`;
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
- e.log('info', `PRD completion summary written to notes/inbox/${summaryFile}`);
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: e.ts(), createdBy: 'engine:plan-completion',
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: e.ts(),
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
- e.log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
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
- e.log('info', `Archived completed PRD: prd/archive/${planFile}`);
267
+ log('info', `Archived completed PRD: prd/archive/${planFile}`);
274
268
  } catch (err) {
275
- e.log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
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
- e.log('info', `Archived source plan: plans/archive/${md}`);
291
- } catch (err) { e.log('warn', `Failed to archive plan ${md}: ${err.message}`); }
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) { e.log('warn', `Plan archive scan: ${err.message}`); }
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) { e.log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
318
+ } catch (err) { log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
325
319
  }
326
320
  }
327
321
  }
328
- if (cleanedWt > 0) e.log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
329
- } catch (err) { e.log('warn', `Worktree cleanup: ${err.message}`); }
322
+ if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
323
+ } catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
330
324
 
331
- e.log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
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
- const e = engine();
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
- e.log('warn', `Plan chaining: no plan files found in plans/ after task ${dispatchItem.id}`);
344
+ log('warn', `Plan chaining: no plan files found in plans/ after task ${dispatchItem.id}`);
351
345
  return;
352
346
  }
353
- e.log('info', `Plan chaining: using mtime fallback — found ${planFileName}`);
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
- e.log('info', `Plan chaining: renamed ${planFileName} → ${mdName} (plans must be .md)`);
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
- e.log('info', `Plan chaining: renamed to .md (not valid JSON)`);
376
- } catch (err) { e.log('warn', `Plan rename fallback: ${err.message}`); }
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
- e.log('error', `Plan chaining: failed to read plan file ${planFile.name}: ${err.message}`);
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
- e.log('error', 'Plan chaining: no target project available');
389
+ log('error', 'Plan chaining: no target project available');
396
390
  return;
397
391
  }
398
392
 
399
- e.log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
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: e.ts(),
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
- const e = engine();
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: e.ts(), reason: reason || undefined };
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 = e.ts();
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 = e.ts();
462
+ target.completedAt = ts();
469
463
  } else if (status === 'failed') {
470
464
  if (reason) target.failReason = reason;
471
- target.failedAt = e.ts();
465
+ target.failedAt = ts();
472
466
  }
473
467
  }
474
468
 
475
469
  shared.safeWrite(wiPath, items);
476
- e.log('info', `Work item ${itemId} → ${status}${reason ? ': ' + reason : ''}`);
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) { engine().log('warn', `PRD status sync: ${err.message}`); }
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
- const e = engine();
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 = e.dateStamp();
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: e.dateStamp(),
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
- e.log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
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
- const e = engine();
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: e.ts(),
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
- e.log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
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
- const e = engine();
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: e.ts() };
676
- e.log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
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: e.ts() };
679
- e.log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
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
- const e = engine();
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
- e.log('info', `Post-merge cleanup: removed worktree ${dir}`);
706
- } catch (err) { e.log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
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) { e.log('warn', `Post-merge worktree cleanup: ${err.message}`); }
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) e.log('info', `Post-merge: marked ${mergedItemId} as implemented for ${pr.id}`);
731
- } catch (err) { e.log('warn', `Post-merge PRD update: ${err.message}`); }
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) { e.log('warn', `Teams post-merge notify failed: ${err.message}`); }
745
+ } catch (err) { log('warn', `Teams post-merge notify failed: ${err.message}`); }
752
746
  }
753
747
 
754
- e.log('info', `Post-merge hooks completed for ${pr.id}`);
748
+ log('info', `Post-merge hooks completed for ${pr.id}`);
755
749
  }
756
750
 
757
751
  function checkForLearnings(agentId, agentInfo, taskDesc) {
758
- const e = engine();
759
- const today = e.dateStamp();
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
- e.log('info', `${agentInfo?.name || agentId} wrote ${agentFiles.length} finding(s) to inbox`);
757
+ log('info', `${agentInfo?.name || agentId} wrote ${agentFiles.length} finding(s) to inbox`);
764
758
  return;
765
759
  }
766
- e.log('warn', `${agentInfo?.name || agentId} didn't write learnings — no follow-up queued`);
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
- const e = engine();
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) { e.log('warn', `Skill block from ${agentName} has no frontmatter, skipping`); continue; }
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) { e.log('warn', `Skill block from ${agentName} has no name, skipping`); continue; }
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: ${e.dateStamp()}\n`);
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: e.ts(), createdBy: `engine:skill-extraction:${agentName}` });
809
+ priority: 'low', status: 'queued', created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
816
810
  shared.safeWrite(centralPath, items);
817
- e.log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
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
- e.log('info', `Extracted skill "${name}" from ${agentName} → ~/.claude/skills/${name.replace(/[^a-z0-9-]/g, '-')}/SKILL.md`);
826
+ log('info', `Extracted skill "${name}" from ${agentName} → ~/.claude/skills/${name.replace(/[^a-z0-9-]/g, '-')}/SKILL.md`);
833
827
  } else {
834
- e.log('info', `Skill "${name}" already exists, skipping`);
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
- const e = engine();
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 = `### ${e.ts()} — ${result}\n` +
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
- e.log('info', `Updated history for ${agentId}`);
856
+ log('info', `Updated history for ${agentId}`);
863
857
  }
864
858
 
865
859
  function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
866
- const e = engine();
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 = e.dateStamp();
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
- e.log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
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
- const e = engine();
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 = e.ts();
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 = e.dateStamp();
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
- const e = engine();
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
- e.log('warn', `Decomposition for ${parentId}: no JSON block found in output`);
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
- e.log('warn', `Decomposition for ${parentId}: invalid JSON — ${err.message}`);
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
- e.log('warn', `Decomposition for ${parentId}: no sub-items produced`);
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
- e.log('info', `Decomposition: ${parentId} → ${subItems.length} sub-items: ${subItems.map(s => s.id).join(', ')}`);
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
- const e = engine();
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) { engine().log('warn', `Session save: ${err.message}`); }
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
- e.log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
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) { e.log('warn', `Retry update: ${err.message}`); }
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) { e.log('warn', `Decompose cleanup: ${err.message}`); }
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) { engine().log('warn', `Meeting collect: ${err.message}`); }
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
- e.log('info', `Post-completion: removed worktree ${dir}`);
1125
+ log('info', `Post-completion: removed worktree ${dir}`);
1132
1126
  } catch (err) {
1133
- e.log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
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
- e.log('warn', `Post-completion worktree cleanup error: ${err.message}`);
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
- e.log('warn', `Agent completed implement task ${meta.item.id} but no PR was created — reverting to failed for retry`);
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
- engine().log('info', `PR sync: reconciled ${totalReconciled} pending work item(s) to done`);
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 { engine().log('warn', `syncPrdFromPrs error: ${err?.message || err}`); } catch { /* engine not available */ }
1222
+ try { log('warn', `syncPrdFromPrs error: ${err?.message || err}`); } catch { /* engine not available */ }
1229
1223
  }
1230
1224
  }
1231
1225