@yemi33/minions 0.1.1877 → 0.1.1879

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1879 (2026-05-12)
4
+
5
+ ### Fixes
6
+ - reconcile stale failed plan follow-ups (#2366) (#2371)
7
+ - Copilot resolver picks POSIX shim on Windows (#2370) (#2372)
8
+
3
9
  ## 0.1.1877 (2026-05-11)
4
10
 
5
11
  ### Fixes
@@ -61,12 +61,60 @@ async function refreshPlans() {
61
61
  } catch (e) { console.error('plans refresh:', e.message); }
62
62
  }
63
63
 
64
+ /**
65
+ * Returns true when a failed plan follow-up work item's intended outcome has
66
+ * been satisfied by a later/alternate path — so the stale failure should not
67
+ * drive the plan's top-level "has failures" badge.
68
+ *
69
+ * Reconciliation rules (issue #2366):
70
+ * - Failed `itemType: 'pr'` (Create PR for plan): satisfied when a PR linked
71
+ * to the same plan exists in any non-terminated state (active/merged/linked).
72
+ * The original failure is preserved on the WI for audit; it just doesn't
73
+ * count toward the rollup once a PR actually got created.
74
+ * - Failed `itemType: 'verify'`: satisfied when a newer verify WI for the
75
+ * same plan + project exists and is done OR in-flight (pending/dispatched).
76
+ *
77
+ * Returns false when the outcome cannot be confirmed satisfied — failure
78
+ * remains live and drives the badge.
79
+ */
80
+ function isFollowUpOutcomeSatisfied(wi, allWorkItems, allPrs) {
81
+ if (!wi || wi.status !== 'failed') return false;
82
+ if (wi.itemType === 'pr') {
83
+ // PR for this plan was created on any non-terminated path.
84
+ // "abandoned" / "closed" are terminal failures; everything else counts as
85
+ // the desired outcome being reached.
86
+ return (allPrs || []).some(pr =>
87
+ pr && pr.sourcePlan === wi.sourcePlan &&
88
+ pr.status !== 'abandoned' && pr.status !== 'closed'
89
+ );
90
+ }
91
+ if (wi.itemType === 'verify') {
92
+ // A newer verify WI for the same plan + project that is in flight or done.
93
+ const wiCreated = wi.created || '';
94
+ return (allWorkItems || []).some(other =>
95
+ other && other !== wi && other.itemType === 'verify' &&
96
+ other.sourcePlan === wi.sourcePlan &&
97
+ (other.project || '') === (wi.project || '') &&
98
+ (other.status === 'done' || other.status === 'pending' || other.status === 'dispatched') &&
99
+ (other.created || '') > wiCreated
100
+ );
101
+ }
102
+ return false;
103
+ }
104
+
64
105
  /**
65
106
  * Derive effective plan/PRD status from work items (single source of truth).
66
107
  * PRD JSON status is treated as user intent (approved, paused, rejected),
67
108
  * but completion/progress is always derived from actual work item state.
109
+ *
110
+ * opts.allPrs — optional PR records used to reconcile stale failed follow-up
111
+ * tasks (issue #2366). Falls back to window._lastStatus.pullRequests in the
112
+ * browser. Pass `[]` to disable reconciliation.
68
113
  */
69
- function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
114
+ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems, opts) {
115
+ const allPrs = (opts && opts.allPrs)
116
+ || (typeof window !== 'undefined' && window._lastStatus && window._lastStatus.pullRequests)
117
+ || [];
70
118
  const wi = workItems.filter(w =>
71
119
  w.sourcePlan === prdFile || w.sourcePlan === mdFile ||
72
120
  (w.type === 'plan-to-prd' && (w.planFile === prdFile || w.planFile === mdFile))
@@ -74,8 +122,18 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
74
122
  const implementWi = wi.filter(w => w.type !== 'plan-to-prd' && w.type !== 'verify');
75
123
  const hasPendingPrd = wi.some(w => w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched'));
76
124
  const hasActiveWork = implementWi.some(w => w.status === 'pending' || w.status === 'dispatched');
77
- const allDone = implementWi.length > 0 && implementWi.every(w => w.status === 'done' || w.status === 'decomposed');
78
- const hasFailed = implementWi.some(w => w.status === 'failed');
125
+ // A WI is "effectively done" when it succeeded normally OR when it is a
126
+ // reconciled follow-up failure (intended outcome satisfied elsewhere — #2366).
127
+ // The original failure record is preserved on the WI for audit; it just
128
+ // stops blocking plan completion and stops driving the red badge.
129
+ const isReconciledFailure = (w) =>
130
+ w.status === 'failed' &&
131
+ (w.itemType === 'pr' || w.itemType === 'verify') &&
132
+ isFollowUpOutcomeSatisfied(w, workItems, allPrs);
133
+ const isEffectivelyDone = (w) =>
134
+ w.status === 'done' || w.status === 'decomposed' || isReconciledFailure(w);
135
+ const allDone = implementWi.length > 0 && implementWi.every(isEffectivelyDone);
136
+ const hasFailed = implementWi.some(w => w.status === 'failed' && !isReconciledFailure(w));
79
137
 
80
138
  // User-set statuses take priority when no work has started
81
139
  if (prdJsonStatus === 'rejected') return 'rejected';
@@ -897,4 +955,4 @@ async function planUnarchive(file, btn) {
897
955
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
898
956
  }
899
957
 
900
- window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, planExecute, planReexecuteModal, planReexecuteSubmit, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
958
+ window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, isFollowUpOutcomeSatisfied, renderPlans, openArchivedPlansModal, planExecute, planReexecuteModal, planReexecuteSubmit, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
@@ -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.1877",
3
+ "version": "0.1.1879",
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"