@yemi33/minions 0.1.2113 → 0.1.2115

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
@@ -231,6 +231,27 @@ function _openStdioLog(name) {
231
231
  return shared.openAppendLogFd(name, dir, { fallback: 'ignore' }).fd;
232
232
  }
233
233
 
234
+ /**
235
+ * Node flag args required for SQLite to load. On Node 22.x, `node:sqlite`
236
+ * is gated behind `--experimental-sqlite`; on Node 24+ it is unflagged.
237
+ * Issue #3035: without this, every engine/dashboard/supervisor spawn on
238
+ * Node 22.x loses the SQL store and falls back to the JSON path —
239
+ * /api/work-items returns [] from the aggregate reader because the
240
+ * Phase-9-canonical SQL path is unreachable.
241
+ *
242
+ * Honors NODE_OPTIONS / MINIONS_FORCE_JSON: if the caller already has the
243
+ * flag set (via NODE_OPTIONS) or has opted out of SQL entirely
244
+ * (MINIONS_FORCE_JSON=1), we add nothing — the child inherits the env.
245
+ */
246
+ function _sqliteSpawnFlags() {
247
+ if (process.env.MINIONS_FORCE_JSON === '1') return [];
248
+ const major = parseInt(String(process.versions.node).split('.')[0], 10);
249
+ if (!Number.isFinite(major) || major < 22 || major >= 24) return [];
250
+ const nodeOpts = String(process.env.NODE_OPTIONS || '');
251
+ if (nodeOpts.includes('--experimental-sqlite')) return [];
252
+ return ['--experimental-sqlite'];
253
+ }
254
+
234
255
  /** Spawn a detached dashboard with self-open suppressed — the CLI decides
235
256
  * when to open a browser based on whether a real tab reconnects post-health.
236
257
  * stdout/stderr land in engine/dashboard-stdio.log so silent startup crashes
@@ -239,7 +260,7 @@ function spawnDashboard() {
239
260
  const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
240
261
  const out = _openStdioLog('dashboard-stdio.log');
241
262
  const err = _openStdioLog('dashboard-stdio.log');
242
- const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
263
+ const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'dashboard.js')], {
243
264
  cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true, env
244
265
  });
245
266
  proc.unref();
@@ -255,7 +276,7 @@ function spawnDashboard() {
255
276
  function spawnSupervisor() {
256
277
  const out = _openStdioLog('supervisor-stdio.log');
257
278
  const err = _openStdioLog('supervisor-stdio.log');
258
- const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine', 'supervisor.js')], {
279
+ const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine', 'supervisor.js')], {
259
280
  cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true,
260
281
  });
261
282
  proc.unref();
@@ -277,7 +298,7 @@ const _supervisorPidPath = () => path.join(MINIONS_HOME, 'engine', 'supervisor.p
277
298
  function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs }) {
278
299
  const engineOut = _openStdioLog('engine-stdio.log');
279
300
  const engineErr = _openStdioLog('engine-stdio.log');
280
- const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
301
+ const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
281
302
  cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true
282
303
  });
283
304
  engineProc.unref();
@@ -631,7 +652,7 @@ function init() {
631
652
  console.log(isUpgrade
632
653
  ? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
633
654
  : '\n Starting engine and dashboard...\n');
634
- const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start'], {
655
+ const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start'], {
635
656
  cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
636
657
  });
637
658
  engineProc.unref();
@@ -840,7 +861,7 @@ function ensureInstalled() {
840
861
 
841
862
  function delegate(script, args) {
842
863
  ensureInstalled();
843
- const child = spawn(process.execPath, [path.join(MINIONS_HOME, script), ...args], {
864
+ const child = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, script), ...args], {
844
865
  stdio: 'inherit',
845
866
  cwd: MINIONS_HOME,
846
867
  env: { ...process.env, MINIONS_HOME },
@@ -54,6 +54,14 @@ function _installExperimentalWarningFilter() {
54
54
  }
55
55
 
56
56
  function getDb() {
57
+ // Hard opt-out (issue #3035, option #3): MINIONS_FORCE_JSON=1 makes
58
+ // every getDb() call throw, forcing the entire stack onto the JSON
59
+ // fallback path. Useful for users who can't enable
60
+ // --experimental-sqlite on Node 22.x and for regression tests that
61
+ // need to exercise the JSON-fallback branches deterministically.
62
+ if (process.env.MINIONS_FORCE_JSON === '1') {
63
+ throw new Error('engine/db: SQL disabled via MINIONS_FORCE_JSON=1');
64
+ }
57
65
  // Re-resolve the DB path on every call so tests that swap MINIONS_TEST_DIR
58
66
  // (or production-side configuration that changes MINIONS_HOME between
59
67
  // operations) get a fresh connection to the new location instead of
@@ -94,10 +102,11 @@ function getDb() {
94
102
  runMigrations(_db);
95
103
  return _db;
96
104
  } catch (e) {
97
- _dbInitError = new Error(
98
- `engine/db: failed to open SQLite — ${e.message}. ` +
99
- `Node 22.5+ required for built-in 'node:sqlite' support.`
100
- );
105
+ const nodeMajor = parseInt(String(process.versions.node).split('.')[0], 10);
106
+ const flagHint = nodeMajor === 22
107
+ ? ` On Node 22.x, node:sqlite is gated behind --experimental-sqlite. Restart with NODE_OPTIONS=--experimental-sqlite, upgrade to Node 24+, or set MINIONS_FORCE_JSON=1 to bypass SQL.`
108
+ : ` Node 22.5+ (with --experimental-sqlite on 22.x) or 24+ required for built-in 'node:sqlite' support.`;
109
+ _dbInitError = new Error(`engine/db: failed to open SQLite — ${e.message}.${flagHint}`);
101
110
  throw _dbInitError;
102
111
  }
103
112
  }
@@ -0,0 +1,61 @@
1
+ // engine/db/migrations/010-pr-links.js
2
+ //
3
+ // Phase 9.1: move `engine/pr-links.json` into SQL.
4
+ //
5
+ // pr-links.json is the *fallback* PR → work-item linkage store. The
6
+ // primary source is `pull_requests.prdItems[]` (managed via the
7
+ // pull-requests-store). Stray PR IDs that aren't covered by a project
8
+ // pull-requests record still need to be discoverable — that's what this
9
+ // table holds.
10
+ //
11
+ // Shape: { [prId]: <value> } where <value> is typically an array of item IDs
12
+ // but may also be a bare string from legacy writes. Readers normalize.
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ function _resolveMinionsDir() {
18
+ const envHome = process.env.MINIONS_HOME || process.env.MINIONS_TEST_DIR;
19
+ if (envHome) return envHome;
20
+ try { return require('../../shared').MINIONS_DIR; } catch { return null; }
21
+ }
22
+
23
+ function _readJsonOr(filePath, fallback) {
24
+ try {
25
+ const raw = fs.readFileSync(filePath, 'utf8');
26
+ return JSON.parse(raw);
27
+ } catch { return fallback; }
28
+ }
29
+
30
+ module.exports = {
31
+ version: 10,
32
+ description: 'pr_links: schema + pr-links.json backfill (fallback static store)',
33
+ up(db) {
34
+ db.exec(`
35
+ CREATE TABLE pr_links (
36
+ pr_id TEXT PRIMARY KEY,
37
+ data TEXT NOT NULL,
38
+ updated_at INTEGER NOT NULL
39
+ );
40
+ `);
41
+
42
+ const minionsDir = _resolveMinionsDir();
43
+ if (!minionsDir) return;
44
+ const now = Date.now();
45
+ let inserted = 0;
46
+
47
+ const raw = _readJsonOr(path.join(minionsDir, 'engine', 'pr-links.json'), null);
48
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
49
+ const ins = db.prepare(`INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?)`);
50
+ for (const [prId, value] of Object.entries(raw)) {
51
+ try {
52
+ ins.run(String(prId), JSON.stringify(value), now);
53
+ inserted += 1;
54
+ } catch { /* duplicate / corrupt — skip */ }
55
+ }
56
+ }
57
+
58
+ // eslint-disable-next-line no-console
59
+ console.log(`[db-migrate] v10: backfilled ${inserted} pr-links rows; pr-links.json kept as dual-write mirror`);
60
+ },
61
+ };
@@ -149,11 +149,44 @@ function readPullRequestsForScope(scope) {
149
149
  return out;
150
150
  }
