@yemi33/minions 0.1.1687 → 0.1.1689

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.1689 (2026-05-03)
4
+
5
+ ### Features
6
+ - prefer az cli for ado tokens (#1996)
7
+
8
+ ## 0.1.1688 (2026-05-02)
9
+
10
+ ### Fixes
11
+ - bug sweep — PRD races, hardcoded retries, capability-flag drift
12
+
3
13
  ## 0.1.1687 (2026-05-02)
4
14
 
5
15
  ### Other
@@ -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.');
@@ -141,7 +141,7 @@ The engine directly polls the host REST API for **all** PR metadata: build/CI st
141
141
  | `buildStatus` | PR statuses (codecoverage/deploy/build/ci contexts) | `passing` / `failing` / `running` / `none` |
142
142
  | `buildFailReason` | Failed status description | Set on failure, cleared otherwise |
143
143
 
144
- **Auth:** Bearer token via `azureauth ado token --mode iwa --mode broker --output token --timeout 1` (cached 30 minutes). The `--timeout 1` flag is required — without it, azureauth can hang indefinitely in headless sessions. (GitHub polling uses the ambient `gh` CLI credentials, not azureauth.)
144
+ **Auth:** Bearer token via shared `engine/ado-token.js`: prefer `az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv`, then fall back to `azureauth ado token --mode iwa --mode broker --output token --timeout 1` (cached 30 minutes). The `--timeout 1` flag is required — without it, azureauth can hang indefinitely in headless sessions. (GitHub polling uses the ambient `gh` CLI credentials, not azureauth.)
145
145
 
146
146
  This feeds `discoverFromPrs` — when `buildStatus` flips to `"failing"`, the next discovery tick dispatches a fix agent. When `status` becomes `"merged"`, the PR drops out of active polling.
147
147
 
@@ -1,24 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Wrapper for @azure-devops/mcp that fetches an ADO token via azureauth
4
- * broker (no browser popup) and sets AZURE_DEVOPS_EXT_PAT before launching
5
- * the MCP server.
3
+ * Wrapper for @azure-devops/mcp that fetches an ADO token via the shared
4
+ * az-first provider chain and sets AZURE_DEVOPS_EXT_PAT before launching the
5
+ * MCP server.
6
6
  */
7
- const { execSync, spawn } = require('child_process');
8
- const path = require('path');
7
+ const { spawn } = require('child_process');
8
+ const { acquireAdoTokenSync } = require('./ado-token');
9
9
 
10
- // Fetch token via azureauth broker (corp tool, no browser)
11
10
  let token;
12
11
  try {
13
- token = execSync('azureauth ado token --mode broker --output token --timeout 1', {
14
- encoding: 'utf8',
15
- timeout: 30000,
16
- windowsHide: true,
17
- }).trim();
12
+ token = acquireAdoTokenSync().token;
18
13
  } catch (e) {
19
- // Broker failed do NOT fall back to web mode (opens browser in automated context)
20
- process.stderr.write('ado-mcp-wrapper: Broker auth failed: ' + e.message + '\n');
21
- process.stderr.write('ado-mcp-wrapper: Run "azureauth ado token --mode web" manually to refresh\n');
14
+ process.stderr.write('ado-mcp-wrapper: ADO auth failed: ' + e.message + '\n');
15
+ process.stderr.write('ado-mcp-wrapper: Run "az login" or refresh azureauth manually, then retry\n');
22
16
  process.exit(1);
23
17
  }
24
18
 
@@ -31,7 +25,7 @@ const child = spawn(process.platform === 'win32' ? 'npx.cmd' : 'npx', [
31
25
  ...args
32
26
  ], {
33
27
  stdio: 'inherit',
34
- env: { ...process.env, AZURE_DEVOPS_EXT_PAT: token },
28
+ env: { ...process.env, AZURE_DEVOPS_EXT_PAT: token, AZURE_DEVOPS_EXT_AZURE_RM_PAT: token },
35
29
  windowsHide: true,
36
30
  shell: false,
37
31
  });
@@ -3,8 +3,8 @@
3
3
  * engine/ado-status.js — CLI shim for querying PR and build status.
4
4
  *
5
5
  * Agents steered to "check on the builds" or "is CI green for PR #123" should
6
- * use this instead of raw curl + azureauth calls. All ADO auth and retry logic
7
- * is handled by ado.js internally.
6
+ * use this instead of raw curl + ad-hoc auth calls. All ADO auth and retry
7
+ * logic is handled by ado.js internally.
8
8
  *
9
9
  * Usage:
10
10
  * node engine/ado-status.js <prNumber>
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Shared Azure DevOps token acquisition.
3
+ *
4
+ * Prefer Azure CLI because it is the most common authenticated tool in agent
5
+ * environments; keep azureauth as the non-interactive fallback for corp setups.
6
+ */
7
+
8
+ const { exec, execSync } = require('child_process');
9
+
10
+ const ADO_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
11
+ const AZ_CLI_ADO_TOKEN_COMMAND = `az account get-access-token --resource ${ADO_RESOURCE_ID} --query accessToken -o tsv`;
12
+ const AZUREAUTH_ADO_TOKEN_COMMAND = 'azureauth ado token --mode iwa --mode broker --output token --timeout 1';
13
+ const DEFAULT_ADO_TOKEN_TIMEOUT_MS = 30000;
14
+
15
+ const ADO_TOKEN_PROVIDERS = Object.freeze([
16
+ Object.freeze({ source: 'az', command: AZ_CLI_ADO_TOKEN_COMMAND }),
17
+ Object.freeze({ source: 'azureauth', command: AZUREAUTH_ADO_TOKEN_COMMAND }),
18
+ ]);
19
+
20
+ function normalizeAdoToken(value) {
21
+ return String(value || '').trim();
22
+ }
23
+
24
+ function isLikelyAdoToken(token) {
25
+ return typeof token === 'string' && token.startsWith('eyJ');
26
+ }
27
+
28
+ function _commandOptions({ timeout = DEFAULT_ADO_TOKEN_TIMEOUT_MS, encoding = 'utf8', windowsHide = true } = {}) {
29
+ return { encoding, timeout, windowsHide };
30
+ }
31
+
32
+ function _attemptMessage(attempt) {
33
+ return `${attempt.source}: ${attempt.error}`;
34
+ }
35
+
36
+ function _buildAdoTokenError(attempts) {
37
+ const err = new Error(`Failed to get ADO token via az CLI or azureauth: ${attempts.map(_attemptMessage).join('; ')}`);
38
+ err.attempts = attempts;
39
+ return err;
40
+ }
41
+
42
+ function _recordInvalidToken(attempts, provider) {
43
+ attempts.push({ source: provider.source, command: provider.command, error: 'invalid token output' });
44
+ }
45
+
46
+ function acquireAdoTokenSync({ execSync: run = execSync, timeout, encoding, windowsHide } = {}) {
47
+ const opts = _commandOptions({ timeout, encoding, windowsHide });
48
+ const attempts = [];
49
+ for (const provider of ADO_TOKEN_PROVIDERS) {
50
+ try {
51
+ const token = normalizeAdoToken(run(provider.command, opts));
52
+ if (isLikelyAdoToken(token)) {
53
+ return { token, source: provider.source, command: provider.command };
54
+ }
55
+ _recordInvalidToken(attempts, provider);
56
+ } catch (e) {
57
+ attempts.push({ source: provider.source, command: provider.command, error: e.message });
58
+ }
59
+ }
60
+ throw _buildAdoTokenError(attempts);
61
+ }
62
+
63
+ function _defaultExecAsync(command, opts) {
64
+ return new Promise((resolve, reject) => {
65
+ exec(command, opts, (err, stdout, stderr) => {
66
+ if (err) {
67
+ err.stdout = stdout;
68
+ err.stderr = stderr;
69
+ reject(err);
70
+ return;
71
+ }
72
+ resolve(stdout);
73
+ });
74
+ });
75
+ }
76
+
77
+ async function acquireAdoToken({ execAsync: run = _defaultExecAsync, timeout, encoding, windowsHide } = {}) {
78
+ const opts = _commandOptions({ timeout, encoding, windowsHide });
79
+ const attempts = [];
80
+ for (const provider of ADO_TOKEN_PROVIDERS) {
81
+ try {
82
+ const token = normalizeAdoToken(await run(provider.command, opts));
83
+ if (isLikelyAdoToken(token)) {
84
+ return { token, source: provider.source, command: provider.command };
85
+ }
86
+ _recordInvalidToken(attempts, provider);
87
+ } catch (e) {
88
+ attempts.push({ source: provider.source, command: provider.command, error: e.message });
89
+ }
90
+ }
91
+ throw _buildAdoTokenError(attempts);
92
+ }
93
+
94
+ module.exports = {
95
+ ADO_RESOURCE_ID,
96
+ AZ_CLI_ADO_TOKEN_COMMAND,
97
+ AZUREAUTH_ADO_TOKEN_COMMAND,
98
+ DEFAULT_ADO_TOKEN_TIMEOUT_MS,
99
+ ADO_TOKEN_PROVIDERS,
100
+ acquireAdoToken,
101
+ acquireAdoTokenSync,
102
+ isLikelyAdoToken,
103
+ normalizeAdoToken,
104
+ };
package/engine/ado.js CHANGED
@@ -8,6 +8,7 @@ const shared = require('./shared');
8
8
  const { exec, execAsync, getAdoOrgBase, log, ts, dateStamp, PR_STATUS, createThrottleTracker } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const { mutateJsonFileLocked } = shared;
11
+ const { acquireAdoToken } = require('./ado-token');
11
12
 
12
13
  // Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
13
14
  let _engine = null;
@@ -199,7 +200,7 @@ function votesToReviewStatus(votes) {
199
200
  // ─── ADO Token Cache ─────────────────────────────────────────────────────────
200
201
 
201
202
  let _adoTokenCache = { token: null, expiresAt: 0 };
202
- let _adoTokenFailedUntil = 0; // backoff: skip azureauth calls until this timestamp
203
+ let _adoTokenFailedUntil = 0; // backoff: skip token acquisition calls until this timestamp
203
204
 
204
205
  // ─── ADO Throttle State ─────────────────────────────────────────────────────
205
206
  // Tracks rate-limiting (HTTP 429/503) from ADO API responses.
@@ -224,23 +225,17 @@ async function getAdoToken() {
224
225
  if (_adoTokenCache.token && Date.now() < _adoTokenCache.expiresAt) {
225
226
  return _adoTokenCache.token;
226
227
  }
227
- // If recent fetch failed, don't retry until backoff expires (avoids repeated browser popups)
228
+ // If recent fetch failed, don't retry until backoff expires.
228
229
  if (Date.now() < _adoTokenFailedUntil) return null;
229
230
  try {
230
- // azureauth supports multiple --mode flags as an ordered fallback chain:
231
- // tries IWA (Integrated Windows Auth) first, falls back to broker if unavailable.
232
- // Uses execAsync to avoid blocking the event loop on Windows (spawnSync ETIMEDOUT).
233
- const token = (await execAsync('azureauth ado token --mode iwa --mode broker --output token --timeout 1', {
234
- timeout: 15000, encoding: 'utf-8', windowsHide: true })).trim();
235
- if (token && token.startsWith('eyJ')) {
236
- _adoTokenCache = { token, expiresAt: Date.now() + 30 * 60 * 1000 };
237
- _adoTokenFailedUntil = 0;
238
- return token;
239
- }
231
+ const { token } = await acquireAdoToken({ execAsync, timeout: 15000 });
232
+ _adoTokenCache = { token, expiresAt: Date.now() + 30 * 60 * 1000 };
233
+ _adoTokenFailedUntil = 0;
234
+ return token;
240
235
  } catch (e) {
241
236
  log('warn', `Failed to get ADO token: ${e.message}`);
242
237
  }
243
- // Back off for 10 minutes to avoid spamming browser auth popups
238
+ // Back off for 10 minutes to avoid spamming auth commands.
244
239
  _adoTokenFailedUntil = Date.now() + 10 * 60 * 1000;
245
240
  return null;
246
241
  }
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-02T19:09:30.042Z"
4
+ "cachedAt": "2026-05-03T15:01:38.322Z"
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
  }
@@ -35,9 +35,9 @@
35
35
  const fs = require('fs');
36
36
  const os = require('os');
37
37
  const path = require('path');
38
- const { execSync } = require('child_process');
39
38
  const { runFile, cleanChildEnv, killGracefully, killImmediate, ts } = require('./shared');
40
39
  const { resolveRuntime } = require('./runtimes');
40
+ const { acquireAdoTokenSync, isLikelyAdoToken } = require('./ado-token');
41
41
 
42
42
  // ─── Pure helpers (exported for tests) ──────────────────────────────────────
43
43
 
@@ -129,20 +129,19 @@ function normalizeRuntimeExit(code, signal) {
129
129
  return 1;
130
130
  }
131
131
 
132
- function injectAdoTokenEnv(env, { execSync: _execSync = execSync, warn = (msg) => process.stderr.write(msg + '\n') } = {}) {
133
- let token;
132
+ function injectAdoTokenEnv(env, { execSync: _execSync, acquireToken, warn = (msg) => process.stderr.write(msg + '\n') } = {}) {
133
+ let result;
134
134
  try {
135
- token = String(_execSync('azureauth ado token --mode iwa --mode broker --output token --timeout 1', {
136
- encoding: 'utf8',
137
- timeout: 30000,
138
- windowsHide: true,
139
- }) || '').trim();
135
+ result = typeof acquireToken === 'function'
136
+ ? acquireToken()
137
+ : acquireAdoTokenSync({ execSync: _execSync });
140
138
  } catch (err) {
141
139
  warn(`spawn-agent.js: ADO token fetch failed: ${err.message}`);
142
140
  return false;
143
141
  }
144
- if (!token || !token.startsWith('eyJ')) {
145
- warn('spawn-agent.js: invalid ADO token from azureauth; continuing without Azure DevOps PAT env');
142
+ const token = typeof result === 'string' ? result : result?.token;
143
+ if (!isLikelyAdoToken(token)) {
144
+ warn('spawn-agent.js: invalid ADO token; continuing without Azure DevOps PAT env');
146
145
  return false;
147
146
  }
148
147
  env.AZURE_DEVOPS_EXT_PAT = token;
@@ -201,6 +200,36 @@ async function writeProcessExitSentinel({
201
200
  return { sentinel, stdoutFlushed, outputPathWritten };
202
201
  }
203
202
 
203
+ /**
204
+ * Build the `--add-dir` list passed to the runtime CLI. Pure: takes
205
+ * `{ runtime, minionsDir, homeDir, exists }` and returns an ordered, deduped
206
+ * array of dirs the agent should be able to read from outside its worktree.
207
+ *
208
+ * Order: minionsDir first (so playbooks/system-prompt are always reachable),
209
+ * followed by every existing dir from `runtime.getUserAssetDirs({ homeDir })`.
210
+ * Non-existent asset dirs are dropped — Claude CLI rejects unknown `--add-dir`
211
+ * entries. The dedup compares resolved paths so we never emit minionsDir twice
212
+ * (e.g. when runtime asset dir IS the minions repo in unusual setups).
213
+ *
214
+ * `exists` is injectable for tests; defaults to `fs.existsSync`.
215
+ */
216
+ function computeAddDirs({ runtime, minionsDir, homeDir, exists = fs.existsSync } = {}) {
217
+ const out = [minionsDir];
218
+ const seen = new Set([path.resolve(minionsDir)]);
219
+ const assetDirs = typeof runtime?.getUserAssetDirs === 'function'
220
+ ? runtime.getUserAssetDirs({ homeDir })
221
+ : [];
222
+ for (const d of assetDirs) {
223
+ if (!d) continue;
224
+ const resolved = path.resolve(d);
225
+ if (seen.has(resolved)) continue;
226
+ if (!exists(d)) continue;
227
+ out.push(d);
228
+ seen.add(resolved);
229
+ }
230
+ return out;
231
+ }
232
+
204
233
  // ─── Main script execution ──────────────────────────────────────────────────
205
234
 
206
235
  function _installHint(name, runtime) {
@@ -252,15 +281,7 @@ function main() {
252
281
  // worktree, so runtime-native global assets would otherwise be invisible.
253
282
  // The adapter owns both where those assets live and how to surface them.
254
283
  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
- }
284
+ const addDirs = computeAddDirs({ runtime, minionsDir, homeDir: os.homedir() });
264
285
 
265
286
  let resolved;
266
287
  try { resolved = runtime.resolveBinary({ env }); }
@@ -378,6 +399,6 @@ function main() {
378
399
  });
379
400
  }
380
401
 
381
- module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, injectAdoTokenEnv, writeProcessExitSentinel };
402
+ module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, injectAdoTokenEnv, writeProcessExitSentinel, computeAddDirs };
382
403
 
383
404
  if (require.main === module) main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1687",
3
+ "version": "0.1.1689",
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"