@yemi33/minions 0.1.297 → 0.1.299

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.299 (2026-04-03)
4
+
5
+ ### Fixes
6
+ - work item agent column falls back to item.agent field
7
+ - CC workType descriptions restored + verify for maintenance/merge tasks
8
+
9
+ ### Other
10
+ - resolve lifecycle.js conflict — accept agent changes
11
+
3
12
  ## 0.1.297 (2026-04-03)
4
13
 
5
14
  ### Features
@@ -52,7 +52,7 @@ function wiRow(item) {
52
52
  '<td>' +
53
53
  (item.completedAgents && item.completedAgents.length > 0
54
54
  ? '<span class="pr-agent">' + escHtml(item.completedAgents.join(', ')) + '</span>'
55
- : '<span class="pr-agent">' + escHtml(item.dispatched_to || '—') + '</span>') +
55
+ : '<span class="pr-agent">' + escHtml(item.dispatched_to || item.agent || '—') + '</span>') +
56
56
  (item.failReason ? '<span style="display:block;font-size:9px;color:var(--red)" title="' + escHtml(item.failReason) + '">' + escHtml(item.failReason.slice(0, 30)) + '</span>' : '') +
57
57
  '</td>' +
58
58
  '<td>' + prLink + '</td>' +
package/dashboard.js CHANGED
@@ -377,7 +377,16 @@ I'll save that as a note and dispatch dallas to fix the bug.
377
377
  If no actions are needed (just answering a question, or you handled it directly), do NOT include the ===ACTIONS=== line.
378
378
 
379
379
  Available action types:
380
- - **dispatch**: Create a work item for an agent. Fields: title, workType (ask/explore/fix/review/test/implement/verify), priority (low/medium/high), agents (array of IDs, optional), project, description. Use \`verify\` when the user wants to build PRs locally, merge branches together, start a dev server, and get a localhost URL to test.
380
+ - **dispatch**: Create a work item for an agent. Fields: title, workType, priority (low/medium/high), agents (array of IDs, optional), project, description.
381
+ workType — choose carefully, this determines the playbook and whether a PR is expected:
382
+ - \`explore\` — research, investigate, read code, gather information, write findings (NO PR)
383
+ - \`ask\` — answer a question, analyze, produce a report (NO PR)
384
+ - \`implement\` — write new code, add a feature (PR REQUIRED — will fail without one)
385
+ - \`fix\` — fix a bug, address review feedback on an existing PR (PR REQUIRED)
386
+ - \`review\` — code review, quality assessment, evaluate completed work (NO PR)
387
+ - \`test\` — run tests, write test cases (PR expected if new tests written)
388
+ - \`verify\` — merge PRs, build locally, start dev server, maintenance ops, any task that doesn't need a new PR (NO PR)
389
+ If unsure between implement and verify: use \`implement\` only when the agent needs to create a NEW pull request. For merging, maintenance, builds, deploys, cleanup — use \`verify\`.
381
390
  - **note**: Save a note/decision to the inbox. Fields: title, content
382
391
  - **pin**: Pin critical context visible to ALL agents on every task. Use when the user says "remember", "pin", "always", "from now on". Fields: title, content, level (optional: "critical" or "warning")
383
392
  - **plan**: Create a multi-step plan. Fields: title, description, project, branchStrategy (parallel/shared-branch)
@@ -5,10 +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, safeReadDir, execSilent, projectPrPath, getPrLinks, addPrLink,
11
- mutateJsonFileLocked, log, ts, dateStamp } = shared;
9
+ const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink,
10
+ log, ts, dateStamp } = shared;
12
11
  const { trackEngineUsage } = require('./llm');
13
12
  const queries = require('./queries');
14
13
  const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
@@ -22,12 +21,7 @@ function checkPlanCompletion(meta, config) {
22
21
  const planPath = path.join(PRD_DIR, planFile);
23
22
  const plan = safeJson(planPath);
24
23
  if (!plan?.missing_features) return;
25
- if (plan.status === 'completed') {
26
- // Idempotency guard: if we already sent the completion notification, skip entirely.
27
- // If _completionNotified is NOT set, fall through — crash recovery path:
28
- // engine crashed after setting status=completed but before creating verify/PR items.
29
- if (plan._completionNotified) return;
30
- }
24
+ if (plan.status === 'completed') return;
31
25
 
32
26
  const projects = shared.getProjects(config);
33
27
 
@@ -138,22 +132,10 @@ function checkPlanCompletion(meta, config) {
138
132
  ...uniquePrs.map(pr => `- ${pr.id}: ${pr.title || ''} ${pr.url || ''}`),
139
133
  ].filter(Boolean).join('\n');
140
134
 
141
- // Write summary to notes/inbox (slug-based dedup prevents duplicates on same day)
142
- const slug = `prd-completion-${planFile.replace('.json', '')}`;
143
- const wrote = shared.writeToInbox('engine', slug, summary);
144
- if (wrote) log('info', `PRD completion summary written to notes/inbox/`);
145
-
146
- // Persist _completionNotified flag atomically BEFORE creating work items.
147
- // This prevents duplicate inbox notes on re-entry. Work item creation below has its own
148
- // existingPrItem/existingVerify guards, so the flag does NOT block crash recovery of those.
149
- plan._completionNotified = true;
150
- mutateJsonFileLocked(planPath, (data) => {
151
- data.status = 'completed';
152
- data.completedAt = plan.completedAt;
153
- data._completionNotified = true;
154
- if (plan._timing) data._timing = plan._timing;
155
- return data;
156
- });
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}`);
157
139
 