151
151
 
152
+ // Enumerate scopes that have a JSON file on disk — used as the
153
+ // SQL-unavailable fallback for `readAllPullRequests`. Mirrors
154
+ // work-items-store's `_enumerateJsonScopes` shape.
155
+ function _enumerateJsonScopes() {
156
+ const shared = require('./shared');
157
+ const scopes = [];
158
+ const centralPath = path.join(shared.MINIONS_DIR, 'pull-requests.json');
159
+ if (fs.existsSync(centralPath)) scopes.push('central');
160
+ const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
161
+ try {
162
+ for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
163
+ if (!d.isDirectory()) continue;
164
+ const fp = path.join(projectsDir, d.name, 'pull-requests.json');
165
+ if (fs.existsSync(fp)) scopes.push(d.name);
166
+ }
167
+ } catch { /* projects dir missing on fresh install */ }
168
+ return scopes;
169
+ }
170
+
152
171
  function readAllPullRequests() {
153
172
  const { getDb } = require('./db');
154
173
  let db;
155
174
  try { db = getDb(); }
156
- catch { return null; }
175
+ catch {
176
+ // Issue #3035: SQLite unavailable (Node 22.x without
177
+ // --experimental-sqlite) — read every JSON scope on disk so the
178
+ // aggregate readers (queries.getPullRequests, shared.getPrLinks)
179
+ // continue to return real data instead of [].
180
+ const out = [];
181
+ for (const scope of _enumerateJsonScopes()) {
182
+ for (const pr of _readJsonArrayFallback(scope)) {
183
+ if (!pr || typeof pr !== 'object') continue;
184
+ pr._scope = scope;
185
+ out.push(pr);
186
+ }
187
+ }
188
+ return out;
189
+ }
157
190
 
158
191
  try { _resyncScopeIfJsonDiverged(db, 'central'); } catch {}
159
192
  const knownScopes = db.prepare('SELECT DISTINCT scope FROM pull_requests').all().map(r => r.scope);
package/engine/shared.js CHANGED
@@ -532,6 +532,10 @@ function _routeJsonReadToSql(p) {
532
532
  const store = require('./small-state-store');
533
533
  return { value: store.readQaSessions() };
534
534
  }
535
+ if (norm.endsWith('/engine/pr-links.json')) {
536
+ const store = require('./small-state-store');
537
+ return { value: store.readPrLinks() };
538
+ }
535
539
  // Per-project work-items.json — match `/projects/<name>/work-items.json`.
536
540
  // When SQL has no rows for the scope AND the JSON file is absent on
537
541
  // disk, preserve the legacy "file missing → null" semantic. This guards
@@ -582,9 +586,9 @@ function _routeJsonReadToSql(p) {
582
586
  *
583
587
  * **SQL routing (Phase 9):** when the path matches a migrated state file
584
588
  * (work-items, pull-requests, dispatch, metrics, watches, schedule-runs,
585
- * pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions),
586
- * the read is served from SQLite via `_routeJsonReadToSql` — the JSON file
587
- * on disk is no longer authoritative.
589
+ * pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions,
590
+ * pr-links), the read is served from SQLite via `_routeJsonReadToSql` — the
591
+ * JSON file on disk is no longer authoritative.
588
592
  *
589
593
  * Counterpart: `safeJsonNoRestore` for terminal artifacts and "missing == gone"
590
594
  * reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
@@ -4988,9 +4992,19 @@ function getPrLinks() {
4988
4992
  mergePrLinkItems(links, pr.id, pr.prdItems || []);
4989
4993
  }
4990
4994
  } catch { /* SQL unavailable — skip primary source */ }
