@yemi33/minions 0.1.2215 → 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
@@ -649,6 +649,28 @@ const { cmd, rest, devMode, devPort } = (() => {
649
649
  const [firstCmd, ...restArgs] = out;
650
650
  return { cmd: firstCmd, rest: restArgs, devMode: dev, devPort: port || DEFAULT_DEV_DASH_PORT };
651
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
+
652
674
  let force = rest.includes('--force');
653
675
  const skipScan = rest.includes('--skip-scan');
654
676
  const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
@@ -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
  }
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. |
@@ -0,0 +1,271 @@
1
+ # Spec: Agent & Role Configurability
2
+
3
+ **Status:** Draft
4
+ **Owner:** @calebt
5
+ **Audience:** Minions contributors working on the dashboard, engine config surface, and playbook/system-prompt rendering.
6
+
7
+ > Companion reading: [`.github/copilot-instructions.md`](../../.github/copilot-instructions.md) and [`CLAUDE.md`](../../CLAUDE.md) for the engine/dashboard split and config conventions.
8
+
9
+ ## 1. Motivation
10
+
11
+ Today a Minions "team" is a fixed roster of five hard-coded agents (`ripley`, `dallas`, `lambert`, `rebecca`, `ralph`) whose personas live in a mix of places: a free-text `role` string in `config.json`, a loosely-related `skills` string array, and a per-agent `charter.md` file. There is **no way to**:
12
+
13
+ - See an agent's full definition (persona + skills + runtime) in one place.
14
+ - Treat a "role" as a reusable, editable unit — it is just a label string today.
15
+ - Add a brand-new role, or stand up a new named agent, from the dashboard.
16
+
17
+ The goal is to make the fleet **user-configurable** through a **Role / Agent Library**, delivered in two phases:
18
+
19
+ - **Phase 1 — Expose & edit.** Surface the skill + definition for each existing role/agent in one editable library view. No new roles or agents yet; just make what exists first-class and editable.
20
+ - **Phase 2 — Create & assign.** Add new roles, and stand up new named agents bound to a role.
21
+
22
+ ## 2. Current state (grounding)
23
+
24
+ This section is the factual baseline the design must respect. Citations are `file:line` against the repo at spec-authoring time.
25
+
26
+ ### 2.1 Agent object
27
+
28
+ Agents live under `config.agents` keyed by lowercase id. Defaults are hard-coded:
29
+
30
+ ```js
31
+ // engine/shared.js:4350-4356
32
+ const DEFAULT_AGENTS = {
33
+ ripley: { name: 'Ripley', emoji: '🏗️', role: 'Lead / Explorer', skills: ['architecture', 'codebase-exploration', 'design-review'] },
34
+ dallas: { name: 'Dallas', emoji: '🔧', role: 'Engineer', skills: ['implementation', 'typescript', 'docker', 'testing'] },
35
+ lambert: { name: 'Lambert', emoji: '📊', role: 'Analyst', skills: ['gap-analysis', 'requirements', 'documentation'] },
36
+ rebecca: { name: 'Rebecca', emoji: '🧠', role: 'Architect', skills: ['system-design', 'api-design', 'scalability', 'implementation'] },
37
+ ralph: { name: 'Ralph', emoji: '⚙️', role: 'Engineer', skills: ['implementation', 'bug-fixes', 'testing', 'scaffolding'] },
38
+ };
39
+ ```
40
+
41
+ Runtime / budget fields are **optional per-agent overrides** resolved against `engine.*` fleet defaults via helpers in `engine/shared.js`:
42
+
43
+ | Field | Resolver | Falls back to |
44
+ |-------|----------|---------------|
45
+ | `cli` | `resolveAgentCli` (shared.js:3142) | `engine.defaultCli` → `'copilot'` |
46
+ | `model` | `resolveAgentModel` (shared.js:3202) | `engine.defaultModel` → undefined |
47
+ | `maxBudgetUsd` / `monthlyBudgetUsd` | `resolveAgentMaxBudget` (shared.js:3231) | `engine.maxBudgetUsd` |
48
+ | `bareMode` | `resolveAgentBareMode` (shared.js:3254) | `engine.claudeBareMode` → false |
49
+
50
+ So the effective agent shape is:
51
+
52
+ ```jsonc
53
+ { "name": "Dallas", "emoji": "🔧", "role": "Engineer",
54
+ "skills": ["implementation","testing"],
55
+ "cli": "claude", "model": "haiku", "monthlyBudgetUsd": 5, "bareMode": false }
56
+ ```
57
+
58
+ ### 2.2 "Role" is a label, not an entity
59
+
60
+ `role` is a **free-text string** on the agent. There is no role registry, no role schema, and routing does **not** reference roles — it references agent ids. The dashboard edits role as a plain text input (`dashboard/js/settings.js:72`) and persists it verbatim (`dashboard.js:10684`).
61
+
62
+ ### 2.3 `skills` is metadata, not real skills
63
+
64
+ Two unrelated things share the word "skill":
65
+
66
+ 1. **`agent.skills`** — a string array, surfaced only as an `Expertise: …` line in the system prompt (`engine/playbook.js:950`). It is **not** linked to any executable capability.
67
+ 2. **Real `SKILL.md` files** — discovered from disk (`.claude/skills/`, `~/.claude/skills`, project harness dirs) by `collectSkillFiles()` / `getSkills()` (`engine/queries.js:1263-1400`), scoped global/project/plugin. These are **not** bound to a specific agent or role; every agent sees the same discovered catalog.
68
+
69
+ This split is a key source of the "agents aren't really configurable" feeling and must be addressed explicitly (see §4.3).
70
+
71
+ ### 2.4 Charter = the real "agent definition"
72
+
73
+ The substantive persona/instructions for an agent is `agents/<id>/charter.md`, read by `getAgentCharter()` (`engine/queries.js:677-678`) and injected into the system prompt as `## Your Charter` (`engine/playbook.js:942-954`). The system prompt assembled in `buildSystemPrompt()` is:
74
+
75
+ ```
76
+ # You are {name} ({role})
77
+ Agent ID: {id}
78
+ Expertise: {skills joined}
79
+
80
+ ## Your Charter
81
+ {charter.md contents}
82
+ ```
83
+ (`engine/playbook.js:940-955`)
84
+
85
+ Charter is already editable today, but only from the **agent detail panel → Charter tab** (`dashboard/js/detail-panel.js:115-126`) via `POST /api/agents/charter` (`dashboard.js:13205-13211`). It is disconnected from the Settings → Agents table.
86
+
87
+ ### 2.5 Routing
88
+
89
+ `routing.md` is a markdown table mapping work-type → preferred/fallback **agent id** (`routing.md:8-27`), parsed by `engine/routing.js:31-52`. `resolveAgent()` (`engine/routing.js:192`) resolves the route, honoring `_author_` / `_any_` tokens, idleness, budget, and the self-review ban. Routing is edited via `POST /api/settings/routing`, which overwrites `routing.md` wholesale (`dashboard.js:10799-10805`).
90
+
91
+ ### 2.6 Config read / write surface
92
+
93
+ - **Read:** `queries.getConfig()` (`engine/queries.js:181-225`); Settings read endpoint returns `{ engine, claude, agents, projects, routing }` (`dashboard.js:10362-10391`).
94
+ - **Write:** `POST /api/settings` edits `engine`, `claude`, **existing** `agents[id]` (role, skills, cli, model, budget — `dashboard.js:10682-10705`), and projects, then saves `config.json`. **There is no agent create/delete path** — the loop `for (const [id, updates] of Object.entries(body.agents))` skips unknown ids (`dashboard.js:10683`).
95
+ - **Reset:** restores `engine` defaults + `shared.DEFAULT_AGENTS` (`dashboard.js:10808-10815`).
96
+ - **UI:** Settings → Runtime pane has an **edit-only** Agents table (Role, Skills, CLI, Model, Budget) — `dashboard/js/settings.js:152-157`. No add/remove.
97
+
98
+ ### 2.7 Gaps summary
99
+
100
+ | Capability | Today |
101
+ |------------|-------|
102
+ | View an agent's full definition in one place | ✗ (split across settings table + charter tab) |
103
+ | Role as a reusable, editable unit | ✗ (free-text label) |
104
+ | Edit charter from the library/settings | ✗ (only detail panel) |
105
+ | Bind skills to a role/agent meaningfully | ✗ (metadata only) |
106
+ | Add a new role | ✗ |
107
+ | Add a new named agent | ✗ |
108
+ | Delete an agent | ✗ |
109
+
110
+ ## 3. Target model: the Role / Agent Library
111
+
112
+ Introduce a **Role** as a first-class, reusable definition, and make **Agents** named instances that reference a role and optionally override pieces of it.
113
+
114
+ ```jsonc
115
+ // config.json (additive — see §5 migration)
116
+ {
117
+ "roles": {
118
+ "engineer": {
119
+ "label": "Engineer", // human-facing name
120
+ "definition": "<markdown persona>", // the role-level charter (system-prompt body)
121
+ "skills": ["implementation", "testing", "bug-fixes"],
122
+ "defaultCli": null, // optional role-level runtime defaults
123
+ "defaultModel": null,
124
+ "builtin": true // seeded from DEFAULT_ROLES; protects from delete in P1
125
+ },
126
+ "architect": { "label": "Architect", "definition": "…", "skills": ["system-design","api-design"] }
127
+ },
128
+ "agents": {
129
+ "dallas": {
130
+ "name": "Dallas", "emoji": "🔧",
131
+ "role": "engineer", // now references roles{} key (was free text)
132
+ "skills": [], // agent-level ADDITIONS to the role's skills
133
+ "definitionOverride": null, // optional per-agent charter override
134
+ "cli": null, "model": null, "monthlyBudgetUsd": null, "bareMode": false
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ **Resolution rules (engine):**
141
+
142
+ - Effective **definition** = `agent.definitionOverride` ?? `roles[agent.role].definition` ?? legacy `agents/<id>/charter.md`. (Keeps existing charters working — see §5.)
143
+ - Effective **skills** = union of `roles[agent.role].skills` and `agent.skills`.
144
+ - Effective **label** shown in `# You are {name} ({label})` = `roles[agent.role].label` ?? raw `agent.role` string (legacy).
145
+ - Effective **cli/model** chain unchanged, with an inserted role tier: `agent.cli` → `roles[agent.role].defaultCli` → `engine.defaultCli` → `'copilot'`.
146
+
147
+ This preserves every existing resolver's external behavior when `roles` is absent or when `agent.role` is still a free-text string (legacy), and layers the library in additively.
148
+
149
+ ## 4. Phase 1 — Expose & edit (no new roles/agents)
150
+
151
+ **User story:** "For each of the different roles, I want to see and edit the skill and agent definition."
152
+
153
+ ### 4.1 Scope
154
+
155
+ - A **Role / Agent Library** view in the dashboard listing the existing roles and the agents bound to each.
156
+ - For each role: edit its **definition** (markdown persona) and **skills** list.
157
+ - For each agent: edit its definition override + skills + runtime (consolidating the Settings table fields and the Charter tab into one place).
158
+ - **No** create/delete of roles or agents in Phase 1.
159
+
160
+ ### 4.2 Data model (Phase 1)
161
+
162
+ - Add `DEFAULT_ROLES` to `engine/shared.js`, derived from the distinct `role` strings in `DEFAULT_AGENTS` (`Lead / Explorer`, `Engineer`, `Analyst`, `Architect`). Seed each role's `definition` from the corresponding agent's existing `charter.md` content where a 1:1 mapping exists; otherwise leave a stub.
163
+ - Migrate `agent.role` free-text → role key on first load (in-memory, mirroring `applyLegacyCcModelMigration`'s no-disk-rewrite pattern in shared.js) by slugifying the label and matching against `roles{}`. Unmatched strings remain as legacy free text and are rendered as an ad-hoc role.
164
+ - Introduce the resolution rules in §3 behind the existing resolver helpers so engine behavior is unchanged when `roles` is absent.
165
+
166
+ ### 4.3 Skills: decide the binding (REQUIRED for this phase)
167
+
168
+ Phase 1 must resolve the "two meanings of skill" ambiguity. Proposed: keep `roles[].skills` / `agent.skills` as **skill references** (tags), and in the system prompt render them as the `Expertise:` line **plus** — when a tag matches a discovered `SKILL.md` name from `getSkills()` — surface that skill's description inline. This makes the skills list meaningful without forcing a disk-layout change in P1. (Full per-role skill *scoping* of `collectSkillFiles` is deferred to a later phase; see §7.)
169
+
170
+ ### 4.4 API (Phase 1)
171
+
172
+ - `GET /api/roles` → `{ roles: { <key>: { label, definition, skills, defaultCli, defaultModel, builtin, agentIds[] } } }`. Aggregates `config.roles` + reverse-maps `config.agents` by role.
173
+ - `POST /api/roles/update` → `{ key, definition?, skills?, defaultCli?, defaultModel? }`. Edits an existing role only (P1). Writes through `mutateJsonFileLocked` on `config.json` (never `safeWrite` — see CLAUDE.md concurrency rules).
174
+ - Extend `POST /api/settings` agent loop to accept `definitionOverride`.
175
+ - Reuse/retire `POST /api/agents/charter`: in P1 it continues to write `agents/<id>/charter.md` for backward-compat, but the library writes `definitionOverride` into config; the resolver prefers the override. (Deprecation tracked in `docs/deprecated.json`.)
176
+
177
+ ### 4.5 UI (Phase 1)
178
+
179
+ - New **Library** entry on the Settings rail (or a top-level sidebar page — decide in review). Layout: left column = role list; right pane = selected role's definition editor (markdown textarea with preview, reusing the Charter tab's editor pattern from `detail-panel.js`) + skills chips + the agents bound to that role (each expandable to its per-agent overrides).
180
+ - Definition editor reuses the existing markdown view/edit toggle and `renderMd()` escaping already used by the Charter tab (`dashboard/js/detail-panel.js:115-126`) to satisfy the no-unsanitized lint gate (`npm run lint`).
181
+ - Settings → Agents table gains a link/affordance into the Library for the definition (so the two surfaces don't drift).
182
+
183
+ ### 4.6 Phase 1 acceptance criteria
184
+
185
+ 1. Library view lists every existing role with its definition + skills, and the agents under each.
186
+ 2. Editing a role's definition changes the system prompt for every agent bound to that role (verified by `buildSystemPrompt` output).
187
+ 3. Editing an agent's definition override wins over the role definition for that agent only.
188
+ 4. Engine behavior is **unchanged** when `config.roles` is absent (back-compat).
189
+ 5. All writes go through `mutateJsonFileLocked`; no `safeWrite` on `config.json`.
190
+ 6. `npm test` green (add source-inspection + resolver unit tests near existing agent/charter tests); `npm run lint` green.
191
+ 7. Settings reset still restores a working fleet (now `DEFAULT_ROLES` + `DEFAULT_AGENTS`).
192
+
193
+ ## 5. Migration & backward compatibility
194
+
195
+ - **Additive config.** `roles` is new and optional. Absent `roles` ⇒ legacy path (free-text role label, per-agent `charter.md`). No forced disk rewrite — migration is in-memory at load, matching `applyLegacyCcModelMigration`.
196
+ - **Charters preserved.** `agents/<id>/charter.md` remains readable and is the lowest-priority definition source. `minions init`/upgrade already preserves charter files (`bin/minions.js:1361-1368`) — do not regress that.
197
+ - **Routing unaffected in P1/P2-by-agent.** Routing still targets agent ids. (Routing-by-role is explicitly out of scope; see §7.)
198
+ - **Deprecation.** If `POST /api/agents/charter` is superseded by the library's `definitionOverride`, add a `docs/deprecated.json` entry with a cleanup path.
199
+
200
+ ## 6. Phase 2 — Create & assign
201
+
202
+ **User story:** "Add new roles, and assign new named agents with these roles."
203
+
204
+ ### 6.1 Scope
205
+
206
+ - **Role CRUD:** create/rename/delete roles in the library. Deleting a role is blocked while agents reference it (or offers reassignment). `builtin` roles are delete-protected.
207
+ - **Agent CRUD:** create a new named agent (id + name + emoji + role + optional overrides) and delete a non-builtin agent.
208
+ - New agents become immediately routable (they appear to `resolveAgent` via `config.agents`) and selectable in `routing.md` (manual) — auto-wiring into routing is out of scope.
209
+
210
+ ### 6.2 Data model / engine
211
+
212
+ - New agent ids validated: lowercase slug, unique, not colliding with `temp-*` reserved prefix.
213
+ - `resolveAgent` already iterates `config.agents` generically, so new agents are picked up with no engine change (`engine/routing.js:192-249`). Deleting an agent must also: cancel/skip pending dispatches targeting it, and leave its history intact (mirror the conservative posture of project removal in `engine/projects.js`).
214
+ - A new agent with no `charter.md` and no `definitionOverride` inherits its role's definition — this is the payoff of §3.
215
+
216
+ ### 6.3 API (Phase 2)
217
+
218
+ - `POST /api/roles/create` `{ key, label, definition, skills, defaultCli?, defaultModel? }`.
219
+ - `POST /api/roles/delete` `{ key, reassignTo? }` — refuses if agents reference it and no `reassignTo` given; refuses on `builtin`.
220
+ - `POST /api/agents/create` `{ id, name, emoji, role, overrides? }`.
221
+ - `POST /api/agents/delete` `{ id }` — refuses on builtin; drains/cancels pending dispatches first.
222
+ - Extend `POST /api/settings/reset` semantics: reset clears user-created roles/agents back to `DEFAULT_ROLES`/`DEFAULT_AGENTS`.
223
+
224
+ ### 6.4 UI (Phase 2)
225
+
226
+ - "+ New role" and "+ New agent" affordances in the Library.
227
+ - Agent creation form (id/name/emoji/role picker + optional runtime overrides).
228
+ - Delete with confirmation + reassignment flow for roles in use.
229
+
230
+ ### 6.5 Phase 2 acceptance criteria
231
+
232
+ 1. A user can create a role with a definition + skills and see it in the library.
233
+ 2. A user can create a named agent bound to a role; with no override it inherits the role definition; the engine can route work to it.
234
+ 3. Deleting a role in use is blocked or forces reassignment; builtin roles/agents cannot be deleted.
235
+ 4. Deleting an agent cancels its pending dispatches and preserves completed history.
236
+ 5. Settings reset returns to the default fleet.
237
+ 6. `npm test` + `npm run lint` green; new CRUD covered by unit tests; concurrency via `mutateJsonFileLocked`.
238
+
239
+ ## 7. Out of scope (explicitly deferred)
240
+
241
+ - **Routing-by-role** (e.g. `routing.md` rows targeting a role instead of an agent id). Routing stays agent-id-based.
242
+ - **Per-role/per-agent skill *scoping*** of on-disk `SKILL.md` discovery (`collectSkillFiles` stays global/project/plugin). P1 only makes the skills *list* meaningful in the prompt (§4.3).
243
+ - **Per-project role overrides** (`projects/<name>/...`).
244
+ - **Importing/sharing role libraries** across installs.
245
+ - Any change to the meeting/plan agent-charter linkage beyond reading the new definition source.
246
+
247
+ ## 8. Open questions (resolve in review)
248
+
249
+ 1. **Library placement** — Settings rail entry vs. a dedicated top-level sidebar page?
250
+ 2. **Role↔agent cardinality framing** — do we surface roles as the primary list (agents nested under them), or keep agents primary with role as an attribute? (Spec assumes roles-primary in the Library; agents-primary stays in Settings.)
251
+ 3. **Charter vs definition naming** — adopt "definition" everywhere, or keep "charter" as the user-facing term to avoid churn in existing UI/tests?
252
+ 4. **Skills binding (§4.3)** — ship the "tag matches discovered SKILL.md → inline description" behavior in P1, or keep skills purely as the `Expertise:` line until a later phase?
253
+ 5. **Default role definitions** — seed from existing per-agent `charter.md`, or author fresh role-level definitions?
254
+
255
+ ## 9. Work breakdown (suggested PRD seeds)
256
+
257
+ **Phase 1**
258
+ - P1-a: `DEFAULT_ROLES` + in-memory legacy role migration + resolver tiering (engine/shared.js, playbook.js).
259
+ - P1-b: `GET /api/roles`, `POST /api/roles/update`, `definitionOverride` on `POST /api/settings` (dashboard.js).
260
+ - P1-c: Library UI (role list + definition editor + skills + nested agents) reusing the charter editor pattern.
261
+ - P1-d: Tests (resolver unit tests, source-inspection for endpoints/UI) + `docs/deprecated.json` entry if `/api/agents/charter` is superseded + settings parity note in CLAUDE.md.
262
+
263
+ **Phase 2**
264
+ - P2-a: Role CRUD API + delete-guard/reassignment.
265
+ - P2-b: Agent CRUD API + dispatch-drain on delete + id validation.
266
+ - P2-c: Library create/delete UI flows.
267
+ - P2-d: Tests + reset semantics + docs.
268
+
269
+ ---
270
+
271
+ _Every new `engine.*`/config surface introduced here must also get a dashboard control and be documented in CLAUDE.md — config without UI is incomplete (see CLAUDE.md "Best Practices" #15)._
package/engine/cleanup.js CHANGED
@@ -567,7 +567,7 @@ async function runCleanup(config, verbose = false) {
567
567
  // change to both locations, producing mirror-write leaks.
568
568
  // We only WARN here — removing someone else's worktree without consent could
569
569
  // destroy in-flight work. The operator runs `git worktree remove <path>`.
570
- // P-a3f9b206: live-mode projects (worktreeMode === 'live') intentionally
570
+ // P-a3f9b206: live-mode projects (checkoutMode === 'live') intentionally
571
571
  // never create linked worktrees — `git worktree list` returns only the
572
572
  // operator's main checkout, which `shared.isPathInside` correctly excludes
573
573
  // (equal-path is not "inside"). An empty/main-only nested-worktree scan
@@ -264,7 +264,7 @@ function _record(plan, fields) {
264
264
  * - `config` — full config; re-checks configured-project so a stale plan can't
265
265
  * re-clone a repo that is already linked.
266
266
  * - `registerOptions` — passed through to projects.addProject (name override,
267
- * worktreeMode, observeAuthors, …).
267
+ * checkoutMode, observeAuthors, …).
268
268
  * - test seams: `gitClone`, `addProject`, `findConfiguredProject`, `fetchPrBranches`,
269
269
  * `token`/`_resolveTokenForSlug`/`_acquireAdoToken`, `cloneRoot`, `_rm`, `_mkdir`.
270
270
  *
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const os = require('os');
13
13
  const path = require('path');
14
14
  const { execSync, execFileSync } = require('child_process');
15
+ const shared = require('./shared');
15
16
 
16
17
  /**
17
18
  * Resolve the Claude Code CLI binary path. Legacy helper preserved for back-
@@ -190,13 +191,20 @@ function runPreflight(opts = {}) {
190
191
  const results = [];
191
192
  let allOk = true;
192
193
 
193
- // 1. Node.js version >= 18
194
+ // 1. Node.js version >= 22.5.0 (built-in node:sqlite — issue #244). HARD FAIL
195
+ // on older Node: node:sqlite is the only state backend post Phase 9.4, and
196
+ // the --experimental-sqlite self-reexec is a no-op below 22.5, so every
197
+ // getDb() throws a raw stack trace. Numeric comparison (shared helper) so
198
+ // the 22.4.x → 22.5.0 boundary is correct and we never string-compare.
194
199
  const nodeVersion = process.versions.node;
195
- const major = parseInt(nodeVersion.split('.')[0], 10);
196
- if (major >= 18) {
200
+ if (shared.nodeSupportsBuiltinSqlite(nodeVersion)) {
197
201
  results.push({ name: 'Node.js', ok: true, message: `v${nodeVersion}` });
198
202
  } else {
199
- results.push({ name: 'Node.js', ok: false, message: `v${nodeVersion} requires >= 18. Upgrade at https://nodejs.org` });
203
+ // Message mirrors the canonical CLI-preflight remediation (minus the
204
+ // leading "Node.js: " label, which printPreflight already renders as the
205
+ // check name) so doctor and the preflight read identically.
206
+ const remediation = shared.nodeSqliteRemediationLine(nodeVersion).replace(/^Node\.js:\s*/, '');
207
+ results.push({ name: 'Node.js', ok: false, message: remediation });
200
208
  allOk = false;
201
209
  }
202
210
 
@@ -239,12 +247,9 @@ function runPreflight(opts = {}) {
239
247
  // us the config. checkOrExit() / cli start() / doctor() pass it; legacy
240
248
  // callers don't, in which case we skip silently.
241
249
  if (opts && opts.config && typeof opts.config === 'object') {
242
- // Hoisted: `shared` is referenced by every check block below, including
243
- // workSources warnings and the worktreeRoot check. Previously declared
244
- // inside the first inner try, which left later blocks reading an
245
- // undefined identifier (ReferenceError silently caught by the wrapping
246
- // try/catch).
247
- const shared = require('./shared');
250
+ // `shared` is module-level (required at top of file) and referenced by
251
+ // every check block below, including workSources warnings and the
252
+ // worktreeRoot check.
248
253
  try {
249
254
  let runtimeNames = [];
250
255
  try { runtimeNames = require('./runtimes').listRuntimes(); }
@@ -544,6 +549,13 @@ function doctor(minionsHome) {
544
549
  catch { /* missing/invalid config is its own check below */ }
545
550
  const { passed, results } = runPreflight({ config: preflightConfig });
546
551
 
552
+ // When Node is too old for built-in node:sqlite (issue #244) the engine
553
+ // cannot boot, so the stale-engine PID check and per-runtime model-discovery
554
+ // probes are GUARANTEED to fail/warn — noise that buries the one root cause.
555
+ // De-prioritize them: skip the model-discovery network probe entirely and
556
+ // mute the stale-PID entry to a single "expected — fix Node first" note.
557
+ const nodeCriticalFail = results.some(r => r.name === 'Node.js' && r.ok === false);
558
+
547
559
  // Runtime checks
548
560
  const runtimeResults = [];
549
561
 
@@ -600,7 +612,13 @@ function doctor(minionsHome) {
600
612
  alive = true;
601
613
  }
602
614
  } catch { /* process may be dead */ }
603
- runtimeResults.push({ name: 'Engine', ok: alive, message: alive ? `running (PID ${control.pid})` : `stale PID ${control.pid} — run: minions start (see docs/engine-restart.md)` });
615
+ if (!alive && nodeCriticalFail) {
616
+ // Expected fallout of the Node version failure above — don't add a
617
+ // second critical fail competing for the operator's attention.
618
+ runtimeResults.push({ name: 'Engine', ok: 'warn', message: `stale PID ${control.pid} — expected while Node is too old; fix the Node.js check above first` });
619
+ } else {
620
+ runtimeResults.push({ name: 'Engine', ok: alive, message: alive ? `running (PID ${control.pid})` : `stale PID ${control.pid} — run: minions start (see docs/engine-restart.md)` });
621
+ }
604
622
  } else {
605
623
  runtimeResults.push({ name: 'Engine', ok: 'warn', message: `${control.state || 'stopped'} — run: minions start (see docs/engine-restart.md)` });
606
624
  }
@@ -641,8 +659,15 @@ function doctor(minionsHome) {
641
659
  // avoids a second JSON.parse round-trip.
642
660
  const fleetSummary = _fleetSummaryResults(preflightConfig);
643
661
  runtimeResults.push(...fleetSummary);
644
- const modelResults = await _modelDiscoveryResults(preflightConfig);
645
- runtimeResults.push(...modelResults);
662
+ if (nodeCriticalFail) {
663
+ // Model discovery shells out to the runtime CLI / REST API, which can't
664
+ // work while the engine can't boot. Skip the probe and emit one muted
665
+ // note instead of a per-runtime "discovery unavailable" warning storm.
666
+ runtimeResults.push({ name: 'Models', ok: 'warn', message: 'discovery skipped — fix the Node.js check above first (engine cannot boot on this Node)' });
667
+ } else {
668
+ const modelResults = await _modelDiscoveryResults(preflightConfig);
669
+ runtimeResults.push(...modelResults);
670
+ }
646
671
  // Verify each runtime CLI still recognizes the headless bypass flags the
647
672
  // adapters inject. Catches "user installed an outdated CLI" before the
648
673
  // first agent silently hangs on a permission prompt.
@@ -432,15 +432,16 @@ function buildPrUrlBase({ repoHost, org, project, repoName, prUrlBase }) {
432
432
  return '';
433
433
  }
434
434
 
435
- function buildProjectEntry({ name, description, localPath, repoHost, repositoryId, org, project, repoName, mainBranch, prUrlBase, worktreeMode, observeAuthors }) {
435
+ function buildProjectEntry({ name, description, localPath, repoHost, repositoryId, org, project, repoName, mainBranch, prUrlBase, checkoutMode, observeAuthors }) {
436
436
  const safeName = (name || 'project').replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'project';
437
437
  const host = repoHost || 'github';
438
438
  const isAdo = host === 'ado';
439
- // P-a3f9b201: validateWorktreeMode returns undefined on absent/empty (so the
440
- // field is omitted and resolves to 'isolated' downstream) and throws an HTTP
441
- // 400 Error on unknown values. Run validation BEFORE building the entry so a
442
- // typo at the dashboard or CLI never lands in config.json.
443
- const resolvedWorktreeMode = shared.validateWorktreeMode(worktreeMode);
439
+ // P-a3f9b201 (consolidated W-mqiaw974): validateCheckoutMode returns undefined
440
+ // on absent/empty (so the field is omitted and resolves to 'worktree'
441
+ // downstream) and throws an HTTP 400 Error on unknown values. Run validation
442
+ // BEFORE building the entry so a typo at the dashboard or CLI never lands in
443
+ // config.json.
444
+ const resolvedCheckoutMode = shared.validateCheckoutMode(checkoutMode);
444
445
  const entry = {
445
446
  name: safeName,
446
447
  description: description || '',
@@ -458,7 +459,7 @@ function buildProjectEntry({ name, description, localPath, repoHost, repositoryI
458
459
  workItems: { enabled: true, cooldownMinutes: 0 },
459
460
  },
460
461
  };
461
- if (resolvedWorktreeMode !== undefined) entry.worktreeMode = resolvedWorktreeMode;
462
+ if (resolvedCheckoutMode !== undefined) entry.checkoutMode = resolvedCheckoutMode;
462
463
  // W-mq8li79a000889fa — observeAuthors is the canonical list of author
463
464
  // identifiers consumed by the `ado-author-prs` watch (and any future
464
465
  // author-scoped watch). Currently only meaningful for ADO projects —
@@ -535,7 +535,7 @@ async function addProject(target, options = {}) {
535
535
  repoName: detected.repoName || name,
536
536
  mainBranch: detected.mainBranch || 'main',
537
537
  prUrlBase: detected.prUrlBase,
538
- worktreeMode: options.worktreeMode,
538
+ checkoutMode: options.checkoutMode ?? options.worktreeMode,
539
539
  observeAuthors: Array.isArray(options.observeAuthors) ? options.observeAuthors : undefined,
540
540
  });
541
541
 
package/engine/shared.js CHANGED
@@ -192,6 +192,54 @@ function ts() { return new Date().toISOString(); }
192
192
  function logTs() { return new Date().toLocaleTimeString(); }
193
193
  function dateStamp() { return new Date().toISOString().slice(0, 10); }
194
194
 
195
+ // ── Node / node:sqlite version gate (issue #244) ────────────────────────────
196
+ // `node:sqlite` (the only state backend post Phase 9.4) is built-in only on
197
+ // Node >= 22.5.0. On older Node the `--experimental-sqlite` self-reexec is a
198
+ // no-op and every getDb() throws a raw stack trace. These helpers give the CLI
199
+ // preflight + `minions doctor` a single, NUMERIC (not lexical) version gate and
200
+ // one canonical remediation line so both surfaces agree.
201
+ const NODE_SQLITE_MIN_VERSION = '22.5.0';
202
+
203
+ /**
204
+ * Numeric semver-ish comparison of two dotted version strings. Compares
205
+ * major/minor/patch as integers so '22.5.0' sorts AFTER '22.10.0' would NOT —
206
+ * i.e. it does NOT lexically compare ('9' > '22' under string compare). Missing
207
+ * components are treated as 0. Returns -1 (a<b), 0 (a==b), or 1 (a>b).
208
+ */
209
+ function compareDottedVersions(a, b) {
210
+ const parse = (v) => String(v == null ? '' : v)
211
+ .split('.')
212
+ .map((n) => parseInt(n, 10) || 0);
213
+ const av = parse(a);
214
+ const bv = parse(b);
215
+ const len = Math.max(av.length, bv.length);
216
+ for (let i = 0; i < len; i++) {
217
+ const x = av[i] || 0;
218
+ const y = bv[i] || 0;
219
+ if (x > y) return 1;
220
+ if (x < y) return -1;
221
+ }
222
+ return 0;
223
+ }
224
+
225
+ /**
226
+ * True when the given Node version (default: the running runtime) can load the
227
+ * built-in `node:sqlite` module — i.e. Node >= 22.5.0. Numeric comparison, so
228
+ * the 22.4.x → 22.5.0 boundary is handled correctly and 22.10+ / 24+ pass.
229
+ */
230
+ function nodeSupportsBuiltinSqlite(version = process.versions.node) {
231
+ return compareDottedVersions(version, NODE_SQLITE_MIN_VERSION) >= 0;
232
+ }
233
+
234
+ /**
235
+ * The canonical one-line remediation emitted by both `minions doctor` (as the
236
+ * failing Node.js check message) and the top-level CLI preflight when Node is
237
+ * too old for built-in node:sqlite. Keep the wording identical across surfaces.
238
+ */
239
+ function nodeSqliteRemediationLine(version = process.versions.node) {
240
+ return `Node.js: v${version} - Minions requires Node >= 22.5 (24 LTS recommended) for built-in node:sqlite. Upgrade Node, then run minions start.`;
241
+ }
242
+
195
243
  // ── F6 (P-f6commentedit): Comment-edit dedup helpers ────────────────────────
196
244
  // Shared by engine/github.js + engine/ado.js pollPrHumanComments so a comment
197
245
  // EDITED after first observation triggers a single re-dispatch (and only one).
@@ -2483,25 +2531,60 @@ function classifyInboxItem(name, content) {
2483
2531
  return 'project-notes';
2484
2532
  }
2485
2533
 
2486
- // ── Worktree Mode Enum (P-a3f9b201) ─────────────────────────────────────────
2487
- // Per-project switch between the default `isolated` mode (engine creates a
2488
- // dedicated worktree per dispatch under ../worktrees) and `live` mode (the
2534
+ // ── Checkout Mode Enum (P-a3f9b201; consolidated W-mqiaw974, issue #241) ──────
2535
+ // Per-project switch between the default `worktree` mode (engine creates a
2536
+ // dedicated git worktree per dispatch under ../worktrees) and `live` mode (the
2489
2537
  // agent runs directly in the operator's working checkout — used for repos
2490
2538
  // where git worktrees are unworkable, e.g. submodules, hooks, large binary
2491
- // caches). Default is `isolated`; absent/undefined `worktreeMode` on a
2492
- // project entry MUST read as 'isolated' everywhere downstream. Unknown
2539
+ // caches). Default is `worktree`; absent/undefined `checkoutMode` on a
2540
+ // project entry MUST read as 'worktree' everywhere downstream. Unknown
2493
2541
  // values are rejected at the validators — never silently coerced — so a
2494
2542
  // typo in the dashboard or in config.json cannot wedge dispatch into an
2495
2543
  // unknown mode.
2496
- const WORKTREE_MODES = Object.freeze({ ISOLATED: 'isolated', LIVE: 'live' });
2497
-
2498
- function validateWorktreeMode(value) {
2544
+ //
2545
+ // LEGACY FIELD (back-compat): this field used to be named `worktreeMode` with
2546
+ // the enum { isolated, live }. The rename consolidated the two overlapping
2547
+ // fields into a single `checkoutMode` { worktree, live }: `isolated` → the
2548
+ // implicit `worktree` behavior, `live` → `live`. `resolveCheckoutMode` still
2549
+ // reads the legacy `worktreeMode` field so existing live-checkout projects in
2550
+ // config.json keep working without an on-disk rewrite. Tracked in
2551
+ // docs/deprecated.json (id: worktreemode-field-rename).
2552
+ const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
2553
+
2554
+ // Resolve a project's effective checkout mode, honoring the legacy
2555
+ // `worktreeMode` field for back-compat. Reading order:
2556
+ // 1. canonical `project.checkoutMode` ('worktree' | 'live')
2557
+ // 2. legacy `project.worktreeMode` ('isolated' → 'worktree', 'live' → 'live')
2558
+ // 3. default → 'worktree'
2559
+ // Always returns one of CHECKOUT_MODES — never undefined — so call sites can
2560
+ // compare against the enum without a falsy guard.
2561
+ function resolveCheckoutMode(project) {
2562
+ if (!project || typeof project !== 'object') return CHECKOUT_MODES.WORKTREE;
2563
+ const canonical = project.checkoutMode;
2564
+ if (canonical === CHECKOUT_MODES.LIVE) return CHECKOUT_MODES.LIVE;
2565
+ if (canonical === CHECKOUT_MODES.WORKTREE) return CHECKOUT_MODES.WORKTREE;
2566
+ // Legacy field fallback (only consulted when checkoutMode is absent/unknown).
2567
+ const legacy = project.worktreeMode;
2568
+ if (legacy === 'live') return CHECKOUT_MODES.LIVE;
2569
+ // legacy 'isolated' (and anything else) → the default worktree behavior.
2570
+ return CHECKOUT_MODES.WORKTREE;
2571
+ }
2572
+
2573
+ // Convenience predicate: does this project dispatch in-place (live checkout)?
2574
+ function isLiveCheckoutProject(project) {
2575
+ return resolveCheckoutMode(project) === CHECKOUT_MODES.LIVE;
2576
+ }
2577
+
2578
+ function validateCheckoutMode(value) {
2499
2579
  if (value === undefined || value === null || value === '') return undefined;
2500
2580
  if (typeof value !== 'string') {
2501
- throw _httpError(400, `Invalid worktreeMode: must be a string (got ${typeof value}). Accepted values: 'isolated', 'live'.`);
2581
+ throw _httpError(400, `Invalid checkoutMode: must be a string (got ${typeof value}). Accepted values: 'worktree', 'live'.`);
2502
2582
  }
2503
- if (value !== WORKTREE_MODES.ISOLATED && value !== WORKTREE_MODES.LIVE) {
2504
- throw _httpError(400, `Invalid worktreeMode: "${value}". Accepted values: 'isolated' (default), 'live'.`);
2583
+ // Back-compat: silently coerce the legacy 'isolated' value to the canonical
2584
+ // 'worktree' so an old client / cached dashboard tab doesn't 400.
2585
+ if (value === 'isolated') return CHECKOUT_MODES.WORKTREE;
2586
+ if (value !== CHECKOUT_MODES.WORKTREE && value !== CHECKOUT_MODES.LIVE) {
2587
+ throw _httpError(400, `Invalid checkoutMode: "${value}". Accepted values: 'worktree' (default), 'live'.`);
2505
2588
  }
2506
2589
  return value;
2507
2590
  }
@@ -5564,7 +5647,9 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
5564
5647
  * field for read-only stages. Only code-mutating pipeline stages need a
5565
5648
  * worktree, and they take the normal code-mutating path below.
5566
5649
  *
5567
- * **Live-checkout mode (P-a3f9b202).** When `project.worktreeMode === 'live'`
5650
+ * **Live-checkout mode (P-a3f9b202).** When the project's resolved checkout
5651
+ * mode is `'live'` (`shared.isLiveCheckoutProject(project)`; canonical
5652
+ * `project.checkoutMode === 'live'` or legacy `project.worktreeMode === 'live'`)
5568
5653
  * the resolver short-circuits BEFORE both branches below and returns
5569
5654
  * `{ cwd: <abs localPath>, worktreeRootDir: null, liveMode: true }` for ALL
5570
5655
  * task types — read-only and code-mutating alike. The agent runs directly
@@ -5591,7 +5676,7 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
5591
5676
  * caught post-resolve). The mutating containment check happens later in
5592
5677
  * spawnAgent, against the actual worktree path.
5593
5678
  *
5594
- * @param {{ localPath?: string|null, worktreeMode?: string|null }|null|undefined} project
5679
+ * @param {{ localPath?: string|null, checkoutMode?: string|null, worktreeMode?: string|null }|null|undefined} project
5595
5680
  * @param {string} type — work type (e.g. 'fix', 'explore', 'meeting')
5596
5681
  * @param {string} minionsDir — MINIONS_DIR fallback anchor (ignored in live mode)
5597
5682
  * @param {{ workdir?: string|null }} [options] — optional per-WI overrides
@@ -5603,7 +5688,7 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
5603
5688
  * then runs shared.applyWorkdir(worktreePath, result.workdir) to land
5604
5689
  * the agent inside the subpackage cwd)
5605
5690
  * The optional `liveMode` discriminator lets callers branch on one boolean
5606
- * instead of re-reading `project.worktreeMode`.
5691
+ * instead of re-resolving the project's checkout mode.
5607
5692
  * @throws {Error} LIVE_CHECKOUT_NO_LOCALPATH (live mode, missing localPath),
5608
5693
  * INVALID_WORKDIR (live or read-only, workdir escapes base),
5609
5694
  * WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT (isolated code-mutating),
@@ -5618,11 +5703,11 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
5618
5703
  // Runs BEFORE the read-only / code-mutating split so live mode is the
5619
5704
  // single decision point regardless of task type — read-only tasks in
5620
5705
  // live mode still report liveMode:true so downstream callers don't have
5621
- // to re-read project.worktreeMode to know they're running in-place.
5622
- if (project?.worktreeMode === WORKTREE_MODES.LIVE) {
5706
+ // to re-resolve the project's checkout mode to know they're running in-place.
5707
+ if (isLiveCheckoutProject(project)) {
5623
5708
  if (!project.localPath) {
5624
5709
  const err = new Error(
5625
- 'live-checkout mode requires project.localPath (worktreeMode === "live" but localPath is missing/falsy).'
5710
+ 'live-checkout mode requires project.localPath (checkoutMode === "live" but localPath is missing/falsy).'
5626
5711
  );
5627
5712
  err.code = 'LIVE_CHECKOUT_NO_LOCALPATH';
5628
5713
  throw err;
@@ -8368,6 +8453,11 @@ module.exports = {
8368
8453
  EDITS_SEEN_CAP, // F6 (P-f6commentedit)
8369
8454
  logTs,
8370
8455
  dateStamp,
8456
+ // Node / node:sqlite version gate (issue #244)
8457
+ NODE_SQLITE_MIN_VERSION,
8458
+ compareDottedVersions,
8459
+ nodeSupportsBuiltinSqlite,
8460
+ nodeSqliteRemediationLine,
8371
8461
  log,
8372
8462
  safeRead,
8373
8463
  safeReadOrNull,
@@ -8561,8 +8651,10 @@ module.exports = {
8561
8651
  HAS_DANGEROUS_KEY_MAX_NODES,
8562
8652
  validateProjectName,
8563
8653
  validateProjectPath,
8564
- WORKTREE_MODES,
8565
- validateWorktreeMode,
8654
+ CHECKOUT_MODES,
8655
+ validateCheckoutMode,
8656
+ resolveCheckoutMode,
8657
+ isLiveCheckoutProject,
8566
8658
  validatePid,
8567
8659
  PR_FIX_CAUSE,
8568
8660
  getPrFixAutomationCause,
package/engine/timeout.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * engine/timeout.js — Runtime timeout, stale-orphan cleanup, steering, and idle checks.
3
3
  *
4
- * Live-checkout dispatches (project.worktreeMode === 'live') are killed by PID
4
+ * Live-checkout dispatches (project.checkoutMode === 'live') are killed by PID
5
5
  * exactly like isolated-mode dispatches — no special handling, no in-place
6
6
  * `git reset`/`git clean`, no working-tree cleanup. The engine only ever sends
7
7
  * SIGTERM/SIGKILL to the tracked process; the operator owns the checkout.
@@ -89,7 +89,7 @@ function getProjectPoolSize(projectName, config) {
89
89
  // borrow path in spawnAgent into running for a project that no longer
90
90
  // wants pooled worktrees. Beats both per-project worktreePoolSize and the
91
91
  // engine-wide fleet default.
92
- if (proj && proj.worktreeMode === shared.WORKTREE_MODES.LIVE) return 0;
92
+ if (shared.isLiveCheckoutProject(proj)) return 0;
93
93
  if (proj && Number.isFinite(Number(proj.worktreePoolSize))) {
94
94
  return Math.max(0, Math.floor(Number(proj.worktreePoolSize)));
95
95
  }
package/engine.js CHANGED
@@ -2046,7 +2046,7 @@ async function spawnAgent(dispatchItem, config) {
2046
2046
  }
2047
2047
 
2048
2048
  // ── Live-checkout mode handling (P-a3f9b204) ─────────────────────────────
2049
- // When project.worktreeMode === 'live', resolveSpawnPaths returned
2049
+ // When the project's checkout mode is 'live', resolveSpawnPaths returned
2050
2050
  // cwd = project.localPath, worktreeRootDir = null, liveMode = true.
2051
2051
  // Instead of creating an engine-managed worktree we run prepareLiveCheckout
2052
2052
  // in-place: it validates a clean tree and then checks out (or creates) the
@@ -9247,13 +9247,13 @@ async function tickInner() {
9247
9247
  if (d.meta?.branch) lockedBranches.add(sanitizeBranch(d.meta.branch));
9248
9248
  }
9249
9249
  // P-a3f9b205: Per-project mutating-concurrency gate for live-mode projects.
9250
- // When project.worktreeMode === 'live', the agent runs in-place inside the
9251
- // operator's localPath instead of a dedicated worktree, so two concurrent
9250
+ // When the project's checkout mode is 'live', the agent runs in-place inside
9251
+ // the operator's localPath instead of a dedicated worktree, so two concurrent
9252
9252
  // mutating dispatches to the same project would clobber each other's
9253
9253
  // index / working tree. Seed `liveProjectsInUse` from dispatch.active so
9254
9254
  // the gate survives across ticks (not just within one allocation pass).
9255
9255
  // Read-only types (meeting/ask/explore/plan/plan-to-prd) never write, so
9256
- // they are excluded from the cap. Isolated-mode projects are also
9256
+ // they are excluded from the cap. Worktree-mode projects are also
9257
9257
  // excluded — they get a fresh worktree per dispatch and are naturally
9258
9258
  // safe.
9259
9259
  const liveProjectsInUse = new Set();
@@ -9262,7 +9262,7 @@ async function tickInner() {
9262
9262
  const projName = d.project || d.meta?.project?.name || null;
9263
9263
  if (!projName) continue;
9264
9264
  const projCfg = shared.findProjectByName(shared.getProjects(config), projName);
9265
- if (projCfg && projCfg.worktreeMode === shared.WORKTREE_MODES.LIVE) {
9265
+ if (shared.isLiveCheckoutProject(projCfg)) {
9266
9266
  liveProjectsInUse.add(projName);
9267
9267
  }
9268
9268
  }
@@ -9439,7 +9439,7 @@ async function tickInner() {
9439
9439
  && !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
9440
9440
  ) {
9441
9441
  const projCfg = shared.findProjectByName(shared.getProjects(config), itemProjName);
9442
- if (projCfg && projCfg.worktreeMode === shared.WORKTREE_MODES.LIVE) {
9442
+ if (shared.isLiveCheckoutProject(projCfg)) {
9443
9443
  liveProjectsInUse.add(itemProjName);
9444
9444
  }
9445
9445
  }
@@ -9517,7 +9517,7 @@ async function tickInner() {
9517
9517
  const projName = d.project || d.meta?.project?.name || null;
9518
9518
  if (!projName) continue;
9519
9519
  const projCfg = shared.findProjectByName(shared.getProjects(config), projName);
9520
- if (projCfg && projCfg.worktreeMode === shared.WORKTREE_MODES.LIVE) {
9520
+ if (shared.isLiveCheckoutProject(projCfg)) {
9521
9521
  postLiveProjectsInUse.add(projName);
9522
9522
  }
9523
9523
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2215",
3
+ "version": "0.1.2216",
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"