@yemi33/minions 0.1.2214 → 0.1.2216

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/bin/minions.js CHANGED
@@ -58,6 +58,7 @@
58
58
  const fs = require('fs');
59
59
  const path = require('path');
60
60
  const os = require('os');
61
+ const net = require('net');
61
62
  const { spawn, spawnSync, execSync } = require('child_process');
62
63
 
63
64
  const PKG_ROOT = path.resolve(__dirname, '..');
@@ -136,6 +137,24 @@ function killByPort(port) {
136
137
 
137
138
  const isPortListening = (port) => getListeningPids(port).length > 0;
138
139
 
140
+ /** Authoritative "is something accepting TCP connections on this port?" probe.
141
+ * Resolves true iff a connection to 127.0.0.1:port completes within timeoutMs.
142
+ * Unlike the netstat-based isPortListening, this can't false-negative under
143
+ * CPU/event-loop load: the TCP handshake is satisfied by the OS listen backlog
144
+ * even when the listener's JS event loop is momentarily blocked. Used by the
145
+ * watchdog to overturn a transient `down` verdict before restarting a live
146
+ * dashboard. Never throws; resolves false on any error/timeout. */
147
+ function tcpPortAccepts(port, timeoutMs = 1500) {
148
+ return new Promise((resolve) => {
149
+ let settled = false;
150
+ const done = (val) => { if (!settled) { settled = true; try { socket.destroy(); } catch {} resolve(val); } };
151
+ const socket = net.connect({ host: '127.0.0.1', port: Number(port) });
152
+ socket.once('connect', () => done(true));
153
+ socket.once('error', () => done(false));
154
+ socket.setTimeout(timeoutMs, () => done(false));
155
+ });
156
+ }
157
+
139
158
  /**
140
159
  * Wait until no process is listening on `port`, retrying a kill on each tick
141
160
  * for any stragglers that re-appeared (e.g. orphan child the original kill
@@ -630,6 +649,28 @@ const { cmd, rest, devMode, devPort } = (() => {
630
649
  const [firstCmd, ...restArgs] = out;
631
650
  return { cmd: firstCmd, rest: restArgs, devMode: dev, devPort: port || DEFAULT_DEV_DASH_PORT };
632
651
  })();
652
+
653
+ // ── Node / node:sqlite version gate (issue #244) ────────────────────────────
654
+ // node:sqlite is the only state backend post Phase 9.4 and exists only on
655
+ // Node >= 22.5; the --experimental-sqlite self-reexec at the top of this file
656
+ // is a no-op below 22.5. Without this gate, any command that touches getDb()
657
+ // (`minions status`, `queue`, engine delegations, …) dumps a raw node:sqlite
658
+ // stack trace. Fail closed with the canonical one-line remediation. Numeric
659
+ // version compare via shared helper — never string compares, never throws.
660
+ //
661
+ // Exempt the commands a stranded user still needs: `doctor` re-emits the same
662
+ // remediation as a structured FAIL (and suppresses the downstream fallout
663
+ // warnings); help/version are pure; uninstall/nuke must stay reachable to
664
+ // clean up. Everything else short-circuits before we spawn a child that would
665
+ // only crash with the same trace.
666
+ const NODE_GATE_EXEMPT = new Set([
667
+ 'doctor', 'version', '--version', '-v', 'help', '--help', '-h', 'uninstall', 'nuke',
668
+ ]);
669
+ if (!shared.nodeSupportsBuiltinSqlite() && cmd && !NODE_GATE_EXEMPT.has(cmd)) {
670
+ console.error(`\n ${shared.nodeSqliteRemediationLine()}\n`);
671
+ process.exit(1);
672
+ }
673
+
633
674
  let force = rest.includes('--force');
634
675
  const skipScan = rest.includes('--skip-scan');
635
676
  const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
@@ -1483,6 +1524,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1483
1524
  dashPort: DASHBOARD_PORT,
1484
1525
  readEnginePid,
1485
1526
  isPortListening,
1527
+ confirmPortUp: tcpPortAccepts,
1486
1528
  isStopIntentSet: shared.isStopIntentSet || (() => false),
1487
1529
  }).then(result => {
1488
1530
  // ALWAYS exit 0 — the scheduler must never see a failure for the
@@ -73,18 +73,18 @@ function _renderProjectBranch(p) {
73
73
  }
74
74
 
75
75
  // Renders a compact pill indicating the project's dispatch mode (W-mqgzcrln002613b3):
76
- // "Worktrees" (isolated — default, agents run in their own git worktree) vs
76
+ // "Worktrees" (worktree — default, agents run in their own git worktree) vs
77
77
  // "Live checkout" (live — agents run in-place inside the operator's working
78
- // tree). Driven by p.worktreeMode (shared.WORKTREE_MODES; engine defaults
79
- // unset → 'isolated'). Live is the riskier/special mode (capped to one mutating
78
+ // tree). Driven by p.checkoutMode (shared.CHECKOUT_MODES; engine defaults
79
+ // unset → 'worktree'). Live is the riskier/special mode (capped to one mutating
80
80
  // dispatch per project, refuses on a dirty tree) so it gets a more prominent
81
- // color than the muted isolated pill.
81
+ // color than the muted worktree pill.
82
82
  function _renderWorktreeModePill(p) {
83
83
  if (!p) return '';
84
- if (p.worktreeMode === 'live') {
84
+ if (p.checkoutMode === 'live') {
85
85
  return ' <span class="project-mode-pill project-mode-live" title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree">⚡ Live checkout</span>';
86
86
  }
87
- return ' <span class="project-mode-pill project-mode-isolated" title="Isolated dispatch mode (default) — each agent runs in its own git worktree">Worktrees</span>';
87
+ return ' <span class="project-mode-pill project-mode-isolated" title="Worktree dispatch mode (default) — each agent runs in its own git worktree">Worktrees</span>';
88
88
  }
89
89
 
90
90
  function _projectCachePath(project) {
@@ -259,20 +259,22 @@ async function openSettings() {
259
259
  '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
260
260
  '</div>' +
261
261
  '</div>';
262
- // P-a3f9b207 — per-project worktreeMode dropdown + warning chip. Default
263
- // 'isolated' (engine creates a dedicated worktree per dispatch); 'live'
264
- // runs the agent directly in p.localPath for repos where worktrees are
265
- // unworkable. Chip is hidden by default and toggled reactively below.
266
- var currentWtMode = (p.worktreeMode === 'live') ? 'live' : 'isolated';
267
- var wtModeSearch = 'worktree mode isolated live checkout dispatch';
262
+ // P-a3f9b207 (consolidated W-mqiaw974) — per-project checkoutMode dropdown
263
+ // + warning chip. Default 'worktree' (engine creates a dedicated worktree
264
+ // per dispatch); 'live' runs the agent directly in p.localPath for repos
265
+ // where worktrees are unworkable. p.checkoutMode is resolved server-side
266
+ // (honors the legacy worktreeMode field). Chip is hidden by default and
267
+ // toggled reactively below.
268
+ var currentWtMode = (p.checkoutMode === 'live') ? 'live' : 'worktree';
269
+ var wtModeSearch = 'checkout mode worktree isolated live dispatch';
268
270
  var worktreeModeBlock =
269
271
  '<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
270
- '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Worktree mode</label>' +
271
- '<select id="set-worktreeMode-' + escHtml(p.name) + '" data-worktree-mode-select="' + escHtml(p.name) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
272
- '<option value="isolated"' + (currentWtMode === 'isolated' ? ' selected' : '') + '>Isolated (default)</option>' +
272
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Checkout mode</label>' +
273
+ '<select id="set-checkoutMode-' + escHtml(p.name) + '" data-checkout-mode-select="' + escHtml(p.name) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
274
+ '<option value="worktree"' + (currentWtMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
273
275
  '<option value="live"' + (currentWtMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
274
276
  '</select>' +
275
- '<div data-worktree-mode-chip="' + escHtml(p.name) + '" style="' + (currentWtMode === 'live' ? '' : 'display:none;') + 'margin-top:6px;padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4">' +
277
+ '<div data-checkout-mode-chip="' + escHtml(p.name) + '" style="' + (currentWtMode === 'live' ? '' : 'display:none;') + 'margin-top:6px;padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4">' +
276
278
  '⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
277
279
  '</div>' +
278
280
  '</div>';
@@ -666,14 +668,14 @@ async function openSettings() {
666
668
  });
667
669
 
668
670
  // P-a3f9b207 — toggle the live-mode warning chip reactively when the
669
- // operator flips the per-project worktreeMode dropdown (before save). The
670
- // chip is rendered once with display:none for isolated projects and
671
+ // operator flips the per-project checkoutMode dropdown (before save). The
672
+ // chip is rendered once with display:none for worktree-mode projects and
671
673
  // visible for already-live projects; this handler only flips the
672
674
  // display style — no markup is regenerated.
673
- document.querySelectorAll('[data-worktree-mode-select]').forEach(function(sel) {
675
+ document.querySelectorAll('[data-checkout-mode-select]').forEach(function(sel) {
674
676
  sel.addEventListener('change', function() {
675
- const projName = sel.getAttribute('data-worktree-mode-select');
676
- const chip = document.querySelector('[data-worktree-mode-chip="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
677
+ const projName = sel.getAttribute('data-checkout-mode-select');
678
+ const chip = document.querySelector('[data-checkout-mode-chip="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
677
679
  if (!chip) return;
678
680
  chip.style.display = (sel.value === 'live') ? '' : 'none';
679
681
  });
@@ -1083,16 +1085,17 @@ async function saveSettings() {
1083
1085
  // Projects. Empty string = clear the override; the field stays optional.
1084
1086
  const mainBranchInput = document.getElementById('set-mainBranch-' + p.name);
1085
1087
  const mainBranchValue = mainBranchInput ? mainBranchInput.value.trim() : (p.mainBranch || '');
1086
- // P-a3f9b207 — per-project worktreeMode. Normalize to 'isolated' for
1087
- // any value other than 'live' so a stale DOM never POSTs garbage; the
1088
- // server-side validator (shared.validateWorktreeMode) is the
1089
- // authoritative gate for unknown values.
1090
- const wtModeInput = document.getElementById('set-worktreeMode-' + p.name);
1091
- const wtModeValue = (wtModeInput && wtModeInput.value === 'live') ? 'live' : 'isolated';
1088
+ // P-a3f9b207 (consolidated W-mqiaw974) — per-project checkoutMode.
1089
+ // Normalize to 'worktree' for any value other than 'live' so a stale DOM
1090
+ // never POSTs garbage; the server-side validator
1091
+ // (shared.validateCheckoutMode) is the authoritative gate for unknown
1092
+ // values.
1093
+ const wtModeInput = document.getElementById('set-checkoutMode-' + p.name);
1094
+ const wtModeValue = (wtModeInput && wtModeInput.value === 'live') ? 'live' : 'worktree';
1092
1095
  return {
1093
1096
  name: p.name,
1094
1097
  mainBranch: mainBranchValue || null,
1095
- worktreeMode: wtModeValue,
1098
+ checkoutMode: wtModeValue,
1096
1099
  workSources: {
1097
1100
  pullRequests: { enabled: document.getElementById('set-ws-prs-' + p.name)?.checked ?? true },
1098
1101
  workItems: { enabled: document.getElementById('set-ws-wi-' + p.name)?.checked ?? true }
package/dashboard.js CHANGED
@@ -358,17 +358,21 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
358
358
  } else {
359
359
  delete currentProject.mainBranch;
360
360
  }
361
- // P-a3f9b208 — mirror worktreeMode the same way: empty / unset on
362
- // candidate clears the field so the engine falls back to "isolated".
361
+ // P-a3f9b208 — mirror checkoutMode the same way: empty / unset on
362
+ // candidate clears the field so the engine falls back to "worktree".
363
363
  // Without this branch the validated POST-body update (mutated on
364
364
  // `candidate` in handleSettingsUpdate) is silently dropped on the way
365
365
  // through mergeSettingsConfigUpdate → mutateDashboardConfig and never
366
366
  // reaches disk — the endpoint would return 200 but persist nothing.
367
- if (Object.prototype.hasOwnProperty.call(candidateProject, 'worktreeMode')) {
368
- currentProject.worktreeMode = candidateProject.worktreeMode;
367
+ if (Object.prototype.hasOwnProperty.call(candidateProject, 'checkoutMode')) {
368
+ currentProject.checkoutMode = candidateProject.checkoutMode;
369
369
  } else {
370
- delete currentProject.worktreeMode;
370
+ delete currentProject.checkoutMode;
371
371
  }
372
+ // W-mqiaw974 (issue #241): the field was renamed from the legacy
373
+ // `worktreeMode`. Drop any stale legacy key on every settings save so a
374
+ // migrated project never carries both fields.
375
+ delete currentProject.worktreeMode;
372
376
  }
373
377
  }
374
378
  shared.pruneDefaultClaudeConfig(current);
@@ -1206,7 +1210,7 @@ const _PROJECT_LOCAL_FOOTGUN_WARNING =
1206
1210
  'git. A fresh `git worktree add` for a mutating dispatch will NOT see them, so ' +
1207
1211
  'the dispatched agent silently underperforms. Fix: commit them, move them to ' +
1208
1212
  'user scope (~/.claude/skills/..., ~/.copilot/skills/...), or flip the project ' +
1209
- 'to live-checkout mode (worktreeMode: live).';
1213
+ 'to live-checkout mode (checkoutMode: live).';
1210
1214
 
1211
1215
  function _walkUncommittedHarnessAssets(absStart, rootDir, projectName) {
1212
1216
  const results = [];
@@ -2351,8 +2355,9 @@ function _buildStatusSlowState() {
2351
2355
  branchMismatch,
2352
2356
  // P-a3f9b209 / W-mqgzcrln002613b3 — surface the per-project dispatch
2353
2357
  // mode so the Projects view can render a "Worktrees" vs "Live checkout"
2354
- // pill. Default to isolated when unset (matches engine spawn behavior).
2355
- worktreeMode: p.worktreeMode || shared.WORKTREE_MODES.ISOLATED,
2358
+ // pill. resolveCheckoutMode honors the legacy worktreeMode field and
2359
+ // defaults to 'worktree' when unset (matches engine spawn behavior).
2360
+ checkoutMode: shared.resolveCheckoutMode(p),
2356
2361
  };
2357
2362
  }),
2358
2363
  autoMode: {
@@ -8817,7 +8822,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8817
8822
  confirmToken: body.confirmToken,
8818
8823
  isValidToken: _consumeProjectConfirmToken,
8819
8824
  name: body.name,
8820
- worktreeMode: body.worktreeMode,
8825
+ checkoutMode: body.checkoutMode ?? body.worktreeMode,
8821
8826
  observeAuthors: Array.isArray(body.observeAuthors) ? body.observeAuthors : undefined,
8822
8827
  });
8823
8828
  } catch (e) {
@@ -10369,10 +10374,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10369
10374
  name: p.name,
10370
10375
  localPath: p.localPath || null,
10371
10376
  mainBranch: p.mainBranch || null,
10372
- // P-a3f9b207 — surface worktreeMode so the Settings UI can pre-fill
10373
- // the per-project dropdown. null === absent (engine defaults to
10374
- // 'isolated' downstream); 'live' opts into the live-checkout path.
10375
- worktreeMode: p.worktreeMode || null,
10377
+ // P-a3f9b207 — surface checkoutMode so the Settings UI can pre-fill
10378
+ // the per-project dropdown. resolveCheckoutMode honors the legacy
10379
+ // worktreeMode field; 'worktree' (default) or 'live'.
10380
+ checkoutMode: shared.resolveCheckoutMode(p),
10376
10381
  workSources: {
10377
10382
  pullRequests: { enabled: p.workSources?.pullRequests?.enabled !== false, cooldownMinutes: p.workSources?.pullRequests?.cooldownMinutes ?? 30 },
10378
10383
  workItems: { enabled: p.workSources?.workItems?.enabled !== false, cooldownMinutes: p.workSources?.workItems?.cooldownMinutes ?? 0 }
@@ -10755,21 +10760,27 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10755
10760
  if (raw) proj.mainBranch = raw;
10756
10761
  else delete proj.mainBranch;
10757
10762
  }
10758
- // P-a3f9b201 — per-project worktreeMode enum ('isolated' default,
10759
- // 'live' for repos where worktrees are unworkable). Empty string /
10760
- // null clears the override (engine falls back to 'isolated'); any
10761
- // other value flows through shared.validateWorktreeMode which throws
10762
- // HTTP 400 on unknown values. Errors propagate to the outer catch
10763
- // and are returned via e.statusCode/e.message.
10764
- if (Object.prototype.hasOwnProperty.call(update, 'worktreeMode')) {
10765
- const raw = update.worktreeMode;
10766
- if (raw === '' || raw === null) {
10767
- delete proj.worktreeMode;
10763
+ // P-a3f9b201 (consolidated W-mqiaw974) — per-project checkoutMode enum
10764
+ // ('worktree' default, 'live' for repos where worktrees are
10765
+ // unworkable). Empty string / null clears the override (engine falls
10766
+ // back to 'worktree'); any other value flows through
10767
+ // shared.validateCheckoutMode which throws HTTP 400 on unknown values
10768
+ // (and silently coerces the legacy 'isolated' → 'worktree'). Accepts
10769
+ // the legacy `worktreeMode` key for back-compat. Errors propagate to
10770
+ // the outer catch and are returned via e.statusCode/e.message.
10771
+ const hasCheckoutMode = Object.prototype.hasOwnProperty.call(update, 'checkoutMode');
10772
+ const hasLegacyMode = Object.prototype.hasOwnProperty.call(update, 'worktreeMode');
10773
+ if (hasCheckoutMode || hasLegacyMode) {
10774
+ const raw = hasCheckoutMode ? update.checkoutMode : update.worktreeMode;
10775
+ if (raw === '' || raw === null || raw === undefined) {
10776
+ delete proj.checkoutMode;
10768
10777
  } else {
10769
- const validated = shared.validateWorktreeMode(raw);
10770
- if (validated === undefined) delete proj.worktreeMode;
10771
- else proj.worktreeMode = validated;
10778
+ const validated = shared.validateCheckoutMode(raw);
10779
+ if (validated === undefined) delete proj.checkoutMode;
10780
+ else proj.checkoutMode = validated;
10772
10781
  }
10782
+ // Drop the legacy field so a migrated project never carries both.
10783
+ delete proj.worktreeMode;
10773
10784
  }
10774
10785
  }
10775
10786
  }
@@ -10909,6 +10920,28 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10909
10920
  } catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
10910
10921
  }
10911
10922
 
10923
+ // W-mqidhwcc000m897e — read-only ADO token-acquisition health snapshot.
10924
+ // Surfaces the graduated-backoff + transient/persistent classification state
10925
+ // so an operator can diagnose "ADO polling paused" from one place:
10926
+ // { token: { lastSuccessAt, lastFailureReason, classification,
10927
+ // backoffUntil, consecutiveFailures } }.
10928
+ async function handleDiagnosticsAdoToken(req, res) {
10929
+ try {
10930
+ let token = { lastSuccessAt: 0, lastFailureReason: null, classification: null, backoffUntil: 0, consecutiveFailures: 0 };
10931
+ if (typeof ado.getAdoTokenHealth === 'function') {
10932
+ const h = ado.getAdoTokenHealth() || {};
10933
+ token = {
10934
+ lastSuccessAt: Number(h.lastSuccessAt) || 0,
10935
+ lastFailureReason: h.lastFailureReason || null,
10936
+ classification: h.classification || null,
10937
+ backoffUntil: Number(h.backoffUntil) || 0,
10938
+ consecutiveFailures: Number(h.consecutiveFailures) || 0,
10939
+ };
10940
+ }
10941
+ return jsonReply(res, 200, { token });
10942
+ } catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
10943
+ }
10944
+
10912
10945
  // P-c3d4e5f6 — /api/diagnostics/memory.
10913
10946
  // Returns the latest in-process dashboard sample, the latest engine
10914
10947
  // sample (read fresh from engine/diagnostics-memory.json via safeJsonObj),
@@ -13651,6 +13684,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13651
13684
  { method: 'POST', path: '/api/diagnostics/refresh', desc: 'Append a dashboard refresh-diagnostic ring buffer batch to engine/dashboard-diagnostics.log (rotated at 1 MB)', params: 'entries[]', handler: handleDiagnosticsRefresh },
13652
13685
  // Diagnostics — per-org ADO throttle state (W-mq03l6zh0006f0a1-d).
13653
13686
  { method: 'GET', path: '/api/diagnostics/ado-throttle', desc: 'Snapshot of per-org ADO throttle tracker state — { orgs: { [orgBase]: { throttled, retryAfter, consecutiveHits } } }. Falls back to a single `global` key when running against pre-per-org engines.', handler: handleDiagnosticsAdoThrottle },
13687
+ { method: 'GET', path: '/api/diagnostics/ado-token', desc: 'Read-only ADO token-acquisition health — { token: { lastSuccessAt, lastFailureReason, classification, backoffUntil, consecutiveFailures } }. Surfaces the graduated-backoff + transient/persistent classification state (W-mqidhwcc000m897e) so an operator can diagnose paused ADO polling from one line.', handler: handleDiagnosticsAdoToken },
13654
13688
  // Diagnostics — engine + dashboard memory baseline (P-c3d4e5f6).
13655
13689
  { method: 'GET', path: '/api/diagnostics/memory', desc: 'Latest in-process dashboard memory sample plus the most-recent engine sample read from engine/diagnostics-memory.json. engineStale=true when the sidecar is missing or its capturedAt is > 5 min old.', handler: handleDiagnosticsMemory },
13656
13690
  { method: 'GET', path: '/api/diagnostics/memory/history', desc: 'In-memory ring buffer of memory samples. process=dashboard returns the dashboard\'s own collector (populated by startPeriodicSampling on boot). process=engine returns the dashboard\'s polled accumulation of engine/diagnostics-memory.json — engine.js only persists the latest sample to the sidecar, so engine-side history is rebuilt by the dashboard poller (dedup by capturedAt). Optional limit caps returned newest-N samples.', params: 'process (engine|dashboard), limit?', handler: handleDiagnosticsMemoryHistory },
package/docs/README.md CHANGED
@@ -15,6 +15,7 @@ Architecture, design proposals, and lifecycle references for people working on t
15
15
 
16
16
  - [branch-derivation.md](branch-derivation.md) — Engine-side branch fallback (`work/<wi-id>`) vs. agent-authored long form, the structured-vs-loose PR-pointer extractors, and the canonical PR-fix duplication incident.
17
17
  - [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
18
+ - [specs/agent-configurability.md](specs/agent-configurability.md) — Design spec for the user-configurable Role / Agent Library: Phase 1 exposes & edits each role's definition + skills; Phase 2 adds role/agent CRUD. Covers the additive `config.roles` model, resolver tiering, migration from per-agent `charter.md`, and API/UI surface.
18
19
  - [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
19
20
  - [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
20
21
  - [constellation-bridge.md](constellation-bridge.md) — Read-only cross-repo bridge: `engine.constellationBridge.enabled` flag, marker-file contract, and the `minions bridge` subcommand for local debugging.
@@ -30,7 +31,7 @@ Architecture, design proposals, and lifecycle references for people working on t
30
31
  - [harness-transparency.md](harness-transparency.md) — The `harnessUsed` self-report contract: capture (agent reports the skills / MCPs / commands / docs it used) → ground (engine cross-checks against `_harnessPropagated` and annotates `grounded:true\|false`, never dropping) → surface (PR comment, notes/inbox digest, work-item modal).
31
32
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
32
33
  - [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
33
- - [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `worktreeMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
34
+ - [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `checkoutMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
34
35
  - [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
35
36
  - [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
36
37
  - [pr-auto-fix-dispatch.md](pr-auto-fix-dispatch.md) — Short reference table mapping each PR auto-fix / review dispatch site in `engine.js#discoverFromPrs` to its gate flag, plus the `pollingPaused` / `autoFixPaused` master kill-switches and the per-provider polling gates.
@@ -142,6 +142,18 @@
142
142
  "targetRemovalDate": "2026-06-16",
143
143
  "notes": "Unlike the three record-field aliases, nothing is written here — this is purely an input read-fallback. Removal scope (DEFERRED — gated on confirming no client still POSTs `autoObserve`, not on the expired calendar date): drop the `!body.autoObserve` fallback in the link handler in dashboard.js, drop `autoObserve?` from the route registry `params` string, and update any client (dashboard JS, ops scripts) that still POSTs `autoObserve`. After removal, callers that still send `autoObserve` will see their value silently ignored."
144
144
  },
145
+ {
146
+ "id": "worktreemode-field-rename",
147
+ "description": "Legacy `project.worktreeMode` config field (enum 'isolated'|'live'). Consolidated by W-mqiaw974 (issue #241) into a single `project.checkoutMode` field (enum 'worktree'|'live'): the old 'isolated' value became the implicit default 'worktree', and the overlapping/never-shipped 'shared' value was removed. The write side is migrated — buildProjectEntry, dashboard settings POST, and projects.addProject all write `checkoutMode`, and the dashboard settings/merge paths delete any stale `worktreeMode` key on save. What survives is a READ-BRIDGE ONLY: `shared.resolveCheckoutMode(project)` (and the `isLiveCheckoutProject` predicate built on it) reads canonical `checkoutMode` first, then falls back to the legacy `worktreeMode` field ('isolated'→'worktree', 'live'→'live') so an un-migrated config.json keeps dispatching live-checkout projects correctly. `validateCheckoutMode` also silently coerces a submitted legacy 'isolated' value to 'worktree'.",
148
+ "code": [
149
+ { "file": "engine/shared.js", "note": "resolveCheckoutMode reads project.worktreeMode as the fallback when checkoutMode is absent; validateCheckoutMode coerces 'isolated'→'worktree'. The only surviving reads of the legacy field are these two back-compat bridges." },
150
+ { "file": "engine/projects.js", "note": "addProject threads options.checkoutMode ?? options.worktreeMode into buildProjectEntry (accepts the legacy options key)." },
151
+ { "file": "dashboard.js", "note": "handleProjectsAdd reads body.checkoutMode ?? body.worktreeMode; handleSettingsUpdate + mergeSettingsConfigUpdate accept the legacy update.worktreeMode key and delete proj.worktreeMode on every save (active migration)." }
152
+ ],
153
+ "deprecated": "2026-06-17",
154
+ "targetRemovalDate": "2026-09-17",
155
+ "notes": "Safe to remove on or after 2026-09-17 (90 days, ~3 release windows) once a sweep of every persisted config.json confirms no `worktreeMode` key remains (the dashboard settings save actively migrates each project the next time it is touched, so the residue shrinks over time) AND no external tooling reads `project.worktreeMode`. Removal scope: drop the legacy-field fallback in shared.resolveCheckoutMode, the 'isolated' coercion in validateCheckoutMode, the `?? options.worktreeMode` / `?? body.worktreeMode` input fallbacks in projects.addProject + dashboard handleProjectsAdd, the `update.worktreeMode` acceptance + `delete proj.worktreeMode` migration in dashboard handleSettingsUpdate/mergeSettingsConfigUpdate, and the legacy back-compat tests in test/unit/{worktree-mode-schema,settings-worktree-mode,live-checkout-mode}.test.js."
156
+ },
145
157
  {
146
158
  "id": "pr-observe-observe-body-param",
147
159
  "description": "Legacy `observe` body parameter on `POST /api/pull-requests/observe`. The W-mq5s5ttx000j7ab8 endpoint sub-WI introduces canonical `contextOnly` as the inverse (`observe: false` ⇔ `contextOnly: true`) and keeps `observe` accepted for backward compat. Registering the deprecation here so the alias has a documented removal path; the WI explicitly notes this entry is the implementer's call (it is kept for backward compat and may live longer than the underscore-prefixed record fields).",
@@ -61,7 +61,7 @@ cwd**. Project-scope skills like `<repo>/.claude/skills/foo/SKILL.md` are
61
61
  loaded by the CLI only if `<cwd>/.claude/skills/foo/SKILL.md` exists at
62
62
  spawn time. See *The worktree-uncommitted footgun* below.
63
63
 
64
- Live-checkout mode (`project.worktreeMode: 'live'`) collapses both branches
64
+ Live-checkout mode (`project.checkoutMode: 'live'`) collapses both branches
65
65
  to `cwd = project.localPath` for every dispatch type, so the agent sees
66
66
  the operator's working tree as-is (including uncommitted assets). The
67
67
  tradeoff is single-mutating-dispatch concurrency per project — see
@@ -149,7 +149,7 @@ so the only workarounds today are:
149
149
  - **Move it user-scope.** Drop it under `~/.claude/skills/bar/` instead —
150
150
  the CLI's user-skill discovery still works inside a worktree because
151
151
  `--add-dir` attaches `~/.claude`.
152
- - **Flip to live-checkout mode.** Set `project.worktreeMode = 'live'` on
152
+ - **Flip to live-checkout mode.** Set `project.checkoutMode = 'live'` on
153
153
  the project so every dispatch runs in `project.localPath`. Caveats in
154
154
  `docs/live-checkout-mode.md`.
155
155
 
@@ -263,7 +263,7 @@ hand; `collectSkillFiles` / `collectCommandFiles` already do.
263
263
  ## Related docs
264
264
 
265
265
  - `docs/runtime-adapters.md` — full adapter interface table.
266
- - `docs/live-checkout-mode.md` — `project.worktreeMode: 'live'`
266
+ - `docs/live-checkout-mode.md` — `project.checkoutMode: 'live'`
267
267
  contract (alternative to the worktree-add-dir story for repos where
268
268
  worktrees are unworkable).
269
269
  - `docs/skills.md` — skill block format and auto-extraction targets.
@@ -1,12 +1,14 @@
1
1
  # Live-checkout dispatch mode
2
2
 
3
- > Per-project opt-in (`project.worktreeMode: 'live'`) that runs Minions agents directly inside the operator's project checkout instead of an engine-managed git worktree.
3
+ > Per-project opt-in (`project.checkoutMode: 'live'`) that runs Minions agents directly inside the operator's project checkout instead of an engine-managed git worktree.
4
4
  >
5
- > Plan: `plan-w-mq5rmtt9000a42f9-2026-06-08`. PRD: `prd/minions-opg-2026-06-10.json` (items `P-a3f9b201` … `P-a3f9b209`). Default for every project remains `'isolated'`; the rest of this doc only applies when a project flips itself to `'live'`.
5
+ > Plan: `plan-w-mq5rmtt9000a42f9-2026-06-08`. PRD: `prd/minions-opg-2026-06-10.json` (items `P-a3f9b201` … `P-a3f9b209`). Default for every project remains `'worktree'`; the rest of this doc only applies when a project flips itself to `'live'`.
6
+ >
7
+ > **Field consolidation (W-mqiaw974 / issue #241).** The dispatch-mode field was renamed from the legacy `worktreeMode` (`'isolated'`|`'live'`) to a single `checkoutMode` (`'worktree'`|`'live'`). `'isolated'` became the implicit `'worktree'` (default). `shared.resolveCheckoutMode(project)` reads the canonical `checkoutMode` first and falls back to the legacy `worktreeMode` (`'isolated'`→`'worktree'`, `'live'`→`'live'`), so existing live-checkout configs keep working with no rewrite. See `docs/deprecated.json` (id `worktreemode-field-rename`).
6
8
 
7
9
  ## Motivation
8
10
 
9
- The default `isolated` mode spawns each dispatch inside its own git worktree under `../worktrees/`. That works for almost every repo, but it falls over when:
11
+ The default `worktree` mode spawns each dispatch inside its own git worktree under `../worktrees/`. That works for almost every repo, but it falls over when:
10
12
 
11
13
  - **`repo`-managed multi-project trees** (Android AOSP / Office mobile / Chromium) where the manifest pins dozens of nested repos and `git worktree add` either errors out or strands sub-projects.
12
14
  - **Submodule-heavy repos** where worktrees skip `.gitmodules` configuration or leave submodules pointing at the wrong SHA.
@@ -60,9 +62,9 @@ Pool short-circuits live in `engine.js:1368` and `engine/cleanup.js`; both gate
60
62
  ### Enabling live mode (dashboard)
61
63
 
62
64
  1. Open the Minions dashboard → **Settings** → **Projects** → expand the target project.
63
- 2. Set **Worktree mode** → **Live checkout**. A yellow warning chip appears immediately:
65
+ 2. Set **Checkout mode** → **Live checkout**. A yellow warning chip appears immediately:
64
66
  > ⚠ Live mode: dispatches run directly in this repo's checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.
65
- 3. Click **Save**. The dashboard POSTs the change through `mergeSettingsConfigUpdate`; `shared.validateWorktreeMode` rejects anything other than `'isolated'` or `'live'` with HTTP 400.
67
+ 3. Click **Save**. The dashboard POSTs the change through `mergeSettingsConfigUpdate`; `shared.validateCheckoutMode` rejects anything other than `'worktree'` or `'live'` with HTTP 400 (the legacy `'isolated'` value is silently coerced to `'worktree'`).
66
68
 
67
69
  ### Enabling live mode (config.json)
68
70
 
@@ -71,13 +73,13 @@ Pool short-circuits live in `engine.js:1368` and `engine/cleanup.js`; both gate
71
73
  "projects": [{
72
74
  "name": "android-aosp",
73
75
  "localPath": "/home/yemi/aosp",
74
- "worktreeMode": "live",
76
+ "checkoutMode": "live",
75
77
  // …
76
78
  }]
77
79
  }
78
80
  ```
79
81
 
80
- Absent / `null` / `''` reads as `'isolated'` (the default) — explicit is preferred.
82
+ Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is preferred. A legacy `"worktreeMode": "live"` is still honored (and `"worktreeMode": "isolated"` reads as `'worktree'`), but new configs should use `checkoutMode`.
81
83
 
82
84
  ### Recovering from `live_checkout_dirty` refusal
83
85
 
@@ -119,11 +121,11 @@ The engine has no opinion about local branches; this hygiene is the operator's r
119
121
 
120
122
  Live-checkout mode is deliberately small. These are NOT supported and will not be added:
121
123
 
122
- - **No `auto` mode.** The choice between `isolated` and `live` is per-project and operator-set. The engine will not auto-detect submodules / `repo` workspaces and silently switch modes.
124
+ - **No `auto` mode.** The choice between `worktree` and `live` is per-project and operator-set. The engine will not auto-detect submodules / `repo` workspaces and silently switch modes.
123
125
  - **No auto-stash on dirty refusal.** The engine refuses and exits; it never `git stash`es to "make room" for a dispatch. Stashes silently mutate the operator's tree and conflate engine state with operator state.
124
126
  - **No concurrent dispatches per project.** The cap is 1; raising it would require per-WI subdirectories, which live mode explicitly does not provide.
125
- - **No per-WI subdirectory isolation.** Live mode is one-checkout-per-project by design. If you need isolation, use `worktreeMode: 'isolated'` (the default).
126
- - **No per-WI override.** `worktreeMode` is per-project only. There is no `meta.worktreeMode` on a work item that overrides the project setting.
127
+ - **No per-WI subdirectory isolation.** Live mode is one-checkout-per-project by design. If you need isolation, use `checkoutMode: 'worktree'` (the default).
128
+ - **No per-WI override.** `checkoutMode` is per-project only. There is no `meta.checkoutMode` on a work item that overrides the project setting.
127
129
  - **No auto-pull / no fast-forward on existing branches.** See Guarantee 3. If a PR branch is checked out locally at a different SHA than `origin/<branch>`, the operator resolves it manually.
128
130
  - **No special timeout / kill handling.** Live-mode dispatches are killed by PID exactly like isolated-mode dispatches (`engine/timeout.js` header comment). The engine sends SIGTERM/SIGKILL to the tracked process and never touches the working tree on kill.
129
131
 
@@ -131,13 +133,13 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
131
133
 
132
134
  | File | Purpose |
133
135
  |---|---|
134
- | `engine/shared.js` — `WORKTREE_MODES`, `validateWorktreeMode` | Enum + validator (P-a3f9b201). |
136
+ | `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
135
137
  | `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
136
138
  | `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper: dirty check, branch resolution from HEAD (no fetch, no `origin/<mainRef>` — issue #226) (P-a3f9b203). |
137
139
  | `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
138
140
  | `engine.js` — dispatcher `liveProjectsInUse` set | Per-project mutating-concurrency cap (P-a3f9b205). |
139
141
  | `engine.js` — worktree-pool / orphan-GC short-circuits | `worktreePath===null` no-ops in live mode (P-a3f9b206). |
140
- | `dashboard/js/settings.js` — worktreeMode dropdown + chip | Operator-facing UI (P-a3f9b207). |
142
+ | `dashboard/js/settings.js` — checkoutMode dropdown + chip | Operator-facing UI (P-a3f9b207). |
141
143
  | `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring}.test.js` | Wiring and contract tests (P-a3f9b208). |
142
144
  | `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` | Non-retryable refusal class. |
143
145
  | `engine/dispatch.js` — `isRetryableFailureReason` neverRetry | Excludes `LIVE_CHECKOUT_DIRTY` from mechanical retry. |