@yemi33/minions 0.1.1671 → 0.1.1673
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/.claudeignore +5 -0
- package/CHANGELOG.md +10 -0
- package/dashboard/js/render-prs.js +2 -2
- package/dashboard/styles.css +1 -2
- package/dashboard.js +24 -11
- package/docs/copilot-cli-schema.md +6 -4
- package/engine/cleanup.js +29 -5
- package/engine/cli.js +2 -2
- package/engine/consolidation.js +52 -67
- package/engine/copilot-models.json +1 -1
- package/engine/kb-sweep.js +7 -3
- package/engine/llm.js +9 -2
- package/engine/pipeline.js +1 -0
- package/engine/runtimes/copilot.js +19 -23
- package/package.json +1 -1
package/.claudeignore
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1673 (2026-05-02)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- cap Signed Off By column width and wrap multi-reviewer overflow
|
|
7
|
+
|
|
8
|
+
## 0.1.1672 (2026-05-02)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- fleet model validation + Copilot alias map + simplify pass
|
|
12
|
+
|
|
3
13
|
## 0.1.1671 (2026-05-02)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
@@ -34,7 +34,7 @@ function prRow(pr) {
|
|
|
34
34
|
'<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
|
|
35
35
|
'<td><span class="' + branchClass + '"' + (branchError ? ' title="' + escapeHtml(branchError) + '"' : '') + '>' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
|
|
36
36
|
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
37
|
-
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
37
|
+
'<td class="pr-col-signoff">' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
38
38
|
'<td><span class="pr-badge ' + buildClass + '"' + (buildTitle ? ' title="' + escapeHtml(buildTitle) + '"' : '') + '>' + escapeHtml(buildLabel) + '</span></td>' +
|
|
39
39
|
'<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
40
40
|
'<td><span class="pr-date">' + escapeHtml((pr.created || '—').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
@@ -44,7 +44,7 @@ function prRow(pr) {
|
|
|
44
44
|
|
|
45
45
|
function prTableHtml(rows) {
|
|
46
46
|
return '<div class="pr-table-wrap"><table class="pr-table"><thead><tr>' +
|
|
47
|
-
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
|
|
47
|
+
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th class="pr-col-signoff">Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
|
|
48
48
|
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
49
49
|
}
|
|
50
50
|
|
package/dashboard/styles.css
CHANGED
|
@@ -213,7 +213,6 @@
|
|
|
213
213
|
.prd-item-row.st-in-progress { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
|
|
214
214
|
@keyframes prdWipPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); } 50% { box-shadow: 0 0 0 4px rgba(210,153,34,0.2); } }
|
|
215
215
|
.prd-item-row.st-failed { border-left-color: var(--red); }
|
|
216
|
-
.prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
|
|
217
216
|
.prd-item-row.st-updated { border-left-color: var(--purple); }
|
|
218
217
|
.prd-item-row.st-paused { border-left-color: var(--muted); opacity: 0.5; }
|
|
219
218
|
.prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: 0.9em; }
|
|
@@ -252,6 +251,7 @@
|
|
|
252
251
|
.pr-table-wrap { overflow-x: auto; }
|
|
253
252
|
.pr-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); table-layout: auto; }
|
|
254
253
|
.pr-table th:last-child, .pr-table td:last-child { width: 36px; min-width: 36px; text-align: center; }
|
|
254
|
+
.pr-table th.pr-col-signoff, .pr-table td.pr-col-signoff { width: 140px; max-width: 140px; white-space: normal; word-break: break-word; }
|
|
255
255
|
.pr-table th { text-align: left; color: var(--muted); font-weight: 500; font-size: var(--text-base); text-transform: uppercase; letter-spacing: 0.5px; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border); }
|
|
256
256
|
.pr-table td { padding: var(--space-5); border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
|
|
257
257
|
.pr-table tr:last-child td { border-bottom: none; }
|
|
@@ -268,7 +268,6 @@
|
|
|
268
268
|
.pr-badge.active { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
|
|
269
269
|
.pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
270
270
|
.pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
271
|
-
.pr-badge.needs-review { background: rgba(227,179,65,0.15); color: var(--orange); border: 1px solid var(--orange); }
|
|
272
271
|
.pr-badge.review-escalated { background: rgba(227,179,65,0.22); color: var(--orange); border: 1px dashed var(--orange); font-weight: 700; }
|
|
273
272
|
.pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
|
|
274
273
|
.pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
|
package/dashboard.js
CHANGED
|
@@ -3357,7 +3357,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3357
3357
|
async function _runKbSweepBackground(body, sweepToken) {
|
|
3358
3358
|
try {
|
|
3359
3359
|
const { runKbSweep } = require('./engine/kb-sweep');
|
|
3360
|
-
const result = await runKbSweep({ pinnedKeys: body.pinnedKeys });
|
|
3360
|
+
const result = await runKbSweep({ pinnedKeys: body.pinnedKeys, engineConfig: CONFIG.engine });
|
|
3361
3361
|
global._kbSweepLastResult = result;
|
|
3362
3362
|
global._kbSweepLastCompletedAt = Date.now();
|
|
3363
3363
|
} catch (e) {
|
|
@@ -5286,7 +5286,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5286
5286
|
|
|
5287
5287
|
const prompt = `Convert this schedule description to a 3-field cron expression (minute hour dayOfWeek, where dayOfWeek is 0=Sun..6=Sat or ranges like 1-5). Return JSON only: {"cron": "...", "description": "..."}. Input: ${text.trim()}`;
|
|
5288
5288
|
try {
|
|
5289
|
-
const result = await llm.callLLM(prompt, '', {
|
|
5289
|
+
const result = await llm.callLLM(prompt, '', {
|
|
5290
|
+
model: 'haiku', maxTurns: 1, timeout: 30000, label: 'schedule-parse', direct: true,
|
|
5291
|
+
engineConfig: CONFIG.engine,
|
|
5292
|
+
});
|
|
5290
5293
|
const parsed = JSON.parse(result.text.trim());
|
|
5291
5294
|
if (!parsed.cron) return jsonReply(res, 422, { error: 'Could not parse schedule' });
|
|
5292
5295
|
return jsonReply(res, 200, { cron: parsed.cron, description: parsed.description || '' });
|
|
@@ -5379,6 +5382,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5379
5382
|
if (!config.agents) config.agents = {};
|
|
5380
5383
|
|
|
5381
5384
|
const _clamped = [];
|
|
5385
|
+
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
5386
|
+
const _engineRuntimes = require('./engine/runtimes');
|
|
5387
|
+
function _resolveModelForRuntime(modelStr, runtimeName) {
|
|
5388
|
+
try {
|
|
5389
|
+
const adapter = _engineRuntimes.resolveRuntime(runtimeName);
|
|
5390
|
+
if (adapter && typeof adapter.resolveModel === 'function') {
|
|
5391
|
+
return adapter.resolveModel(modelStr) || modelStr;
|
|
5392
|
+
}
|
|
5393
|
+
} catch { /* unknown/free-text runtime */ }
|
|
5394
|
+
return modelStr;
|
|
5395
|
+
}
|
|
5382
5396
|
if (body.engine) {
|
|
5383
5397
|
const e = body.engine;
|
|
5384
5398
|
const D = shared.ENGINE_DEFAULTS;
|
|
@@ -5440,10 +5454,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5440
5454
|
// (where gpt-5.5 doesn't actually exist) cascaded into every agent
|
|
5441
5455
|
// that didn't pin its own model. Reject when the model is known to
|
|
5442
5456
|
// belong to a different runtime than the one it'll spawn against.
|
|
5443
|
-
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
5444
|
-
const _engineRuntimes = require('./engine/runtimes');
|
|
5445
5457
|
async function _validateFleetModel(modelStr, resolvedRuntime) {
|
|
5446
5458
|
if (!modelStr) return null;
|
|
5459
|
+
const runtimeModelStr = _resolveModelForRuntime(modelStr, resolvedRuntime);
|
|
5447
5460
|
let knownForResolved = null;
|
|
5448
5461
|
try {
|
|
5449
5462
|
const list = await _engineModelDiscovery.getRuntimeModels(resolvedRuntime, { config });
|
|
@@ -5451,11 +5464,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5451
5464
|
knownForResolved = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
|
|
5452
5465
|
}
|
|
5453
5466
|
} catch { /* unknown runtime */ }
|
|
5454
|
-
if (knownForResolved && !knownForResolved.has(
|
|
5467
|
+
if (knownForResolved && !knownForResolved.has(runtimeModelStr)) {
|
|
5455
5468
|
return `not a valid model for runtime "${resolvedRuntime}" (known: ${[...knownForResolved].slice(0, 4).join(', ')}${knownForResolved.size > 4 ? '…' : ''})`;
|
|
5456
5469
|
}
|
|
5457
5470
|
if (!knownForResolved) {
|
|
5458
|
-
// Free-text runtime (Claude). Reject only if model belongs to a different runtime's published list.
|
|
5471
|
+
// Free-text runtime (Claude). Reject only if the raw model belongs to a different runtime's published list.
|
|
5459
5472
|
for (const rt of _engineRuntimes.listRuntimes()) {
|
|
5460
5473
|
if (rt === resolvedRuntime) continue;
|
|
5461
5474
|
try {
|
|
@@ -5548,13 +5561,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5548
5561
|
// claude+gpt-* / copilot+claude-* combinations before they crash a
|
|
5549
5562
|
// dispatch (see #model-validation: a stray engine.defaultModel='gpt-5.5'
|
|
5550
5563
|
// pinned every Claude agent into a 404 spawn loop).
|
|
5551
|
-
const _modelDiscovery = require('./engine/model-discovery');
|
|
5552
5564
|
const _runtimeModelsCache = new Map(); // runtimeName → Set<modelId> (or null when unknown / Claude)
|
|
5553
5565
|
async function _modelsFor(runtimeName) {
|
|
5554
5566
|
if (_runtimeModelsCache.has(runtimeName)) return _runtimeModelsCache.get(runtimeName);
|
|
5555
5567
|
let set = null;
|
|
5556
5568
|
try {
|
|
5557
|
-
const list = await
|
|
5569
|
+
const list = await _engineModelDiscovery.getRuntimeModels(runtimeName, { config });
|
|
5558
5570
|
if (Array.isArray(list?.models) && list.models.length > 0) {
|
|
5559
5571
|
set = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
|
|
5560
5572
|
}
|
|
@@ -5562,11 +5574,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5562
5574
|
_runtimeModelsCache.set(runtimeName, set);
|
|
5563
5575
|
return set;
|
|
5564
5576
|
}
|
|
5565
|
-
// Returns the runtime that "owns" this model, or null if no other
|
|
5577
|
+
// Returns the runtime that "owns" this raw model, or null if no other
|
|
5566
5578
|
// runtime claims it. Catches "claude + gpt-5.5" by spotting that
|
|
5567
5579
|
// gpt-5.5 belongs to copilot's list.
|
|
5568
5580
|
async function _ownerOfModel(modelId) {
|
|
5569
|
-
for (const rt of
|
|
5581
|
+
for (const rt of _engineRuntimes.listRuntimes()) {
|
|
5570
5582
|
const set = await _modelsFor(rt);
|
|
5571
5583
|
if (set && set.has(modelId)) return rt;
|
|
5572
5584
|
}
|
|
@@ -5594,6 +5606,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5594
5606
|
else {
|
|
5595
5607
|
const candidate = String(updates.model);
|
|
5596
5608
|
const resolvedCli = config.agents[id].cli || config.engine.defaultCli || 'claude';
|
|
5609
|
+
const runtimeModelStr = _resolveModelForRuntime(candidate, resolvedCli);
|
|
5597
5610
|
const knownModels = await _modelsFor(resolvedCli);
|
|
5598
5611
|
// Two validation paths:
|
|
5599
5612
|
// 1. If the runtime publishes a model list, enforce membership.
|
|
@@ -5601,7 +5614,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5601
5614
|
// model belongs to a DIFFERENT runtime's list — that's how
|
|
5602
5615
|
// we catch claude+gpt-5.5 (gpt-5.5 is in Copilot's list).
|
|
5603
5616
|
let rejection = null;
|
|
5604
|
-
if (knownModels && !knownModels.has(
|
|
5617
|
+
if (knownModels && !knownModels.has(runtimeModelStr)) {
|
|
5605
5618
|
rejection = `not a valid model for runtime "${resolvedCli}" (known: ${[...knownModels].slice(0, 4).join(', ')}${knownModels.size > 4 ? '…' : ''})`;
|
|
5606
5619
|
} else if (!knownModels) {
|
|
5607
5620
|
const owner = await _ownerOfModel(candidate);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| `capabilities.systemPromptFile` | **`false`** | No `--system-prompt-file` flag exists. Inject system prompt via a `<system>` block prepended to stdin. |
|
|
21
21
|
| `capabilities.effortLevels` | **`true`** | `--effort` accepts `low|medium|high|xhigh` (no `max`). Adapter must map `'max' → 'xhigh'`. |
|
|
22
22
|
| `capabilities.costTracking` | **`false`** | `result.usage` contains `premiumRequests` (count, not USD), no token counts, no cost. |
|
|
23
|
-
| `capabilities.modelShorthands` | **`false`** |
|
|
23
|
+
| `capabilities.modelShorthands` | **`false`** | The Copilot CLI requires full model IDs (`claude-sonnet-4.5`, `gpt-5.4`). Minions may accept internal aliases (`haiku`, `sonnet`, `opus`), but the adapter translates them to Copilot model IDs before invoking the CLI. |
|
|
24
24
|
| `capabilities.budgetCap` | **`false`** | No `--max-budget-usd` flag. |
|
|
25
25
|
| `capabilities.bareMode` | **`false`** | No `--bare`. Closest equivalent is `--no-custom-instructions` (suppresses AGENTS.md only, not all auto-discovery). |
|
|
26
26
|
| `capabilities.fallbackModel` | **`false`** | No `--fallback-model` flag. |
|
|
@@ -594,9 +594,11 @@ When implementing `engine/runtimes/copilot.js`:
|
|
|
594
594
|
**Never** emit `--verbose`.
|
|
595
595
|
4. `buildPrompt()` injects `<system>...</system>\n\n` block when sysprompt is
|
|
596
596
|
non-empty; passthrough otherwise (§2).
|
|
597
|
-
5. `resolveModel()`
|
|
598
|
-
|
|
599
|
-
|
|
597
|
+
5. `resolveModel()` translates Minions internal aliases before the CLI boundary:
|
|
598
|
+
`'haiku'` → `'claude-haiku-4.5'`, `'sonnet'` → `'claude-sonnet-4.5'`, and
|
|
599
|
+
`'opus'` → `'claude-opus-4.5'`. All other model IDs pass through unchanged.
|
|
600
|
+
Keep `capabilities.modelShorthands` false because aliases are never passed
|
|
601
|
+
to the Copilot CLI.
|
|
600
602
|
6. `_mapEffort()` private helper does `'max' → 'xhigh'`; pass through otherwise.
|
|
601
603
|
7. `parseOutput(raw)` produces:
|
|
602
604
|
- `text`: concatenation of all `assistant.message.data.content` (multi-turn
|
package/engine/cleanup.js
CHANGED
|
@@ -39,6 +39,30 @@ function worktreeDirMatchesBranch(dirLower, branch) {
|
|
|
39
39
|
return dirLower === branchSlug || dirLower.includes(branchSlug + '-') || dirLower.endsWith('-' + branchSlug);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
let _orphanPidProcessNamesCache = null;
|
|
43
|
+
function _orphanPidProcessNames() {
|
|
44
|
+
if (_orphanPidProcessNamesCache) return _orphanPidProcessNamesCache;
|
|
45
|
+
const names = new Set(['node']);
|
|
46
|
+
try {
|
|
47
|
+
for (const name of require('./runtimes').listRuntimes()) names.add(String(name).toLowerCase());
|
|
48
|
+
// Copilot can run through the GitHub CLI fallback (`gh copilot`), so allow
|
|
49
|
+
// gh only when the copilot runtime is registered.
|
|
50
|
+
if (names.has('copilot')) names.add('gh');
|
|
51
|
+
} catch {
|
|
52
|
+
names.add('claude');
|
|
53
|
+
}
|
|
54
|
+
_orphanPidProcessNamesCache = names;
|
|
55
|
+
return names;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _processNameAllowedForOrphanKill(processText) {
|
|
59
|
+
const firstLine = String(processText || '').trim().split(/\r?\n/).find(Boolean) || '';
|
|
60
|
+
const imageName = path.basename(firstLine.trim().split(/\s+/)[0] || '').toLowerCase().replace(/\.exe$/, '');
|
|
61
|
+
if (!imageName) return false;
|
|
62
|
+
return _orphanPidProcessNames().has(imageName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
42
66
|
/**
|
|
43
67
|
* Kill orphaned processes whose dispatch ID appears in the worktree dir name.
|
|
44
68
|
* Only kills processes NOT in the active dispatch queue — never kills live agents.
|
|
@@ -68,18 +92,18 @@ function _killProcessInWorktree(dir, activeProcesses, activeIds) {
|
|
|
68
92
|
if (isActive) continue; // still active — do not kill
|
|
69
93
|
const pid = parseInt(fs.readFileSync(path.join(tmpDir, f), 'utf8').trim(), 10);
|
|
70
94
|
if (pid > 0) {
|
|
71
|
-
// Verify the
|
|
95
|
+
// Verify the PID still belongs to a Minions runtime process before killing
|
|
72
96
|
try {
|
|
73
97
|
if (process.platform === 'win32') {
|
|
74
98
|
const taskInfo = exec(`tasklist /FI "PID eq ${pid}" /NH`, { encoding: 'utf8', timeout: 3000, windowsHide: true });
|
|
75
99
|
const taskLower = taskInfo.toLowerCase();
|
|
76
|
-
if (!
|
|
100
|
+
if (!_processNameAllowedForOrphanKill(taskLower)) continue;
|
|
77
101
|
exec(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
78
102
|
} else {
|
|
79
|
-
// Verify
|
|
103
|
+
// Verify the process name before killing (prevent recycled PID kill)
|
|
80
104
|
try {
|
|
81
105
|
const psOut = exec(`ps -p ${pid} -o comm=`, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
82
|
-
if (!
|
|
106
|
+
if (!_processNameAllowedForOrphanKill(psOut)) continue;
|
|
83
107
|
} catch { continue; } // process dead or ps failed
|
|
84
108
|
try { process.kill(-pid, 'SIGKILL'); } catch { process.kill(pid, 'SIGKILL'); }
|
|
85
109
|
}
|
|
@@ -331,7 +355,7 @@ function runCleanup(config, verbose = false) {
|
|
|
331
355
|
} // end worktreeRoots loop
|
|
332
356
|
}
|
|
333
357
|
|
|
334
|
-
// 4. Kill zombie
|
|
358
|
+
// 4. Kill zombie agent processes not tracked by the engine
|
|
335
359
|
// List all node processes, check if any are running spawn-agent.js for our minions
|
|
336
360
|
try {
|
|
337
361
|
const dispatch = getDispatch();
|
package/engine/cli.js
CHANGED
|
@@ -157,8 +157,8 @@ function _modelLooksIncompatible(runtime, model) {
|
|
|
157
157
|
return true; // gpt-*, o3-*, codex, etc. — wrong CLI for these
|
|
158
158
|
}
|
|
159
159
|
if (runtime === 'copilot') {
|
|
160
|
-
// Copilot
|
|
161
|
-
return
|
|
160
|
+
// Copilot adapter maps Minions' family aliases before spawning.
|
|
161
|
+
return false;
|
|
162
162
|
}
|
|
163
163
|
return false;
|
|
164
164
|
}
|
package/engine/consolidation.js
CHANGED
|
@@ -8,11 +8,11 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const shared = require('./shared');
|
|
11
|
-
const { safeRead, safeWrite,
|
|
12
|
-
|
|
13
|
-
const { trackEngineUsage } = require('./llm');
|
|
11
|
+
const { safeRead, safeWrite,
|
|
12
|
+
classifyInboxItem, KB_CATEGORIES, log, ts, dateStamp } = shared;
|
|
13
|
+
const { callLLM, trackEngineUsage } = require('./llm');
|
|
14
14
|
const queries = require('./queries');
|
|
15
|
-
const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR,
|
|
15
|
+
const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR,
|
|
16
16
|
NOTES_PATH, KNOWLEDGE_DIR, ARCHIVE_DIR } = queries;
|
|
17
17
|
|
|
18
18
|
// Track in-flight LLM consolidation to prevent concurrent runs
|
|
@@ -150,51 +150,18 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
const prompt = buildConsolidationPrompt(items, existingNotes, kbPaths);
|
|
153
|
-
|
|
154
|
-
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
155
|
-
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
156
|
-
const promptPath = path.join(tmpDir, 'consolidate-prompt.md');
|
|
157
|
-
safeWrite(promptPath, prompt);
|
|
158
|
-
|
|
159
153
|
const sysPrompt = 'You are a concise knowledge manager. Output only markdown. No preamble. No code fences around your output.';
|
|
160
|
-
const sysPromptPath = path.join(tmpDir, 'consolidate-sysprompt.md');
|
|
161
|
-
safeWrite(sysPromptPath, sysPrompt);
|
|
162
|
-
|
|
163
|
-
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
164
|
-
const args = [
|
|
165
|
-
'--output-format', 'stream-json',
|
|
166
|
-
'--max-turns', '1',
|
|
167
|
-
'--model', 'haiku',
|
|
168
|
-
'--permission-mode', 'bypassPermissions',
|
|
169
|
-
'--verbose',
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
log('info', 'Spawning Haiku for LLM consolidation...');
|
|
173
|
-
|
|
174
|
-
const proc = runFile(process.execPath, [spawnScript, promptPath, sysPromptPath, ...args], {
|
|
175
|
-
cwd: MINIONS_DIR,
|
|
176
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
177
|
-
env: cleanChildEnv()
|
|
178
|
-
});
|
|
179
154
|
|
|
180
|
-
|
|
181
|
-
let stderr = '';
|
|
182
|
-
proc.stdout.on('data', d => { stdout += d.toString(); if (stdout.length > 100000) stdout = stdout.slice(-50000); });
|
|
183
|
-
proc.stderr.on('data', d => { stderr += d.toString(); if (stderr.length > 50000) stderr = stderr.slice(-25000); });
|
|
155
|
+
log('info', 'Starting LLM consolidation...');
|
|
184
156
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
_forceResetTimeout = setTimeout(() => {
|
|
189
|
-
if (!_cleared) log('warn', 'Consolidation flag force-reset after SIGKILL');
|
|
190
|
-
_clearProcessingState();
|
|
191
|
-
}, 10000);
|
|
192
|
-
}, 180000);
|
|
157
|
+
let _cleared = false; // idempotency guard — timeout and promise resolution can race
|
|
158
|
+
let timeoutHandle = null;
|
|
159
|
+
let fallbackDone = false;
|
|
193
160
|
|
|
194
|
-
let _cleared = false; // idempotency guard — both 'error' and 'close' can fire for the same process
|
|
195
161
|
function _clearProcessingState() {
|
|
196
162
|
if (_cleared) return;
|
|
197
163
|
_cleared = true;
|
|
164
|
+
clearTimeout(timeoutHandle);
|
|
198
165
|
clearTimeout(_forceResetTimeout);
|
|
199
166
|
_forceResetTimeout = null;
|
|
200
167
|
for (const f of files) _processingFiles.delete(f);
|
|
@@ -202,17 +169,43 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
202
169
|
_consolidationStartedAt = 0;
|
|
203
170
|
}
|
|
204
171
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
172
|
+
function _fallback(message, err) {
|
|
173
|
+
if (_cleared || fallbackDone) return;
|
|
174
|
+
fallbackDone = true;
|
|
175
|
+
if (message) log('warn', message);
|
|
176
|
+
if (err?.message) log('debug', `LLM error: ${err.message}`);
|
|
177
|
+
consolidateWithRegex(items, files);
|
|
178
|
+
}
|
|
209
179
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
180
|
+
const llmCall = callLLM(prompt, sysPrompt, {
|
|
181
|
+
timeout: 180000,
|
|
182
|
+
label: 'consolidation',
|
|
183
|
+
model: 'haiku',
|
|
184
|
+
maxTurns: 1,
|
|
185
|
+
direct: true,
|
|
186
|
+
engineConfig: config.engine,
|
|
187
|
+
});
|
|
213
188
|
|
|
214
|
-
|
|
215
|
-
|
|
189
|
+
timeoutHandle = setTimeout(() => {
|
|
190
|
+
if (_cleared) return;
|
|
191
|
+
log('warn', 'LLM consolidation timed out after 3m — aborting and falling back to regex');
|
|
192
|
+
try { if (llmCall && typeof llmCall.abort === 'function') llmCall.abort(); } catch { /* ignore */ }
|
|
193
|
+
_forceResetTimeout = setTimeout(() => {
|
|
194
|
+
if (!_cleared) log('warn', 'Consolidation flag force-reset after timeout');
|
|
195
|
+
_fallback();
|
|
196
|
+
_clearProcessingState();
|
|
197
|
+
}, 10000);
|
|
198
|
+
}, 180000);
|
|
199
|
+
|
|
200
|
+
llmCall.then((result) => {
|
|
201
|
+
if (_cleared) return;
|
|
202
|
+
clearTimeout(timeoutHandle);
|
|
203
|
+
trackEngineUsage('consolidation', result.usage);
|
|
204
|
+
|
|
205
|
+
const extractedText = result.text || '';
|
|
206
|
+
const rawText = result.raw || '';
|
|
207
|
+
if (result.code === 0 && (extractedText || rawText).trim().length > 50) {
|
|
208
|
+
let digest = (extractedText || rawText).trim();
|
|
216
209
|
digest = digest.replace(/^\`\`\`\w*\n?/gm, '').replace(/\n?\`\`\`$/gm, '').trim();
|
|
217
210
|
|
|
218
211
|
if (!digest.startsWith('### ')) {
|
|
@@ -220,9 +213,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
220
213
|
if (sectionIdx >= 0) {
|
|
221
214
|
digest = digest.slice(sectionIdx);
|
|
222
215
|
} else {
|
|
223
|
-
|
|
224
|
-
consolidateWithRegex(items, files);
|
|
225
|
-
_clearProcessingState();
|
|
216
|
+
_fallback('LLM consolidation output missing expected format — falling back to regex');
|
|
226
217
|
return;
|
|
227
218
|
}
|
|
228
219
|
}
|
|
@@ -257,21 +248,15 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
257
248
|
});
|
|
258
249
|
classifyToKnowledgeBase(items);
|
|
259
250
|
archiveInboxFiles(files);
|
|
260
|
-
log('info', `LLM consolidation complete: ${files.length} notes processed
|
|
251
|
+
log('info', `LLM consolidation complete: ${files.length} notes processed`);
|
|
261
252
|
} else {
|
|
262
|
-
|
|
263
|
-
if (stderr) log('debug', `LLM stderr: ${stderr.slice(0, 500)}`);
|
|
264
|
-
consolidateWithRegex(items, files);
|
|
253
|
+
_fallback(`LLM consolidation failed (code=${result.code}) — falling back to regex`, result.stderr ? { message: result.stderr.slice(0, 500) } : null);
|
|
265
254
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
log('warn', `LLM consolidation spawn error: ${err.message} — falling back to regex`);
|
|
272
|
-
try { safeUnlink(promptPath); } catch (unlinkErr) { log('warn', `Temp file cleanup failed: ${promptPath} — ${unlinkErr.message}`); }
|
|
273
|
-
try { safeUnlink(sysPromptPath); } catch (unlinkErr) { log('warn', `Temp file cleanup failed: ${sysPromptPath} — ${unlinkErr.message}`); }
|
|
274
|
-
consolidateWithRegex(items, files);
|
|
255
|
+
}).catch((err) => {
|
|
256
|
+
if (_cleared) return;
|
|
257
|
+
clearTimeout(timeoutHandle);
|
|
258
|
+
_fallback(`LLM consolidation error: ${err.message} — falling back to regex`, err);
|
|
259
|
+
}).finally(() => {
|
|
275
260
|
_clearProcessingState();
|
|
276
261
|
});
|
|
277
262
|
}
|
package/engine/kb-sweep.js
CHANGED
|
@@ -104,7 +104,7 @@ function _hashDedup(manifest, opts = {}) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** Batched LLM sweep — finds within-batch dupes, reclassifies, removes stale. */
|
|
107
|
-
async function _llmBatchSweep(manifest, callLLM, trackEngineUsage) {
|
|
107
|
+
async function _llmBatchSweep(manifest, callLLM, trackEngineUsage, opts = {}) {
|
|
108
108
|
const plan = { duplicates: [], reclassify: [], remove: [] };
|
|
109
109
|
const batches = [];
|
|
110
110
|
for (let i = 0; i < manifest.length; i += LLM_BATCH_SIZE) {
|
|
@@ -130,7 +130,10 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
130
130
|
|
|
131
131
|
let result;
|
|
132
132
|
try {
|
|
133
|
-
result = await callLLM(prompt, 'Output only JSON.', {
|
|
133
|
+
result = await callLLM(prompt, 'Output only JSON.', {
|
|
134
|
+
timeout: 120000, label: 'kb-sweep', model: 'haiku', maxTurns: 1, direct: true,
|
|
135
|
+
engineConfig: opts.engineConfig,
|
|
136
|
+
});
|
|
134
137
|
trackEngineUsage('kb-sweep', result.usage);
|
|
135
138
|
} catch (e) { log('warn', `[kb-sweep] batch ${b + 1} LLM error: ${e.message}`); continue; }
|
|
136
139
|
|
|
@@ -206,6 +209,7 @@ ${body}`;
|
|
|
206
209
|
try {
|
|
207
210
|
const result = await callLLM(REWRITE_PROMPT(c.entry, c.body), 'Output ONLY the template body.', {
|
|
208
211
|
timeout: 120000, label: 'kb-rewrite', model: 'haiku', maxTurns: 1, direct: true,
|
|
212
|
+
engineConfig: opts.engineConfig,
|
|
209
213
|
});
|
|
210
214
|
trackEngineUsage('kb-sweep', result.usage);
|
|
211
215
|
let newBody = (result.text || '').trim();
|
|
@@ -326,7 +330,7 @@ async function runKbSweep(opts = {}) {
|
|
|
326
330
|
// 2. LLM batch sweep — within-batch dupes + reclassify + remove stale
|
|
327
331
|
// Only runs against survivors, but we need indices that match the LIST sent to the LLM
|
|
328
332
|
const llmManifest = afterHash;
|
|
329
|
-
const plan = await _llmBatchSweep(llmManifest, callLLM, trackEngineUsage);
|
|
333
|
+
const plan = await _llmBatchSweep(llmManifest, callLLM, trackEngineUsage, opts);
|
|
330
334
|
const llmActions = _applyLlmPlan(plan, llmManifest, opts);
|
|
331
335
|
summary.llmDuplicatesArchived = llmActions.merged;
|
|
332
336
|
summary.staleRemoved = llmActions.removed;
|
package/engine/llm.js
CHANGED
|
@@ -403,6 +403,12 @@ function _resolveModelFor(callOpts) {
|
|
|
403
403
|
return undefined;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
function _resolveModelForRuntime(runtime, callOpts) {
|
|
407
|
+
const selected = _resolveModelFor(callOpts || {});
|
|
408
|
+
if (!runtime || typeof runtime.resolveModel !== 'function') return selected;
|
|
409
|
+
return runtime.resolveModel(selected);
|
|
410
|
+
}
|
|
411
|
+
|
|
406
412
|
function _resolveRuntimeFeatureOpts({
|
|
407
413
|
stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
|
|
408
414
|
} = {}) {
|
|
@@ -431,7 +437,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
431
437
|
} = opts;
|
|
432
438
|
|
|
433
439
|
const runtime = _resolveRuntimeFor({ cli: cliOverride, engineConfig });
|
|
434
|
-
const model =
|
|
440
|
+
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
435
441
|
const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
|
|
436
442
|
stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
|
|
437
443
|
});
|
|
@@ -528,7 +534,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
528
534
|
} = opts;
|
|
529
535
|
|
|
530
536
|
const runtime = _resolveRuntimeFor({ cli: cliOverride, engineConfig });
|
|
531
|
-
const model =
|
|
537
|
+
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
532
538
|
const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
|
|
533
539
|
stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
|
|
534
540
|
});
|
|
@@ -619,5 +625,6 @@ module.exports = {
|
|
|
619
625
|
_resetBinCache,
|
|
620
626
|
_resolveRuntimeFor,
|
|
621
627
|
_resolveModelFor,
|
|
628
|
+
_resolveModelForRuntime,
|
|
622
629
|
_resolveRuntimeFeatureOpts,
|
|
623
630
|
};
|
package/engine/pipeline.js
CHANGED
|
@@ -486,6 +486,7 @@ async function executePlanStage(stage, stageState, run, config) {
|
|
|
486
486
|
const fullPrompt = meetingContext + '\n\n---\n\n' + planPrompt;
|
|
487
487
|
const result = await llm.callLLM(fullPrompt, '', {
|
|
488
488
|
timeout: 120000, label: 'pipeline-plan', model: 'sonnet', maxTurns: 1,
|
|
489
|
+
engineConfig: config.engine,
|
|
489
490
|
});
|
|
490
491
|
if (result.text) {
|
|
491
492
|
content = result.text;
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
* buildPrompt() prepends a <system> block.
|
|
16
16
|
* - costTracking: false — result.usage has premiumRequests count
|
|
17
17
|
* and durations only; no USD or per-token.
|
|
18
|
-
* - modelShorthands: false — full model IDs like
|
|
19
|
-
* "
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* - modelShorthands: false — Copilot itself expects full model IDs like
|
|
19
|
+
* "claude-sonnet-4.5" or "gpt-5.4". The
|
|
20
|
+
* adapter translates Minions' internal family
|
|
21
|
+
* aliases before argv construction.
|
|
22
22
|
* - modelDiscovery: true — GET https://api.githubcopilot.com/models
|
|
23
23
|
* with `gh auth token` Bearer returns the
|
|
24
24
|
* catalog (24 models on the test account).
|
|
@@ -152,26 +152,23 @@ function resolveBinary({ env = process.env } = {}) {
|
|
|
152
152
|
|
|
153
153
|
// ── Model Resolution ────────────────────────────────────────────────────────
|
|
154
154
|
//
|
|
155
|
-
// Copilot models are full IDs (`claude-sonnet-4.5`, `gpt-5.4`, ...).
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
// Copilot models are full IDs (`claude-sonnet-4.5`, `gpt-5.4`, ...). Minions
|
|
156
|
+
// still uses the family aliases `haiku` / `sonnet` / `opus` internally, so this
|
|
157
|
+
// adapter maps those aliases to Copilot model IDs while passing all other input
|
|
158
|
+
// through verbatim. The capability remains false: Copilot CLI does not accept
|
|
159
|
+
// these aliases directly.
|
|
160
|
+
|
|
161
|
+
const _MINIONS_MODEL_ALIASES = {
|
|
162
|
+
haiku: 'claude-haiku-4.5',
|
|
163
|
+
sonnet: 'claude-sonnet-4.5',
|
|
164
|
+
opus: 'claude-opus-4.5',
|
|
165
|
+
};
|
|
164
166
|
|
|
165
|
-
function resolveModel(input
|
|
167
|
+
function resolveModel(input) {
|
|
166
168
|
if (input == null || input === '') return undefined;
|
|
167
169
|
const s = String(input);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const warn = (logger && typeof logger.warn === 'function') ? logger.warn.bind(logger) : null;
|
|
172
|
-
if (warn) warn(`[copilot] "${s}" is a Claude family shorthand; Copilot expects a full model id (e.g. claude-sonnet-4.5). Passing through verbatim — Copilot will likely reject it.`);
|
|
173
|
-
} catch { /* logger may be unwired during tests */ }
|
|
174
|
-
}
|
|
170
|
+
const mapped = _MINIONS_MODEL_ALIASES[s.toLowerCase()];
|
|
171
|
+
if (mapped) return mapped;
|
|
175
172
|
return s;
|
|
176
173
|
}
|
|
177
174
|
|
|
@@ -783,8 +780,7 @@ module.exports = {
|
|
|
783
780
|
createStreamConsumer,
|
|
784
781
|
// Exposed for unit tests — engine code MUST go through resolveRuntime + the
|
|
785
782
|
// adapter contract; never reach into these helpers directly.
|
|
786
|
-
|
|
787
|
-
_resetShorthandWarning,
|
|
783
|
+
_MINIONS_MODEL_ALIASES,
|
|
788
784
|
_mapEffort,
|
|
789
785
|
_copilotAssistantMessageHasTools,
|
|
790
786
|
KNOWN_EVENT_TYPES,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1673",
|
|
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"
|