158
140
  // Resolve the primary project for writing new work items (PR, verify)
159
141
  const projectName = plan.project;
@@ -204,12 +186,11 @@ function checkPlanCompletion(meta, config) {
204
186
 
205
187
  // Build per-project checkout commands: one worktree, merge all PR branches into it
206
188
  const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
207
- const localPath = p.localPath.replace(/\\/g, '/');
208
- const wtPath = `${localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
189
+ const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
209
190
  const branches = prs.map(pr => pr.branch).filter(Boolean);
210
191
  const lines = [
211
192
  `# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
212
- `cd "${localPath}"`,
193
+ `cd "${p.localPath}"`,
213
194
  `git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
214
195
  `git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
215
196
  `cd "${wtPath}"`,
@@ -230,10 +211,9 @@ function checkPlanCompletion(meta, config) {
230
211
  ).join('\n');
231
212
 
232
213
  // List projects and their worktree paths for the agent
233
- const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) => {
234
- const lp = p.localPath.replace(/\\/g, '/');
235
- return `- **${name}**: see setup commands below (\`${lp}/../worktrees/verify-${name}-${planSlug}-*\`)`;
236
- }).join('\n');
214
+ const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
215
+ `- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
216
+ ).join('\n');
237
217
 
238
218
  const description = [
239
219
  `Verification task for completed plan \`${planFile}\`.`,
@@ -278,75 +258,46 @@ function checkPlanCompletion(meta, config) {
278
258
  log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
279
259
  }
280
260
 
281
- // 5. Archive deferred until verify completes (see runPostCompletionHooks).
282
- // Plan stays active until verification finishes so artifacts are visible.
283
-
284
- log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
285
- }
286
-
287
- // ─── Plan Archiving (called after verify completes) ─────────────────────────
288
-
289
- function archivePlan(planFile, plan, projects, config) {
290
- const planPath = path.join(PRD_DIR, planFile);
291
- const projectName = plan.project || '';
292
-
293
- // Archive PRD .json to prd/archive/
261
+ // 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
294
262
  const prdArchiveDir = path.join(PRD_DIR, 'archive');
295
263
  if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
296
- if (fs.existsSync(planPath)) {
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}`);
297
270
  shared.safeWrite(planPath, plan);
298
- try {
299
- fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
300
- log('info', `Archived completed PRD: prd/archive/${planFile}`);
301
- } catch (err) { log('warn', `Failed to archive PRD ${planFile}: ${err.message}`); }
302
271
  }
303
272
 
304
- // Archive the source .md plan
273
+ // Also archive the source .md plan if it exists
305
274
  const planArchiveDir = path.join(PLANS_DIR, 'archive');
306
275
  if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
307
- if (plan.source_plan) {
308
- const mdPath = path.join(PLANS_DIR, plan.source_plan);
309
- if (fs.existsSync(mdPath)) {
310
- try {
311
- fs.renameSync(mdPath, path.join(planArchiveDir, plan.source_plan));
312
- log('info', `Archived source plan: plans/archive/${plan.source_plan}`);
313
- } catch (err) { log('warn', `Failed to archive source plan ${plan.source_plan}: ${err.message}`); }
314
- }
315
- } else {
316
- try {
317
- const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
318
- for (const md of mdFiles) {
319
- const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
320
- if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
321
- try {
322
- fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
323
- log('info', `Archived source plan: plans/archive/${md}`);
324
- } catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
325
- break;
326
- }
327
- }
328
- } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
329
- }
330
-
331
- // Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
332
276
  try {
333
- let allWi = [];
334
- for (const p of projects) {
335
- try { allWi = allWi.concat(safeJson(shared.projectWorkItemsPath(p)) || []); } catch {}
336
- }
337
- const planWi = allWi.filter(w => w.sourcePlan === planFile && w.itemType !== 'verify');
338
- const allPrs = [];
339
- for (const p of projects) {
340
- try { allPrs.push(...(safeJson(shared.projectPrPath(p)) || [])); } catch {}
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
+ }
341
288
  }
289
+ } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
342
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
343
294
  const branchSlugs = new Set();
344
295
  if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
345
- for (const w of planWi) {
296
+ for (const w of doneItems) {
346
297
  if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
347
298
  if (w.id) branchSlugs.add(w.id.toLowerCase());
348
299
  }
349
- for (const pr of allPrs.filter(pr => (pr.prdItems || []).some(id => planWi.find(w => w.id === id)))) {
300
+ for (const pr of uniquePrs) {
350
301
  if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
351
302
  }
352
303
 
@@ -368,8 +319,10 @@ function archivePlan(planFile, plan, projects, config) {
368
319
  }
369
320
  }
370
321
  }
371
- if (cleanedWt > 0) log('info', `Plan archive: cleaned ${cleanedWt} worktree(s)`);
322
+ if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
372
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`);
373
326
  }
