@yemi33/minions 0.1.2215 → 0.1.2217

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/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.2217",
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"