@yemi33/minions 0.1.1876 → 0.1.1878

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1878 (2026-05-12)
4
+
5
+ ### Fixes
6
+ - Copilot resolver picks POSIX shim on Windows (#2370) (#2372)
7
+
8
+ ## 0.1.1877 (2026-05-11)
9
+
10
+ ### Fixes
11
+ - PRD header showing Completed when items are still missing (#2364)
12
+
3
13
  ## 0.1.1876 (2026-05-11)
4
14
 
5
15
  ### Features
@@ -35,7 +35,9 @@ function renderPrd(prd, prog) {
35
35
  // Single PRD — show status + actions in header
36
36
  const implementItems = allWi.filter(w => prdItems.some(pi => pi.id === w.id));
37
37
  const allDone = implementItems.length > 0 && implementItems.every(w => w.status === 'done');
38
- const hasActive = implementItems.some(w => w.status === 'pending' || w.status === 'dispatched');
38
+ // Unmaterialized PRD items (status === 'missing') count as active — they still need work.
39
+ const hasMissing = prdItems.some(i => i.status === 'missing');
40
+ const hasActive = hasMissing || implementItems.some(w => w.status === 'pending' || w.status === 'dispatched');
39
41
  const prdFile = existing[0]?.file || '';
40
42
  const prdStatus = existing[0]?.status || '';
41
43
  const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? 'dispatched' : prdStatus || 'active';
@@ -65,7 +67,9 @@ function renderPrd(prd, prog) {
65
67
  const items = prdItems.filter(i => i.source === p.file);
66
68
  const wiForPrd = allWi.filter(w => items.some(pi => pi.id === w.id));
67
69
  const allDone = wiForPrd.length > 0 && wiForPrd.every(w => w.status === 'done');
68
- const hasActive = wiForPrd.some(w => w.status === 'pending' || w.status === 'dispatched');
70
+ // Unmaterialized PRD items (status === 'missing') count as active — they still need work.
71
+ const hasMissing = items.some(i => i.status === 'missing');
72
+ const hasActive = hasMissing || wiForPrd.some(w => w.status === 'pending' || w.status === 'dispatched');
69
73
  const s = allDone && !hasActive ? 'completed' : hasActive ? 'dispatched' : p.status || 'active';
70
74
  counts[s] = (counts[s] || 0) + 1;
71
75
  }
package/engine/queries.js CHANGED
@@ -1313,6 +1313,7 @@ function getPrdInfo(config) {
1313
1313
  if (_prdResultCache && hash === _prdResultInputHash) return _prdResultCache;
1314
1314
 
1315
1315
  let allPrdItems = [];
1316
+ const existingPrds = [];
1316
1317
  let latestStat = null;
1317
1318
 
1318
1319
  // Check if directory listings need refresh
@@ -1353,6 +1354,13 @@ function getPrdInfo(config) {
1353
1354
  if (recorded && sourceMtime > recorded) planStale = true;
1354
1355
  } catch { /* optional */ }
1355
1356
  }
1357
+ existingPrds.push({
1358
+ file: pf,
1359
+ status: plan.status || 'active',
1360
+ planStale: planStale || plan.planStale || false,
1361
+ completedAt: plan.completedAt || '',
1362
+ _archived: archived,
1363
+ });
1356
1364
  for (const f of plan.missing_features) {
1357
1365
  allPrdItems.push({
1358
1366
  ...f, _source: pf, _planStatus: plan.status || 'active',
@@ -1497,7 +1505,7 @@ function getPrdInfo(config) {
1497
1505
 
1498
1506
  const status = {
1499
1507
  exists: true, age: latestStat ? timeSince(latestStat.mtimeMs) : 'unknown',
1500
- existing: 0, missing: items.filter(i => i.status === 'missing').length, questions: 0, summary: '',
1508
+ existing: existingPrds, missing: items.filter(i => i.status === 'missing').length, questions: 0, summary: '',
1501
1509
  missingList: items.filter(i => i.status === 'missing').map(f => ({ id: f.id, name: f.name || f.title, priority: f.priority, complexity: f.estimated_complexity || f.size })),
1502
1510
  };
1503
1511
 
@@ -55,6 +55,14 @@ const isWin = process.platform === 'win32';
55
55
  const CAPS_FILE = path.join(_CACHE_DIR, 'copilot-caps.json');
56
56
  const MODELS_CACHE = path.join(_CACHE_DIR, 'copilot-models.json');
57
57
 
58
+ // Bump when cache shape or resolution semantics change so we can ignore stale
59
+ // entries from earlier resolver versions. v1 (pre-issue-2370): picked
60
+ // `where copilot` line 1, which silently selected the POSIX bash shim on
61
+ // Windows npm installs and chained into a node.exe-loads-bash-script crash.
62
+ // v2: parses the full `where` output, prefers .exe, and resolves npm shims
63
+ // to the sibling node_modules/@github/copilot/index.js JS entry.
64
+ const CAPS_SCHEMA_VERSION = 2;
65
+
58
66
  function _safeJson(p) {
59
67
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
60
68
  }
@@ -68,16 +76,149 @@ function _execSyncCapture(cmd, env, timeoutMs = 10000) {
68
76
  }
69
77
 
70
78
  /**
71
- * Probe PATH for a standalone `copilot` binary. Returns the absolute path or
79
+ * Pure helper: choose the best `copilot` binary from a `where` / `which`
80
+ * output buffer. Returns `{ bin, native }` or null.
81
+ *
82
+ * Background (issue #2370): on Windows `where copilot` enumerates every file
83
+ * along PATH in PATHEXT order, e.g. for an `npm i -g @github/copilot` install:
84
+ * C:\Users\<u>\AppData\Roaming\npm\copilot (POSIX `#!/bin/sh` shim)
85
+ * C:\Users\<u>\AppData\Roaming\npm\copilot.cmd (Windows cmd shim)
86
+ * C:\Users\<u>\AppData\Roaming\npm\copilot.ps1 (PowerShell shim)
87
+ * The legacy resolver took `.split('\n')[0]` (the extensionless shim) and
88
+ * marked it `native: false`, which made engine/llm.js spawn `node.exe <shim>`.
89
+ * Node parsed the bash script as JS and exit-1'd in <1s, silently breaking
90
+ * every Copilot dispatch.
91
+ *
92
+ * Resolution priority on Windows:
93
+ * 1. `.exe` → native:true, run directly
94
+ * 2. JS entry sibling of a `.cmd`/`.bat`/shim (npm install pattern) → native:false
95
+ * 3. `.js` → native:false, loaded by Node
96
+ * Anything else (`.cmd`/`.bat` without a JS sibling, raw POSIX shim) is
97
+ * skipped — spawning it via the existing llm.js code path would fail.
98
+ *
99
+ * On non-Windows the standard `which` always returns one usable file, so we
100
+ * pick the first that exists. `existsSync` is parameterized so unit tests
101
+ * can simulate sibling-file presence without touching the real filesystem.
102
+ */
103
+ function _pickStandaloneCopilotFromOutput(rawOutput, { existsSync = fs.existsSync, isWin: _isWin = isWin } = {}) {
104
+ if (rawOutput == null) return null;
105
+ const lines = String(rawOutput).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
106
+ if (lines.length === 0) return null;
107
+ const existing = lines.filter(l => {
108
+ try { return existsSync(l); } catch { return false; }
109
+ });
110
+ if (existing.length === 0) return null;
111
+
112
+ if (!_isWin) {
113
+ // POSIX `which` returns one line; first existing entry wins. Mark native
114
+ // when there's no extension or an executable extension — but since
115
+ // engine/llm.js only checks `.exe` for native on Windows and otherwise
116
+ // routes through process.execPath on non-native, we keep native:true on
117
+ // POSIX (matches legacy behavior — spawn(<bin>) works directly).
118
+ return { bin: existing[0], native: true };
119
+ }
120
+
121
+ const byExt = (ext) => existing.find(l => l.toLowerCase().endsWith(ext));
122
+ const exe = byExt('.exe');
123
+ if (exe) return { bin: exe, native: true };
124
+
125
+ // npm shim path: copilot.cmd / copilot.bat / extensionless shim → look for
126
+ // a Node-loadable JS entry next to it. npm places the package at
127
+ // <shim_dir>\node_modules\@github\copilot\index.js for `@github/copilot`.
128
+ // The .cmd shim itself just invokes `node "%~dp0\node_modules\@github\copilot\index.js"`,
129
+ // so resolving directly to the JS entry sidesteps both the .cmd-via-node
130
+ // crash AND the POSIX-shim-via-node crash.
131
+ const shim = byExt('.cmd') || byExt('.bat') || existing.find(l => !path.extname(l));
132
+ if (shim) {
133
+ const jsEntry = _resolveNpmCopilotJsEntry(shim, existsSync);
134
+ if (jsEntry) return { bin: jsEntry, native: false };
135
+ }
136
+
137
+ // Fall back to a bare `.js` listing (rare — npm rarely emits a `.js`
138
+ // alongside the shims, but a hand-rolled install may).
139
+ const js = byExt('.js');
140
+ if (js) return { bin: js, native: false };
141
+
142
+ // Nothing safely spawnable. Let resolveBinary fall through to the gh
143
+ // extension probe rather than caching a known-broken path.
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Given a Windows shim path like `C:\...\npm\copilot.cmd` (or the
149
+ * extensionless POSIX shim sibling), look for the actual JS entry the shim
150
+ * dispatches to. Returns the absolute path or null when none exists.
151
+ *
152
+ * Layout produced by `npm i -g @github/copilot` on Windows:
153
+ * <prefix>\copilot (POSIX shim)
154
+ * <prefix>\copilot.cmd (Windows shim)
155
+ * <prefix>\copilot.ps1
156
+ * <prefix>\node_modules\@github\copilot\index.js ← what the shim invokes
157
+ *
158
+ * We probe a small list of conventional JS entry locations rather than
159
+ * parsing the shim itself — shim contents are npm-version-specific and not
160
+ * a stable contract.
161
+ */
162
+ function _resolveNpmCopilotJsEntry(shimPath, existsSync = fs.existsSync) {
163
+ if (!shimPath) return null;
164
+ // Separator-agnostic dirname. path.dirname() on POSIX hosts (e.g. Linux CI
165
+ // runners) does not recognize backslashes, so feeding a Windows shim path
166
+ // like `C:\Users\u\AppData\Roaming\npm\copilot.cmd` returns '.' and breaks
167
+ // every npm-layout probe. Split on whichever separator appears last.
168
+ const lastSep = Math.max(shimPath.lastIndexOf('\\'), shimPath.lastIndexOf('/'));
169
+ const dir = lastSep >= 0 ? shimPath.slice(0, lastSep) : '.';
170
+ const candidates = [
171
+ path.join(dir, 'node_modules', '@github', 'copilot', 'index.js'),
172
+ path.join(dir, 'node_modules', '@github', 'copilot', 'dist', 'index.js'),
173
+ path.join(dir, 'node_modules', '@github', 'copilot', 'bin', 'index.js'),
174
+ path.join(dir, 'node_modules', '@github', 'copilot-cli', 'index.js'),
175
+ ];
176
+ for (const c of candidates) {
177
+ try { if (existsSync(c)) return c; } catch { /* best effort */ }
178
+ }
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Probe PATH for a standalone `copilot` binary. Returns `{ bin, native }` or
72
184
  * null. Resilient to non-zero exits (where/which return 1 when nothing found).
185
+ *
186
+ * Native flag:
187
+ * - true → spawn directly (`.exe`, POSIX `which` hit)
188
+ * - false → spawn via process.execPath (Node), bin must be JS-loadable
73
189
  */
74
190
  function _findStandaloneCopilot(env) {
75
191
  try {
76
192
  const cmd = isWin ? 'where copilot 2>NUL' : 'which copilot 2>/dev/null';
77
- const which = _execSyncCapture(cmd, env).trim().split('\n')[0].trim();
78
- if (which && fs.existsSync(which)) return which;
79
- } catch { /* PATH probe is optional */ }
80
- return null;
193
+ const raw = _execSyncCapture(cmd, env);
194
+ return _pickStandaloneCopilotFromOutput(raw);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Validate a cached resolveBinary entry against the current schema. Returns
202
+ * true when the cache is safe to reuse, false when it must be re-resolved.
203
+ *
204
+ * Hard invalidation rules:
205
+ * - missing/non-matching schema version (pre-#2370 caches)
206
+ * - on Windows, copilotIsNative=false but bin lacks a Node-loadable
207
+ * extension (extensionless POSIX shim, .cmd, .bat — any of which would
208
+ * re-trigger the issue #2370 crash on next spawn)
209
+ */
210
+ function _isCachedBinUsable(cached) {
211
+ if (!cached || typeof cached !== 'object') return false;
212
+ if (typeof cached.copilotBin !== 'string' || !cached.copilotBin) return false;
213
+ if (cached.version !== CAPS_SCHEMA_VERSION) return false;
214
+ try { if (!fs.existsSync(cached.copilotBin)) return false; } catch { return false; }
215
+ if (isWin && cached.copilotIsNative === false) {
216
+ const ext = path.extname(cached.copilotBin).toLowerCase();
217
+ // Only Node-loadable extensions are safe with native:false on Windows.
218
+ // Anything else (extensionless shim, .cmd, .bat) would crash on spawn.
219
+ if (ext !== '.js' && ext !== '.mjs' && ext !== '.cjs') return false;
220
+ }
221
+ return true;
81
222
  }
82
223
 
83
224
  /**
@@ -114,32 +255,40 @@ function _findGhCopilotExtension(env) {
114
255
  * standalone CLI" warnings without re-probing.
115
256
  */
116
257
  function resolveBinary({ env = process.env } = {}) {
117
- // 1. Cache hit — fastest path
258
+ // 1. Cache hit — fastest path. Validate before trusting: stale pre-#2370
259
+ // entries that point at a POSIX bash shim with native:false would re-crash
260
+ // every spawn until manually wiped, so we re-resolve when the schema or
261
+ // shape doesn't match what _findStandaloneCopilot now produces.
118
262
  const cached = _safeJson(CAPS_FILE);
119
- if (cached?.copilotBin && fs.existsSync(cached.copilotBin)) {
263
+ if (_isCachedBinUsable(cached)) {
120
264
  const leadingArgs = Array.isArray(cached.leadingArgs) ? cached.leadingArgs : [];
121
265
  return { bin: cached.copilotBin, native: !!cached.copilotIsNative, leadingArgs };
122
266
  }
123
267
 
124
- // 2. Standalone `copilot` first (preferred)
268
+ // 2. Standalone `copilot` first (preferred). Now returns { bin, native }
269
+ // so the resolver no longer has to guess at native-ness from extension alone
270
+ // — _pickStandaloneCopilotFromOutput owns that decision.
125
271
  const standalone = _findStandaloneCopilot(env);
126
272
  if (standalone) {
127
- const native = !isWin || path.extname(standalone).toLowerCase() === '.exe';
128
273
  _safeWriteJson(CAPS_FILE, {
129
- copilotBin: standalone,
130
- copilotIsNative: native,
274
+ version: CAPS_SCHEMA_VERSION,
275
+ copilotBin: standalone.bin,
276
+ copilotIsNative: standalone.native,
131
277
  leadingArgs: [],
132
278
  source: 'standalone',
133
279
  resolvedAt: new Date().toISOString(),
134
280
  });
135
- return { bin: standalone, native, leadingArgs: [] };
281
+ return { bin: standalone.bin, native: standalone.native, leadingArgs: [] };
136
282
  }
137
283
 
138
- // 3. `gh copilot` extension fallback (best-effort)
284
+ // 3. `gh copilot` extension fallback (best-effort). gh is itself a native
285
+ // binary (gh.exe / `gh` POSIX), so the .exe / POSIX-passthrough heuristic
286
+ // is fine here — no shim layer to worry about.
139
287
  const gh = _findGhCopilotExtension(env);
140
288
  if (gh) {
141
289
  const native = !isWin || path.extname(gh).toLowerCase() === '.exe';
142
290
  _safeWriteJson(CAPS_FILE, {
291
+ version: CAPS_SCHEMA_VERSION,
143
292
  copilotBin: gh,
144
293
  copilotIsNative: native,
145
294
  leadingArgs: ['copilot'],
@@ -1028,5 +1177,9 @@ module.exports = {
1028
1177
  _copilotAssistantMessageHasTools,
1029
1178
  _readCatalogIds,
1030
1179
  _hyphenToDotVersion,
1180
+ _pickStandaloneCopilotFromOutput,
1181
+ _resolveNpmCopilotJsEntry,
1182
+ _isCachedBinUsable,
1183
+ CAPS_SCHEMA_VERSION,
1031
1184
  KNOWN_EVENT_TYPES,
1032
1185
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1876",
3
+ "version": "0.1.1878",
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"