374
327
 
375
328
  // ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
@@ -440,12 +393,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
440
393
  log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
441
394
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
442
395
  let items = [];
443
- try {
444
- items = JSON.parse(fs.readFileSync(wiPath, 'utf8'));
445
- } catch (err) {
446
- log('warn', `Plan chaining: failed to parse ${wiPath}, falling back to empty list: ${err.message}`);
447
- try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch (_) { /* backup best-effort */ }
448
- }
396
+ try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
449
397
  items.push({
450
398
  id: 'W-' + shared.uid(),
451
399
  title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
@@ -508,7 +456,6 @@ function updateWorkItemStatus(meta, status, reason) {
508
456
  }
509
457
  } else {
510
458
  target.status = status;
511
- delete target._pendingReason;
512
459
  if (status === 'done') {
513
460
  delete target.failReason;
514
461
  delete target.failedAt;
@@ -534,18 +481,14 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
534
481
  const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
535
482
  for (const pf of files) {
536
483
  const fpath = path.join(prdDir, pf);
537
- mutateJsonFileLocked(fpath, (plan) => {
538
- if (!plan?.missing_features) return plan;
539
- const feature = plan.missing_features.find(f => f.id === itemId);
540
- if (feature && feature.status !== status) {
541
- feature.status = status;
542
- }
543
- return plan;
544
- });
545
- // Check if we found it (read back to verify)
546
484
  const plan = safeJson(fpath);
547
- const feature = plan?.missing_features?.find(f => f.id === itemId);
548
- if (feature && feature.status === status) return;
485
+ if (!plan?.missing_features) continue;
486
+ const feature = plan.missing_features.find(f => f.id === itemId);
487
+ if (feature && feature.status !== status) {
488
+ feature.status = status;
489
+ shared.safeWrite(fpath, plan);
490
+ return;
491
+ }
549
492
  }
550
493
  } catch (err) { log('warn', `PRD status sync: ${err.message}`); }
551
494
  }
@@ -568,7 +511,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
568
511
  for (const block of content) {
569
512
  if (block.type === 'tool_result' && block.content) {
570
513
  const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
571
- if (text.includes('pullRequestId') || text.includes('create_pull_request') || text.includes('/pull/') || text.includes('pullrequest/')) {
514
+ if (text.includes('pullRequestId') || text.includes('create_pull_request')) {
572
515
  while ((match = urlPattern.exec(text)) !== null) prMatches.add(match[1] || match[2]);
573
516
  }
574
517
  }
@@ -637,7 +580,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
637
580
  dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
638
581
  }
639
582
  const entry = dirtyTargets.get(targetName);
640
- if (entry.prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
583
+ if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
641
584
 
642
585
  let title = meta?.item?.title || '';
643
586
  const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
@@ -663,22 +606,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
663
606
  }
664
607
 
665
608
  for (const [name, entry] of dirtyTargets) {
666
- mutateJsonFileLocked(entry.prPath, (existingPrs) => {
667
- // Merge new PRs into existing data to avoid losing concurrent writes
668
- for (const pr of entry.prs) {
669
- const idx = existingPrs.findIndex(p => p.id === pr.id);
670
- if (idx >= 0) {
671
- // Backfill prdItems if needed
672
- if (pr.prdItems?.length) {
673
- const merged = new Set([...(existingPrs[idx].prdItems || []), ...pr.prdItems]);
674
- existingPrs[idx].prdItems = [...merged];
675
- }
676
- } else {
677
- existingPrs.push(pr);
678
- }
679
- }
680
- return existingPrs;
681
- }, { defaultValue: [] });
609
+ shared.safeWrite(entry.prPath, entry.prs);
682
610
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
683
611
  }
684
612
  return added;
@@ -702,7 +630,6 @@ function updatePrAfterReview(agentId, pr, project) {
702
630
 
703
631
  // Set reviewStatus to 'waiting' (single source of truth — synced from ADO/GitHub votes on next poll)
704
632
  target.reviewStatus = 'waiting';
705
- target._reviewedOnce = true;
706
633
  target.minionsReview = {
707
634
  reviewer: reviewerName,
708
635
  reviewedAt: ts(),
@@ -716,16 +643,15 @@ function updatePrAfterReview(agentId, pr, project) {
716
643
  const authorAgentId = (pr.agent || '').toLowerCase();
717
644
  if (authorAgentId && config.agents?.[authorAgentId]) {
718
645
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
719
- mutateJsonFileLocked(metricsPath, (metrics) => {
720
- if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
721
- if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
722
- metrics[agentId].reviewsDone = (metrics[agentId].reviewsDone || 0) + 1;
723
- return metrics;
724
- });
646
+ const metrics = safeJson(metricsPath) || {};
647
+ if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
648
+ if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
649
+ metrics[agentId].reviewsDone = (metrics[agentId].reviewsDone || 0) + 1;
650
+ shared.safeWrite(metricsPath, metrics);
725
651
  }
726
652
 
727
653
  shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
728
- log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
654
+ log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
729
655
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
730
656
  }
731
657
 
@@ -779,9 +705,6 @@ async function handlePostMerge(pr, project, config, newStatus) {
779
705
 
780
706
  if (newStatus !== 'merged') return;
781
707
 
782
- // Mark review as approved since it was merged
783
- pr.reviewStatus = 'approved';
784
-
785
708
  // Resolve linked work item from pr-links or PR branch name
786
709
  let mergedItemId = getPrLinks()[pr.id];
787
710
  if (!mergedItemId && pr.branch) {
@@ -796,16 +719,12 @@ async function handlePostMerge(pr, project, config, newStatus) {
796
719
  const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
797
720
  let updated = 0;
798
721
  for (const pf of planFiles) {
799
- const prdPath = path.join(prdDir, pf);
800
- const plan = safeJson(prdPath);
722
+ const plan = safeJson(path.join(prdDir, pf));
801
723
  if (!plan?.missing_features) continue;
802
724
  const feature = plan.missing_features.find(f => f.id === mergedItemId);
803
725
  if (feature && feature.status !== 'implemented') {
804
- mutateJsonFileLocked(prdPath, (data) => {
805
- const feat = data.missing_features?.find(f => f.id === mergedItemId);
806
- if (feat && feat.status !== 'implemented') feat.status = 'implemented';
807
- return data;
808
- });
726
+ feature.status = 'implemented';
727
+ shared.safeWrite(path.join(prdDir, pf), plan);
809
728
  updated++;
810
729
  }
811
730
  }
@@ -817,20 +736,17 @@ async function handlePostMerge(pr, project, config, newStatus) {
817
736
  for (const p of shared.getProjects(config)) wiPaths.push(shared.projectWorkItemsPath(p));
818
737
  for (const wiPath of wiPaths) {
819
738
  try {
820
- let found = false;
821
- mutateJsonFileLocked(wiPath, (items) => {
822
- const item = items.find(i => i.id === mergedItemId);
823
- if (item && item.status !== 'done') {
824
- log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
825
- item.status = 'done';
826
- item.completedAt = ts();
827
- delete item._pendingReason;
828
- item._mergedVia = pr.id;
829
- found = true;
830
- }
831
- return items;
832
- }, { defaultValue: [] });
833
- if (found) break;
739
+ const items = safeJson(wiPath);
740
+ if (!items) continue;
741
+ const item = items.find(i => i.id === mergedItemId);
742
+ if (item && item.status !== 'done') {
743
+ log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
744
+ item.status = 'done';
745
+ item.completedAt = e.ts();
746
+ item._mergedVia = pr.id;
747
+ shared.safeWrite(wiPath, items);
748
+ break;
749
+ }
834
750
  } catch (err) { log('warn', `Post-merge work item update: ${err.message}`); }
835
751
  }
836
752
  }
@@ -838,11 +754,10 @@ async function handlePostMerge(pr, project, config, newStatus) {
838
754
  const agentId = (pr.agent || '').toLowerCase();
839
755
  if (agentId && config.agents?.[agentId]) {
840
756
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
841
- mutateJsonFileLocked(metricsPath, (metrics) => {
842
- if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, prsMerged:0, reviewsDone:0, lastTask:null, lastCompleted:null };
843
- metrics[agentId].prsMerged = (metrics[agentId].prsMerged || 0) + 1;
844
- return metrics;
845
- });
757
+ const metrics = safeJson(metricsPath) || {};
758
+ if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, prsMerged:0, reviewsDone:0, lastTask:null, lastCompleted:null };
759
+ metrics[agentId].prsMerged = (metrics[agentId].prsMerged || 0) + 1;
760
+ shared.safeWrite(metricsPath, metrics);
846
761
  }
847
762
 
848
763
  const teamsUrl = process.env.TEAMS_PLAN_FLOW_URL;
@@ -924,7 +839,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
924
839
  }
925
840
  } else {
926
841
  // Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
927
- const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
842
+ const claudeSkillsDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'skills');
928
843
  const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
929
844
  const skillPath = path.join(skillDir, 'SKILL.md');
930
845
  if (!fs.existsSync(skillPath)) {
@@ -977,7 +892,8 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
977
892
  const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
978
893
  if (reviewFiles.length === 0) return;
979
894
  const reviewContent = reviewFiles.map(f => safeRead(path.join(INBOX_DIR, f))).join('\n\n');
980
- const slug = `feedback-from-${reviewerAgentId}-${pr.id}`;
895
+ const feedbackFile = `feedback-${authorAgentId}-from-${reviewerAgentId}-${pr.id}-${today}.md`;
896
+ const feedbackPath = shared.uniquePath(path.join(INBOX_DIR, feedbackFile));
981
897
  const content = `# Review Feedback for ${config.agents[authorAgentId]?.name || authorAgentId}\n\n` +
982
898
  `**PR:** ${pr.id} — ${pr.title || ''}\n` +
983
899
  `**Reviewer:** ${config.agents[reviewerAgentId]?.name || reviewerAgentId}\n` +
@@ -986,32 +902,8 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
986
902
  `## Action Required\n\nRead this feedback carefully. When you work on similar tasks in the future, ` +
987
903
  `avoid the patterns flagged here. If you are assigned to fix this PR, ` +
988
904
  `address every point raised above.\n`;
989
- const wrote = shared.writeToInbox(authorAgentId, slug, content);
990
- if (wrote) log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
991
- }
992
-
993
- function recordContextPressureOnWorkItem(meta, turnCount, outputLogSizeBytes, hitTurnLimit) {
994
- const itemId = meta.item?.id;
995
- if (!itemId) return;
996
-
997
- let wiPath;
998
- if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
999
- wiPath = path.join(MINIONS_DIR, 'work-items.json');
1000
- } else if (meta.source === 'work-item' && meta.project?.name) {
1001
- wiPath = path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
1002
- }
1003
- if (!wiPath) return;
1004
-
1005
- shared.mutateJsonFileLocked(wiPath, (items) => {
1006
- if (!Array.isArray(items)) return items;
1007
- const target = items.find(i => i.id === itemId);
1008
- if (target) {
1009
- target._turnCount = turnCount;
1010
- target._outputLogSizeBytes = outputLogSizeBytes;
1011
- target._hitTurnLimit = hitTurnLimit;
1012
- }
1013
- return items;
1014
- }, { defaultValue: [] });
905
+ shared.safeWrite(feedbackPath, content);
906
+ log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
1015
907
  }
1016
908
 
1017
909
  function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount, model) {
@@ -1059,20 +951,6 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
1059
951
  for (const day of Object.keys(metrics._daily)) {
1060
952
  if (day < cutoffStr) delete metrics._daily[day];
1061
953
  }
1062
-
1063
- // Update contextPressure aggregate
1064
- if (taskUsage && taskUsage.numTurns > 0) {
1065
- if (!metrics._contextPressure) metrics._contextPressure = { totalTurns: 0, dispatches: 0, maxTurns: 0, turnLimitHits: 0 };
1066
- const cp = metrics._contextPressure;
1067
- cp.totalTurns += taskUsage.numTurns;
1068
- cp.dispatches++;
1069
- if (taskUsage.numTurns > cp.maxTurns) cp.maxTurns = taskUsage.numTurns;
1070
- // Check if this dispatch hit the turn limit
1071
- const engineConfig = require('./queries').getConfig()?.engine || {};
1072
- const turnLimit = engineConfig.maxTurns || 100;
1073
- if (taskUsage.numTurns >= turnLimit) cp.turnLimitHits++;
1074
- }
1075
-
1076
954
  shared.safeWrite(metricsPath, metrics);
1077
955
  }
1078
956
 
@@ -1083,40 +961,6 @@ function parseAgentOutput(stdout) {
1083
961
  return { resultSummary: text, taskUsage: usage, sessionId, model };
1084
962
  }
1085
963
 
1086
- /**
1087
- * Resolve work-items.json path from dispatch meta.
1088
- * Central items → MINIONS_DIR/work-items.json; project items → projects/<name>/work-items.json.
1089
- */
1090
- function resolveWiPath(meta) {
1091
- if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
1092
- return path.join(MINIONS_DIR, 'work-items.json');
1093
- }
1094
- if (meta.project?.name) {
1095
- return path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
1096
- }
1097
- return null;
1098
- }
1099
-
1100
- /**
1101
- * Parse structured eval verdict from review agent output.
1102
- * Looks for a JSON block with { pass, build, tests, criteria_met, criteria_failed, feedback }.
1103
- * Returns parsed object or null if not found.
1104
- */
1105
- function parseEvalVerdict(text) {
1106
- if (!text) return null;
1107
- // Look for JSON fenced block first, then bare JSON
1108
- const fenced = text.match(/```(?:json)?\s*\n(\{[\s\S]*?"pass"\s*:[\s\S]*?\})\s*\n```/);
1109
- if (fenced) {
1110
- try { return JSON.parse(fenced[1]); } catch { /* fall through */ }
1111
- }
1112
- // Try bare JSON with "pass" key
1113
- const bare = text.match(/(\{[\s\S]*?"pass"\s*:[\s\S]*?\})/);
1114
- if (bare) {
1115
- try { return JSON.parse(bare[1]); } catch { /* ignore */ }
1116
- }
1117
- return null;
1118
- }
1119
-
1120
964
  /**
1121
965
  * Handle decomposition result — parse sub-items from agent output and create child work items.
1122
966
  * Called from runPostCompletionHooks when type === 'decompose'.
@@ -1211,47 +1055,6 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1211
1055
  } catch (err) { log('warn', `Session save: ${err.message}`); }
1212
1056
  }
1213
1057
 
1214
- // ── Accumulate per-work-item cost tracking ──────────────────────────────────
1215
- if (taskUsage && meta?.item?.id) {
1216
- try {
1217
- const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1218
- ? path.join(MINIONS_DIR, 'work-items.json')
1219
- : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1220
- if (wiPath) {
1221
- mutateJsonFileLocked(wiPath, (items) => {
1222
- if (!Array.isArray(items)) return items;
1223
- const wi = items.find(i => i.id === meta.item.id);
1224
- if (wi) {
1225
- wi._totalCostUsd = (wi._totalCostUsd || 0) + (taskUsage.costUsd || 0);
1226
- wi._totalInputTokens = (wi._totalInputTokens || 0) + (taskUsage.inputTokens || 0);
1227
- wi._totalOutputTokens = (wi._totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
1228
- }
1229
- return items;
1230
- }, { defaultValue: [] });
1231
-
1232
- // Cost ceiling circuit breaker — treat like evalMaxIterations exceeded
1233
- const engineCfg = config?.engine || {};
1234
- const evalMaxCost = engineCfg.evalMaxCost != null ? engineCfg.evalMaxCost : shared.ENGINE_DEFAULTS.evalMaxCost;
1235
- if (evalMaxCost != null && evalMaxCost > 0) {
1236
- const freshItems = safeJson(wiPath) || [];
1237
- const wi = freshItems.find(i => i.id === meta.item.id);
1238
- if (wi && wi._totalCostUsd > evalMaxCost && wi.status !== 'needs-human-review') {
1239
- mutateJsonFileLocked(wiPath, (items) => {
1240
- if (!Array.isArray(items)) return items;
1241
- const target = items.find(i => i.id === meta.item.id);
1242
- if (target) {
1243
- target.status = 'needs-human-review';
1244
- target.failReason = `Cumulative cost $${wi._totalCostUsd.toFixed(2)} exceeds evalMaxCost ceiling $${evalMaxCost.toFixed(2)}`;
1245
- log('warn', `Work item ${meta.item.id} exceeded cost ceiling ($${wi._totalCostUsd.toFixed(2)} > $${evalMaxCost.toFixed(2)}) — needs-human-review`);
1246
- }
1247
- return items;
1248
- }, { defaultValue: [] });
1249
- }
1250
- }
1251
- }
1252
- } catch (err) { log('warn', `Cost accumulation: ${err.message}`); }
1253
- }
1254
-
1255
1058
  // Handle decomposition results — create sub-items from decompose agent output
1256
1059
  let skipDoneStatus = false;
1257
1060
  if (type === 'decompose' && isSuccess && meta?.item?.id) {
@@ -1260,126 +1063,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1260
1063
  // If decomposition produced nothing, fall through to mark parent as done
1261
1064
  }
1262
1065
 
1263
- // Record context utilization metrics on the work item
1264
- if (meta?.item?.id) {
1265
- try {
1266
- const engineConfig = (config.engine || {});
1267
- const turnLimit = engineConfig.maxTurns || 100;
1268
- const turnCount = taskUsage?.numTurns || 0;
1269
- const hitTurnLimit = turnCount >= turnLimit;
1270
- let outputLogSizeBytes = 0;
1271
- try {
1272
- const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
1273
- const stat = fs.statSync(liveLogPath);
1274
- outputLogSizeBytes = stat.size;
1275
- } catch { /* file may not exist */ }
1276
- recordContextPressureOnWorkItem(meta, turnCount, outputLogSizeBytes, hitTurnLimit);
1277
- } catch (err) { log('warn', `Context pressure record: ${err.message}`); }
1278
- }
1279
-
1280
1066
  if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