4991
- // Fallback: static pr-links.json for entries not covered above
4995
+ // Fallback: pr_links SQL store (was static pr-links.json file). Phase 9.1
4996
+ // routes both reads and writes through small-state-store; the JSON file is
4997
+ // kept as a dual-write mirror. SQL failures fall through to the JSON file.
4998
+ let static_ = null;
4992
4999
  try {
4993
- const static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8'));
5000
+ const store = require('./small-state-store');
5001
+ static_ = store.readPrLinks();
5002
+ } catch { static_ = null; }
5003
+ if (!static_ || Object.keys(static_).length === 0) {
5004
+ try { static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8')); }
5005
+ catch { static_ = null; }
5006
+ }
5007
+ if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
4994
5008
  for (const [k, v] of Object.entries(static_)) {
4995
5009
  const canonical = parseCanonicalPrId(k);
4996
5010
  let normalizedKey = canonical ? `${canonical.scope}#${canonical.prNumber}` : k;
@@ -5004,7 +5018,7 @@ function getPrLinks() {
5004
5018
  }
5005
5019
  if (!links[normalizedKey]) mergePrLinkItems(links, normalizedKey, v);
5006
5020
  }
5007
- } catch { /* missing */ }
5021
+ }
5008
5022
  return links;
5009
5023
  }
