@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 +9 -0
- package/dashboard/js/render-work-items.js +1 -1
- package/dashboard.js +10 -1
- package/engine/lifecycle.js +95 -431
- package/package.json +1 -1
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
|
|
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)
|
package/engine/lifecycle.js
CHANGED
|
@@ -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,
|
|
11
|
-
|
|
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
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
334
|
-
for (const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
548
|
-
|
|
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')
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
805
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|