@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 +10 -0
- package/dashboard/js/render-skills.js +6 -4
- package/dashboard.js +1 -32
- package/docs/auto-discovery.md +1 -1
- package/engine/ado-mcp-wrapper.js +9 -15
- package/engine/ado-status.js +2 -2
- package/engine/ado-token.js +104 -0
- package/engine/ado.js +8 -13
- package/engine/cleanup.js +11 -8
- package/engine/cli.js +9 -12
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +7 -6
- package/engine/pipeline.js +11 -8
- package/engine/runtimes/claude.js +28 -6
- package/engine/runtimes/copilot.js +6 -0
- package/engine/shared.js +11 -5
- package/engine/spawn-agent.js +41 -20
- package/package.json +1 -1
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
|
|
73
|
-
//
|
|
74
|
-
//
|
|
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;
|
|
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
|
-
|
|
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/docs/auto-discovery.md
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
8
|
-
const
|
|
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 =
|
|
14
|
-
encoding: 'utf8',
|
|
15
|
-
timeout: 30000,
|
|
16
|
-
windowsHide: true,
|
|
17
|
-
}).trim();
|
|
12
|
+
token = acquireAdoTokenSync().token;
|
|
18
13
|
} catch (e) {
|
|
19
|
-
|
|
20
|
-
process.stderr.write('ado-mcp-wrapper:
|
|
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
|
});
|
package/engine/ado-status.js
CHANGED
|
@@ -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 +
|
|
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
|
|
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
|
|
228
|
+
// If recent fetch failed, don't retry until backoff expires.
|
|
228
229
|
if (Date.now() < _adoTokenFailedUntil) return null;
|
|
229
230
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
|
625
|
-
if (!
|
|
624
|
+
const peek = safeJson(prdPath);
|
|
625
|
+
if (!peek?.missing_features) continue;
|
|
626
626
|
let reset = 0;
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
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,
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
/**
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
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 ===
|
|
1051
|
-
log('warn', `Auto-link fallback: no open PR found on branch ${meta.branch} after
|
|
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 ===
|
|
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
|
|
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 <
|
|
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}`);
|
package/engine/pipeline.js
CHANGED
|
@@ -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
|
-
|
|
734
|
-
|
|
735
|
-
prd.status
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 [
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
scope: 'project',
|
|
655
|
-
|
|
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 +
|
|
1100
|
-
// CC system prompt. `--bare` suppresses CLAUDE.md
|
|
1101
|
-
// lose project context unless the user wires an
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/engine/spawn-agent.js
CHANGED
|
@@ -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
|
|
133
|
-
let
|
|
132
|
+
function injectAdoTokenEnv(env, { execSync: _execSync, acquireToken, warn = (msg) => process.stderr.write(msg + '\n') } = {}) {
|
|
133
|
+
let result;
|
|
134
134
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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 =
|
|
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.
|
|
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"
|