1281
-
1282
- // Auto-dispatch review work item after implement or fix completes successfully
1283
- if (isSuccess && !skipDoneStatus && (type === 'implement' || (type === 'fix' && meta?.item?._evalParentId)) && meta?.item?.id) {
1284
- const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
1285
- if (evalLoop) {
1286
- try {
1287
- const wiPath = resolveWiPath(meta);
1288
- if (wiPath) {
1289
- mutateJsonFileLocked(wiPath, (items) => {
1290
- // For fix items, target the original implement parent; for implement, target self
1291
- const evalTargetId = type === 'fix' ? meta.item._evalParentId : meta.item.id;
1292
- // Dedup: skip if a review item already exists for this parent
1293
- const existing = items.find(i => i._evalParentId === evalTargetId && i.type === 'review' && i.status === 'pending');
1294
- if (existing) {
1295
- log('info', `Eval loop: review item ${existing.id} already exists for ${evalTargetId}, skipping`);
1296
- return items;
1297
- }
1298
- const parentItem = items.find(i => i.id === evalTargetId);
1299
- const evalItem = {
1300
- id: 'W-' + shared.uid(),
1301
- title: `Review: ${parentItem?.title || meta.item.title || evalTargetId}`,
1302
- type: 'review',
1303
- priority: meta.item.priority || 'high',
1304
- status: 'pending',
1305
- created: ts(),
1306
- createdBy: 'engine:eval-loop',
1307
- project: meta.project?.name || meta.item.project,
1308
- branch_name: parentItem?.branch_name || meta.item.branch_name || meta.branch || null,
1309
- pr_url: parentItem?.pr_url || meta.item.pr_url || null,
1310
- acceptance_criteria: parentItem?.acceptance_criteria || meta.item.acceptance_criteria || null,
1311
- _evalParentId: evalTargetId,
1312
- };
1313
- if (parentItem?.sourcePlan) evalItem.sourcePlan = parentItem.sourcePlan;
1314
- // Mark parent as eval-dispatched before writing to prevent duplicates on re-run
1315
- if (parentItem) parentItem._evalDispatched = true;
1316
- items.push(evalItem);
1317
- log('info', `Eval loop: created ${evalItem.id} for completed ${type} ${meta.item.id} (parent: ${evalTargetId})`);
1318
- return items;
1319
- }, { defaultValue: [] });
1320
- }
1321
- } catch (err) {
1322
- log('warn', `Eval loop dispatch error: ${err.message}`);
1323
- }
1324
- }
1325
- }
1326
-
1327
- // Evaluate completion: parse verdict and handle eval→fix iteration loop
1328
- if (isSuccess && type === 'review' && meta?.item?._evalParentId) {
1329
- try {
1330
- const verdict = parseEvalVerdict(resultSummary || stdout);
1331
- const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
1332
- const maxIter = config.engine?.evalMaxIterations ?? shared.ENGINE_DEFAULTS.evalMaxIterations;
1333
-
1334
- if (verdict && !verdict.pass && evalLoop) {
1335
- const wiPath = resolveWiPath(meta);
1336
- if (wiPath) {
1337
- mutateJsonFileLocked(wiPath, (items) => {
1338
- const parent = items.find(i => i.id === meta.item._evalParentId);
1339
- if (!parent) return items;
1340
- const iterations = (parent._evalIterations || 0) + 1;
1341
- parent._evalIterations = iterations;
1342
-
1343
- if (iterations >= maxIter) {
1344
- // Max iterations reached — escalate to human review
1345
- parent.status = 'needs-human-review';
1346
- parent._evalEscalatedAt = ts();
1347
- log('info', `Eval loop: ${parent.id} reached ${iterations}/${maxIter} iterations — escalated to needs-human-review`);
1348
- } else {
1349
- // Create fix work item with evaluator feedback
1350
- const fixItem = {
1351
- id: 'W-' + shared.uid(),
1352
- title: `Fix: ${parent.title || parent.id} (eval iteration ${iterations})`,
1353
- type: 'fix',
1354
- priority: parent.priority || 'high',
1355
- status: 'pending',
1356
- created: ts(),
1357
- createdBy: 'engine:eval-loop',
1358
- project: meta.project?.name || meta.item.project,
1359
- branch_name: parent.branch_name || meta.item.branch_name || null,
1360
- pr_url: parent.pr_url || meta.item.pr_url || null,
1361
- acceptance_criteria: parent.acceptance_criteria || null,
1362
- _evalParentId: parent.id,
1363
- _evalFeedback: verdict.feedback || null,
1364
- _evalCriteriaFailed: verdict.criteria_failed || null,
1365
- };
1366
- if (parent.sourcePlan) fixItem.sourcePlan = parent.sourcePlan;
1367
- // Clear eval-dispatched flag so next fix→eval cycle can dispatch
1368
- parent._evalDispatched = false;
1369
- // Parent stays 'done' — fix item picks up from here
1370
- parent.status = 'done';
1371
- items.push(fixItem);
1372
- log('info', `Eval loop: created fix ${fixItem.id} for failed eval on ${parent.id} (iteration ${iterations}/${maxIter})`);
1373
- }
1374
- return items;
1375
- }, { defaultValue: [] });
1376
- }
1377
- }
1378
- } catch (err) {
1379
- log('warn', `Eval verdict processing error: ${err.message}`);
1380
- }
1381
- }
1382
-
1383
1067
  if (!isSuccess && meta?.item?.id) {
1384
1068
  // Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
1385
1069
  let retries = (meta.item._retryCount || 0);
@@ -1402,14 +1086,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1402
1086
  ? path.join(MINIONS_DIR, 'work-items.json')
1403
1087
  : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1404
1088
  if (wiPath) {
1405
- mutateJsonFileLocked(wiPath, (items) => {
1406
- const wi = items.find(i => i.id === meta.item.id);
1407
- if (wi) {
1408
- wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
1409
- if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
1410
- }
1411
- return items;
1412
- }, { defaultValue: [] });
1089
+ const items = safeJson(wiPath) || [];
1090
+ const wi = items.find(i => i.id === meta.item.id);
1091
+ if (wi) {
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
1094
+ shared.safeWrite(wiPath, items);
1095
+ }
1413
1096
  }
1414
1097
  } catch (err) { log('warn', `Retry update: ${err.message}`); }