5010
5024
 
@@ -5030,7 +5044,7 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
5030
5044
  const effectivePrId = canonicalPrId || prId;
5031
5045
  if (!effectivePrId || !itemId) return;
5032
5046
  const legacyPrId = String(prId || '');
5033
- mutateJsonFileLocked(PR_LINKS_PATH, (links) => {
5047
+ const mutator = (links) => {
5034
5048
  if (!links || Array.isArray(links) || typeof links !== 'object') links = {};
5035
5049
  const mergedCurrent = new Set(normalizePrLinkItems(links[effectivePrId]));
5036
5050
  if (legacyPrId && legacyPrId !== effectivePrId && links[legacyPrId]) {
@@ -5040,7 +5054,24 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
5040
5054
  if (!mergedCurrent.has(itemId)) mergedCurrent.add(itemId);
5041
5055
  links[effectivePrId] = [...mergedCurrent];
5042
5056
  return links;
5043
- }, { defaultValue: {} });
5057
+ };
5058
+ // Phase 9.1: pr-links is SQL-canonical via small-state-store; the JSON file
5059
+ // is a dual-write mirror. SQLite failures fall through to the legacy JSON
5060
+ // path so older installs without --experimental-sqlite still work.
5061
+ let routedViaSql = false;
5062
+ try {
5063
+ const store = require('./small-state-store');
5064
+ store.applyPrLinksMutation(mutator);
5065
+ try { store._mirrorPrLinksJson(); } catch { /* mirror best-effort */ }
5066
+ routedViaSql = true;
5067
+ } catch (e) {
5068
+ if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
5069
+ throw e;
5070
+ }
5071
+ }
5072
+ if (!routedViaSql) {
5073
+ mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
5074
+ }
5044
5075
 
5045
5076
  if (!project) return;
5046
5077
  const prPath = projectPrPath(project);
@@ -829,6 +829,118 @@ function _mirrorQaSessionsJson(filePath) {
829
829
  } catch { /* mirror best-effort */ }
830
830
  }
831
831
 
