@yemi33/minions 0.1.1770 → 0.1.1772
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 +55 -14
- package/docs/copilot-cli-schema.md +1 -0
- package/engine/cleanup.js +111 -1
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +10 -4
- package/engine/runtimes/claude.js +2 -0
- package/engine/runtimes/copilot.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -10,6 +10,7 @@ const zlib = require('zlib');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const llm = require('./engine/llm');
|
|
13
|
+
const { resolveRuntime } = require('./engine/runtimes');
|
|
13
14
|
|
|
14
15
|
// Dashboard version stamp — captured at module load so it reflects the code actually running
|
|
15
16
|
const _dashboardVersion = {
|
|
@@ -1262,9 +1263,14 @@ function _readCcTabSessions({ prune = true } = {}) {
|
|
|
1262
1263
|
const CC_CARRYOVER_MAX_TURNS = 20;
|
|
1263
1264
|
const CC_CARRYOVER_PER_MSG_CHARS = 2000;
|
|
1264
1265
|
|
|
1265
|
-
function _buildTranscriptCarryover(transcript, { previousRuntime } = {}) {
|
|
1266
|
+
function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage } = {}) {
|
|
1266
1267
|
if (!Array.isArray(transcript) || transcript.length === 0) return '';
|
|
1267
|
-
|
|
1268
|
+
let filtered = transcript.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.text === 'string' && m.text.trim());
|
|
1269
|
+
const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
|
|
1270
|
+
if (current && filtered.length > 0) {
|
|
1271
|
+
const last = filtered[filtered.length - 1];
|
|
1272
|
+
if (last.role === 'user' && last.text.trim() === current) filtered = filtered.slice(0, -1);
|
|
1273
|
+
}
|
|
1268
1274
|
if (filtered.length === 0) return '';
|
|
1269
1275
|
const recent = filtered.slice(-CC_CARRYOVER_MAX_TURNS);
|
|
1270
1276
|
const truncated = filtered.length > recent.length;
|
|
@@ -1281,6 +1287,19 @@ function _buildTranscriptCarryover(transcript, { previousRuntime } = {}) {
|
|
|
1281
1287
|
return `${header}\n\n${truncationNote}${lines.join('\n\n')}\n\n--- Current message follows ---`;
|
|
1282
1288
|
}
|
|
1283
1289
|
|
|
1290
|
+
function _ccRuntimeNeedsResumeCarryover(runtimeName) {
|
|
1291
|
+
try {
|
|
1292
|
+
const runtime = resolveRuntime(runtimeName);
|
|
1293
|
+
return !!runtime?.capabilities?.resumePromptCarryover;
|
|
1294
|
+
} catch {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function _joinCcPromptParts(...parts) {
|
|
1300
|
+
return parts.filter(Boolean).join('\n\n---\n\n');
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1284
1303
|
// Load persisted CC session on startup. CC chat sessions are non-expiring;
|
|
1285
1304
|
// only restore-time validity checks here are sessionId presence (anything
|
|
1286
1305
|
// else would auto-expire the user's chat without their consent).
|
|
@@ -2415,7 +2434,7 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
|
|
|
2415
2434
|
* @param {number} opts.maxTurns - Max tool-use turns
|
|
2416
2435
|
* @param {string} opts.allowedTools - Comma-separated tool list
|
|
2417
2436
|
*/
|
|
2418
|
-
async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
|
|
2437
|
+
async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript } = {}) {
|
|
2419
2438
|
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
2420
2439
|
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
2421
2440
|
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
@@ -2428,10 +2447,15 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2428
2447
|
|
|
2429
2448
|
const existing = resolveSession(store, sessionKey);
|
|
2430
2449
|
let sessionId = existing ? existing.sessionId : null;
|
|
2450
|
+
const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
|
|
2431
2451
|
|
|
2432
|
-
function buildPrompt({ includePreamble = true } = {}) {
|
|
2452
|
+
function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
|
|
2433
2453
|
const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
2434
2454
|
if (extraContext) parts.push(extraContext);
|
|
2455
|
+
if (includeCarryover) {
|
|
2456
|
+
const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
|
|
2457
|
+
if (carryover) parts.push(carryover);
|
|
2458
|
+
}
|
|
2435
2459
|
parts.push(message);
|
|
2436
2460
|
return parts.join('\n\n---\n\n');
|
|
2437
2461
|
}
|
|
@@ -2440,7 +2464,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2440
2464
|
|
|
2441
2465
|
// Attempt 1: resume existing session — skip preamble (session already has context)
|
|
2442
2466
|
if (sessionId && maxTurns > 1) {
|
|
2443
|
-
const p1 = llm.callLLM(buildPrompt({ includePreamble: false }), '', {
|
|
2467
|
+
const p1 = llm.callLLM(buildPrompt({ includePreamble: false, includeCarryover: resumeNeedsCarryover }), '', {
|
|
2444
2468
|
timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
|
|
2445
2469
|
engineConfig: CONFIG.engine,
|
|
2446
2470
|
});
|
|
@@ -2477,7 +2501,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2477
2501
|
}
|
|
2478
2502
|
|
|
2479
2503
|
// Attempt 2: fresh session (include preamble for full context)
|
|
2480
|
-
const freshPrompt = buildPrompt();
|
|
2504
|
+
const freshPrompt = buildPrompt({ includeCarryover: resumeNeedsCarryover });
|
|
2481
2505
|
const p2 = llm.callLLM(freshPrompt, systemPrompt, {
|
|
2482
2506
|
timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
|
|
2483
2507
|
engineConfig: CONFIG.engine,
|
|
@@ -2511,7 +2535,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2511
2535
|
return result;
|
|
2512
2536
|
}
|
|
2513
2537
|
|
|
2514
|
-
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse, onRetry, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
|
|
2538
|
+
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse, onRetry, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript } = {}) {
|
|
2515
2539
|
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
2516
2540
|
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
2517
2541
|
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
@@ -2524,10 +2548,15 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2524
2548
|
|
|
2525
2549
|
const existing = resolveSession(store, sessionKey);
|
|
2526
2550
|
let sessionId = existing ? existing.sessionId : null;
|
|
2551
|
+
const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
|
|
2527
2552
|
|
|
2528
|
-
function buildPrompt({ includePreamble = true } = {}) {
|
|
2553
|
+
function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
|
|
2529
2554
|
const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
2530
2555
|
if (extraContext) parts.push(extraContext);
|
|
2556
|
+
if (includeCarryover) {
|
|
2557
|
+
const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
|
|
2558
|
+
if (carryover) parts.push(carryover);
|
|
2559
|
+
}
|
|
2531
2560
|
parts.push(message);
|
|
2532
2561
|
return parts.join('\n\n---\n\n');
|
|
2533
2562
|
}
|
|
@@ -2535,7 +2564,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2535
2564
|
let result;
|
|
2536
2565
|
|
|
2537
2566
|
if (sessionId && maxTurns > 1) {
|
|
2538
|
-
const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false }), '', {
|
|
2567
|
+
const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false, includeCarryover: resumeNeedsCarryover }), '', {
|
|
2539
2568
|
timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
|
|
2540
2569
|
engineConfig: CONFIG.engine,
|
|
2541
2570
|
onChunk,
|
|
@@ -2572,7 +2601,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2572
2601
|
}
|
|
2573
2602
|
|
|
2574
2603
|
if (onRetry) onRetry(2);
|
|
2575
|
-
const freshPrompt = buildPrompt();
|
|
2604
|
+
const freshPrompt = buildPrompt({ includeCarryover: resumeNeedsCarryover });
|
|
2576
2605
|
const p2 = llm.callLLMStreaming(freshPrompt, systemPrompt, {
|
|
2577
2606
|
timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
|
|
2578
2607
|
engineConfig: CONFIG.engine,
|
|
@@ -5754,7 +5783,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5754
5783
|
}
|
|
5755
5784
|
const wasResume = !!(body.sessionId && body.sessionId === ccSession.sessionId && ccSessionValid());
|
|
5756
5785
|
|
|
5757
|
-
const result = await ccCall(body.message, { store: 'cc' });
|
|
5786
|
+
const result = await ccCall(body.message, { store: 'cc', transcript: body.transcript });
|
|
5758
5787
|
|
|
5759
5788
|
// Non-zero exit with text = max_turns or partial success — still usable
|
|
5760
5789
|
if (!result.text) {
|
|
@@ -5989,13 +6018,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5989
6018
|
sessionReset = true;
|
|
5990
6019
|
sessionResetReason = 'runtimeChanged';
|
|
5991
6020
|
previousRuntime = tabEntry.runtime;
|
|
6021
|
+
} else if (tabEntry.sessionId && tabEntry.sessionId !== tabSessionId) {
|
|
6022
|
+
tabSessionId = tabEntry.sessionId;
|
|
5992
6023
|
}
|
|
5993
6024
|
}
|
|
5994
6025
|
const wasResume = !!tabSessionId;
|
|
5995
6026
|
const sessionId = tabSessionId || null;
|
|
6027
|
+
const resumeNeedsCarryover = wasResume && _ccRuntimeNeedsResumeCarryover(currentRuntime);
|
|
5996
6028
|
const preamble = wasResume ? '' : buildCCStatePreamble();
|
|
5997
|
-
const carryover = sessionReset
|
|
5998
|
-
|
|
6029
|
+
const carryover = (sessionReset || resumeNeedsCarryover)
|
|
6030
|
+
? _buildTranscriptCarryover(body.transcript, {
|
|
6031
|
+
previousRuntime: sessionReset ? previousRuntime : null,
|
|
6032
|
+
currentMessage: body.message,
|
|
6033
|
+
})
|
|
6034
|
+
: '';
|
|
6035
|
+
const prompt = _joinCcPromptParts(preamble, carryover, body.message);
|
|
5999
6036
|
|
|
6000
6037
|
const { trackEngineUsage: trackUsage } = require('./engine/llm');
|
|
6001
6038
|
const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
@@ -6023,7 +6060,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6023
6060
|
// Resume failed (stale/expired session) — auto-retry as fresh session (skip if client already disconnected)
|
|
6024
6061
|
console.log(`[CC-stream] Resume failed (code=${result.code}) — retrying fresh`);
|
|
6025
6062
|
const freshPreamble = buildCCStatePreamble();
|
|
6026
|
-
const
|
|
6063
|
+
const freshCarryover = _buildTranscriptCarryover(body.transcript, { currentMessage: body.message });
|
|
6064
|
+
const freshPrompt = _joinCcPromptParts(freshPreamble, freshCarryover, body.message);
|
|
6027
6065
|
toolUses = []; // discard stale metadata from the failed resume attempt
|
|
6028
6066
|
const retryPromise = _invokeCcStream({
|
|
6029
6067
|
prompt: freshPrompt, sessionId: undefined, liveState, toolUses,
|
|
@@ -7695,6 +7733,9 @@ module.exports = {
|
|
|
7695
7733
|
_createPipelineFromAction: createPipelineFromAction,
|
|
7696
7734
|
executeCCActions,
|
|
7697
7735
|
buildCCStatePreamble,
|
|
7736
|
+
_buildTranscriptCarryover,
|
|
7737
|
+
_ccRuntimeNeedsResumeCarryover,
|
|
7738
|
+
_joinCcPromptParts,
|
|
7698
7739
|
_captureApiRoutesMeta,
|
|
7699
7740
|
_formatCcApiRoutesIndex,
|
|
7700
7741
|
_formatCcCliCommandsIndex,
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| `capabilities.modelDiscovery` | **`true`** | `GET https://api.githubcopilot.com/models` with a `gh auth token` Bearer returns HTTP 200 + a 24-model JSON catalog. |
|
|
18
18
|
| `capabilities.streaming` | **`true`** | `--stream on` (default) emits `assistant.message_delta` events incrementally; `--stream off` suppresses deltas but the final `assistant.message` always arrives. |
|
|
19
19
|
| `capabilities.sessionResume` | **`true`** | `--resume <session-id>` documented, and every `result` event emits `sessionId`. |
|
|
20
|
+
| `capabilities.resumePromptCarryover` | **`true`** | Command Center resume turns should prepend the browser's recent Q&A transcript because Copilot's session store is opaque to Minions and can resume without enough conversational context. |
|
|
20
21
|
| `capabilities.systemPromptFile` | **`false`** | No `--system-prompt-file` flag exists. Inject system prompt via a `<system>` block prepended to stdin. |
|
|
21
22
|
| `capabilities.effortLevels` | **`true`** | `--effort` accepts `low|medium|high|xhigh` (no `max`). Adapter must map `'max' → 'xhigh'`. |
|
|
22
23
|
| `capabilities.costTracking` | **`false`** | `result.usage` contains `premiumRequests` (count, not USD), no token counts, no cost. |
|
package/engine/cleanup.js
CHANGED
|
@@ -48,6 +48,105 @@ function worktreeMatchesBranch(dirLower, branch, actualBranch = '') {
|
|
|
48
48
|
return worktreeBranchMatches(actualBranch, branch) || worktreeDirMatchesBranch(dirLower, branch);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function normalizeLocalBranchName(branch) {
|
|
52
|
+
return String(branch || '').trim().replace(/^refs\/heads\//i, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isSafeLocalBranchName(branch) {
|
|
56
|
+
if (!branch || branch !== sanitizeBranch(branch)) return false;
|
|
57
|
+
if (branch.startsWith('-') || branch.includes('..') || branch.includes('@{')) return false;
|
|
58
|
+
if (branch.endsWith('/') || branch.endsWith('.lock')) return false;
|
|
59
|
+
return branch.split('/').every(part => part && part !== '.' && part !== '..' && !part.endsWith('.lock'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isProtectedLocalBranch(branch, project = {}) {
|
|
63
|
+
const protectedBranches = new Set(['main', 'master', 'trunk', 'develop', 'development', 'head']);
|
|
64
|
+
const configuredMain = normalizeLocalBranchName(project.mainBranch);
|
|
65
|
+
if (configuredMain) protectedBranches.add(configuredMain.toLowerCase());
|
|
66
|
+
return protectedBranches.has(branch.toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function localBranchWorktreeInUse(root, branch) {
|
|
70
|
+
try {
|
|
71
|
+
const out = String(shared.execSilent('git worktree list --porcelain', {
|
|
72
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
73
|
+
}) || '');
|
|
74
|
+
return out.split(/\r?\n/).some(line => line.trim() === `branch refs/heads/${branch}`);
|
|
75
|
+
} catch {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cleanupMergedPrLocalBranch(root, project, pr) {
|
|
81
|
+
const branch = normalizeLocalBranchName(pr?.branch);
|
|
82
|
+
const result = { deleted: false, forced: false, skipped: null };
|
|
83
|
+
if (pr?.status !== shared.PR_STATUS.MERGED) { result.skipped = 'not-merged'; return result; }
|
|
84
|
+
if (!root || !branch) { result.skipped = 'missing-branch'; return result; }
|
|
85
|
+
if (!isSafeLocalBranchName(branch)) { result.skipped = 'unsafe-branch-name'; return result; }
|
|
86
|
+
if (isProtectedLocalBranch(branch, project)) { result.skipped = 'protected-branch'; return result; }
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const current = String(shared.execSilent('git branch --show-current', {
|
|
90
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
91
|
+
}) || '').trim();
|
|
92
|
+
if (current === branch) { result.skipped = 'current-branch'; return result; }
|
|
93
|
+
} catch {
|
|
94
|
+
result.skipped = 'current-branch-unknown';
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (localBranchWorktreeInUse(root, branch)) { result.skipped = 'branch-in-worktree'; return result; }
|
|
99
|
+
|
|
100
|
+
let localHead = '';
|
|
101
|
+
try {
|
|
102
|
+
localHead = String(shared.execSilent(`git rev-parse --verify "refs/heads/${branch}"`, {
|
|
103
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
104
|
+
}) || '').trim();
|
|
105
|
+
} catch {
|
|
106
|
+
result.skipped = 'missing-local-branch';
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
shared.execSilent(`git branch -d -- "${branch}"`, {
|
|
112
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 15000, windowsHide: true,
|
|
113
|
+
});
|
|
114
|
+
log('info', `Post-merge cleanup: deleted local branch ${branch}`);
|
|
115
|
+
return { deleted: true, forced: false, skipped: null };
|
|
116
|
+
} catch (deleteErr) {
|
|
117
|
+
const localHeadLower = localHead.toLowerCase();
|
|
118
|
+
// Only use -D when the local tip still matches the merged PR head (or its remote-tracking ref);
|
|
119
|
+
// otherwise a reused local branch could lose unrelated work after the PR merged.
|
|
120
|
+
const proofHeads = [pr.headSha, pr._adoSourceCommit, pr.sourceCommit]
|
|
121
|
+
.map(v => String(v || '').trim().toLowerCase())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
let safeToForce = proofHeads.includes(localHeadLower);
|
|
124
|
+
if (!safeToForce) {
|
|
125
|
+
try {
|
|
126
|
+
const remoteHead = String(shared.execSilent(`git rev-parse --verify "refs/remotes/origin/${branch}"`, {
|
|
127
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
128
|
+
}) || '').trim().toLowerCase();
|
|
129
|
+
safeToForce = !!remoteHead && remoteHead === localHeadLower;
|
|
130
|
+
} catch { /* no matching remote-tracking branch */ }
|
|
131
|
+
}
|
|
132
|
+
if (!safeToForce) {
|
|
133
|
+
result.skipped = 'unproven-force-delete';
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
shared.execSilent(`git branch -D -- "${branch}"`, {
|
|
138
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 15000, windowsHide: true,
|
|
139
|
+
});
|
|
140
|
+
log('info', `Post-merge cleanup: force-deleted local branch ${branch} after merged PR confirmation`);
|
|
141
|
+
return { deleted: true, forced: true, skipped: null };
|
|
142
|
+
} catch (forceErr) {
|
|
143
|
+
log('warn', `Post-merge cleanup: failed to delete local branch ${branch}: ${forceErr.message || deleteErr.message}`);
|
|
144
|
+
result.skipped = 'delete-failed';
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
51
150
|
/**
|
|
52
151
|
* Sweep leaked test-fixture meetings from a `meetings/` directory.
|
|
53
152
|
*
|
|
@@ -342,9 +441,11 @@ async function runCleanup(config, verbose = false) {
|
|
|
342
441
|
// Check if this worktree's branch is merged/abandoned
|
|
343
442
|
// Prefer actual git branch metadata; compact Windows dirs intentionally omit branch names.
|
|
344
443
|
const dirLower = dir.toLowerCase();
|
|
444
|
+
let matchedMergedBranch = '';
|
|
345
445
|
for (const branch of mergedBranches) {
|
|
346
446
|
if (worktreeMatchesBranch(dirLower, branch, actualBranch)) {
|
|
347
447
|
shouldClean = true;
|
|
448
|
+
matchedMergedBranch = branch;
|
|
348
449
|
break;
|
|
349
450
|
}
|
|
350
451
|
}
|
|
@@ -392,7 +493,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
392
493
|
} catch (e) { log('warn', 'check shared-branch protection: ' + e.message); }
|
|
393
494
|
}
|
|
394
495
|
|
|
395
|
-
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected, actualBranch });
|
|
496
|
+
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected, actualBranch, matchedMergedBranch });
|
|
396
497
|
}
|
|
397
498
|
|
|
398
499
|
// Enforce max worktree cap — if over limit, mark oldest unprotected for cleanup
|
|
@@ -412,10 +513,14 @@ async function runCleanup(config, verbose = false) {
|
|
|
412
513
|
// the initial status check and the actual deletion (Bug #15: TOCTOU race)
|
|
413
514
|
const freshPrs = safeJson(projectPrPath(project)) || [];
|
|
414
515
|
const freshMergedBranches = new Set();
|
|
516
|
+
const freshMergedPrByBranch = new Map();
|
|
415
517
|
for (const pr of freshPrs) {
|
|
416
518
|
if (pr.status === shared.PR_STATUS.MERGED || pr.status === shared.PR_STATUS.ABANDONED || pr.status === shared.PLAN_STATUS.COMPLETED) {
|
|
417
519
|
if (pr.branch) freshMergedBranches.add(pr.branch);
|
|
418
520
|
}
|
|
521
|
+
if (pr.status === shared.PR_STATUS.MERGED && pr.branch) {
|
|
522
|
+
freshMergedPrByBranch.set(sanitizeBranch(normalizeLocalBranchName(pr.branch)).toLowerCase(), pr);
|
|
523
|
+
}
|
|
419
524
|
}
|
|
420
525
|
|
|
421
526
|
for (const entry of wtEntries) {
|
|
@@ -446,6 +551,10 @@ async function runCleanup(config, verbose = false) {
|
|
|
446
551
|
_killProcessInWorktree(entry.dir, activeProcesses, activeDispatchIds);
|
|
447
552
|
if (shared.removeWorktree(entry.wtPath, root, worktreeRoot)) {
|
|
448
553
|
cleaned.worktrees++;
|
|
554
|
+
const mergedPr = entry.matchedMergedBranch
|
|
555
|
+
? freshMergedPrByBranch.get(sanitizeBranch(normalizeLocalBranchName(entry.matchedMergedBranch)).toLowerCase())
|
|
556
|
+
: null;
|
|
557
|
+
if (mergedPr) cleanupMergedPrLocalBranch(root, project, mergedPr);
|
|
449
558
|
if (verbose) console.log(` Removed worktree: ${entry.wtPath}`);
|
|
450
559
|
} else {
|
|
451
560
|
if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}`);
|
|
@@ -935,4 +1044,5 @@ module.exports = {
|
|
|
935
1044
|
worktreeDirMatchesBranch, // exported for testing
|
|
936
1045
|
worktreeMatchesBranch, // exported for testing
|
|
937
1046
|
getWorktreeBranch, // exported for lifecycle cleanup
|
|
1047
|
+
cleanupMergedPrLocalBranch, // exported for lifecycle cleanup and testing
|
|
938
1048
|
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -7,14 +7,14 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
|
-
const { safeRead, safeJson, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems,
|
|
10
|
+
const { safeRead, safeJson, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, projectPrPath, getPrLinks,
|
|
11
11
|
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
14
14
|
const { resolveRuntime } = require('./runtimes');
|
|
15
15
|
const queries = require('./queries');
|
|
16
16
|
const { isBranchActive } = require('./cooldown');
|
|
17
|
-
const { worktreeMatchesBranch, getWorktreeBranch } = require('./cleanup');
|
|
17
|
+
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
18
18
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
19
19
|
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, PRD_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
20
20
|
|
|
@@ -1838,6 +1838,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1838
1838
|
if (pr.branch && project) {
|
|
1839
1839
|
const root = path.resolve(project.localPath);
|
|
1840
1840
|
const wtRoot = path.resolve(root, config.engine?.worktreeRoot || '../worktrees');
|
|
1841
|
+
let removedBranchWorktree = false;
|
|
1841
1842
|
// Find worktrees matching this branch; compact Windows dirs require branch metadata.
|
|
1842
1843
|
try {
|
|
1843
1844
|
const dirs = require('fs').readdirSync(wtRoot);
|
|
@@ -1847,11 +1848,16 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1847
1848
|
if (worktreeMatchesBranch(dirLower, pr.branch, getWorktreeBranch(wtPath)) || dir === pr.branch || dir === `bt-${prNum}`) {
|
|
1848
1849
|
try {
|
|
1849
1850
|
if (!require('fs').statSync(wtPath).isDirectory()) continue;
|
|
1850
|
-
|
|
1851
|
-
|
|
1851
|
+
if (shared.removeWorktree(wtPath, root, wtRoot)) {
|
|
1852
|
+
removedBranchWorktree = true;
|
|
1853
|
+
log('info', `Post-merge cleanup: removed worktree ${dir}`);
|
|
1854
|
+
}
|
|
1852
1855
|
} catch (err) { log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
|
|
1853
1856
|
}
|
|
1854
1857
|
}
|
|
1858
|
+
if (removedBranchWorktree && newStatus === PR_STATUS.MERGED) {
|
|
1859
|
+
cleanupMergedPrLocalBranch(root, project, pr);
|
|
1860
|
+
}
|
|
1855
1861
|
} catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
|
|
1856
1862
|
}
|
|
1857
1863
|
|
|
@@ -717,6 +717,8 @@ const capabilities = {
|
|
|
717
717
|
fallbackModel: true,
|
|
718
718
|
// Engine controls session persistence (writes session.json on completion)
|
|
719
719
|
sessionPersistenceControl: true,
|
|
720
|
+
// Claude resume reliably restores prior turns; do not duplicate browser transcript.
|
|
721
|
+
resumePromptCarryover: false,
|
|
720
722
|
// Adapter implements createStreamConsumer(ctx) — required by llm.js accumulator
|
|
721
723
|
streamConsumer: true,
|
|
722
724
|
};
|
|
@@ -874,6 +874,9 @@ const capabilities = {
|
|
|
874
874
|
fallbackModel: false,
|
|
875
875
|
// Copilot manages session state internally in ~/.copilot/session-state/
|
|
876
876
|
sessionPersistenceControl: false,
|
|
877
|
+
// CC resumes should include recent visible Q&A in stdin because Minions cannot
|
|
878
|
+
// inspect or repair Copilot's opaque session-state store when it drops context.
|
|
879
|
+
resumePromptCarryover: true,
|
|
877
880
|
// Adapter implements createStreamConsumer(ctx) — required by llm.js accumulator
|
|
878
881
|
streamConsumer: true,
|
|
879
882
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1772",
|
|
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"
|