@yemi33/minions 0.1.2026 → 0.1.2028

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.
@@ -13,6 +13,7 @@ const _pageCounters = {
13
13
  pipelines: function(d) { return (d.pipelines || []).length + '|' + (d.pipelines || []).reduce(function(s, p) { return s + (p.runs || []).length; }, 0); },
14
14
  schedule: function(d) { return (d.schedules || []).length; },
15
15
  engine: function(d) { return (d.dispatch?.completed || []).filter(function(c) { return c.result === 'error'; }).length; },
16
+ qa: function(d) { return (d.qaRuns?.total || 0) + '|' + (d.qaRuns?.sig || ''); },
16
17
  };
17
18
  let _prevCounts = {};
18
19
  function _detectPageChanges(data) {
package/dashboard.js CHANGED
@@ -1589,7 +1589,9 @@ function _ifNoneMatchHasEtag(headerValue, currentEtag) {
1589
1589
  // delegate so any module that contributes to `_buildStatusFastState()` can
1590
1590
  // register its mtime inputs in one place.
1591
1591
  const _mtimeTrackedFiles = () => queries.getStatusFastStateMtimePaths(CONFIG);
1592
- let _lastMtimes = {}; // { filePath: mtimeMs }
1592
+ const _slowMtimeTrackedFiles = () => queries.getStatusSlowStateMtimePaths(CONFIG);
1593
+ let _lastMtimes = {}; // { filePath: mtimeMs } — fast-state baseline
1594
+ let _lastSlowMtimes = {}; // { filePath: mtimeMs } — slow-state baseline
1593
1595
 
1594
1596
  function _getMtimes() {
1595
1597
  const result = {};
@@ -1599,6 +1601,14 @@ function _getMtimes() {
1599
1601
  return result;
1600
1602
  }
1601
1603
 
1604
+ function _getSlowMtimes() {
1605
+ const result = {};
1606
+ for (const fp of _slowMtimeTrackedFiles()) {
1607
+ try { result[fp] = fs.statSync(fp).mtimeMs; } catch { result[fp] = 0; }
1608
+ }
1609
+ return result;
1610
+ }
1611
+
1602
1612
  function _mtimesChanged(prev, curr) {
1603
1613
  for (const fp of Object.keys(curr)) {
1604
1614
  if (prev[fp] !== curr[fp]) return true;
@@ -1668,6 +1678,21 @@ function _buildStatusFastState() {
1668
1678
  workItems: getWorkItems(),
1669
1679
  watches: watchesMod.getWatches(),
1670
1680
  meetings: (() => { try { return require('./engine/meeting').getMeetings(); } catch { return []; } })(),
1681
+ // QA runs — surfaced for the sidebar activity-dot counter and any future
1682
+ // CC/aggregate view. Tab-level rendering keeps its own /api/qa/runs poll
1683
+ // (5 s while the QA page is mounted). qa-runs.json is in the mtime tracker
1684
+ // so a new run lights the dot within one /api/status poll cycle (~4 s).
1685
+ qaRuns: (() => {
1686
+ try {
1687
+ const runs = require('./engine/qa-runs').listRuns({ limit: 50 }) || [];
1688
+ return {
1689
+ total: runs.length,
1690
+ // Signature of (id, status) for the most recent 20 runs so the
1691
+ // sidebar counter advances on status flips AND on new entries.
1692
+ sig: runs.slice(0, 20).map(r => (r && r.id || '') + ':' + (r && r.status || '')).join(','),
1693
+ };
1694
+ } catch { return { total: 0, sig: '' }; }
1695
+ })(),
1671
1696
  };
1672
1697
  }
1673
1698
 
@@ -1772,8 +1797,16 @@ function getStatus() {
1772
1797
  if (_mtimesChanged(_lastMtimes, currMtimes)) fastStale = true;
1773
1798
  }
1774
1799
 
1775
- // Slow state: 60s TTL, pure TTL (no mtime check these files change rarely)
1776
- const slowStale = !_slowState || (now - _slowStateTs) >= SLOW_STATE_TTL;
1800
+ // Slow state: 60s TTL with mtime-based validation for early bust.
1801
+ // The mtime tracker covers engine-driven slow-state writes (PRD updates,
1802
+ // pipeline-runs.json, schedule-runs.json, verify guides, project skills)
1803
+ // so changes surface within one SPA poll (~4 s) instead of waiting up to
1804
+ // 60 s for TTL. Same pre-build snapshot semantics as fast-state below.
1805
+ let slowStale = !_slowState || (now - _slowStateTs) >= SLOW_STATE_TTL;
1806
+ if (!slowStale) {
1807
+ const currSlowMtimes = _getSlowMtimes();
1808
+ if (_mtimesChanged(_lastSlowMtimes, currSlowMtimes)) slowStale = true;
1809
+ }
1777
1810
 
1778
1811
  // If nothing stale, return cached merged result
1779
1812
  if (!fastStale && !slowStale && _statusCache) return _statusCache;
@@ -1795,10 +1828,14 @@ function getStatus() {
1795
1828
  _lastMtimes = preBuildMtimes;
1796
1829
  }
1797
1830
 
1798
- // Rebuild slow state (rarely-changing data: ~8-15 reads, 60s TTL)
1831
+ // Rebuild slow state (rarely-changing data: ~8-15 reads, 60s TTL).
1832
+ // Same pre-build snapshot pattern as fast-state — capture mtimes BEFORE
1833
+ // disk reads so any write landing mid-build busts the next poll.
1799
1834
  if (slowStale) {
1835
+ const preBuildSlowMtimes = _getSlowMtimes();
1800
1836
  _slowState = _buildStatusSlowState();
1801
1837
  _slowStateTs = now;
1838
+ _lastSlowMtimes = preBuildSlowMtimes;
1802
1839
  }
1803
1840
 
1804
1841
  // Merge both tiers — no API contract change
@@ -1839,7 +1876,11 @@ function refreshStatusAsync() {
1839
1876
  const currMtimes = _getMtimes();
1840
1877
  if (_mtimesChanged(_lastMtimes, currMtimes)) fastStale = true;
1841
1878
  }
1842
- const slowStale = !_slowState || (now - _slowStateTs) >= SLOW_STATE_TTL;
1879
+ let slowStale = !_slowState || (now - _slowStateTs) >= SLOW_STATE_TTL;
1880
+ if (!slowStale) {
1881
+ const currSlowMtimes = _getSlowMtimes();
1882
+ if (_mtimesChanged(_lastSlowMtimes, currSlowMtimes)) slowStale = true;
1883
+ }
1843
1884
 
1844
1885
  if (!fastStale && !slowStale && _statusCache) return _statusCache;
1845
1886
 
@@ -1866,7 +1907,9 @@ function refreshStatusAsync() {
1866
1907
  }
1867
1908
 
1868
1909
  let slow = _slowState;
1910
+ let preBuildSlowMtimes = null;
1869
1911
  if (slowStale) {
1912
+ preBuildSlowMtimes = _getSlowMtimes();
1870
1913
  slow = _buildStatusSlowState();
1871
1914
  }
1872
1915
 
@@ -1887,6 +1930,7 @@ function refreshStatusAsync() {
1887
1930
  if (slowStale) {
1888
1931
  _slowState = slow;
1889
1932
  _slowStateTs = now;
1933
+ _lastSlowMtimes = preBuildSlowMtimes;
1890
1934
  }
1891
1935
  _statusCache = { ..._fastState, ..._slowState, timestamp: new Date().toISOString() };
1892
1936
  _markStatusCacheBuilt();
@@ -1921,6 +1965,7 @@ function _resetStatusCacheForTesting() {
1921
1965
  _statusInvalidationGeneration = 0;
1922
1966
  _statusRefreshHook = null;
1923
1967
  _lastMtimes = {};
1968
+ _lastSlowMtimes = {};
1924
1969
  }
1925
1970
 
1926
1971
  /** Return cached JSON string of status — single stringify, reused by SSE and /api/status */
package/engine/queries.js CHANGED
@@ -1918,11 +1918,29 @@ function getStatusFastStateMtimePaths(config) {
1918
1918
  // entry-add/remove signal even on Windows NTFS. Without it, PR-comment
1919
1919
  // notifications, agent-failure summaries, follow-up build alerts, and
1920
1920
  // meeting-transcript dumps all lagged up to 10 s before appearing on
1921
- // the dashboard's inbox view. The meeting directory is still excluded
1922
- // (see "Files intentionally NOT tracked") because round transitions
1923
- // edit existing files in-place, which dir mtime can't detect.
1921
+ // the dashboard's inbox view.
1924
1922
  INBOX_DIR,
1923
+ // engine/qa-runs.json (surfaced by listRuns via fast-state qaRuns slice)
1924
+ // — new QA runs and status flips need to light the sidebar activity dot
1925
+ // within one SPA poll cycle. Single file, mtime advances on each write.
1926
+ path.join(ENGINE_DIR, 'qa-runs.json'),
1925
1927
  ];
1928
+ // meetings/<id>.json (surfaced by meeting.getMeetings) — round transitions
1929
+ // edit each file in-place via mutateMeeting, so the parent dir's mtime
1930
+ // does NOT advance on Windows. Tracking each file individually catches
1931
+ // in-file edits. Bounded by meeting count (typically <50 active); a 50-
1932
+ // meeting fleet adds ~50 statSync calls per cache miss — still cheap.
1933
+ // Filter `.backup` sidecars (from safe-write tempfile pattern) and
1934
+ // non-*.json entries so corrupted state can't pollute the registry.
1935
+ try {
1936
+ const meetingsDir = path.join(MINIONS_DIR, 'meetings');
1937
+ const entries = fs.readdirSync(meetingsDir);
1938
+ for (const f of entries) {
1939
+ if (f.endsWith('.json') && !f.endsWith('.backup.json')) {
1940
+ files.push(path.join(meetingsDir, f));
1941
+ }
1942
+ }
1943
+ } catch { /* meetings dir absent → no meetings to track */ }
1926
1944
  // Per-project work-items (surfaced by getWorkItems) and pull-requests
1927
1945
  // (surfaced by getPullRequests). The PR file was the biggest miss in the
1928
1946
  // original tracked list — PR status flips (running → passing, waiting →
@@ -1947,6 +1965,79 @@ function getStatusFastStateMtimePaths(config) {
1947
1965
  return files;
1948
1966
  }
1949
1967
 
1968
+ /**
1969
+ * Slow-state mtime tracker — symmetric with the fast-state registry above.
1970
+ *
1971
+ * Slow-state slices (`prdProgress`, `prd`, `verifyGuides`, `archivedPrds`,
1972
+ * `skills`, `mcpServers`, `schedules`, `pipelines`, `pinned`, `projects`,
1973
+ * `version`, etc.) live behind a 60 s TTL in `dashboard.js`. Without mtime
1974
+ * tracking, engine-driven writes to `prd/*.json`, `engine/schedule-runs.json`,
1975
+ * `engine/pipeline-runs.json`, or `prd/guides/*.md` waited up to the full
1976
+ * 60 s before surfacing in the dashboard — which produced the visible
1977
+ * "plan status doesn't update, pipelines never advance" symptom.
1978
+ *
1979
+ * The same conventions as fast-state apply:
1980
+ * - Entries must back something `_buildStatusSlowState()` reads; otherwise
1981
+ * the rebuild silently no-ops on detected change.
1982
+ * - Files mutated through a `mutate*` helper are reliable (`safeWrite`
1983
+ * rename advances mtime). Append-only logs reset the cache often but
1984
+ * are not relevant here.
1985
+ * - Per-project paths must use `shared.project*` so newly-added projects
1986
+ * are picked up without registry edits.
1987
+ *
1988
+ * Files intentionally NOT tracked here:
1989
+ * - `mcpServers`, version, autoMode, installId — change only on human/
1990
+ * CLI edits, which already pop the slow-state via reloadConfig + the
1991
+ * 60 s TTL.
1992
+ * - `~/.claude/skills/`, `~/.copilot/skills/` — user-home dirs that
1993
+ * `extractSkillsFromOutput` writes to from the agent-close path,
1994
+ * which already calls `invalidateStatusCache()` directly.
1995
+ * - project git state — already invalidated via the
1996
+ * `_setOnProjectGitStatusChanged` callback into `invalidateStatusCache`
1997
+ * (W-mpgrk5cy fix); also tracked in fast-state via `.git/logs/HEAD`.
1998
+ */
1999
+ function getStatusSlowStateMtimePaths(config) {
2000
+ const projects = getProjects(config || getConfig());
2001
+ const files = [
2002
+ // prd/*.json (surfaced by getPrdInfo) — engine writes via syncPrdFromPrs,
2003
+ // the materializer, and plan-to-prd outputs. Dir mtime advances on entry
2004
+ // add/remove; in-file edits use getPrdInfo's own per-file mtime cache so
2005
+ // small-content flips still surface via the 10 s TTL backstop, but the
2006
+ // big "new PRD created" event surfaces immediately.
2007
+ PRD_DIR,
2008
+ // prd/archive/*.json — manual archive moves PRDs here; dir mtime catches
2009
+ // the move.
2010
+ path.join(PRD_DIR, 'archive'),
2011
+ // prd/guides/*.md — verify agent writes new files here on E2E completion.
2012
+ path.join(MINIONS_DIR, 'prd', 'guides'),
2013
+ // engine/schedule-runs.json — scheduler rewrites this on every cron fire.
2014
+ path.join(ENGINE_DIR, 'schedule-runs.json'),
2015
+ // engine/pipeline-runs.json — pipeline executor rewrites this on each
2016
+ // stage transition (the most user-visible slow-state lag pre-fix).
2017
+ path.join(ENGINE_DIR, 'pipeline-runs.json'),
2018
+ // pipelines/*.json — pipeline definitions, edited by humans + plan agents.
2019
+ // Dir mtime is fine because pipeline edits are wholesale file replacements
2020
+ // (no in-place tweaks once a pipeline is authored).
2021
+ path.join(MINIONS_DIR, 'pipelines'),
2022
+ // pinned.md — single file, dashboard-side writes already call
2023
+ // invalidateStatusCache({includeSlow:true}); tracker entry catches any
2024
+ // CLI/editor edit that bypasses the API.
2025
+ path.join(MINIONS_DIR, 'pinned.md'),
2026
+ // engine/skill-states/ — engine writes when agents extract new skills.
2027
+ // Dir mtime catches new skill files. Skips per-skill in-place edits which
2028
+ // the agent-close invalidate already covers.
2029
+ SKILLS_DIR,
2030
+ ];
2031
+ // Per-project local skill dirs — agents extract project-scoped skills here.
2032
+ for (const p of projects) {
2033
+ if (p && p.localPath) {
2034
+ files.push(path.join(p.localPath, '.claude', 'skills'));
2035
+ files.push(path.join(p.localPath, '.github', 'skills'));
2036
+ }
2037
+ }
2038
+ return files;
2039
+ }
2040
+
1950
2041
  // ── Exports ─────────────────────────────────────────────────────────────────
1951
2042
 
1952
2043
  module.exports = {
@@ -1967,6 +2058,7 @@ module.exports = {
1967
2058
  _setOnProjectGitStatusChanged,
1968
2059
  // W-mpftp7na000td0f4 — engine→dashboard cache-invalidation registry
1969
2060
  getStatusFastStateMtimePaths,
2061
+ getStatusSlowStateMtimePaths,
1970
2062
 
1971
2063
  // Core state
1972
2064
  getConfig, getControl, getDispatch, getDispatchQueue, getDispatchCompletionReport, invalidateDispatchCache,
package/engine.js CHANGED
@@ -588,7 +588,23 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
588
588
  *
589
589
  * @returns {Promise<{skipped: boolean, reason?: string}>}
590
590
  */
591
- async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {}) {
591
+ async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {}, opts = {}) {
592
+ // W-mph6n4p00006ce38: Freshen origin/<mainRef> and fast-forward the
593
+ // worktree to it BEFORE the branch sync. Without this, reused worktrees
594
+ // inherit whatever stale local master the engine clone happens to be on,
595
+ // so downstream dep-merges (and zero-dep agent work) get layered onto a
596
+ // stale base. Shared-branch / useExistingBranch dispatches MUST NOT
597
+ // auto-pick-up master mid-flight, so the carve-out is explicit.
598
+ const { mainRef, isSharedBranch } = opts;
599
+ if (mainRef && !isSharedBranch) {
600
+ try { await shared.shellSafeGit(['fetch', 'origin', mainRef], { ...gitOpts, cwd: rootDir }); }
601
+ catch (e) { log('warn', `git: failed to fetch origin/${mainRef} during reuse-sync: ${e.message}`); }
602
+ try { await shared.shellSafeGit(['merge', `origin/${mainRef}`, '--no-edit', '--no-ff'], { ...gitOpts, cwd: worktreePath }); }
603
+ catch (e) {
604
+ log('warn', `git: failed to merge origin/${mainRef} into ${branchName} during reuse-sync: ${e.message}`);
605
+ try { await shared.shellSafeGit(['merge', '--abort'], { ...gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
606
+ }
607
+ }
592
608
  // ls-remote --exit-code returns 2 when no matching refs are found on the
593
609
  // remote. The probe only lists refs (no object transfer), so it's cheap
594
610
  // even on slow links.
@@ -1063,7 +1079,12 @@ async function spawnAgent(dispatchItem, config) {
1063
1079
  // (orphan/timeout retry before first push) would otherwise emit a
1064
1080
  // "couldn't find remote ref" warn pair on every reuse.
1065
1081
  _phaseT.reuseSyncStart = Date.now();
1066
- await syncReusedWorktree(rootDir, existingWt, branchName, _gitOpts);
1082
+ // W-mph6n4p00006ce38: hand syncReusedWorktree the main ref + shared-branch
1083
+ // flag so it can freshen origin/<mainRef> into the worktree (skipped on
1084
+ // shared-branch dispatches — see syncReusedWorktree's opts contract).
1085
+ const _reuseMainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
1086
+ const _reuseIsShared = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
1087
+ await syncReusedWorktree(rootDir, existingWt, branchName, _gitOpts, { mainRef: _reuseMainRef, isSharedBranch: _reuseIsShared });
1067
1088
  _phaseT.reuseSyncEnd = Date.now();
1068
1089
  } else {
1069
1090
  _phaseT.createWorktreeStart = Date.now();
@@ -1167,8 +1188,24 @@ async function spawnAgent(dispatchItem, config) {
1167
1188
  } else {
1168
1189
  log('info', `Creating worktree: ${worktreePath} on branch ${branchName}`);
1169
1190
  const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
1191
+ // W-mph6n4p00006ce38: mirror the pool-borrow path (~line 1110-1114)
1192
+ // — fetch fresh origin/<mainRef> and start the new branch off it,
1193
+ // not the local ref. Without this, fresh-create dispatches inherit
1194
+ // whatever stale local master the engine clone happens to be on
1195
+ // (most painful: long-lived engine processes between restarts).
1196
+ // Non-fatal: if the fetch fails (network blip, transient auth),
1197
+ // fall back to local mainRef so the dispatch still progresses;
1198
+ // the dep-merge phase's own fetch + the on-failure
1199
+ // `git reset --hard origin/<mainRef>` recovery remain as safety nets.
1200
+ let _freshCreateBase = mainRef;
1170
1201
  try {
1171
- await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, mainRef], _worktreeGitOpts, worktreeCreateRetries);
1202
+ await shared.shellSafeGit(['fetch', 'origin', mainRef], { ..._gitOpts, cwd: rootDir, timeout: 30000 });
1203
+ _freshCreateBase = `origin/${mainRef}`;
1204
+ } catch (mainFetchErr) {
1205
+ log('warn', `Failed to fetch origin/${mainRef} before fresh-create worktree for ${branchName}: ${mainFetchErr.message} — falling back to local ${mainRef}`);
1206
+ }
1207
+ try {
1208
+ await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, worktreeCreateRetries);
1172
1209
  } catch (e1) {
1173
1210
  const branchExists = e1.message?.includes('already exists');
1174
1211
  log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
@@ -1180,7 +1217,7 @@ async function spawnAgent(dispatchItem, config) {
1180
1217
  // Clean up partial worktree directory from failed attempt
1181
1218
  try { if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true }); } catch { /* optional */ }
1182
1219
  try {
1183
- await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, mainRef], _worktreeGitOpts, 0);
1220
+ await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, 0);
1184
1221
  } catch (e1b) {
1185
1222
  log('error', `Worktree -b retry also failed for ${branchName}: ${e1b.message?.split('\n')[0]}`);
1186
1223
  throw e1b;
@@ -1303,6 +1340,38 @@ async function spawnAgent(dispatchItem, config) {
1303
1340
  // of FAILURE_CLASS.MERGE_CONFLICT (which retries 3x against the
1304
1341
  // same broken auth path).
1305
1342
  let _depAuthFailed = false;
1343
+ // W-mph6n4p00006ce38: Refresh origin/<mainRef> + freshen the worktree
1344
+ // BEFORE the parallel dep fetches and preflight sim. Without this,
1345
+ // preflight + real dep-merges run against whatever stale local master
1346
+ // the engine clone happens to be on, producing false-positive
1347
+ // conflicts whenever master drifted ahead of the engine's last fetch.
1348
+ // The pool-borrow + fresh-create paths already start at fresh
1349
+ // origin/<mainRef>, but the reuse path can outrun its sync; and even
1350
+ // for the start-fresh paths, master can advance between worktree
1351
+ // creation and dep-merge (long-lived dispatches). Shared-branch
1352
+ // dispatches MUST NOT auto-pick-up master mid-flight — the carve-out
1353
+ // is explicit. Non-fatal: failures log + continue with the stale base,
1354
+ // and the existing on-failure `git reset --hard origin/<mainRef>`
1355
+ // recovery (~line 1452) remains the safety net.
1356
+ const _depMainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
1357
+ const _depIsSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
1358
+ if (!_failedRefCache.has(_depMainRef)) {
1359
+ try {
1360
+ await adoGitAuth.runAdoGit(project, ['fetch', 'origin', _depMainRef], { ..._gitOpts, cwd: rootDir });
1361
+ } catch (mainFetchErr) {
1362
+ log('warn', `Failed to fetch origin/${_depMainRef} before dep merge: ${mainFetchErr.message} — proceeding with stale base`);
1363
+ if (adoGitAuth.isAdoAuthFailure(mainFetchErr)) _depAuthFailed = true;
1364
+ _failedRefCache.add(_depMainRef);
1365
+ }
1366
+ }
1367
+ if (!_depIsSharedBranch) {
1368
+ try {
1369
+ await shared.shellSafeGit(['merge', `origin/${_depMainRef}`, '--no-edit', '--no-ff'], { ..._gitOpts, cwd: worktreePath });
1370
+ } catch (mainMergeErr) {
1371
+ log('warn', `Failed to merge origin/${_depMainRef} into ${branchName} before dep merge: ${mainMergeErr.message} — proceeding without main refresh`);
1372
+ try { await shared.shellSafeGit(['merge', '--abort'], { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
1373
+ }
1374
+ }
1306
1375
  // Fetch all dependency branches in parallel (git fetches are independent)
1307
1376
  const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
1308
1377
  const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2026",
3
+ "version": "0.1.2028",
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"