832
+ // ─── pr_links ───────────────────────────────────────────────────────────────
833
+ // Shape: { [prId]: <value> } where <value> is typically array of item IDs
834
+ // but may also be a bare string (legacy). Readers normalize.
835
+
836
+ let _prLinksHash = null;
837
+
838
+ function _hydratePrLinks(db) {
839
+ const fp = _resolveFilePath('pr-links.json');
840
+ const raw = _readJson(fp) || {};
841
+ db.prepare('DELETE FROM pr_links').run();
842
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
843
+ const now = Date.now();
844
+ const ins = db.prepare('INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(pr_id) DO NOTHING');
845
+ for (const [prId, value] of Object.entries(raw)) {
846
+ ins.run(String(prId), JSON.stringify(value), now);
847
+ }
848
+ }
849
+
850
+ function _resyncPrLinksIfDiverged(db) {
851
+ const fp = _resolveFilePath('pr-links.json');
852
+ const currentHash = _fileContentHash(fp);
853
+ if (currentHash == null) return;
854
+ if (_prLinksHash != null && currentHash === _prLinksHash) return;
855
+ if (_prLinksHash == null) {
856
+ const sqlHas = db.prepare('SELECT 1 FROM pr_links LIMIT 1').get();
857
+ if (sqlHas) {
858
+ _prLinksHash = currentHash;
859
+ return;
860
+ }
861
+ }
862
+ _hydratePrLinks(db);
863
+ _prLinksHash = currentHash;
864
+ }
865
+
866
+ function _readPrLinksFromSql(db) {
867
+ const rows = db.prepare('SELECT pr_id, data FROM pr_links').all();
868
+ const out = {};
869
+ for (const row of rows) {
870
+ try { out[row.pr_id] = JSON.parse(row.data); }
871
+ catch { /* skip malformed */ }
872
+ }
873
+ return out;
874
+ }
875
+
876
+ function readPrLinks() {
877
+ const { getDb } = require('./db');
878
+ let db;
879
+ try { db = getDb(); }
880
+ catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
881
+ try { _resyncPrLinksIfDiverged(db); }
882
+ catch { /* table may be missing on a stale install — fall back to JSON */ }
883
+ let out;
884
+ try { out = _readPrLinksFromSql(db); }
885
+ catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
886
+ if (Object.keys(out).length === 0) {
887
+ const fallback = _readJson(_resolveFilePath('pr-links.json'));
888
+ if (fallback && Object.keys(fallback).length > 0) return fallback;
889
+ return {};
890
+ }
891
+ return out;
892
+ }
893
+
894
+ function applyPrLinksMutation(mutator) {
895
+ const { getDb, withTransaction } = require('./db');
896
+ let db;
897
+ try { db = getDb(); }
898
+ catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
899
+
900
+ return withTransaction(db, () => {
901
+ _resyncPrLinksIfDiverged(db);
902
+ const before = _readPrLinksFromSql(db);
903
+ const beforeSnap = JSON.parse(JSON.stringify(before));
904
+ const next = mutator(before);
905
+ const after = (next === undefined || next === null) ? before : next;
906
+ if (!after || typeof after !== 'object' || Array.isArray(after)) {
907
+ return { wrote: false, result: beforeSnap };
908
+ }
909
+ const afterIds = new Set(Object.keys(after));
910
+ const beforeIds = new Set(Object.keys(beforeSnap));
911
+ let wrote = false;
912
+ const now = Date.now();
913
+ const upsert = db.prepare(`
914
+ INSERT INTO pr_links (pr_id, data, updated_at)
915
+ VALUES (?, ?, ?)
916
+ ON CONFLICT(pr_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
917
+ `);
918
+ const del = db.prepare('DELETE FROM pr_links WHERE pr_id = ?');
919
+ for (const id of afterIds) {
920
+ if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
921
+ upsert.run(id, JSON.stringify(after[id]), now);
922
+ wrote = true;
923
+ }
924
+ }
925
+ for (const id of beforeIds) {
926
+ if (!afterIds.has(id)) { del.run(id); wrote = true; }
927
+ }
928
+ return { wrote, result: after };
929
+ });
930
+ }
931
+
932
+ function _mirrorPrLinksJson(filePath) {
933
+ try {
934
+ const shared = require('./shared');
935
+ const { getDb } = require('./db');
936
+ const obj = _readPrLinksFromSql(getDb());
937
+ const target = filePath || _resolveFilePath('pr-links.json');
938
+ shared.safeWrite(target, obj);
939
+ const h = _fileContentHash(target);
940
+ if (h != null) _prLinksHash = h;
941
+ } catch { /* mirror best-effort */ }
942
+ }
943
+
832
944
  // ─── Test seam ─────────────────────────────────────────────────────────────
833
945
 
