@yemi33/minions 0.1.1686 → 0.1.1688

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.1688 (2026-05-02)
4
+
5
+ ### Fixes
6
+ - bug sweep — PRD races, hardcoded retries, capability-flag drift
7
+
8
+ ## 0.1.1687 (2026-05-02)
9
+
10
+ ### Other
11
+ - Show scan add feedback in modal
12
+
3
13
  ## 0.1.1686 (2026-05-02)
4
14
 
5
15
  ### Fixes
@@ -335,6 +335,7 @@ async function openScanProjectsModal() {
335
335
  '<button onclick="_runProjectScan()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap">Scan</button>' +
336
336
  '</div>' +
337
337
  '<div id="scan-results" style="color:var(--muted);font-size:12px">Click Scan to find git repos in the directory.</div>' +
338
+ '<div class="cmd-toast" id="scan-toast" style="margin-top:0"></div>' +
338
339
  '</div>';
339
340
  document.getElementById('modal-body').style.whiteSpace = 'normal';
340
341
  document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
@@ -410,19 +411,20 @@ async function _addSelectedProjects() {
410
411
  var data = await res.json().catch(function() { return {}; });
411
412
  if (res.ok) {
412
413
  added++;
414
+ var addedName = data.name || repo.name;
413
415
  optimisticallyAddProject({
414
- name: data.name || repo.name,
416
+ name: addedName,
415
417
  description: (data.detected && data.detected.description) || repo.description || '',
416
418
  path: data.path || repo.path,
417
419
  localPath: data.path || repo.path,
418
420
  });
419
421
  cb.disabled = true;
420
422
  cb.closest('label').style.opacity = '0.5';
423
+ showToast('scan-toast', added + ' project(s) added', true);
421
424
  }
422
425
  } catch { /* continue with next */ }
423
426
  }
424
427
  if (added > 0) {
425
- showToast('cmd-toast', added + ' project(s) added', true);
426
428
  refresh();
427
429
  }
428
430
  }
@@ -69,12 +69,14 @@ function renderSkills(skills) {
69
69
  }
70
70
  html += '</div>';
71
71
 
72
- // Note clarifying skill visibility — agents read these on demand, runtime
73
- // assets (~/.claude/skills, ~/.copilot/skills) are not auto-injected for
74
- // the OTHER runtime. A Copilot agent only sees Copilot-native + plugin skills.
72
+ // Note clarifying skill visibility — agents read these on demand. Runtime-
73
+ // native locations (~/.claude/skills, ~/.copilot/skills, plugin skills) are
74
+ // only visible to that runtime. The "agent" tab (~/.agents/skills) is the
75
+ // cross-runtime portable bucket and IS visible to every runtime.
75
76
  html += '<div style="font-size:9px;color:var(--muted);margin-bottom:8px;line-height:1.4">' +
76
77
  'Skills are reference docs agents read on demand — they are not injected wholesale into prompts. ' +
77
- 'Each tab reflects what the matching runtime would see; cross-runtime skills are NOT visible to a different runtime.' +
78
+ 'Each tab reflects what the matching runtime would see; runtime-native skills are NOT cross-visible. ' +
79
+ 'The agent tab (~/.agents/skills) is the cross-runtime portable bucket — visible to every runtime.' +
78
80
  '</div>';
79
81
 
80
82
  // Filter by tab
package/dashboard.js CHANGED
@@ -4502,39 +4502,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4502
4502
  const source = params.get('source') || '';
4503
4503
  if (!_isValidSkillFileName(file)) { res.statusCode = 400; res.end('Invalid file'); return; }
4504
4504
 
4505
- let content = '';
4506
4505
  const skillPath = _resolveSkillReadPath({ file, dir, source, config: CONFIG });
