@yemi33/minions 0.1.217 → 0.1.218

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.218 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/cli.js
8
+
9
+ ### Other
10
+ - bin/minions.js
11
+ - minions.js
12
+ - team.md
13
+
3
14
  ## 0.1.217 (2026-04-02)
4
15
 
5
16
  ### Engine
package/bin/minions.js CHANGED
@@ -438,8 +438,8 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
438
438
  Minions — Central AI dev team manager
439
439
 
440
440
  Setup:
441
- minions init [--skip-scan] Bootstrap ~/.minions/ (first time)
442
- minions init --force Upgrade engine code + add new files (auto-skip scan)
441
+ minions init Bootstrap ~/.minions/ (first time)
442
+ minions init --force Upgrade engine code + add new files
443
443
  minions version Show installed vs package version
444
444
  minions doctor Check prerequisites and runtime health
445
445
  minions add <project-dir> Link a project (interactive)
package/engine/cli.js CHANGED
@@ -688,7 +688,7 @@ const commands = {
688
688
  : projects[0];
689
689
 
690
690
  if (!targetProject) {
691
- console.log('No projects configured. Run: node minions.js add <dir>');
691
+ console.log('No projects configured. Run: minions add <dir>');
692
692
  return;
693
693
  }
694
694
 
package/engine.js CHANGED
@@ -24,7 +24,7 @@
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
26
  const shared = require('./engine/shared');
27
- const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS, CENTRAL_WI_PATH, resolveWiPath } = shared;
27
+ const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS } = shared;
28
28
  const queries = require('./engine/queries');
29
29
 
30
30
  // ─── Paths ──────────────────────────────────────────────────────────────────
@@ -120,7 +120,7 @@ const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
120
120
 
121
121
  // ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
122
122
 