834
946
  function _resetAllForTest() {
@@ -841,6 +953,7 @@ function _resetAllForTest() {
841
953
  db.exec('DELETE FROM worktree_pool');
842
954
  try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
843
955
  try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
956
+ try { db.exec('DELETE FROM pr_links'); } catch { /* migration not applied */ }
844
957
  } catch { /* not initialized */ }
845
958
  _scheduleRunsHash = null;
846
959
  _pipelineRunsHash = null;
@@ -848,6 +961,7 @@ function _resetAllForTest() {
848
961
  _worktreePoolHash = null;
849
962
  _qaRunsHash = null;
850
963
  _qaSessionsHash = null;
964
+ _prLinksHash = null;
851
965
  }
852
966
 
853
967
  module.exports = {
@@ -875,6 +989,10 @@ module.exports = {
875
989
  readQaSessions,
876
990
  applyQaSessionsMutation,
877
991
  _mirrorQaSessionsJson,
992
+ // pr_links
993
+ readPrLinks,
994
+ applyPrLinksMutation,
995
+ _mirrorPrLinksJson,
878
996
  // test seam
879
997
  _resetAllForTest,
880
998
  };
@@ -133,10 +133,19 @@ function openAppendFd(name) {
133
133
  try { return fs.openSync(path.join(dir, name), 'a'); } catch { return 'ignore'; }
134
134
  }
135
135
 
136
+ function _sqliteSpawnFlags() {
137
+ if (process.env.MINIONS_FORCE_JSON === '1') return [];
138
+ const major = parseInt(String(process.versions.node).split('.')[0], 10);
139
+ if (!Number.isFinite(major) || major < 22 || major >= 24) return [];
140
+ const nodeOpts = String(process.env.NODE_OPTIONS || '');
141
+ if (nodeOpts.includes('--experimental-sqlite')) return [];
142
+ return ['--experimental-sqlite'];
143
+ }
144
+
136
145
  function spawnEngine() {
137
146
  const out = openAppendFd('engine-stdio.log');
138
147
  const err = openAppendFd('engine-stdio.log');
139
- const proc = spawn(process.execPath, [path.join(MINIONS_DIR, 'engine.js'), 'start'], {
148
+ const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_DIR, 'engine.js'), 'start'], {
140
149
  cwd: MINIONS_DIR,
141
150
  stdio: ['ignore', out, err],
142
151
  detached: true,
@@ -150,7 +159,7 @@ function spawnDashboard() {
150
159
  const out = openAppendFd('dashboard-stdio.log');
151
160
  const err = openAppendFd('dashboard-stdio.log');
152
161
  const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
153
- const proc = spawn(process.execPath, [path.join(MINIONS_DIR, 'dashboard.js')], {
162
+ const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_DIR, 'dashboard.js')], {
154
163
  cwd: MINIONS_DIR,
155
164
  stdio: ['ignore', out, err],
156
165
  detached: true,
@@ -138,14 +138,52 @@ function _resyncScopeIfJsonDiverged(db, scope) {
138
138
  _lastMirrorHashByScope.set(scope, currentHash);
139
139
  }
140
140
 
141
+ // Enumerate scopes that have a JSON file on disk — used as the SQL-
142
+ // unavailable fallback for `readAllWorkItems`. Walks `<MINIONS_DIR>/
143
+ // projects/*/work-items.json` plus the central `<MINIONS_DIR>/work-
144
+ // items.json`. Silently swallows missing dirs (fresh installs).
145
+ function _enumerateJsonScopes() {
146
+ const shared = require('./shared');
147
+ const scopes = [];
148
+ const centralPath = path.join(shared.MINIONS_DIR, 'work-items.json');
149
+ if (fs.existsSync(centralPath)) scopes.push('central');
150
+ const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
151
+ try {
152
+ for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
153
+ if (!d.isDirectory()) continue;
154
+ const fp = path.join(projectsDir, d.name, 'work-items.json');
155
+ if (fs.existsSync(fp)) scopes.push(d.name);
156
+ }
157
+ } catch { /* projects dir missing on fresh install */ }
158
+ return scopes;
159
+ }
160
+
141
161
  // Read all rows across all scopes — used by queries.getWorkItems which
142
162
  // needs to surface central + every project's items in a single shot,
143
163
  // tagged with their source scope.
164
+ //
165
+ // Issue #3035: SQL-unavailable installs (Node 22.x without
166
+ // --experimental-sqlite, prior to v0.1.2113) returned [] from this path
167
+ // because `getDb()` throws and the legacy `return null` signal was
168
+ // swallowed by `queries.getWorkItems`'s `|| []`. Mirror the per-scope
169
+ // reader's JSON-fallback shape so the aggregate API stays useful on
170
+ // Node versions that don't have node:sqlite enabled.
144
171
  function readAllWorkItems() {
145
172
  const { getDb } = require('./db');
146
173
  let db;
147
174
  try { db = getDb(); }
148
- catch { return null; } // signal: SQL unavailable, caller must fall back
175
+ catch {
176
+ // SQLite unavailable — read every JSON scope on disk directly.
177
+ const out = [];
178
+ for (const scope of _enumerateJsonScopes()) {
179
+ for (const wi of _readJsonArrayFallback(scope)) {
180
+ if (!wi || typeof wi !== 'object') continue;
181
+ wi._source = scope;
182
+ out.push(wi);
183
+ }
184
+ }
185
+ return out;
186
+ }
149
187
 
150
188
  // Pick up any external JSON edits for every scope SQL knows about.
151
189
  // Also resync 'central' explicitly so first-time reads on a JSON-only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2113",
3
+ "version": "0.1.2115",
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"