4507
- if (skillPath) content = safeRead(skillPath) || '';
4508
- // Fallback when caller didn't supply `dir`: try the source's known native
4509
- // locations. `_resolveSkillReadPath` only matches entries returned by
4510
- // `collectSkillFiles`, so a skill that already has `dir` will resolve there.
4511
- if (!content && !dir) {
4512
- const home = os.homedir();
4513
- const skillStem = file.replace(/\.md$/, '').replace(/^SKILL$/, '');
4514
- const candidates = [];
4515
- if (source === 'claude-code' || !source) {
4516
- candidates.push(path.join(home, '.claude', 'skills', skillStem, 'SKILL.md'));
4517
- }
4518
- if (source === 'copilot') {
4519
- candidates.push(path.join(home, '.copilot', 'skills', skillStem, 'SKILL.md'));
4520
- }
4521
- if (source === 'agent-skill') {
4522
- candidates.push(path.join(home, '.agents', 'skills', skillStem, 'SKILL.md'));
4523
- }
4524
- if (source.startsWith('project:')) {
4525
- const proj = PROJECTS.find(p => p.name === source.slice('project:'.length));
4526
- if (proj?.localPath) {
4527
- for (const sub of ['.claude', '.github', '.agents']) {
4528
- candidates.push(path.join(proj.localPath, sub, 'skills', file));
4529
- candidates.push(path.join(proj.localPath, sub, 'skills', skillStem, 'SKILL.md'));
4530
- }
4531
- }
4532
- }
4533
- for (const c of candidates) {
4534
- content = safeRead(c) || '';
4535
- if (content) break;
4536
- }
4537
- }
4506
+ const content = skillPath ? (safeRead(skillPath) || '') : '';
4538
4507
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
4539
4508
  res.setHeader('Access-Control-Allow-Origin', '*');
4540
4509
  res.end(content || 'Skill not found.');
