@yemi33/minions 0.1.227 → 0.1.229

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,8 +1,22 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.227 (2026-04-03)
3
+ ## 0.1.229 (2026-04-03)
4
+
5
+ ### Fixes
6
+ - consolidate temp agent metrics into one row
7
+
8
+ ## 0.1.228 (2026-04-03)
9
+
10
+ ### Features
11
+ - scan for projects UI — modal with checkbox multi-select
12
+
13
+ ### Fixes
14
+ - CC system prompt now defaults to delegating work to agents
15
+
16
+ ## 0.1.226 (2026-04-03)
4
17
 
5
18
  ### Features
19
+ - add evaluate type to routing and playbooks (#70)
6
20
  - fix cooldown context accumulation bloat (#71)
7
21
  - wire dead test and extract progress bar helper (#64)
8
22
 
@@ -16,7 +16,8 @@ function renderProjects(projects) {
16
16
  (p.description ? '<span style="color:var(--muted);font-weight:400;margin-left:6px;font-size:10px">' + escHtml(p.description.slice(0, 60)) + (p.description.length > 60 ? '...' : '') + '</span>' : '') +
17
17
  '</span>'
18
18
  ).join('') +
19
- '<span onclick="addProject()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--muted);font-weight:500;cursor:pointer;border-style:dashed">+ Add</span>';
19
+ '<span onclick="addProject()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--muted);font-weight:500;cursor:pointer;border-style:dashed">+ Add</span>' +
20
+ '<span onclick="openScanProjectsModal()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:pointer;border-style:dashed;font-size:10px">Scan</span>';
20
21
 
21
22
  }
22
23
 
@@ -47,8 +48,26 @@ function renderMetrics(metrics) {
47
48
  renderContextPressure(metrics);
48
49
  return;
49
50
  }
51
+ // Consolidate temp-* agents into one row
52
+ var permanent = agents.filter(function(a) { return !a[0].startsWith('temp-'); });
53
+ var temps = agents.filter(function(a) { return a[0].startsWith('temp-'); });
54
+ var rows = permanent.slice();
55
+ if (temps.length > 0) {
56
+ var merged = { tasksCompleted: 0, tasksErrored: 0, prsCreated: 0, prsApproved: 0, prsRejected: 0, reviewsDone: 0 };
57
+ for (var t = 0; t < temps.length; t++) {
58
+ var tm = temps[t][1];
59
+ merged.tasksCompleted += tm.tasksCompleted || 0;
60
+ merged.tasksErrored += tm.tasksErrored || 0;
61
+ merged.prsCreated += tm.prsCreated || 0;
62
+ merged.prsApproved += tm.prsApproved || 0;
63
+ merged.prsRejected += tm.prsRejected || 0;
64
+ merged.reviewsDone += tm.reviewsDone || 0;
65
+ }
66
+ rows.push(['Temp Agents (' + temps.length + ')', merged]);
67
+ }
68
+
50
69
  let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
51
- for (const [id, m] of agents) {
70
+ for (const [id, m] of rows) {
52
71
  const rate = m.prsCreated > 0 ? Math.round((m.prsApproved / m.prsCreated) * 100) + '%' : '-';
53
72
  const rateColor = m.prsCreated > 0 ? (m.prsApproved / m.prsCreated >= 0.7 ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
54
73
  html += '<tr>' +
@@ -140,8 +159,25 @@ function renderTokenUsage(metrics) {
140
159
  html += '</div>';
141
160
  }
142
161
 
143
- // Per-agent token table
144
- const agentsWithUsage = agents.filter(([, m]) => (m.totalCostUsd || 0) > 0);
162
+ // Per-agent token table (consolidate temp agents)
163
+ var permAgents = agents.filter(function(a) { return !a[0].startsWith('temp-'); });
164
+ var tempAgents = agents.filter(function(a) { return a[0].startsWith('temp-'); });
165
+ var tokenRows = permAgents.slice();
166
+ if (tempAgents.length > 0) {
167
+ var tempMerged = { totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0, tasksCompleted: 0, tasksErrored: 0, model: '' };
168
+ for (var ti = 0; ti < tempAgents.length; ti++) {
169
+ var tm = tempAgents[ti][1];
170
+ tempMerged.totalCostUsd += tm.totalCostUsd || 0;
171
+ tempMerged.totalInputTokens += tm.totalInputTokens || 0;
172
+ tempMerged.totalOutputTokens += tm.totalOutputTokens || 0;
173
+ tempMerged.totalCacheRead += tm.totalCacheRead || 0;
174
+ tempMerged.tasksCompleted += tm.tasksCompleted || 0;
175
+ tempMerged.tasksErrored += tm.tasksErrored || 0;
176
+ if (!tempMerged.model && tm.model) tempMerged.model = tm.model;
177
+ }
178
+ tokenRows.push(['Temp Agents (' + tempAgents.length + ')', tempMerged]);
179
+ }
180
+ var agentsWithUsage = tokenRows.filter(function(a) { return (a[1].totalCostUsd || 0) > 0; });
145
181
  if (agentsWithUsage.length > 0) {
146
182
  html += '<div style="font-size:10px;color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Agent Usage</div>';
147
183
  html += '<table class="token-agent-table"><thead><tr><th>Agent</th><th>Model</th><th>Cost</th><th>Input</th><th>Output</th><th>Cache</th><th>$/task</th></tr></thead><tbody>';
@@ -206,4 +242,99 @@ function renderContextPressure(metrics) {
206
242
  el.innerHTML = html;
207
243
  }
208
244
 
209
- window.MinionsOther = { renderProjects, renderMcpServers, renderMetrics, renderTokenUsage, renderContextPressure };
245
+ async function openScanProjectsModal() {
246
+ document.getElementById('modal-title').textContent = 'Scan for Projects';
247
+ document.getElementById('modal-body').innerHTML =
248
+ '<div style="display:flex;flex-direction:column;gap:12px">' +
249
+ '<div style="display:flex;gap:8px;align-items:flex-end">' +
250
+ '<label style="flex:1;color:var(--text);font-size:var(--text-md)">Directory to scan' +
251
+ '<input id="scan-path" value="' + escHtml((typeof os !== 'undefined' ? os.homedir() : '~') || '~') + '" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)">' +
252
+ '</label>' +
253
+ '<label style="width:60px;color:var(--text);font-size:var(--text-md)">Depth' +
254
+ '<input id="scan-depth" type="number" value="3" min="1" max="6" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)">' +
255
+ '</label>' +
256
+ '<button onclick="_runProjectScan()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap">Scan</button>' +
257
+ '</div>' +
258
+ '<div id="scan-results" style="color:var(--muted);font-size:12px">Click Scan to find git repos in the directory.</div>' +
259
+ '</div>';
260
+ document.getElementById('modal-body').style.whiteSpace = 'normal';
261
+ document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
262
+ document.getElementById('modal').classList.add('open');
263
+ }
264
+
265
+ async function _runProjectScan() {
266
+ var scanPath = document.getElementById('scan-path')?.value?.trim();
267
+ var depth = document.getElementById('scan-depth')?.value || '3';
268
+ var resultsEl = document.getElementById('scan-results');
269
+ if (!scanPath) { alert('Enter a path'); return; }
270
+ resultsEl.innerHTML = '<span style="color:var(--blue)">Scanning...</span>';
271
+
272
+ try {
273
+ var res = await fetch('/api/projects/scan', {
274
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
275
+ body: JSON.stringify({ path: scanPath, depth: Number(depth) })
276
+ });
277
+ var data = await res.json();
278
+ if (!res.ok) { resultsEl.innerHTML = '<span style="color:var(--red)">Error: ' + escHtml(data.error) + '</span>'; return; }
279
+ var repos = data.repos || [];
280
+ if (repos.length === 0) { resultsEl.innerHTML = '<span style="color:var(--muted)">No git repos found in ' + escHtml(scanPath) + '</span>'; return; }
281
+
282
+ var html = '<div style="margin-bottom:8px;font-size:11px;color:var(--muted)">' + repos.length + ' repos found — select to add:</div>';
283
+ html += '<div style="max-height:400px;overflow-y:auto;display:flex;flex-direction:column;gap:4px">';
284
+ repos.forEach(function(r, i) {
285
+ var linked = r.linked;
286
+ var hostBadge = '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:' +
287
+ (r.host === 'GitHub' ? 'rgba(88,166,255,0.15);color:var(--blue)' : r.host === 'ADO' ? 'rgba(188,140,255,0.15);color:var(--purple)' : 'var(--surface2);color:var(--muted)') +
288
+ '">' + escHtml(r.host) + '</span>';
289
+ html += '<label style="display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;cursor:' + (linked ? 'default' : 'pointer') + ';opacity:' + (linked ? '0.5' : '1') + '">' +
290
+ '<input type="checkbox" data-scan-idx="' + i + '" ' + (linked ? 'disabled checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px">' +
291
+ '<div style="flex:1;min-width:0">' +
292
+ '<div style="font-weight:600;font-size:12px">' + escHtml(r.name) + ' ' + hostBadge + (linked ? ' <span style="font-size:9px;color:var(--green)">linked</span>' : '') + '</div>' +
293
+ '<div style="font-size:10px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(r.path) + '</div>' +
294
+ (r.description ? '<div style="font-size:10px;color:var(--muted)">' + escHtml(r.description) + '</div>' : '') +
295
+ '</div>' +
296
+ '</label>';
297
+ });
298
+ html += '</div>';
299
+ html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px">' +
300
+ '<div style="display:flex;gap:8px">' +
301
+ '<span onclick="_scanSelectAll()" style="font-size:10px;color:var(--blue);cursor:pointer;text-decoration:underline">Select all</span>' +
302
+ '<span onclick="_scanSelectNone()" style="font-size:10px;color:var(--muted);cursor:pointer;text-decoration:underline">Clear</span>' +
303
+ '</div>' +
304
+ '<button onclick="_addSelectedProjects()" style="padding:6px 16px;background:var(--green);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Add Selected</button>' +
305
+ '</div>';
306
+ resultsEl.innerHTML = html;
307
+ window._scanRepos = repos;
308
+ } catch (e) { resultsEl.innerHTML = '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>'; }
309
+ }
310
+
311
+ function _scanSelectAll() {
312
+ document.querySelectorAll('[data-scan-idx]').forEach(function(cb) { if (!cb.disabled) cb.checked = true; });
313
+ }
314
+ function _scanSelectNone() {
315
+ document.querySelectorAll('[data-scan-idx]').forEach(function(cb) { if (!cb.disabled) cb.checked = false; });
316
+ }
317
+
318
+ async function _addSelectedProjects() {
319
+ var checkboxes = document.querySelectorAll('[data-scan-idx]:checked:not(:disabled)');
320
+ if (checkboxes.length === 0) { alert('Select at least one repo'); return; }
321
+ var repos = window._scanRepos || [];
322
+ var added = 0;
323
+ for (var cb of checkboxes) {
324
+ var repo = repos[parseInt(cb.dataset.scanIdx)];
325
+ if (!repo) continue;
326
+ try {
327
+ var res = await fetch('/api/projects/add', {
328
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
329
+ body: JSON.stringify({ path: repo.path })
330
+ });
331
+ if (res.ok) { added++; cb.disabled = true; cb.closest('label').style.opacity = '0.5'; }
332
+ } catch { /* continue with next */ }
333
+ }
334
+ if (added > 0) {
335
+ showToast('cmd-toast', added + ' project(s) added', true);
336
+ refresh();
337
+ }
338
+ }
339
+
340
+ window.MinionsOther = { renderProjects, renderMcpServers, renderMetrics, renderTokenUsage, renderContextPressure, openScanProjectsModal };
@@ -52,8 +52,8 @@
52
52
  <div style="font-size:18px;margin-bottom:6px;text-align:center">Getting Started</div>
53
53
  <div style="color:var(--muted);font-size:13px;margin-bottom:12px;text-align:center">No projects linked yet. Link a project so agents know what code to work on.</div>
54
54
  <div style="display:flex;flex-direction:column;gap:8px;align-items:center">
55
- <code style="background:var(--bg);padding:6px 16px;border-radius:4px;font-size:13px;color:var(--blue);border:1px solid var(--border)">minions add &lt;path-to-your-repo&gt;</code>
56
- <div style="font-size:11px;color:var(--muted)">Then try: <code style="background:var(--bg);padding:2px 6px;border-radius:3px;font-size:11px">minions work "Explore the codebase"</code> or use the Command Center above</div>
55
+ <button onclick="openScanProjectsModal()" style="padding:8px 20px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:13px;font-weight:600">Scan for Projects</button>
56
+ <div style="font-size:11px;color:var(--muted)">Or via CLI: <code style="background:var(--bg);padding:2px 6px;border-radius:3px;font-size:11px">minions add &lt;path-to-your-repo&gt;</code></div>
57
57
  </div>
58
58
  </div>
59
59
 
package/dashboard.js CHANGED
@@ -340,19 +340,26 @@ ${MINIONS_DIR}/
340
340
 
341
341
  Projects are configured in \`config.json\` under \`projects[]\`. Per-project state lives centrally in \`${MINIONS_DIR}/projects/{name}/\` — NOT inside project repos. There are no \`.minions/\` folders inside project repos.
342
342
 
343
- ## Direct Execution
343
+ ## Default: Delegate to Agents
344
344
 
345
- You have Bash, Write, Edit, and all standard tools. Use them directly when the task is straightforward:
346
- - **Build & run projects** — \`cd <project> && npm install && npm run dev\`
347
- - **Inspect code** — read files, grep, explore
348
- - **Edit project files** — fix configs, update docs, tweak settings
349
- - **Git operations** — fetch, checkout, merge, diff (but do NOT push without the user confirming)
350
- - **Start dev servers** — for long-running servers, use detached processes so they survive after you finish
345
+ Your primary job is to **orchestrate**, not implement. For most requests, dispatch work to agents rather than doing it yourself. Agents have full Claude Code sessions with project context, worktrees, and MCP tools they are better equipped for real work.
351
346
 
352
- **When to do it yourself vs delegate to an agent:**
353
- - Quick, one-shot tasks (build, read, check, install, start a server) → do it yourself
354
- - Complex multi-file code changes, PR creation, code review → dispatch to an agent
355
- - Anything that needs deep codebase knowledge or iterative coding → dispatch to an agent
347
+ **Delegate to agents (default):**
348
+ - Code changes of any size even "small" fixes
349
+ - Bug fixes, feature implementation, refactoring
350
+ - PR creation and code review
351
+ - Testing and verification
352
+ - Codebase exploration and architecture analysis
353
+ - Plan creation and execution
354
+
355
+ **Do it yourself (only these):**
356
+ - Reading a specific file or status to answer a question
357
+ - Quick lookups (check a config value, find a file, read an error log)
358
+ - Creating notes, editing plans, updating routing/charters
359
+ - Starting a dev server or running a build command the user asked for
360
+ - Git operations the user explicitly asked YOU to do
361
+
362
+ When in doubt, **dispatch an agent**. The user is talking to you because they want work delegated, not because they want you to be the one coding.
356
363
 
357
364
  ## Minions Actions (Delegation)
358
365
 
@@ -2904,6 +2911,52 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2904
2911
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
2905
2912
  }
2906
2913
 
2914
+ async function handleProjectsScan(req, res) {
2915
+ try {
2916
+ const body = await readBody(req);
2917
+ const scanRoot = path.resolve(body.path || os.homedir());
2918
+ const maxDepth = Math.min(Number(body.depth) || 3, 6);
2919
+ if (!fs.existsSync(scanRoot)) return jsonReply(res, 400, { error: 'path does not exist' });
2920
+
2921
+ // Find git repos recursively (same logic as minions.js findGitRepos)
2922
+ const skipDirs = new Set(['node_modules', '.git', '.hg', 'AppData', '$Recycle.Bin', 'Windows',
2923
+ 'Program Files', 'Program Files (x86)', '.cache', '.npm', '.yarn', '.nuget', 'worktrees', '.minions', '.squad']);
2924
+ const repos = [];
2925
+ function walk(dir, depth) {
2926
+ if (depth > maxDepth) return;
2927
+ let entries;
2928
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
2929
+ for (const e of entries) {
2930
+ if (!e.isDirectory() || (e.name.startsWith('.') && e.name !== '.git') || skipDirs.has(e.name)) continue;
2931
+ if (e.name === '.git') { repos.push(dir); return; }
2932
+ walk(path.join(dir, e.name), depth + 1);
2933
+ }
2934
+ }
2935
+ walk(scanRoot, 0);
2936
+
2937
+ // Enrich each repo with metadata
2938
+ const existingPaths = new Set(PROJECTS.map(p => path.resolve(p.localPath)));
2939
+ const results = repos.map(repoPath => {
2940
+ const result = { path: repoPath.replace(/\\/g, '/'), name: path.basename(repoPath), host: 'git', linked: existingPaths.has(path.resolve(repoPath)) };
2941
+ try {
2942
+ const remoteUrl = require('child_process').execSync('git remote get-url origin', { cwd: repoPath, encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
2943
+ const gh = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2944
+ const ado = remoteUrl.match(/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/) || remoteUrl.match(/([^.]+)\.visualstudio\.com.*?\/([^/]+)\/_git\/([^/\s]+)/);
2945
+ if (gh) { result.host = 'GitHub'; result.org = gh[1]; result.name = gh[2]; }
2946
+ else if (ado) { result.host = 'ADO'; result.org = ado[1]; result.name = ado[3] || ado[2]; }
2947
+ } catch { /* no remote */ }
2948
+ try {
2949
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoPath, 'package.json'), 'utf8'));
2950
+ if (pkg.name) result.name = pkg.name.replace(/@[^/]+\//, '');
2951
+ if (pkg.description) result.description = pkg.description.slice(0, 100);
2952
+ } catch { /* no package.json */ }
2953
+ return result;
2954
+ });
2955
+
2956
+ return jsonReply(res, 200, { repos: results });
2957
+ } catch (e) { return jsonReply(res, 500, { error: e.message }); }
2958
+ }
2959
+
2907
2960
  async function handleCommandCenterNewSession(req, res) {
2908
2961
  ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
2909
2962
  ccInFlight = false; // Reset concurrency guard so a stuck request doesn't block new sessions
@@ -3444,6 +3497,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3444
3497
 
3445
3498
  // Projects
3446
3499
  { method: 'POST', path: '/api/projects/browse', desc: 'Open folder picker dialog, return selected path', handler: handleProjectsBrowse },
3500
+ { method: 'POST', path: '/api/projects/scan', desc: 'Scan a directory for git repos', params: 'path?, depth?', handler: handleProjectsScan },
3447
3501
  { method: 'POST', path: '/api/projects/add', desc: 'Auto-discover and add a project to config', params: 'path, name?', handler: handleProjectsAdd },
3448
3502
 
3449
3503
  // Command Center
package/engine/cleanup.js CHANGED
@@ -35,20 +35,23 @@ function runCleanup(config, verbose = false) {
35
35
  const projects = getProjects(config);
36
36
  let cleaned = { tempFiles: 0, liveOutputs: 0, worktrees: 0, zombies: 0 };
37
37
 
38
- // 1. Clean stale temp prompt/sysprompt files (older than 1 hour)
38
+ // 1. Clean stale temp prompt/sysprompt files and orphaned safeWrite .tmp.* files (older than 1 hour)
39
39
  const oneHourAgo = Date.now() - 3600000;
40
40
  try {
41
41
  const tmpDir = path.join(ENGINE_DIR, 'tmp');
42
42
  const scanDirs = [ENGINE_DIR, ...(fs.existsSync(tmpDir) ? [tmpDir] : [])];
43
43
  for (const dir of scanDirs) {
44
44
  for (const f of fs.readdirSync(dir)) {
45
- if (f.startsWith('prompt-') || f.startsWith('sysprompt-') || f.startsWith('tmp-sysprompt-')) {
45
+ const isPromptTemp = f.startsWith('prompt-') || f.startsWith('sysprompt-') || f.startsWith('tmp-sysprompt-');
46
+ const isSafeWriteTemp = /\.tmp\.\d+\.\d+$/.test(f);
47
+ if (isPromptTemp || isSafeWriteTemp) {
46
48
  const fp = path.join(dir, f);
47
49
  try {
48
50
  const stat = fs.statSync(fp);
49
51
  if (stat.mtimeMs < oneHourAgo) {
50
52
  fs.unlinkSync(fp);
51
53
  cleaned.tempFiles++;
54
+ if (isSafeWriteTemp) log('info', `Cleaned orphaned temp file: ${f}`);
52
55
  }
53
56
  } catch { /* cleanup */ }
54
57
  }
@@ -630,7 +630,22 @@ function syncPrsFromOutput(output, agentId, meta, config) {
630
630
  }
631
631
 
632
632
  for (const [name, entry] of dirtyTargets) {
633
- shared.safeWrite(entry.prPath, entry.prs);
633
+ mutateJsonFileLocked(entry.prPath, (existingPrs) => {
634
+ // Merge new PRs into existing data to avoid losing concurrent writes
635
+ for (const pr of entry.prs) {
636
+ const idx = existingPrs.findIndex(p => p.id === pr.id);
637
+ if (idx >= 0) {
638
+ // Backfill prdItems if needed
639
+ if (pr.prdItems?.length) {
640
+ const merged = new Set([...(existingPrs[idx].prdItems || []), ...pr.prdItems]);
641
+ existingPrs[idx].prdItems = [...merged];
642
+ }
643
+ } else {
644
+ existingPrs.push(pr);
645
+ }
646
+ }
647
+ return existingPrs;
648
+ }, { defaultValue: [] });
634
649
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
635
650
  }
636
651
  return added;
@@ -668,11 +683,12 @@ function updatePrAfterReview(agentId, pr, project) {
668
683
  const authorAgentId = (pr.agent || '').toLowerCase();
669
684
  if (authorAgentId && config.agents?.[authorAgentId]) {
670
685
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
671
- const metrics = safeJson(metricsPath) || {};
672
- if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
673
- if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
674
- metrics[agentId].reviewsDone = (metrics[agentId].reviewsDone || 0) + 1;
675
- shared.safeWrite(metricsPath, metrics);
686
+ mutateJsonFileLocked(metricsPath, (metrics) => {
687
+ if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
688
+ if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
689
+ metrics[agentId].reviewsDone = (metrics[agentId].reviewsDone || 0) + 1;
690
+ return metrics;
691
+ });
676
692
  }
677
693
 
678
694
  shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
@@ -747,12 +763,16 @@ async function handlePostMerge(pr, project, config, newStatus) {
747
763
  const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
748
764
  let updated = 0;
749
765
  for (const pf of planFiles) {
750
- const plan = safeJson(path.join(prdDir, pf));
766
+ const prdPath = path.join(prdDir, pf);
767
+ const plan = safeJson(prdPath);
751
768
  if (!plan?.missing_features) continue;
752
769
  const feature = plan.missing_features.find(f => f.id === mergedItemId);
753
770
  if (feature && feature.status !== 'implemented') {
754
- feature.status = 'implemented';
755
- shared.safeWrite(path.join(prdDir, pf), plan);
771
+ mutateJsonFileLocked(prdPath, (data) => {
772
+ const feat = data.missing_features?.find(f => f.id === mergedItemId);
773
+ if (feat && feat.status !== 'implemented') feat.status = 'implemented';
774
+ return data;
775
+ });
756
776
  updated++;
757
777
  }
758
778
  }
@@ -764,17 +784,19 @@ async function handlePostMerge(pr, project, config, newStatus) {
764
784
  for (const p of shared.getProjects(config)) wiPaths.push(shared.projectWorkItemsPath(p));
765
785
  for (const wiPath of wiPaths) {
766
786
  try {
767
- const items = safeJson(wiPath);
768
- if (!items) continue;
769
- const item = items.find(i => i.id === mergedItemId);
770
- if (item && item.status !== 'done') {
771
- log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
772
- item.status = 'done';
773
- item.completedAt = ts();
774
- item._mergedVia = pr.id;
775
- shared.safeWrite(wiPath, items);
776
- break;
777
- }
787
+ let found = false;
788
+ mutateJsonFileLocked(wiPath, (items) => {
789
+ const item = items.find(i => i.id === mergedItemId);
790
+ if (item && item.status !== 'done') {
791
+ log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
792
+ item.status = 'done';
793
+ item.completedAt = ts();
794
+ item._mergedVia = pr.id;
795
+ found = true;
796
+ }
797
+ return items;
798
+ }, { defaultValue: [] });
799
+ if (found) break;
778
800
  } catch (err) { log('warn', `Post-merge work item update: ${err.message}`); }
779
801
  }
780
802
  }
@@ -782,10 +804,11 @@ async function handlePostMerge(pr, project, config, newStatus) {
782
804
  const agentId = (pr.agent || '').toLowerCase();
783
805
  if (agentId && config.agents?.[agentId]) {
784
806
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
785
- const metrics = safeJson(metricsPath) || {};
786
- if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, prsMerged:0, reviewsDone:0, lastTask:null, lastCompleted:null };
787
- metrics[agentId].prsMerged = (metrics[agentId].prsMerged || 0) + 1;
788
- shared.safeWrite(metricsPath, metrics);
807
+ mutateJsonFileLocked(metricsPath, (metrics) => {
808
+ if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, prsMerged:0, reviewsDone:0, lastTask:null, lastCompleted:null };
809
+ metrics[agentId].prsMerged = (metrics[agentId].prsMerged || 0) + 1;
810
+ return metrics;
811
+ });
789
812
  }
790
813
 
791
814
  const teamsUrl = process.env.TEAMS_PLAN_FLOW_URL;
@@ -1229,14 +1252,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1229
1252
  try {
1230
1253
  const wiPath = resolveWiPath(meta);
1231
1254
  if (wiPath) {
1232
- const items = safeJson(wiPath) || [];
1233
- // For fix items, target the original implement parent; for implement, target self
1234
- const evalTargetId = type === 'fix' ? meta.item._evalParentId : meta.item.id;
1235
- // Dedup: skip if an evaluate item already exists for this parent
1236
- const existing = items.find(i => i._evalParentId === evalTargetId && i.type === 'evaluate' && i.status === 'pending');
1237
- if (existing) {
1238
- log('info', `Eval loop: evaluate item ${existing.id} already exists for ${evalTargetId}, skipping`);
1239
- } else {
1255
+ mutateJsonFileLocked(wiPath, (items) => {
1256
+ // For fix items, target the original implement parent; for implement, target self
1257
+ const evalTargetId = type === 'fix' ? meta.item._evalParentId : meta.item.id;
1258
+ // Dedup: skip if an evaluate item already exists for this parent
1259
+ const existing = items.find(i => i._evalParentId === evalTargetId && i.type === 'evaluate' && i.status === 'pending');
1260
+ if (existing) {
1261
+ log('info', `Eval loop: evaluate item ${existing.id} already exists for ${evalTargetId}, skipping`);
1262
+ return items;
1263
+ }
1240
1264
  const parentItem = items.find(i => i.id === evalTargetId);
1241
1265
  const evalItem = {
1242
1266
  id: 'W-' + shared.uid(),
@@ -1256,9 +1280,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1256
1280
  // Mark parent as eval-dispatched before writing to prevent duplicates on re-run
1257
1281
  if (parentItem) parentItem._evalDispatched = true;
1258
1282
  items.push(evalItem);
1259
- shared.safeWrite(wiPath, items);
1260
1283
  log('info', `Eval loop: created ${evalItem.id} for completed ${type} ${meta.item.id} (parent: ${evalTargetId})`);
1261
- }
1284
+ return items;
1285
+ }, { defaultValue: [] });
1262
1286
  }
1263
1287
  } catch (err) {
1264
1288
  log('warn', `Eval loop dispatch error: ${err.message}`);
@@ -1276,9 +1300,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1276
1300
  if (verdict && !verdict.pass && evalLoop) {
1277
1301
  const wiPath = resolveWiPath(meta);
1278
1302
  if (wiPath) {
1279
- const items = safeJson(wiPath) || [];
1280
- const parent = items.find(i => i.id === meta.item._evalParentId);
1281
- if (parent) {
1303
+ mutateJsonFileLocked(wiPath, (items) => {
1304
+ const parent = items.find(i => i.id === meta.item._evalParentId);
1305
+ if (!parent) return items;
1282
1306
  const iterations = (parent._evalIterations || 0) + 1;
1283
1307
  parent._evalIterations = iterations;
1284
1308
 
@@ -1286,7 +1310,6 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1286
1310
  // Max iterations reached — escalate to human review
1287
1311
  parent.status = 'needs-human-review';
1288
1312
  parent._evalEscalatedAt = ts();
1289
- shared.safeWrite(wiPath, items);
1290
1313
  log('info', `Eval loop: ${parent.id} reached ${iterations}/${maxIter} iterations — escalated to needs-human-review`);
1291
1314
  } else {
1292
1315
  // Create fix work item with evaluator feedback
@@ -1312,10 +1335,10 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1312
1335
  // Parent stays 'done' — fix item picks up from here
1313
1336
  parent.status = 'done';
1314
1337
  items.push(fixItem);
1315
- shared.safeWrite(wiPath, items);
1316
1338
  log('info', `Eval loop: created fix ${fixItem.id} for failed eval on ${parent.id} (iteration ${iterations}/${maxIter})`);
1317
1339
  }
1318
- }
1340
+ return items;
1341
+ }, { defaultValue: [] });
1319
1342
  }
1320
1343
  }
1321
1344
  } catch (err) {
@@ -1345,13 +1368,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1345
1368
  ? path.join(MINIONS_DIR, 'work-items.json')
1346
1369
  : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1347
1370
  if (wiPath) {
1348
- const items = safeJson(wiPath) || [];
1349
- const wi = items.find(i => i.id === meta.item.id);
1350
- if (wi) {
1351
- wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
1352
- if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
1353
- shared.safeWrite(wiPath, items);
1354
- }
1371
+ mutateJsonFileLocked(wiPath, (items) => {
1372
+ const wi = items.find(i => i.id === meta.item.id);
1373
+ if (wi) {
1374
+ wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
1375
+ if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
1376
+ }
1377
+ return items;
1378
+ }, { defaultValue: [] });
1355
1379
  }
1356
1380
  } catch (err) { log('warn', `Retry update: ${err.message}`); }
1357
1381
  } else {
@@ -1364,9 +1388,11 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1364
1388
  ? path.join(MINIONS_DIR, 'work-items.json')
1365
1389
  : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1366
1390
  if (wiPath) {
1367
- const items = safeJson(wiPath) || [];
1368
- const wi = items.find(i => i.id === meta.item.id);
1369
- if (wi) { delete wi._decomposing; shared.safeWrite(wiPath, items); }
1391
+ mutateJsonFileLocked(wiPath, (items) => {
1392
+ const wi = items.find(i => i.id === meta.item.id);
1393
+ if (wi) { delete wi._decomposing; }
1394
+ return items;
1395
+ }, { defaultValue: [] });
1370
1396
  }
1371
1397
  } catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
1372
1398
  }
@@ -481,7 +481,7 @@ function selectPlaybook(workType, item) {
481
481
  if (workType === 'review' && !item?._pr && !item?.pr_id) {
482
482
  return 'work-item';
483
483
  }
484
- const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
484
+ const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'evaluate', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
485
485
  return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
486
486
  }
487
487
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.227",
3
+ "version": "0.1.229",
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"
@@ -0,0 +1,149 @@
1
+ # Playbook: Evaluate
2
+
3
+ You are {{agent_name}}, the {{agent_role}} on the {{project_name}} project.
4
+ TEAM ROOT: {{team_root}}
5
+
6
+ ## Your Task
7
+
8
+ Evaluate the implementation quality of a completed work item against its acceptance criteria and code quality standards.
9
+
10
+ ## Work Item Under Evaluation
11
+
12
+ - **ID:** {{item_id}}
13
+ - **Title:** {{item_title}}
14
+ - **Description:** {{item_description}}
15
+ - **Branch:** `{{branch_name}}`
16
+ - **Project:** {{project_name}} (`{{project_path}}`)
17
+
18
+ {{#acceptance_criteria}}
19
+ ## Acceptance Criteria
20
+
21
+ {{acceptance_criteria}}
22
+ {{/acceptance_criteria}}
23
+
24
+ {{#references}}
25
+ ## References
26
+
27
+ {{references}}
28
+ {{/references}}
29
+
30
+ ## Evaluation Rubric
31
+
32
+ Score each category on a 1-5 scale. A category **passes** at 3 or above.
33
+
34
+ ### 1. Correctness (weight: 30%)
35
+
36
+ Does the implementation do what the task description and acceptance criteria require?
37
+
38
+ - **5 — Excellent:** All acceptance criteria met, edge cases handled, no functional gaps
39
+ - **4 — Good:** All core criteria met, minor edge cases not handled
40
+ - **3 — Adequate:** Most criteria met, one minor gap that doesn't block usage
41
+ - **2 — Deficient:** One or more acceptance criteria not met
42
+ - **1 — Failing:** Core functionality missing or broken
43
+
44
+ **Pass threshold:** 3
45
+
46
+ ### 2. Completeness (weight: 25%)
47
+
48
+ Is the implementation finished end-to-end? No TODO stubs, no half-wired features, no missing integration points.
49
+
50
+ - **5 — Excellent:** Fully integrated, no loose ends, documentation updated if applicable
51
+ - **4 — Good:** Feature complete, minor polish items remain (comments, naming)
52
+ - **3 — Adequate:** Core feature works, one non-critical integration point incomplete
53
+ - **2 — Deficient:** Significant pieces missing or stubbed out
54
+ - **1 — Failing:** Skeleton or partial implementation only
55
+
56
+ **Pass threshold:** 3
57
+
58
+ ### 3. Code Quality (weight: 25%)
59
+
60
+ Does the code follow existing project patterns, naming conventions, and architectural decisions?
61
+
62
+ - **5 — Excellent:** Clean, idiomatic, follows all project conventions, well-structured
63
+ - **4 — Good:** Follows conventions, minor style inconsistencies
64
+ - **3 — Adequate:** Generally follows patterns, one area deviates without justification
65
+ - **2 — Deficient:** Multiple convention violations, poor structure
66
+ - **1 — Failing:** Ignores project patterns, introduces anti-patterns
67
+
68
+ **Pass threshold:** 3
69
+
70
+ ### 4. Test Coverage (weight: 20%)
71
+
72
+ Are there tests for the new functionality? Do existing tests still pass?
73
+
74
+ - **5 — Excellent:** Comprehensive tests for happy path and edge cases, all passing
75
+ - **4 — Good:** Tests cover core functionality, existing tests pass
76
+ - **3 — Adequate:** At least one test for the main feature, no regressions
77
+ - **2 — Deficient:** No new tests, but existing tests pass
78
+ - **1 — Failing:** No tests, or existing tests broken
79
+
80
+ **Pass threshold:** 3
81
+
82
+ ## Evaluation Steps
83
+
84
+ 1. **Fetch and review the diff:**
85
+ ```bash
86
+ git fetch origin
87
+ git diff {{main_branch}}...origin/{{branch_name}}
88
+ ```
89
+
90
+ 2. **Check acceptance criteria** one by one — mark each as MET or NOT MET with evidence
91
+
92
+ 3. **Review code quality** — check for pattern adherence, naming, structure
93
+
94
+ 4. **Verify tests:**
95
+ ```bash
96
+ cd {{project_path}}
97
+ npm test
98
+ ```
99
+
100
+ 5. **Calculate scores** using the rubric above
101
+
102
+ 6. **Determine verdict:**
103
+ - **PASS** — all four categories score 3 or above
104
+ - **FAIL** — any category scores below 3
105
+
106
+ ## Output Format
107
+
108
+ Structure your evaluation result as follows:
109
+
110
+ ```
111
+ ## Evaluation Result
112
+
113
+ **Item:** {{item_id}} — {{item_title}}
114
+ **Verdict:** PASS | FAIL
115
+ **Weighted Score:** X.X / 5.0
116
+
117
+ ### Scores
118
+
119
+ | Category | Score | Pass | Notes |
120
+ |----------|-------|------|-------|
121
+ | Correctness | X/5 | YES/NO | ... |
122
+ | Completeness | X/5 | YES/NO | ... |
123
+ | Code Quality | X/5 | YES/NO | ... |
124
+ | Test Coverage | X/5 | YES/NO | ... |
125
+
126
+ ### Acceptance Criteria Checklist
127
+
128
+ - [x] Criterion 1 — evidence
129
+ - [ ] Criterion 2 — what's missing
130
+
131
+ ### Issues Found
132
+
133
+ 1. **[severity]** Description (file:line)
134
+
135
+ ### Recommendations
136
+
137
+ - What to fix before merging (if FAIL)
138
+ - Suggestions for improvement (if PASS)
139
+ ```
140
+
141
+ ## Rules
142
+
143
+ - Base your evaluation on **evidence from the diff and test output** — not assumptions
144
+ - If acceptance criteria are missing, evaluate against the task description
145
+ - A FAIL verdict should include actionable feedback — what specifically needs to change
146
+ - Do NOT modify any code — this is a read-only evaluation
147
+ - NEVER checkout branches in the main working tree — use `git diff` and `git show` only
148
+
149
+ **Note:** Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
package/routing.md CHANGED
@@ -17,6 +17,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
17
17
  | test | dallas | ralph |
18
18
  | ask | ripley | rebecca |
19
19
  | verify | dallas | ralph |
20
+ | evaluate | ripley | rebecca |
20
21
  | decompose | ripley | rebecca |
21
22
  | meeting | ripley | rebecca |
22
23