123
- const { renderPlaybook, getLastRenderError, buildSystemPrompt, buildAgentContext, selectPlaybook,
123
+ const { renderPlaybook, buildSystemPrompt, buildAgentContext, selectPlaybook,
124
124
  buildBaseVars, buildPrDispatch, resolveTaskContext,
125
125
  getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
126
126
 
@@ -307,7 +307,7 @@ function spawnAgent(dispatchItem, config) {
307
307
 
308
308
  if (isSharedBranch) {
309
309
  log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
310
- try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git fetch: ' + e.message); }
310
+ try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
311
311
  try {
312
312
  runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
313
313
  } catch (eShared) {
@@ -317,11 +317,6 @@ function spawnAgent(dispatchItem, config) {
317
317
  log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
318
318
  worktreePath = existingWtPath;
319
319
  } else { throw eShared; }
320
- } else if (eShared.message?.includes('invalid reference') || eShared.message?.includes('not a valid branch')) {
321
- // Branch doesn't exist yet — create it from main
322
- log('info', `Shared branch ${branchName} not found — creating from ${project.mainBranch || 'main'}`);
323
- const mainRef = sanitizeBranch(project.mainBranch || 'main');
324
- runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${mainRef}`, _worktreeGitOpts, worktreeCreateRetries);
325
320
  } else { throw eShared; }
326
321
  }
327
322
  } else {
@@ -396,7 +391,6 @@ function spawnAgent(dispatchItem, config) {
396
391
  } else {
397
392
  log('error', `Failed to create worktree for ${branchName}: ${err.message}${err.stderr ? '\n' + err.stderr.toString().slice(0, 500) : ''}`);
398
393
  completeDispatch(id, 'error', 'Worktree creation failed: ' + (err.message || '').slice(0, 200));
399
- try { updateMetrics(agentId, dispatchItem, 'error', null, 0, null); } catch { /* optional */ }
400
394
  return null;
401
395
  }
402
396
  }
@@ -749,7 +743,7 @@ function areDependenciesMet(item, config) {
749
743
  } catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
750
744
  }
751
745
  // PRD item statuses that count as "done" for dep resolution
752
- const PRD_MET_STATUSES = new Set(['done']);
746
+ const PRD_MET_STATUSES = new Set(['done', 'in-pr', 'implemented', 'complete']);
753
747
 
754
748
  for (const depId of deps) {
755
749
  const depItem = allWorkItems.find(w => w.id === depId);
@@ -788,7 +782,8 @@ function detectDependencyCycles(items) {
788
782
  // writeInboxAlert — now in engine/dispatch.js
789
783
 
790
784
  // Reconciles work items against known PRs.
791
- // Primary linkage comes from prdItems in pull-requests.json (via getPrLinks()).
785
+ // Primary linkage comes from prdItems in pull-requests.json; fallback linkage
786
+ // uses engine/pr-links.json so matching does not depend on branch/title parsing.
792
787
  // onlyIds: if provided, only items whose ID is in this Set are eligible.
793
788
  function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
794
789
  const prLinks = shared.getPrLinks();
@@ -879,7 +874,7 @@ const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
879
874
  // Auto-clean pending/failed work items for a PRD so they re-materialize with updated plan data
880
875
  function autoCleanPrdWorkItems(prdFile, config) {
881
876
  const allProjects = getProjects(config);
882
- const wiPaths = [CENTRAL_WI_PATH];
877
+ const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
883
878
  for (const proj of allProjects) wiPaths.push(projectWorkItemsPath(proj));
884
879
  const deletedIds = [];
885
880
  for (const wiPath of wiPaths) {
@@ -982,7 +977,7 @@ function materializePlansAsWorkItems(config) {
982
977
  log('info', `PRD ${file} invalidated (was awaiting-approval) — queuing regeneration from revised plan`);
983
978
 
984
979
  // Collect completed items to carry over to new PRD
985
- const completedStatuses = new Set(['done']);
980
+ const completedStatuses = new Set(['done', 'in-pr', 'implemented']); // in-pr kept for backward compat
986
981
  const completedItems = (plan.missing_features || [])
987
982
  .filter(f => completedStatuses.has(f.status))
988
983
  .map(f => ({ id: f.id, name: f.name, status: f.status }));
@@ -1001,7 +996,8 @@ function materializePlansAsWorkItems(config) {
1001
996
  const allProjects = getProjects(config);
1002
997
  const targetProject = allProjects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || allProjects[0];
1003
998
  if (targetProject) {
1004
- const centralItems = safeJson(CENTRAL_WI_PATH) || [];
999
+ const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
1000
+ const centralItems = safeJson(centralWiPath) || [];
1005
1001
  const alreadyQueued = centralItems.some(w =>
1006
1002
  w.type === 'plan-to-prd' && w.planFile === plan.source_plan && (w.status === 'pending' || w.status === 'dispatched')
1007
1003
  );
@@ -1061,7 +1057,7 @@ function materializePlansAsWorkItems(config) {
1061
1057
  const useCentral = !defaultProject;
1062
1058
 
1063
1059
  const statusFilter = ['missing', 'planned'];
1064
- // Also materialize done items that never got a work item (race with PR status sync)
1060
+ // Also materialize in-pr/done items that never got a work item (race with PR status sync)
1065
1061
  const allExistingWiIds = new Set();
1066
1062
  for (const p of allProjects) {
1067
1063
  for (const w of (safeJson(projectWorkItemsPath(p)) || [])) {
@@ -1069,12 +1065,12 @@ function materializePlansAsWorkItems(config) {
1069
1065
  }
1070
1066
  }
1071
1067
  // Also check central work-items.json
1072
- for (const w of (safeJson(CENTRAL_WI_PATH) || [])) {
1068
+ for (const w of (safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [])) {
1073
1069
  if (w.id) allExistingWiIds.add(w.id);
1074
1070
  }
1075
1071
  const items = plan.missing_features.filter(f =>
1076
1072
  statusFilter.includes(f.status) ||
1077
- (f.status === 'done' && f.id && !allExistingWiIds.has(f.id))
1073
+ ((f.status === 'in-pr' || f.status === 'done') && f.id && !allExistingWiIds.has(f.id))
1078
1074
  );
1079
1075
 
1080
1076
  // Group items by target project (per-item project field overrides plan-level project)
@@ -1114,7 +1110,7 @@ function materializePlansAsWorkItems(config) {
1114
1110
 
1115
1111
  let totalCreated = 0;
1116
1112
  for (const [projName, { project, items: projItems }] of itemsByProject) {
1117
- const wiPath = project ? projectWorkItemsPath(project) : CENTRAL_WI_PATH;
1113
+ const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
1118
1114
  const existingItems = safeJson(wiPath) || [];
1119
1115
  let created = 0;
1120
1116
  const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
@@ -1135,7 +1131,7 @@ function materializePlansAsWorkItems(config) {
1135
1131
 
1136
1132
  const id = item.id; // Work item ID = PRD item ID — no indirection
1137
1133
  const complexity = item.estimated_complexity || 'medium';
1138
- const criteria = (Array.isArray(item.acceptance_criteria) ? item.acceptance_criteria : []).map(c => `- ${c}`).join('\n');
1134
+ const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
1139
1135
 
1140
1136
  const newItem = {
1141
1137
  id,
@@ -1194,17 +1190,9 @@ function materializePlansAsWorkItems(config) {
1194
1190
  const root = path.resolve(firstProject.localPath);
1195
1191
  const mainBranch = firstProject.mainBranch || 'main';
1196
1192
  const branch = sanitizeBranch(plan.feature_branch);
1197
- // Create branch from main — verify it actually succeeded
1198
- try {
1199
- exec(`git branch "${branch}" "${mainBranch}"`, { cwd: root, stdio: 'pipe', windowsHide: true });
1200
- } catch (e) {
1201
- // Branch may already exist — that's fine
1202
- if (!e.message?.includes('already exists')) throw e;
1203
- }
1204
- // Push to remote (best-effort — may not have a remote)
1205
- try {
1206
- exec(`git push -u origin "${branch}"`, { cwd: root, stdio: 'pipe', windowsHide: true, timeout: 15000 });
1207
- } catch { /* no remote or push failed — branch still exists locally */ }
1193
+ // Create branch from main (idempotent ignores if exists)
1194
+ exec(`git branch "${branch}" "${mainBranch}" 2>/dev/null || true`, { cwd: root, stdio: 'pipe' });
1195
+ exec(`git push -u origin "${branch}" 2>/dev/null || true`, { cwd: root, stdio: 'pipe' });
1208
1196
  log('info', `Shared branch pre-created: ${branch} for plan ${file}`);
1209
1197
  } catch (err) {
1210
1198
  log('warn', `Failed to pre-create shared branch for ${file}: ${err.message}`);
@@ -1258,9 +1246,9 @@ function discoverFromPrs(config, project) {
1258
1246
  // minionsReview tracks metadata (reviewer, note) but not the authoritative status
1259
1247
  const reviewStatus = pr.reviewStatus || 'pending';
1260
1248
 
1261
- // PRs needing review: only dispatch if evalLoop is enabled
1262
- const evalLoop = config.engine?.evalLoop !== false;
1263
- const needsReview = evalLoop && reviewStatus === 'pending';
1249
+ // PRs needing review: pending or waiting (review dispatched but no verdict yet)
1250
+ const autoReview = config.engine?.autoReview !== false;
1251
+ const needsReview = autoReview && reviewStatus === 'pending';
1264
1252
  if (needsReview) {
1265
1253
  const key = `review-${project?.name || 'default'}-${pr.id}`;
1266
1254
  if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
@@ -1268,8 +1256,8 @@ function discoverFromPrs(config, project) {
1268
1256
  if (!agentId) continue;
1269
1257
 
1270
1258
  const item = buildPrDispatch(agentId, config, project, pr, 'review', {
1271
- pr_id: pr.id, pr_number: prNumber, pr_title: pr.title ? ': ' + pr.title : '', pr_branch: pr.branch || '',
1272
- branch_name: pr.branch || '', pr_author: pr.agent || '', pr_url: pr.url || '',
1259
+ pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
1260
+ pr_author: pr.agent || '', pr_url: pr.url || '',
1273
1261
  }, `Review PR ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
1274
1262
  if (item) { newWork.push(item); setCooldown(key); }
1275
1263
  }
@@ -1385,7 +1373,6 @@ function discoverFromWorkItems(config, project) {
1385
1373
  }
1386
1374
  }
1387
1375
 
1388
- if (item.status === 'needs-human-review') continue; // Explicit skip — flagged for human attention
1389
1376
  if (item.status !== 'queued' && item.status !== 'pending') continue;
1390
1377
 
1391
1378
  // Dependency gate: skip items whose depends_on are not yet met; propagate failure
@@ -1484,47 +1471,9 @@ function discoverFromWorkItems(config, project) {
1484
1471
  '- [' + (r.title || r.url) + '](' + r.url + ')' + (r.type ? ' (' + r.type + ')' : '')
1485
1472
  ).join('\n');
1486
1473
  vars.references = refs ? '## References\n\n' + refs : '';
1487
- const ac = (Array.isArray(item.acceptanceCriteria) ? item.acceptanceCriteria : []).map(c => '- [ ] ' + c).join('\n');
1474
+ const ac = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1488
1475
  vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
1489
1476
 
1490
- // Inject PR section — conditional based on skipPr flag
1491
- vars.pr_section = item.skipPr
1492
- ? '## Push Branch\n\n**PR creation is skipped for this work item.** Push your branch and report the branch name.\n\n```bash\ngit push -u origin {{branch_name}}\n```\n\nInclude the branch name in your completion summary.'
1493
- : '## Create PR (MANDATORY)\n\n**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.\n\n{{pr_create_instructions}}\n- sourceRefName: `refs/heads/{{branch_name}}`\n- targetRefName: `refs/heads/{{main_branch}}`\n- title: `{{commit_message}}`\n- labels: `["minions:{{agent_id}}"]`\n\nInclude in the PR description:\n- What was built and why\n- Files changed\n- How to build and test, browser URL if applicable\n- Test plan\n\n## Post self-review on PR\n\n{{pr_comment_instructions}}\n- pullRequestId: `<from PR creation>`\n- Re-read your own diff critically before posting\n- Sign: `Built by Minions ({{agent_name}} — {{agent_role}})`';
1494
-
1495
- // Inject checkpoint context if agent left a checkpoint.json from a prior run
1496
- vars.checkpoint_context = '';
1497
- try {
1498
- const wtPath = vars.worktree_path || root;
1499
- const cpPath = path.join(wtPath, 'checkpoint.json');
1500
- if (fs.existsSync(cpPath)) {
1501
- const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
1502
- const cpCount = (item._checkpointCount || 0) + 1;
1503
- if (cpCount > 3) {
1504
- log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1505
- item.status = 'needs-human-review';
1506
- item._checkpointCount = cpCount;
1507
- needsWrite = true;
1508
- continue;
1509
- }
1510
- item._checkpointCount = cpCount;
1511
- needsWrite = true;
1512
- const cpSummary = [
1513
- `## Checkpoint (Resume #${cpCount}/3)`,
1514
- '',
1515
- 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1516
- '',
1517
- Array.isArray(cpData.completed) && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1518
- Array.isArray(cpData.remaining) && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1519
- Array.isArray(cpData.blockers) && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1520
- cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1521
- ].filter(Boolean).join('\n');
1522
- vars.checkpoint_context = cpSummary;
1523
- log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
1524
- try { fs.unlinkSync(cpPath); } catch (ue) { log('warn', `checkpoint cleanup for ${item.id}: ${ue.message}`); }
1525
- }
1526
- } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1527
-
1528
1477
  // Inject ask-specific variables for the ask playbook
1529
1478
  if (workType === 'ask') {
1530
1479
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
@@ -1544,17 +1493,9 @@ function discoverFromWorkItems(config, project) {
1544
1493
  if (playbookName === 'work-item' && workType === 'review') {
1545
1494
  log('info', `Work item ${item.id} is type "review" but has no PR — using work-item playbook`);
1546
1495
  }
1547
- const rendered = renderPlaybook(playbookName, vars);
1548
- const renderError = getLastRenderError();
1549
- // If critical vars are missing, block dispatch entirely — don't fall through to work-item playbook
1550
- const prompt = item.prompt || rendered || (renderError ? null : (renderPlaybook('work-item', vars) || item.description));
1496
+ const prompt = item.prompt || renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars) || item.description;
1551
1497
  if (!prompt) {
1552
- if (renderError) {
1553
- log('warn', `Skipping ${item.id}: ${renderError.message}`);
1554
- if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; needsWrite = true; }
1555
- } else {
1556
- log('warn', `No playbook rendered for ${item.id} (type: ${workType}, playbook: ${playbookName}) — skipping`);
1557
- }
1498
+ log('warn', `No playbook rendered for ${item.id} (type: ${workType}, playbook: ${playbookName}) — skipping`);
1558
1499
  continue;
1559
1500
  }
1560
1501
 
@@ -1578,13 +1519,15 @@ function discoverFromWorkItems(config, project) {
1578
1519
  setCooldown(key);
1579
1520
  }
1580
1521
 
1581
- // Write back updated statuses needsWrite covers mutation-only ticks, newWork covers dispatches
1582
- if (needsWrite || newWork.length > 0) {
1522
+ // Write back updated statuses (always, since we mark items dispatched before newWork check)
1523
+ if (newWork.length > 0) {
1583
1524
  const workItemsPath = projectWorkItemsPath(project);
1584
1525
  safeWrite(workItemsPath, items);
1585
1526
  for (const s of prdSyncQueue) syncPrdItemStatus(s.id, 'dispatched', s.sourcePlan);
1586
1527
  }
1587
1528
 
1529
+ if (needsWrite) safeWrite(projectWorkItemsPath(project), items);
1530
+
1588
1531
  const skipTotal = skipped.gated + skipped.noAgent;
1589
1532
  if (skipTotal > 0) {
1590
1533
  log('debug', `Work item discovery (${project?.name}): skipped ${skipTotal} items (${skipped.gated} gated, ${skipped.noAgent} no agent)`);
@@ -1768,14 +1711,12 @@ function extractSpecInfo(filePath, projectRoot_) {
1768
1711
  * Uses the shared work-item.md playbook with multi-project context injected.
1769
1712
  */
1770
1713
  function discoverCentralWorkItems(config) {
1771
- const centralPath = CENTRAL_WI_PATH;
1714
+ const centralPath = path.join(MINIONS_DIR, 'work-items.json');
1772
1715
  const items = safeJson(centralPath) || [];
1773
1716
  const projects = getProjects(config);
1774
1717
  const newWork = [];
1775
- let needsWrite = false;
1776
1718
 
1777
1719
  for (const item of items) {
1778
- if (item.status === 'needs-human-review') continue; // Explicit skip — flagged for human attention
1779
1720
  if (item.status !== 'queued' && item.status !== 'pending') continue;
1780
1721
 
1781
1722
  const key = `central-work-${item.id}`;
@@ -1803,42 +1744,6 @@ function discoverCentralWorkItems(config) {
1803
1744
  assignedProject: projects.length > 0 ? projects[i % projects.length] : null
1804
1745
  }));
1805
1746
 
1806
- // Inject checkpoint context if agent left a checkpoint.json from a prior run
1807
- let fanOutCheckpointContext = '';
1808
- try {
1809
- const fanFirstProject = projects[0];
1810
- const fanBranch = item.branch || `work/${item.id}`;
1811
- const fanWtPath = fanFirstProject?.localPath
1812
- ? path.resolve(fanFirstProject.localPath, config.engine?.worktreeRoot || '../worktrees', fanBranch)
1813
- : '';
1814
- const fanCpPath = fanWtPath ? path.join(fanWtPath, 'checkpoint.json') : '';
1815
- if (fanCpPath && fs.existsSync(fanCpPath)) {
1816
- const fanCpData = JSON.parse(fs.readFileSync(fanCpPath, 'utf8'));
1817
- const fanCpCount = (item._checkpointCount || 0) + 1;
1818
- if (fanCpCount > 3) {
1819
- log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1820
- item.status = 'needs-human-review';
1821
- item._checkpointCount = fanCpCount;
1822
- needsWrite = true;
1823
- continue;
1824
- }
1825
- item._checkpointCount = fanCpCount;
1826
- const fanCpSummary = [
1827
- `## Checkpoint (Resume #${fanCpCount}/3)`,
1828
- '',
1829
- 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1830
- '',
1831
- Array.isArray(fanCpData.completed) && fanCpData.completed.length > 0 ? `### Completed\n${fanCpData.completed.map(s => '- ' + s).join('\n')}` : '',
1832
- Array.isArray(fanCpData.remaining) && fanCpData.remaining.length > 0 ? `### Remaining\n${fanCpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1833
- Array.isArray(fanCpData.blockers) && fanCpData.blockers.length > 0 ? `### Blockers\n${fanCpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1834
- fanCpData.branch_state ? `### Branch State\n${fanCpData.branch_state}` : '',
1835
- ].filter(Boolean).join('\n');
1836
- fanOutCheckpointContext = fanCpSummary;
1837
- log('info', `Injecting checkpoint context for ${item.id} (resume #${fanCpCount})`);
1838
- try { fs.unlinkSync(fanCpPath); } catch (ue) { log('warn', `checkpoint cleanup for ${item.id}: ${ue.message}`); }
1839
- }
1840
- } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1841
-
1842
1747
  for (const { agent, assignedProject } of assignments) {
1843
1748
  const fanKey = `${key}-${agent.id}`;
1844
1749
  if (isAlreadyDispatched(fanKey)) continue;
@@ -1861,19 +1766,9 @@ function discoverCentralWorkItems(config) {
1861
1766
  '- [' + (r.title || r.url) + '](' + r.url + ')' + (r.type ? ' (' + r.type + ')' : '')
1862
1767
  ).join('\n');
1863
1768
  vars.references = fanRefs ? '## References\n\n' + fanRefs : '';
1864
- const fanAc = (Array.isArray(item.acceptanceCriteria) ? item.acceptanceCriteria : []).map(c => '- [ ] ' + c).join('\n');
1769
+ const fanAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1865
1770
  vars.acceptance_criteria = fanAc ? '## Acceptance Criteria\n\n' + fanAc : '';
1866
1771
 
1867
- // Inject PR section — conditional based on skipPr flag
1868
- const fanBranch = '{{branch_name}}';
1869
- vars.pr_section = item.skipPr
1870
- ? '## Push Branch\n\n**PR creation is skipped for this work item.** Push your branch and report the branch name.\n\n```bash\ngit push -u origin ' + fanBranch + '\n```\n\nInclude the branch name in your completion summary.'
1871
- : '## Create PR (MANDATORY)\n\n**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.\n\n{{pr_create_instructions}}\n- sourceRefName: `refs/heads/' + fanBranch + '`\n- targetRefName: `refs/heads/{{main_branch}}`\n- title: `{{commit_message}}`\n- labels: `["minions:{{agent_id}}"]`\n\nInclude in the PR description:\n- What was built and why\n- Files changed\n- How to build and test, browser URL if applicable\n- Test plan\n\n## Post self-review on PR\n\n{{pr_comment_instructions}}\n- pullRequestId: `<from PR creation>`\n- Re-read your own diff critically before posting\n- Sign: `Built by Minions ({{agent_name}} — {{agent_role}})`';
1872
-
1873
- // Inject checkpoint context (computed once above the loop)
1874
- vars.checkpoint_context = fanOutCheckpointContext;
1875
-
1876
-
1877
1772
  if (workType === 'ask') {
1878
1773
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
1879
1774
  vars.task_id = item.id;
@@ -1887,16 +1782,9 @@ function discoverCentralWorkItems(config) {
1887
1782
  }
1888
1783
 
1889
1784
  const playbookName = selectPlaybook(workType, item);
1890
- const rendered = renderPlaybook(playbookName, vars);
1891
- const renderError = getLastRenderError();
1892
- const prompt = rendered || (renderError ? null : renderPlaybook('work-item', vars));
1785
+ const prompt = renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars);
1893
1786
  if (!prompt) {
1894
- if (renderError) {
1895
- log('warn', `Fan-out: ${item.id} → ${agent.id}: ${renderError.message}`);
1896
- if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; needsWrite = true; }
1897
- } else {
1898
- log('warn', `Fan-out: playbook '${playbookName}' failed to render for ${item.id} → ${agent.id}, skipping`);
1899
- }
1787
+ log('warn', `Fan-out: playbook '${playbookName}' failed to render for ${item.id} → ${agent.id}, skipping`);
1900
1788
  continue;
1901
1789
  }
1902
1790
 
@@ -1919,8 +1807,6 @@ function discoverCentralWorkItems(config) {
1919
1807
  item.dispatched_to = idleAgents.map(a => a.id).join(', ');
1920
1808
  item.scope = 'fan-out';
1921
1809
  item.fanOutAgents = idleAgents.map(a => a.id);
1922
- delete item._pendingReason;
1923
- needsWrite = true;
1924
1810
  setCooldown(key);
1925
1811
  log('info', `Fan-out: ${item.id} dispatched to ${idleAgents.length} agents: ${idleAgents.map(a => a.name).join(', ')}`);
1926
1812
 
@@ -1955,43 +1841,9 @@ function discoverCentralWorkItems(config) {
1955
1841
  '- [' + (r.title || r.url) + '](' + r.url + ')' + (r.type ? ' (' + r.type + ')' : '')
1956
1842
  ).join('\n');
1957
1843
  vars.references = normRefs ? '## References\n\n' + normRefs : '';
1958
- const normAc = (Array.isArray(item.acceptanceCriteria) ? item.acceptanceCriteria : []).map(c => '- [ ] ' + c).join('\n');
1844
+ const normAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1959
1845
  vars.acceptance_criteria = normAc ? '## Acceptance Criteria\n\n' + normAc : '';
1960
1846
 
1961
- // Inject checkpoint context if agent left a checkpoint.json from a prior run
1962
- vars.checkpoint_context = '';
1963
- try {
1964
- const centralBranch = item.branch || `work/${item.id}`;
1965
- const centralWtPath = firstProject?.localPath
1966
- ? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
1967
- : '';
1968
- const cpPath = centralWtPath ? path.join(centralWtPath, 'checkpoint.json') : '';
1969
- if (cpPath && fs.existsSync(cpPath)) {
1970
- const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
1971
- const cpCount = (item._checkpointCount || 0) + 1;
1972
- if (cpCount > 3) {
1973
- log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1974
- item.status = 'needs-human-review';
1975
- item._checkpointCount = cpCount;
1976
- continue;
1977
- }
1978
- item._checkpointCount = cpCount;
1979
- const cpSummary = [
1980
- `## Checkpoint (Resume #${cpCount}/3)`,
1981
- '',
1982
- 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1983
- '',
1984
- Array.isArray(cpData.completed) && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1985
- Array.isArray(cpData.remaining) && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1986
- Array.isArray(cpData.blockers) && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1987
- cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1988
- ].filter(Boolean).join('\n');
1989
- vars.checkpoint_context = cpSummary;
1990
- log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
1991
- try { fs.unlinkSync(cpPath); } catch (ue) { log('warn', `checkpoint cleanup for ${item.id}: ${ue.message}`); }
1992
- }
1993
- } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1994
-
1995
1847
  // Inject plan-specific variables for the plan playbook
1996
1848
  if (workType === 'plan') {
1997
1849
  // Ensure plans directory exists before agent tries to write
@@ -2042,19 +1894,10 @@ function discoverCentralWorkItems(config) {
2042
1894
  }
2043
1895
 
2044
1896
  const playbookName = selectPlaybook(workType, item);
2045
- const rendered = renderPlaybook(playbookName, vars);
2046
- const renderError = getLastRenderError();
2047
- const prompt = rendered || (renderError ? null : renderPlaybook('work-item', vars));
1897
+ const prompt = renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars);
2048
1898
  if (!prompt) {
2049
- if (renderError) {
2050
- log('warn', `Dispatch: ${item.id}: ${renderError.message}`);
2051
- item._pendingReason = 'critical_vars_missing';
2052
- needsWrite = true;
2053
- } else {
2054
- log('warn', `Dispatch: playbook '${playbookName}' failed to render for ${item.id}, resetting to pending`);
2055
- }
1899
+ log('warn', `Dispatch: playbook '${playbookName}' failed to render for ${item.id}, resetting to pending`);
2056
1900
  item.status = 'pending';
2057
- needsWrite = true;
2058
1901
  continue;
2059
1902
  }
2060
1903
 
@@ -2071,13 +1914,11 @@ function discoverCentralWorkItems(config) {
2071
1914
  item.status = 'dispatched';
2072
1915
  item.dispatched_at = ts();
2073
1916
  item.dispatched_to = agentId;
2074
- delete item._pendingReason;
2075
- needsWrite = true;
2076
1917
  setCooldown(key);
2077
1918
  }
2078
1919
  }
2079
1920
 
2080
- if (needsWrite || newWork.length > 0) safeWrite(centralPath, items);
1921
+ if (newWork.length > 0) safeWrite(centralPath, items);
2081
1922
  return newWork;
2082
1923
  }
2083
1924
 
@@ -2123,14 +1964,23 @@ function discoverWork(config) {
2123
1964
  const { discoverScheduledWork } = require('./engine/scheduler');
2124
1965
  const scheduledWork = discoverScheduledWork(config);
2125
1966
  if (scheduledWork.length > 0) {
2126
- const centralPath = CENTRAL_WI_PATH;
1967
+ const { createMeeting, getMeetings } = require('./engine/meeting');
1968
+ const centralPath = path.join(MINIONS_DIR, 'work-items.json');
2127
1969
  const items = safeJson(centralPath) || [];
2128
1970
  let added = 0;
2129
1971
  for (const item of scheduledWork) {
2130
- if (!items.some(i => i._scheduleId === item._scheduleId && i.status !== 'done' && i.status !== 'failed')) {
2131
- items.push(item);
2132
- added++;
2133
- log('info', `Scheduled task fired: ${item._scheduleId} ${item.title}`);
1972
+ if (item.type === 'meeting') {
1973
+ // Create a real multi-agent meeting instead of a single-agent work item
1974
+ const sched = (config.schedules || []).find(s => s.id === item._scheduleId);
1975
+ const participants = (sched && sched.participants) || [];
1976
+ const meeting = createMeeting({ title: item.title, agenda: item.description, participants });
1977
+ log('info', `Scheduled meeting created: ${item._scheduleId} → ${meeting.id} (${participants.length} participants)`);
1978
+ } else {
1979
+ if (!items.some(i => i._scheduleId === item._scheduleId && i.status !== 'done' && i.status !== 'failed')) {
1980
+ items.push(item);
1981
+ added++;
1982
+ log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
1983
+ }
2134
1984
  }
2135
1985
  }
2136
1986
  if (added > 0) safeWrite(centralPath, items);
@@ -2197,7 +2047,7 @@ function discoverWork(config) {
2197
2047
  for (const item of allWork) {
2198
2048
  addToDispatch(item);
2199
2049
  if (item.meta?.source === 'pr-human-feedback') {
2200
- clearPendingHumanFeedbackFlag(item.meta?.project, item.meta?.pr?.id);
2050
+ clearPendingHumanFeedbackFlag(item.meta.project, item.meta.pr?.id);
2201
2051
  }
2202
2052
  }
2203
2053
 
@@ -2221,15 +2071,12 @@ let tickRunning = false;
2221
2071
  async function tick() {
2222
2072
  if (tickRunning) return; // prevent overlapping ticks
2223
2073
  tickRunning = true;
2224
- const tickStart = Date.now();
2225
2074
  try {
2226
2075
  await tickInner();
2227
2076
  } catch (e) {
2228
2077
  log('error', `Tick error: ${e.message}`);
2229
2078
  } finally {
2230
2079
  tickRunning = false;
2231
- const elapsed = Date.now() - tickStart;
2232
- if (elapsed > 30000) log('warn', `Slow tick: ${(elapsed / 1000).toFixed(1)}s`);
2233
2080
  }
2234
2081
  }
2235
2082
 
@@ -2247,9 +2094,9 @@ async function tickInner() {
2247
2094
  tickCount++;
2248
2095
 
2249
2096
  // 1. Check for timed-out agents, steering messages, and idle threshold
2250
- try { checkTimeouts(config); } catch (e) { log('warn', `checkTimeouts: ${e.message}`); }
2251
- try { checkSteering(config); } catch (e) { log('warn', `checkSteering: ${e.message}`); }
2252
- try { checkIdleThreshold(config); } catch (e) { log('warn', `checkIdleThreshold: ${e.message}`); }
2097
+ checkTimeouts(config);
2098
+ checkSteering(config);
2099
+ checkIdleThreshold(config);
2253
2100
 
2254
2101
  // 1b. Check for meeting round timeouts
2255
2102
  try {
@@ -2264,11 +2111,11 @@ async function tickInner() {
2264
2111
  }
2265
2112
 
2266
2113
  // 2. Consolidate inbox
2267
- try { consolidateInbox(config); } catch (e) { log('warn', `consolidateInbox: ${e.message}`); }
2114
+ consolidateInbox(config);
2268
2115
 
2269
2116
  // 2.5. Periodic cleanup + MCP sync (every 10 ticks = ~5 minutes)
2270
2117
  if (tickCount % 10 === 0) {
2271
- try { runCleanup(config); } catch (e) { log('warn', `runCleanup: ${e.message}`); }
2118
+ runCleanup(config);
2272
2119
  }
2273
2120
 
2274
2121
  // 2.6. Poll PR status: build, review, merge (every 6 ticks = ~3 minutes)
@@ -2278,7 +2125,7 @@ async function tickInner() {
2278
2125
  try { await ghPollPrStatus(config); } catch (err) { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
2279
2126
  // Sync PR status back to PRD items (missing → done when active PR exists)
2280
2127
  try { syncPrdFromPrs(config); } catch (err) { log('warn', `PRD sync error: ${err?.message || err}`); }
2281
- // Check if any plans can be marked completed (all features done)
2128
+ // Check if any plans can be marked completed (all features done/in-pr)
2282
2129
  try {
2283
2130
  const prdFiles = safeReadDir(PRD_DIR).filter(f => f.endsWith('.json'));
2284
2131
  for (const file of prdFiles) {
@@ -2399,15 +2246,14 @@ async function tickInner() {
2399
2246
  }
2400
2247
 
2401
2248
  // 3. Discover new work from sources
2402
- try { discoverWork(config); } catch (e) { log('warn', `discoverWork: ${e.message}`); }
2249
+ discoverWork(config);
2403
2250
 
2404
2251
  // 4. Update snapshot
2405
- try { updateSnapshot(config); } catch (e) { log('warn', `updateSnapshot: ${e.message}`); }
2252
+ updateSnapshot(config);
2406
2253
 
2407
2254
  // 5. Process pending dispatches — auto-spawn agents
2408
- let dispatch, activeCount;
2409
- try { dispatch = getDispatch(); } catch (e) { log('warn', `getDispatch: ${e.message}`); return; }
2410
- activeCount = (dispatch.active || []).length;
2255
+ const dispatch = getDispatch();
2256
+ const activeCount = (dispatch.active || []).length;
2411
2257
  const maxConcurrent = config.engine?.maxConcurrent || 5;
2412
2258
 
2413
2259
  if (activeCount >= maxConcurrent) {
@@ -2458,7 +2304,9 @@ async function tickInner() {
2458
2304
  // Defensive: ensure the work item is re-queued if completeDispatch didn't fire
2459
2305
  if (item.meta?.item?.id) {
2460
2306
  try {
2461
- const wiPath = resolveWiPath(item.meta);
2307
+ const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
2308
+ ? path.join(ENGINE_DIR, '..', 'work-items.json')
2309
+ : item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
2462
2310
  if (wiPath) {
2463
2311
  const items = safeJson(wiPath) || [];
2464
2312
  const wi = items.find(i => i.id === item.meta.item.id);
@@ -2539,7 +2387,6 @@ module.exports = {
2539
2387
 
2540
2388
  // Playbooks
2541
2389
  renderPlaybook,
2542
- getLastRenderError,
2543
2390
 
2544
2391
  // Timeout / Steering / Idle (re-exported from engine/timeout.js)
2545
2392
  checkTimeouts, checkSteering, checkIdleThreshold,
package/minions.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Minions Init — Link a project to the central minions
4
4
  *
5
5
  * Usage:
6
- * node minions.js <project-dir> Add a project interactively
7
- * node minions.js <project-dir> --remove Remove a project
8
- * node minions.js --list List linked projects
6
+ * minions add <project-dir> Add a project interactively
7
+ * minions remove <project-dir> Remove a project
8
+ * minions list List linked projects
9
9
  *
10
10
  * This adds the project to ~/.minions/config.json's projects array.
11
11
  * The minions engine and dashboard run centrally from ~/.minions/.
@@ -228,7 +228,7 @@ function listProjects() {
228
228
  const projects = config.projects || [];
229
229
  console.log(`\n Minions Projects (${projects.length})\n`);
230
230
  if (projects.length === 0) {
231
- console.log(' No projects linked. Run: node minions.js <project-dir>\n');
231
+ console.log(' No projects linked. Run: minions add <project-dir>\n');
232
232
  rl.close();
233
233
  return;
234
234
  }
@@ -406,7 +406,7 @@ async function scanAndAdd({ root, depth } = {}) {
406
406
 
407
407
  saveConfig(config);
408
408
  console.log(`\n Done. ${config.projects.length} total project(s) linked.`);
409
- console.log(` Run "node minions.js list" to verify.\n`);
409
+ console.log(` Run "minions list" to verify.\n`);
410
410
  rl.close();
411
411
  }
412
412
 
@@ -456,7 +456,7 @@ async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
456
456
  }
457
457
 
458
458
  if (skipScan) {
459
- console.log(' Skipping repo scan (--skip-scan). Run "node minions.js scan" later to link projects.\n');
459
+ console.log(' Run "minions scan" or "minions add <dir>" to link projects.\n');
460
460
  rl.close();
461
461
  } else {
462
462
  // Auto-chain into scan (scanAndAdd closes rl)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.217",
3
+ "version": "0.1.218",
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"