package/engine/cleanup.js CHANGED
@@ -621,17 +621,20 @@ function runCleanup(config, verbose = false) {
621
621
  catch { orphanPrdEntries = []; }
622
622
  for (const pf of orphanPrdEntries) {
623
623
  const prdPath = path.join(PRD_DIR, pf);
624
- const prd = safeJson(prdPath);
625
- if (!prd?.missing_features) continue;
624
+ const peek = safeJson(prdPath);
625
+ if (!peek?.missing_features) continue;
626
626
  let reset = 0;
627
- for (const feat of prd.missing_features) {
628
- if ((feat.status === shared.WI_STATUS.DISPATCHED || feat.status === shared.WI_STATUS.FAILED) && !wiIds.has(feat.id)) {
629
- feat.status = shared.WI_STATUS.PENDING;
630
- reset++;
627
+ mutateJsonFileLocked(prdPath, (prd) => {
628
+ if (!prd?.missing_features) return prd;
629
+ for (const feat of prd.missing_features) {
630
+ if ((feat.status === shared.WI_STATUS.DISPATCHED || feat.status === shared.WI_STATUS.FAILED) && !wiIds.has(feat.id)) {
631
+ feat.status = shared.WI_STATUS.PENDING;
632
+ reset++;
633
+ }
631
634
  }
632
- }
635
+ return prd;
636
+ }, { skipWriteIfUnchanged: true });
633
637
  if (reset > 0) {
634
- safeWrite(prdPath, prd);
635
638
  log('info', `Reset ${reset} orphaned PRD item status(es) → pending in ${pf}`);
636
639
  cleaned.orphanedPrdStatuses += reset;
637
640
  }
package/engine/cli.js CHANGED
@@ -146,21 +146,18 @@ function _parseRuntimeFlags(args) {
146
146
  * Heuristic flag for "this model is obviously wrong for this runtime". Used
147
147
  * to surface the "pass --model '' to clear" hint when a user switches CLIs
148
148
  * but leaves a stale model behind. Errs on the side of false-negatives —
149
- * unknown runtime → no opinion, unknown model on Copilot → no opinion.
149
+ * unknown runtime → no opinion, runtime adapter without `modelLooksFamiliar`
150
+ * → no opinion. Runtime-specific knowledge lives in the adapter (see
151
+ * claude.js#modelLooksFamiliar) so adding a new runtime never requires
152
+ * editing cli.js.
150
153
  */
151
154
  function _modelLooksIncompatible(runtime, model) {
152
155
  if (!model) return false;
153
- const m = String(model).toLowerCase();
154
- if (runtime === 'claude') {
155
- if (m.startsWith('claude-')) return false;
156
- if (m === 'sonnet' || m === 'opus' || m === 'haiku') return false;
157
- return true; // gpt-*, o3-*, codex, etc. — wrong CLI for these
158
- }
159
- if (runtime === 'copilot') {
160
- // Copilot adapter maps Minions' family aliases before spawning.
161
- return false;
162
- }
163
- return false;
156
+ let adapter;
157
+ try { adapter = require('./runtimes').resolveRuntime(runtime); }
158
+ catch { return false; } // unknown runtime → no opinion
159
+ if (typeof adapter.modelLooksFamiliar !== 'function') return false; // adapter doesn't claim a model namespace no opinion
160
+ return !adapter.modelLooksFamiliar(model);
164
161
  }
165
162
 
166
163
  /**
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T18:15:36.411Z"
4
+ "cachedAt": "2026-05-02T19:44:35.476Z"
5
5
  }
@@ -1037,7 +1037,8 @@ async function findOpenPrForBranch(meta, config) {
1037
1037
  if (host === 'github') {
1038
1038
  const ghSlug = projectObj.prUrlBase?.match(/github\.com\/([^/]+\/[^/]+)\/pull/)?.[1];
1039
1039
  if (!ghSlug) return null;
1040
- for (let attempt = 0; attempt < 3; attempt++) {
1040
+ const maxAttempts = ENGINE_DEFAULTS.prAutoLinkRetries;
1041
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1041
1042
  if (attempt > 0) await new Promise(r => setTimeout(r, 3000));
1042
1043
  let raw = '';
1043
1044
  try {
@@ -1047,13 +1048,13 @@ async function findOpenPrForBranch(meta, config) {
1047
1048
  if (hits.length > 0 && hits[0].state === 'OPEN') {
1048
1049
  return { project: projectObj, prNumber: hits[0].number, url: hits[0].url };
1049
1050
  }
1050
- if (attempt === 2) {
1051
- log('warn', `Auto-link fallback: no open PR found on branch ${meta.branch} after 3 attempts (raw: ${(raw || '').slice(0, 200)})`);
1051
+ if (attempt === maxAttempts - 1) {
1052
+ log('warn', `Auto-link fallback: no open PR found on branch ${meta.branch} after ${maxAttempts} attempts (raw: ${(raw || '').slice(0, 200)})`);
1052
1053
  }
1053
1054
  } catch (err) {
1054
- if (attempt === 2) {
1055
+ if (attempt === maxAttempts - 1) {
1055
1056
  const rawSuffix = raw ? ` (raw: ${raw.slice(0, 200)})` : '';
1056
- log('warn', `Auto-link fallback: gh pr list lookup failed on branch ${meta.branch} after 3 attempts: ${err.message}${rawSuffix}`);
1057
+ log('warn', `Auto-link fallback: gh pr list lookup failed on branch ${meta.branch} after ${maxAttempts} attempts: ${err.message}${rawSuffix}`);
1057
1058
  }
1058
1059
  }
1059
1060
  }
@@ -1475,7 +1476,7 @@ async function processPendingRebases(config) {
1475
1476
  const result = await rebaseBranchOntoMain(pr, project, config);
1476
1477
  if (!result.success) {
1477
1478
  entry.attempts = (entry.attempts || 0) + 1;
1478
- if (entry.attempts < 3) {
1479
+ if (entry.attempts < ENGINE_DEFAULTS.rebaseQueueRetries) {
1479
1480
  remaining.push(entry);
1480
1481
  } else {
1481
1482
  log('warn', `Rebase failed after retries for ${pr.id} on ${pr.branch}: ${result.error}`);
@@ -730,14 +730,17 @@ function isStageComplete(stage, stageState, run, config) {
730
730
  if (stage.autoApprove && artifacts.prds?.length > 0) {
731
731
  for (const prdFile of artifacts.prds) {
732
732
  const prdPath = path.join(prdDir, prdFile);
733
- const prd = safeJson(prdPath);
734
- if (prd && prd.status === PLAN_STATUS.AWAITING_APPROVAL) {
735
- prd.status = PLAN_STATUS.APPROVED;
736
- prd.approvedAt = ts();
737
- prd.approvedBy = 'pipeline:' + run.pipelineId;
738
- safeWrite(prdPath, prd);
739
- log('info', `Pipeline ${run.pipelineId}: auto-approved PRD ${prdFile}`);
740
- }
733
+ let approved = false;
734
+ mutateJsonFileLocked(prdPath, (prd) => {
735
+ if (prd && prd.status === PLAN_STATUS.AWAITING_APPROVAL) {
736
+ prd.status = PLAN_STATUS.APPROVED;
737
+ prd.approvedAt = ts();
738
+ prd.approvedBy = 'pipeline:' + run.pipelineId;
739
+ approved = true;
740
+ }
741
+ return prd;
742
+ }, { skipWriteIfUnchanged: true });
743
+ if (approved) log('info', `Pipeline ${run.pipelineId}: auto-approved PRD ${prdFile}`);
741
744
  }
742
745
  }
743
746
 
@@ -640,20 +640,28 @@ const capabilities = {
640
640
  // (fatal error message). Multi-line so all platforms see actionable guidance.
641
641
  const INSTALL_HINT = 'install from https://claude.ai/download or: npm install -g @anthropic-ai/claude-code';
642
642
 
643
+ // Asset roots passed to spawn as `--add-dir` so worktrees can read globally
644
+ // installed skills. `~/.agents/skills` is the cross-runtime portable location;
645
+ // every runtime adapter exposes it so a skill placed there is genuinely visible
646
+ // to every runtime (matches the directory name's promise).
643
647
  function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
644
- return [path.join(homeDir, '.claude')];
648
+ return [
649
+ path.join(homeDir, '.claude'),
650
+ path.join(homeDir, '.agents'),
651
+ ];
645
652
  }
646
653
 
647
654
  function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
648
655
  const roots = [
649
656
  { dir: path.join(homeDir, '.claude', 'skills'), scope: 'claude-code' },
657
+ { dir: path.join(homeDir, '.agents', 'skills'), scope: 'agent-skill' },
650
658
  ];
651
659
  if (project?.localPath) {
652
- roots.push({
653
- dir: path.join(project.localPath, '.claude', 'skills'),
654
- scope: 'project',
655
- projectName: project.name || path.basename(project.localPath),
656
- });
660
+ const projectName = project.name || path.basename(project.localPath);
661
+ roots.push(
662
+ { dir: path.join(project.localPath, '.claude', 'skills'), scope: 'project', projectName },
663
+ { dir: path.join(project.localPath, '.agents', 'skills'), scope: 'project', projectName },
664
+ );
657
665
  }
658
666
  return roots;
659
667
  }
@@ -666,6 +674,19 @@ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
666
674
  return targets;
667
675
  }
668
676
 
677
+ // Heuristic: does `model` look like a Claude model identifier? Powers the
678
+ // preflight "stale model after CLI switch" warning in cli.js. Returning false
679
+ // means "this looks wrong for Claude" — gpt-5.4 / o3-* / codex etc. Keep this
680
+ // here (not in cli.js) so the runtime owns its own model namespace and adding
681
+ // a future runtime never requires editing cli.js.
682
+ function modelLooksFamiliar(model) {
683
+ if (!model) return true;
684
+ const m = String(model).toLowerCase();
685
+ if (m.startsWith('claude-')) return true;
686
+ if (m === 'sonnet' || m === 'opus' || m === 'haiku') return true;
687
+ return false;
688
+ }
689
+
669
690
  module.exports = {
670
691
  name: 'claude',
671
692
  capabilities,
@@ -688,6 +709,7 @@ module.exports = {
688
709
  usesSystemPromptFile,
689
710
  classifyFailure,
690
711
  resolveModel,
712
+ modelLooksFamiliar,
691
713
  parseOutput,
692
714
  parseStreamChunk,
693
715
  parseError,
@@ -798,6 +798,11 @@ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
798
798
  ];
799
799
  }
800
800
 
801
+ // Copilot CLI reads project skills from .github/skills, .claude/skills, AND
802
+ // .agents/skills per the official docs (see "Adding agent skills for GitHub
803
+ // Copilot CLI"). Listing all three keeps the dashboard accurate and ensures
804
+ // spawned Copilot agents receive `--add-dir` for every dir Copilot would
805
+ // natively read from.
801
806
  function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
802
807
  const roots = [
803
808
  { dir: path.join(homeDir, '.copilot', 'skills'), scope: 'copilot' },
@@ -807,6 +812,7 @@ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
807
812
  const projectName = project.name || path.basename(project.localPath);
808
813
  roots.push(
809
814
  { dir: path.join(project.localPath, '.github', 'skills'), scope: 'project', projectName },
815
+ { dir: path.join(project.localPath, '.claude', 'skills'), scope: 'project', projectName },
810
816
  { dir: path.join(project.localPath, '.agents', 'skills'), scope: 'project', projectName },
811
817
  );
812
818
  }
package/engine/shared.js CHANGED
@@ -793,6 +793,8 @@ const ENGINE_DEFAULTS = {
793
793
  minRetryGapMs: 120000, // 2min — minimum gap between retry dispatches for the same work item; prevents tight retry loops when an idempotent agent (e.g. review bailing out on a duplicate) cannot produce the expected output (#1770)
794
794
  pipelineApiRetries: 2, // max attempts for pipeline API calls
795
795
  pipelineApiRetryDelay: 2000, // ms delay between pipeline API retries
796
+ prAutoLinkRetries: 3, // max attempts for gh pr list lookup when auto-linking PR after merge (3s backoff between attempts)
797
+ rebaseQueueRetries: 3, // max rebase attempts per queued PR before giving up
796
798
  versionCheckInterval: 3600000, // 1 hour — how often to check npm for updates (ms)
797
799
  logFlushInterval: 5000, // 5s — how often to flush buffered log entries to disk
798
800
  logBufferSize: 50, // flush immediately when buffer exceeds this many entries
@@ -1096,15 +1098,19 @@ function runtimeConfigWarnings(config, registeredRuntimes) {
1096
1098
  }
1097
1099
  }
1098
1100
 
1099
- // 3. Bare-mode misconfig: claudeBareMode + Claude as CC runtime + no explicit
1100
- // CC system prompt. `--bare` suppresses CLAUDE.md auto-discovery; CC will
1101
- // lose project context unless the user wires an explicit prompt.
1101
+ // 3. Bare-mode misconfig: claudeBareMode + a CC runtime that honours
1102
+ // `--bare` + no explicit CC system prompt. `--bare` suppresses CLAUDE.md
1103
+ // auto-discovery; CC will lose project context unless the user wires an
1104
+ // explicit prompt. Gated on `capabilities.bareMode` rather than runtime
1105
+ // name so any future runtime that adopts the same flag is covered.
1102
1106
  if (engine.claudeBareMode === true) {
1103
1107
  const ccCli = resolveCcCli(engine);
1104
- if (ccCli === 'claude' && !_isMeaningful(engine.ccSystemPrompt)) {
1108
+ let ccRuntime = null;
1109
+ try { ccRuntime = require('./runtimes').resolveRuntime(ccCli); } catch { /* unknown runtime — skip */ }
1110
+ if (ccRuntime?.capabilities?.bareMode === true && !_isMeaningful(engine.ccSystemPrompt)) {
1105
1111
  warnings.push({
1106
1112
  id: 'bare-mode-misconfig',
1107
- message: 'engine.claudeBareMode is true but CC runs on Claude with no engine.ccSystemPrompt — CLAUDE.md auto-discovery is suppressed and CC will lose project context.',
1113
+ message: `engine.claudeBareMode is true but CC runs on ${ccCli} (which honours --bare) with no engine.ccSystemPrompt — CLAUDE.md auto-discovery is suppressed and CC will lose project context.`,
1108
1114
  });
1109
1115
  }
1110
1116
  }
@@ -201,6 +201,36 @@ async function writeProcessExitSentinel({
201
201
  return { sentinel, stdoutFlushed, outputPathWritten };
202
202
  }
203
203
 
204
+ /**
205
+ * Build the `--add-dir` list passed to the runtime CLI. Pure: takes
206
+ * `{ runtime, minionsDir, homeDir, exists }` and returns an ordered, deduped
207
+ * array of dirs the agent should be able to read from outside its worktree.
208
+ *
209
+ * Order: minionsDir first (so playbooks/system-prompt are always reachable),
210
+ * followed by every existing dir from `runtime.getUserAssetDirs({ homeDir })`.
211
+ * Non-existent asset dirs are dropped — Claude CLI rejects unknown `--add-dir`
212
+ * entries. The dedup compares resolved paths so we never emit minionsDir twice
213
+ * (e.g. when runtime asset dir IS the minions repo in unusual setups).
214
+ *
215
+ * `exists` is injectable for tests; defaults to `fs.existsSync`.
216
+ */
217
+ function computeAddDirs({ runtime, minionsDir, homeDir, exists = fs.existsSync } = {}) {
218
+ const out = [minionsDir];
219
+ const seen = new Set([path.resolve(minionsDir)]);
220
+ const assetDirs = typeof runtime?.getUserAssetDirs === 'function'
221
+ ? runtime.getUserAssetDirs({ homeDir })
222
+ : [];
223
+ for (const d of assetDirs) {
224
+ if (!d) continue;
225
+ const resolved = path.resolve(d);
226
+ if (seen.has(resolved)) continue;
227
+ if (!exists(d)) continue;
228
+ out.push(d);
229
+ seen.add(resolved);
230
+ }
231
+ return out;
232
+ }
233
+
204
234
  // ─── Main script execution ──────────────────────────────────────────────────
205
235
 
206
236
  function _installHint(name, runtime) {
@@ -252,15 +282,7 @@ function main() {
252
282
  // worktree, so runtime-native global assets would otherwise be invisible.
253
283
  // The adapter owns both where those assets live and how to surface them.
254
284
  const minionsDir = path.resolve(__dirname, '..');
255
- const addDirs = [minionsDir];
256
- const runtimeAssetDirs = typeof runtime.getUserAssetDirs === 'function'
257
- ? runtime.getUserAssetDirs({ homeDir: os.homedir() })
258
- : [];
259
- for (const dir of runtimeAssetDirs) {
260
- if (dir && fs.existsSync(dir) && path.resolve(dir) !== path.resolve(minionsDir)) {
261
- addDirs.push(dir);
262
- }
263
- }
285
+ const addDirs = computeAddDirs({ runtime, minionsDir, homeDir: os.homedir() });
264
286
 
265
287
  let resolved;
266
288
  try { resolved = runtime.resolveBinary({ env }); }
@@ -378,6 +400,6 @@ function main() {
378
400
  });
379
401
  }
380
402
 
381
- module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, injectAdoTokenEnv, writeProcessExitSentinel };
403
+ module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, injectAdoTokenEnv, writeProcessExitSentinel, computeAddDirs };
382
404
 
383
405
  if (require.main === module) main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1686",
3
+ "version": "0.1.1688",
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"