1415
1098
  } else {
@@ -1422,11 +1105,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1422
1105
  ? path.join(MINIONS_DIR, 'work-items.json')
1423
1106
  : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1424
1107
  if (wiPath) {
1425
- mutateJsonFileLocked(wiPath, (items) => {
1426
- const wi = items.find(i => i.id === meta.item.id);
1427
- if (wi) { delete wi._decomposing; }
1428
- return items;
1429
- }, { defaultValue: [] });
1108
+ const items = safeJson(wiPath) || [];
1109
+ const wi = items.find(i => i.id === meta.item.id);
1110
+ if (wi) { delete wi._decomposing; shared.safeWrite(wiPath, items); }
1430
1111
  }
1431
1112
  } catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
1432
1113
  }
@@ -1445,19 +1126,6 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1445
1126
  let prsCreatedCount = 0;
1446
1127
  if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
1447
1128
 
1448
- // Archive plan after verify task completes (AFTER PR sync so E2E PR is linked)
1449
- if (meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
1450
- try {
1451
- const vPlanFile = meta.item.sourcePlan;
1452
- const vPlanPath = path.join(PRD_DIR, vPlanFile);
1453
- const vPlan = safeJson(vPlanPath);
1454
- if (vPlan) {
1455
- const vProjects = shared.getProjects(config);
1456
- archivePlan(vPlanFile, vPlan, vProjects, config);
1457
- }
1458
- } catch (err) { log('warn', `Verify archive: ${err.message}`); }
1459
- }
1460
-
1461
1129
  // Clean up worktree for non-shared-branch tasks after completion
