@yemi33/minions 0.1.2112 → 0.1.2114

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 },
@@ -4,7 +4,7 @@ const dir = path.resolve(__dirname, '..');
4
4
 
5
5
  console.log('=== Work Items (non-done) ===');
6
6
  let items = [];
7
- try { items = JSON.parse(fs.readFileSync(path.join(dir, 'work-items.json'), 'utf8')); } catch {}
7
+ try { items = require('./queries').getWorkItems() || []; } catch {}
8
8
  items.filter(i => i.status !== 'done').forEach(i => {
9
9
  console.log(i.id, (i.status || '').padEnd(12), (i.type || '').padEnd(12), (i.title || '').slice(0, 60));
10
10
  });
@@ -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
  }
@@ -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
@@ -4970,33 +4970,24 @@ function getPrLinks() {
4970
4970
  if (!knownPrIdsByDisplay.has(displayId)) knownPrIdsByDisplay.set(displayId, new Set());
4971
4971
  knownPrIdsByDisplay.get(displayId).add(pr.id);
4972
4972
  };
4973
- // Primary source: derive from all projects/*/pull-requests.json prdItems
4974
- const projectsDir = path.join(MINIONS_DIR, 'projects');
4973
+ // Primary source: derive from all per-project + central PR records via the
4974
+ // SQL-canonical store (Phase 9). `readAllPullRequests` decorates each row
4975
+ // with `_scope` so we can look up the owning project for legacy-ID
4976
+ // normalization.
4975
4977
  const projectsByName = new Map(getProjects().map(project => [project.name || path.basename(project.localPath || ''), project]));
4976
4978
  try {
4977
- for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
4978
- if (!d.isDirectory()) continue;
4979
- try {
4980
- const prs = JSON.parse(fs.readFileSync(path.join(projectsDir, d.name, 'pull-requests.json'), 'utf8'));
4981
- const project = projectsByName.get(d.name) || null;
4982
- normalizePrRecords(prs, project);
4983
- for (const pr of prs) {
4984
- if (!pr.id) continue;
4985
- registerPrId(pr);
4986
- mergePrLinkItems(links, pr.id, pr.prdItems || []);
4987
- }
4988
- } catch { /* missing or invalid */ }
4989
- }
4990
- } catch { /* projects dir missing */ }
4991
- try {
4992
- const centralPrs = JSON.parse(fs.readFileSync(path.join(MINIONS_DIR, 'pull-requests.json'), 'utf8'));
4993
- normalizePrRecords(centralPrs, null);
4994
- for (const pr of centralPrs) {
4995
- if (!pr.id) continue;
4979
+ const store = require('./pull-requests-store');
4980
+ const allPrs = store.readAllPullRequests() || [];
4981
+ for (const pr of allPrs) {
4982
+ if (!pr?.id) continue;
4983
+ const scope = pr._scope;
4984
+ delete pr._scope;
4985
+ const project = scope && scope !== 'central' ? (projectsByName.get(scope) || null) : null;
4986
+ normalizePrRecords([pr], project);
4996
4987
  registerPrId(pr);
4997
4988
  mergePrLinkItems(links, pr.id, pr.prdItems || []);
4998
4989
  }
4999
- } catch { /* central file optional */ }
4990
+ } catch { /* SQL unavailable skip primary source */ }
5000
4991
  // Fallback: static pr-links.json for entries not covered above
5001
4992
  try {
5002
4993
  const static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8'));
@@ -5054,27 +5045,21 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
5054
5045
  if (!project) return;
5055
5046
  const prPath = projectPrPath(project);
5056
5047
  const effectivePrNumber = getPrNumber(prNumber ?? effectivePrId);
5057
- const prLockPath = `${prPath}.lock`;
5058
- withFileLock(prLockPath, () => {
5059
- if (!fs.existsSync(prPath)) return;
5060
- let prs = safeJson(prPath);
5061
- if (!Array.isArray(prs)) prs = [];
5048
+ // Phase 9 SQL routing: mutatePullRequests writes through pull-requests-store
5049
+ // so SQL stays canonical; the JSON mirror is regenerated by the store.
5050
+ mutatePullRequests(prPath, (prs) => {
5051
+ if (!Array.isArray(prs) || prs.length === 0) return prs;
5062
5052
  normalizePrRecords(prs, project);
5063
5053
  const existingPr = prs.find(pr =>
5064
5054
  pr?.id === effectivePrId
5065
5055
  || (url && pr?.url === url)
5066
5056
  || (effectivePrNumber != null && getPrNumber(pr) === effectivePrNumber)
5067
5057
  );
5068
- if (!existingPr) return;
5069
- const backupPath = prPath + '.backup';
5070
- try { if (fs.existsSync(prPath)) fs.copyFileSync(prPath, backupPath); } catch { /* backup is best-effort */ }
5058
+ if (!existingPr) return prs;
5071
5059
  existingPr.prdItems = Array.isArray(existingPr.prdItems) ? existingPr.prdItems : [];
5072
- if (existingPr.prdItems.includes(itemId)) return;
5060
+ if (existingPr.prdItems.includes(itemId)) return prs;
5073
5061
  existingPr.prdItems.push(itemId);
5074
- safeWrite(prPath, prs);
5075
- }, {
5076
- retries: ENGINE_DEFAULTS.lockRetries,
5077
- retryBackoffMs: ENGINE_DEFAULTS.lockRetryBackoffMs
5062
+ return prs;
5078
5063
  });
5079
5064
  }
5080
5065
 
@@ -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.2112",
3
+ "version": "0.1.2114",
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"