1462
1130
  if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
1463
1131
  try {
@@ -1472,7 +1140,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1472
1140
  return d.includes(branchSlug) && fs.statSync(path.join(worktreeRoot, d)).isDirectory();
1473
1141
  });
1474
1142
  // Only remove if no other active dispatch uses this branch
1475
- const dispatch = getDispatch();
1143
+ const dispatch = e.getDispatch();
1476
1144
  const otherActive = ((dispatch.active || []).concat(dispatch.pending || [])).some(d =>
1477
1145
  d.id !== dispatchItem.id && d.meta?.branch && shared.sanitizeBranch && shared.sanitizeBranch(d.meta.branch) === branchSlug
1478
1146
  );
@@ -1493,9 +1161,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1493
1161
  }
1494
1162
  }
1495
1163
 
1496
- // Detect implement tasks that completed without creating a PR.
1497
- // Exempt: tasks operating on an existing PR (meta.pr set) or human feedback fix tasks.
1498
- if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id && !meta.pr && meta.source !== 'pr-human-feedback') {
1164
+ // Detect implement tasks that completed without creating a PR
1165
+ if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id) {
1499
1166
  // Check if a PR already exists linked to this work item (from a previous attempt)
1500
1167
  const projects = shared.getProjects(config);
1501
1168
  const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
@@ -1520,10 +1187,10 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1520
1187
  wi._retryCount = retries + 1;
1521
1188
  delete wi.dispatched_at;
1522
1189
  delete wi.dispatched_to;
1523
- log('info', `Auto-retry ${retries + 1}/3 for ${meta.item.id} (no PR created)`);
1190
+ e.log('info', `Auto-retry ${retries + 1}/3 for ${meta.item.id} (no PR created)`);
1524
1191
  } else {
1525
1192
  wi.status = 'failed';
1526
- log('warn', `${meta.item.id} failed after 3 retries — no PR created`);
1193
+ e.log('warn', `${meta.item.id} failed after 3 retries — no PR created`);
1527
1194
  }
1528
1195
  shared.safeWrite(wiPath, items);
1529
1196
  }
@@ -1585,7 +1252,6 @@ function syncPrdFromPrs(config) {
1585
1252
 
1586
1253
  module.exports = {
1587
1254
  checkPlanCompletion,
1588
- archivePlan,
1589
1255
  updateWorkItemStatus,
1590
1256
  syncPrdItemStatus,
1591
1257
  syncPrsFromOutput,
@@ -1597,9 +1263,7 @@ module.exports = {
1597
1263
  updateAgentHistory,
1598
1264
  createReviewFeedbackForAuthor,
1599
1265
  updateMetrics,
1600
- recordContextPressureOnWorkItem,
1601
1266
  parseAgentOutput,
1602
- parseEvalVerdict,
1603
1267
  runPostCompletionHooks,
1604
1268
  syncPrdFromPrs,
1605
1269
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.297",
3
+ "version": "0.1.299",
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"