@yemi33/minions 0.1.2382 → 0.1.2384
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/bin/minions.js +1 -0
- package/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +37 -19
- package/docs/completion-reports.md +20 -1
- package/docs/keep-processes.md +7 -2
- package/docs/live-checkout-mode.md +7 -7
- package/docs/managed-spawn.md +14 -1
- package/docs/runtime-adapters.md +61 -26
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +36 -0
- package/engine/acp-transport.js +273 -58
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +278 -54
- package/engine/cc-worker-pool.js +12 -3
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +56 -28
- package/engine/comment-format.js +51 -14
- package/engine/create-pr-worktree.js +57 -79
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +126 -45
- package/engine/live-checkout.js +4 -2
- package/engine/llm.js +59 -83
- package/engine/managed-spawn.js +112 -5
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +512 -45
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +97 -9
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +349 -82
- package/engine/spawn-agent.js +85 -116
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +418 -241
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/engine/pr-action.js
CHANGED
|
@@ -184,19 +184,17 @@ function buildCreatePrFollowups({ project, branch, contextOnly = false, checkout
|
|
|
184
184
|
|
|
185
185
|
let message;
|
|
186
186
|
if (checkoutMode === 'worktree') {
|
|
187
|
-
// Worktree-mode projects:
|
|
188
|
-
// isolated worktree
|
|
189
|
-
// push / PR all happen OFF the operator's tree. CC never runs git in the
|
|
190
|
-
// live checkout — that is what kept leaking commits onto `main`.
|
|
187
|
+
// Worktree-mode projects: copy pre-existing operator changes into an
|
|
188
|
+
// isolated worktree without modifying their checkout.
|
|
191
189
|
message =
|
|
192
|
-
`Create a PR from the
|
|
193
|
-
`This project uses isolated worktrees, so do NOT commit, branch, or push in its
|
|
194
|
-
`Steps: (1) call POST /api/pr-action/prepare-create-pr-worktree with {"project":"${proj}"} — the server
|
|
190
|
+
`Create a PR from the pre-existing operator changes in the ${proj} project. ` +
|
|
191
|
+
`This project uses isolated worktrees, so do NOT edit, commit, branch, clean, reset, or push in its operator checkout. ` +
|
|
192
|
+
`Steps: (1) call POST /api/pr-action/prepare-create-pr-worktree with {"project":"${proj}"} — the server copies the uncommitted changes into a fresh isolated worktree, leaves the operator checkout unchanged, and returns {worktreePath, branch, baseBranch}; ` +
|
|
195
193
|
`(2) run all git from inside worktreePath (use \`git -C <worktreePath> ...\`): stage and commit the changes on the returned branch with a clear conventional-commit message; ` +
|
|
196
194
|
`(3) push that branch; (4) open a PR against baseBranch using the right CLI for the repo host (gh for GitHub, az repos for ADO); ` +
|
|
197
195
|
`(5) link it by calling POST /api/pull-requests/link with ${linkBody} ${trackNote}; ` +
|
|
198
196
|
`(6) finally call POST /api/pr-action/cleanup-create-pr-worktree with {"project":"${proj}","worktreePath":"<worktreePath>"} to remove the worktree. ` +
|
|
199
|
-
`
|
|
197
|
+
`Do not clean up the operator's original changes; report that they remain untouched. Then show me the PR URL.`;
|
|
200
198
|
} else {
|
|
201
199
|
// Live-checkout projects: operate in place, but ALWAYS build the PR on a
|
|
202
200
|
// fresh branch off the up-to-date origin/<mainBranch> tip — never off
|
package/engine/process-utils.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
-
const { execSync: _execSync } = require('child_process');
|
|
17
|
+
const { execSync: _execSync, execFile: _execFile } = require('child_process');
|
|
18
18
|
|
|
19
19
|
// Shared.js-local PID liveness check, moved here unchanged. Avoids a circular
|
|
20
20
|
// require on engine/cli.js (which has its own isPidAlive) and engine/timeout.js
|
|
@@ -24,7 +24,12 @@ const { execSync: _execSync } = require('child_process');
|
|
|
24
24
|
function isPidAlive(pid) {
|
|
25
25
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
26
26
|
try { process.kill(pid, 0); return true; }
|
|
27
|
-
catch {
|
|
27
|
+
catch (err) {
|
|
28
|
+
// Signal 0 returning EPERM means the process exists but this caller cannot
|
|
29
|
+
// inspect it. Treating that as dead lets lock recovery delete a live
|
|
30
|
+
// holder's lock and breaks cross-process mutual exclusion.
|
|
31
|
+
return err && err.code === 'EPERM';
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
|
|
@@ -206,6 +211,7 @@ function _parseWmicCsv(text) {
|
|
|
206
211
|
const ppidIdx = idx('ParentProcessId');
|
|
207
212
|
const pidIdx = idx('ProcessId');
|
|
208
213
|
const cmdIdx = idx('CommandLine');
|
|
214
|
+
const creationIdx = idx('CreationDate');
|
|
209
215
|
if (nameIdx < 0 || ppidIdx < 0 || pidIdx < 0) return [];
|
|
210
216
|
const out = [];
|
|
211
217
|
for (let i = 1; i < lines.length; i++) {
|
|
@@ -218,28 +224,31 @@ function _parseWmicCsv(text) {
|
|
|
218
224
|
const ppid = parseInt(cols[ppidIdx], 10);
|
|
219
225
|
const name = cols[nameIdx] || '';
|
|
220
226
|
const cmd = cmdIdx >= 0 ? cols.slice(cmdIdx).join(',') : '';
|
|
221
|
-
|
|
227
|
+
const created = creationIdx >= 0
|
|
228
|
+
? String(cols[creationIdx] || '').match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{3})\d{3}([+-]\d{3})$/)
|
|
229
|
+
: null;
|
|
230
|
+
const startedAt = created
|
|
231
|
+
? Date.UTC(
|
|
232
|
+
Number(created[1]), Number(created[2]) - 1, Number(created[3]),
|
|
233
|
+
Number(created[4]), Number(created[5]), Number(created[6]), Number(created[7])
|
|
234
|
+
) - Number(created[8]) * 60000
|
|
235
|
+
: 0;
|
|
236
|
+
out.push({ pid, ppid: Number.isInteger(ppid) ? ppid : 0, name, cmd, startedAt });
|
|
222
237
|
}
|
|
223
238
|
return out;
|
|
224
239
|
}
|
|
225
240
|
|
|
241
|
+
const PROCESS_LIST_PS_SCRIPT = "Get-CimInstance Win32_Process | ForEach-Object { [PSCustomObject]@{ Name = $_.Name; ParentProcessId = $_.ParentProcessId; ProcessId = $_.ProcessId; CommandLine = $_.CommandLine; StartedAt = if ($_.CreationDate) { [int64](([datetimeoffset]$_.CreationDate).ToUnixTimeMilliseconds()) } else { 0 } } } | ConvertTo-Json -Compress -Depth 2";
|
|
242
|
+
|
|
226
243
|
// PowerShell Get-CimInstance is the modern path (wmic is removed on Win11
|
|
227
244
|
// 24H2+). We try it first and fall back to wmic for older Windows hosts.
|
|
228
245
|
function _psListProcesses() {
|
|
229
|
-
const script = "Get-CimInstance Win32_Process | Select-Object Name,ParentProcessId,ProcessId,CommandLine | ConvertTo-Json -Compress -Depth 2";
|
|
230
246
|
try {
|
|
231
247
|
const out = _execSync(
|
|
232
|
-
`powershell -NoProfile -NonInteractive -Command "${
|
|
248
|
+
`powershell -NoProfile -NonInteractive -Command "${PROCESS_LIST_PS_SCRIPT}"`,
|
|
233
249
|
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }
|
|
234
250
|
);
|
|
235
|
-
|
|
236
|
-
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
237
|
-
return arr.map(p => ({
|
|
238
|
-
pid: Number(p.ProcessId),
|
|
239
|
-
ppid: Number(p.ParentProcessId) || 0,
|
|
240
|
-
name: p.Name || '',
|
|
241
|
-
cmd: p.CommandLine || '',
|
|
242
|
-
})).filter(p => Number.isInteger(p.pid) && p.pid > 0);
|
|
251
|
+
return _parsePowerShellProcesses(out);
|
|
243
252
|
} catch { return null; }
|
|
244
253
|
}
|
|
245
254
|
|
|
@@ -261,11 +270,7 @@ function _unixListProcesses() {
|
|
|
261
270
|
'ps -A -o pid=,ppid=,comm=',
|
|
262
271
|
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }
|
|
263
272
|
);
|
|
264
|
-
return out
|
|
265
|
-
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
266
|
-
if (!m) return null;
|
|
267
|
-
return { pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), name: m[3], cmd: '' };
|
|
268
|
-
}).filter(Boolean);
|
|
273
|
+
return _parseUnixProcesses(out);
|
|
269
274
|
} catch { return []; }
|
|
270
275
|
}
|
|
271
276
|
|
|
@@ -273,6 +278,103 @@ function listAllProcesses() {
|
|
|
273
278
|
return process.platform === 'win32' ? _winListProcesses() : _unixListProcesses();
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
function _parsePowerShellProcesses(out) {
|
|
282
|
+
const parsed = JSON.parse(out);
|
|
283
|
+
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
284
|
+
return arr.map(p => ({
|
|
285
|
+
pid: Number(p.ProcessId),
|
|
286
|
+
ppid: Number(p.ParentProcessId) || 0,
|
|
287
|
+
name: p.Name || '',
|
|
288
|
+
cmd: p.CommandLine || '',
|
|
289
|
+
startedAt: Number(p.StartedAt) || 0,
|
|
290
|
+
})).filter(p => Number.isInteger(p.pid) && p.pid > 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function _parseUnixProcesses(out) {
|
|
294
|
+
return out.split(/\r?\n/).map(line => {
|
|
295
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
296
|
+
if (!m) return null;
|
|
297
|
+
return { pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), name: m[3], cmd: '' };
|
|
298
|
+
}).filter(Boolean);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _parseUnixProcessesWithStart(out) {
|
|
302
|
+
return out.split(/\r?\n/).map(line => {
|
|
303
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.{24})\s+(.*)$/);
|
|
304
|
+
if (!m) return null;
|
|
305
|
+
const startedAt = Date.parse(m[3].trim());
|
|
306
|
+
return {
|
|
307
|
+
pid: parseInt(m[1], 10),
|
|
308
|
+
ppid: parseInt(m[2], 10),
|
|
309
|
+
name: m[4],
|
|
310
|
+
cmd: '',
|
|
311
|
+
startedAt: Number.isNaN(startedAt) ? 0 : startedAt,
|
|
312
|
+
};
|
|
313
|
+
}).filter(Boolean);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function _applyLinuxStartTicks(processes) {
|
|
317
|
+
if (process.platform !== 'linux') return processes;
|
|
318
|
+
await Promise.all(processes.map(async (entry) => {
|
|
319
|
+
try {
|
|
320
|
+
const stat = await fs.promises.readFile(`/proc/${entry.pid}/stat`, 'utf8');
|
|
321
|
+
const fields = stat.slice(stat.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
322
|
+
const startTicks = Number(fields[19]);
|
|
323
|
+
entry.startedAt = Number.isFinite(startTicks) && startTicks > 0 ? startTicks : 0;
|
|
324
|
+
} catch {
|
|
325
|
+
entry.startedAt = 0;
|
|
326
|
+
}
|
|
327
|
+
}));
|
|
328
|
+
return processes;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function _execFileText(file, args, opts) {
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
_execFile(file, args, opts, (err, stdout) => {
|
|
334
|
+
if (err) reject(err);
|
|
335
|
+
else resolve(stdout);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function listAllProcessesAsync() {
|
|
341
|
+
if (process.platform === 'win32') {
|
|
342
|
+
try {
|
|
343
|
+
const out = await _execFileText(
|
|
344
|
+
'powershell',
|
|
345
|
+
['-NoProfile', '-NonInteractive', '-Command', PROCESS_LIST_PS_SCRIPT],
|
|
346
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 4000, maxBuffer: 4 * 1024 * 1024 }
|
|
347
|
+
);
|
|
348
|
+
const parsed = _parsePowerShellProcesses(out);
|
|
349
|
+
if (parsed.length > 0) return parsed;
|
|
350
|
+
} catch { /* fall through to wmic */ }
|
|
351
|
+
try {
|
|
352
|
+
const out = await _execFileText(
|
|
353
|
+
'wmic',
|
|
354
|
+
['process', 'get', 'CreationDate,Name,ParentProcessId,ProcessId', '/format:csv'],
|
|
355
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 4000, maxBuffer: 4 * 1024 * 1024 }
|
|
356
|
+
);
|
|
357
|
+
return _parseWmicCsv(out);
|
|
358
|
+
} catch {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const out = await _execFileText(
|
|
364
|
+
'ps',
|
|
365
|
+
['-A', '-o', 'pid=,ppid=,lstart=,comm='],
|
|
366
|
+
{
|
|
367
|
+
encoding: 'utf8',
|
|
368
|
+
timeout: 5000,
|
|
369
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
return await _applyLinuxStartTicks(_parseUnixProcessesWithStart(out));
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
276
378
|
// W-mq6f2fe0000557fa — orphan-worktree GC: identify OS processes whose cwd
|
|
277
379
|
// is at or inside `dir`. Cross-platform with a fail-open contract: on any
|
|
278
380
|
// error or timeout we return [] (the caller continues with its existing
|
|
@@ -315,6 +417,7 @@ function findProcessesWithCwdInside(dir, opts = {}) {
|
|
|
315
417
|
if (process.platform === 'win32') {
|
|
316
418
|
return _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
317
419
|
}
|
|
420
|
+
|
|
318
421
|
if (process.platform === 'linux') {
|
|
319
422
|
return _findLinuxProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
320
423
|
}
|
|
@@ -322,6 +425,37 @@ function findProcessesWithCwdInside(dir, opts = {}) {
|
|
|
322
425
|
} catch { return []; }
|
|
323
426
|
}
|
|
324
427
|
|
|
428
|
+
async function findProcessesWithCwdInsideAsync(dir, opts = {}) {
|
|
429
|
+
const timeoutMs = Number(opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 5000;
|
|
430
|
+
const script = [
|
|
431
|
+
'const utils = require(process.argv[1]);',
|
|
432
|
+
'(async () => {',
|
|
433
|
+
'const timeoutMs = Number(process.argv[3]);',
|
|
434
|
+
"const rows = process.platform === 'win32'",
|
|
435
|
+
'? utils.findProcessCwdHolders(process.argv[2], { timeoutMs })',
|
|
436
|
+
': utils.findProcessesWithCwdInside(process.argv[2], { timeoutMs });',
|
|
437
|
+
'const processes = await utils.listAllProcessesAsync();',
|
|
438
|
+
'const byPid = new Map(processes.map((entry) => [Number(entry.pid), entry]));',
|
|
439
|
+
'const enriched = rows.map((entry) => ({',
|
|
440
|
+
'...entry, startedAt: Number(byPid.get(Number(entry.pid))?.startedAt) || 0,',
|
|
441
|
+
'})).filter((entry) => entry.startedAt > 0);',
|
|
442
|
+
'process.stdout.write(JSON.stringify(enriched));',
|
|
443
|
+
'})().catch((err) => { console.error(err.message); process.exitCode = 1; });',
|
|
444
|
+
].join('');
|
|
445
|
+
const out = await _execFileText(
|
|
446
|
+
process.execPath,
|
|
447
|
+
['-e', script, __filename, String(dir || ''), String(timeoutMs)],
|
|
448
|
+
{
|
|
449
|
+
encoding: 'utf8',
|
|
450
|
+
timeout: timeoutMs + 2000,
|
|
451
|
+
windowsHide: true,
|
|
452
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
const parsed = JSON.parse(out || '[]');
|
|
456
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
457
|
+
}
|
|
458
|
+
|
|
325
459
|
function _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now) {
|
|
326
460
|
// Win32_Process exposes CommandLine but not the cwd. Match by basename of
|
|
327
461
|
// the worktree dir (the dispatch id like W-mq1k8z6o003acd89) appearing in
|
|
@@ -695,7 +829,9 @@ module.exports = {
|
|
|
695
829
|
_winListProcesses,
|
|
696
830
|
_unixListProcesses,
|
|
697
831
|
listAllProcesses,
|
|
832
|
+
listAllProcessesAsync,
|
|
698
833
|
findProcessesWithCwdInside,
|
|
834
|
+
findProcessesWithCwdInsideAsync,
|
|
699
835
|
_findWindowsProcessesWithCwdInside,
|
|
700
836
|
_findLinuxProcessesWithCwdInside,
|
|
701
837
|
_findMacProcessesWithCwdInside,
|
|
@@ -6,23 +6,9 @@
|
|
|
6
6
|
* everything Claude-CLI-specific: binary resolution, arg construction, prompt
|
|
7
7
|
* preparation, output parsing, and error normalization.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - resolveBinary() → { bin, native, leadingArgs }
|
|
13
|
-
* - capsFile: absolute path of the binary-resolution cache for this runtime
|
|
14
|
-
* - listModels() → Promise<{id,name,provider}[] | null>
|
|
15
|
-
* - modelsCache: absolute path of the model-list cache for this runtime
|
|
16
|
-
* - spawnScript: absolute path of the spawn wrapper (or null if direct-only)
|
|
17
|
-
* - buildArgs(opts) → string[] — CLI args excluding the binary
|
|
18
|
-
* - buildPrompt(promptText, sysPromptText) → string — final prompt delivered
|
|
19
|
-
* - getUserAssetDirs(opts) → string[] — runtime-native global asset roots
|
|
20
|
-
* - getSkillRoots(opts) → {scope,dir,projectName?}[] — skill discovery roots
|
|
21
|
-
* - getSkillWriteTargets(opts) → {personal,project} — extraction targets
|
|
22
|
-
* - resolveModel(input) → string|undefined — shorthand expansion / passthrough
|
|
23
|
-
* - parseOutput(raw) → { text, usage, sessionId, model }
|
|
24
|
-
* - parseStreamChunk(line) → parsed event object or null
|
|
25
|
-
* - parseError(rawOutput) → { message, code, retriable }
|
|
9
|
+
* The versioned adapter contract and compatibility defaults live in
|
|
10
|
+
* engine/runtimes/contract.js. Keep harness-specific argv, prompt, parser,
|
|
11
|
+
* workspace, and persistent-worker behavior in this module.
|
|
26
12
|
*/
|
|
27
13
|
|
|
28
14
|
const fs = require('fs');
|
|
@@ -36,6 +22,8 @@ const MINIONS_DIR = path.resolve(ENGINE_DIR, '..');
|
|
|
36
22
|
const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
|
|
37
23
|
|
|
38
24
|
const isWin = process.platform === 'win32';
|
|
25
|
+
const RUNTIME_NAME = 'claude';
|
|
26
|
+
const SPAWN_STARTUP_SUBTYPES = new Set(['init', 'hook_started', 'hook_response']);
|
|
39
27
|
|
|
40
28
|
// ── Binary Resolution ────────────────────────────────────────────────────────
|
|
41
29
|
// Mirrors engine/spawn-agent.js:26-91. Cached at engine/claude-caps.json so the
|
|
@@ -253,7 +241,7 @@ function buildArgs(opts = {}) {
|
|
|
253
241
|
}
|
|
254
242
|
|
|
255
243
|
function buildSpawnFlags(opts = {}) {
|
|
256
|
-
const flags = ['--runtime',
|
|
244
|
+
const flags = ['--runtime', RUNTIME_NAME];
|
|
257
245
|
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
258
246
|
if (opts.model) flags.push('--model', String(opts.model));
|
|
259
247
|
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
@@ -268,12 +256,13 @@ function buildSpawnFlags(opts = {}) {
|
|
|
268
256
|
return flags;
|
|
269
257
|
}
|
|
270
258
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
259
|
+
function resolveInvocationOptions({ options = {}, engineConfig = {} } = {}) {
|
|
260
|
+
const resolved = { ...options };
|
|
261
|
+
if (resolved.fallbackModel == null && engineConfig.claudeFallbackModel) {
|
|
262
|
+
resolved.fallbackModel = engineConfig.claudeFallbackModel;
|
|
263
|
+
}
|
|
264
|
+
return resolved;
|
|
265
|
+
}
|
|
277
266
|
|
|
278
267
|
/**
|
|
279
268
|
* Compute the directory Claude CLI uses to persist a project's conversation
|
|
@@ -934,6 +923,81 @@ function getMcpConfigPaths({ homeDir = os.homedir(), project = null } = {}) {
|
|
|
934
923
|
return paths;
|
|
935
924
|
}
|
|
936
925
|
|
|
926
|
+
function prepareWorkspace({
|
|
927
|
+
worktreePath,
|
|
928
|
+
homeDir,
|
|
929
|
+
engineConfig,
|
|
930
|
+
mutateJsonFileLocked,
|
|
931
|
+
existsSync = fs.existsSync,
|
|
932
|
+
readFileSync = fs.readFileSync,
|
|
933
|
+
} = {}) {
|
|
934
|
+
if (engineConfig && engineConfig.claudePreApproveWorkspaceMcps === false) {
|
|
935
|
+
return { wrote: false, reason: 'flag-off', servers: [] };
|
|
936
|
+
}
|
|
937
|
+
if (!worktreePath || typeof worktreePath !== 'string') {
|
|
938
|
+
return { wrote: false, reason: 'no-worktree', servers: [] };
|
|
939
|
+
}
|
|
940
|
+
const workspaceMcpPath = path.join(worktreePath, '.mcp.json');
|
|
941
|
+
if (!existsSync(workspaceMcpPath)) {
|
|
942
|
+
return { wrote: false, reason: 'no-workspace-mcp', servers: [] };
|
|
943
|
+
}
|
|
944
|
+
let workspaceMcp;
|
|
945
|
+
try {
|
|
946
|
+
workspaceMcp = JSON.parse(readFileSync(workspaceMcpPath, 'utf8'));
|
|
947
|
+
} catch {
|
|
948
|
+
return { wrote: false, reason: 'invalid-workspace-mcp', servers: [] };
|
|
949
|
+
}
|
|
950
|
+
const serversObj = workspaceMcp && typeof workspaceMcp === 'object' ? workspaceMcp.mcpServers : null;
|
|
951
|
+
const servers = serversObj && typeof serversObj === 'object' && !Array.isArray(serversObj)
|
|
952
|
+
? Object.keys(serversObj).filter(name => typeof name === 'string' && name.length > 0)
|
|
953
|
+
: [];
|
|
954
|
+
if (servers.length === 0) {
|
|
955
|
+
return { wrote: false, reason: 'no-servers', servers: [] };
|
|
956
|
+
}
|
|
957
|
+
if (!homeDir || typeof mutateJsonFileLocked !== 'function') {
|
|
958
|
+
return { wrote: false, reason: 'no-home-or-mutator', servers };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
962
|
+
let didWrite = false;
|
|
963
|
+
mutateJsonFileLocked(claudeJsonPath, (data) => {
|
|
964
|
+
const root = data && typeof data === 'object' ? data : {};
|
|
965
|
+
if (!root.projects || typeof root.projects !== 'object') root.projects = {};
|
|
966
|
+
const projectState = root.projects[worktreePath] && typeof root.projects[worktreePath] === 'object'
|
|
967
|
+
? root.projects[worktreePath]
|
|
968
|
+
: {};
|
|
969
|
+
const enabled = Array.isArray(projectState.enabledMcpjsonServers)
|
|
970
|
+
? projectState.enabledMcpjsonServers.slice()
|
|
971
|
+
: [];
|
|
972
|
+
const disabled = Array.isArray(projectState.disabledMcpjsonServers)
|
|
973
|
+
? projectState.disabledMcpjsonServers.slice()
|
|
974
|
+
: [];
|
|
975
|
+
const enabledSet = new Set(enabled.filter(name => typeof name === 'string'));
|
|
976
|
+
let mutated = false;
|
|
977
|
+
for (const name of servers) {
|
|
978
|
+
if (!enabledSet.has(name)) {
|
|
979
|
+
enabledSet.add(name);
|
|
980
|
+
mutated = true;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const nextDisabled = disabled.filter(name => typeof name === 'string' && !servers.includes(name));
|
|
984
|
+
if (nextDisabled.length !== disabled.length) mutated = true;
|
|
985
|
+
if (!mutated) return data;
|
|
986
|
+
projectState.enabledMcpjsonServers = [...enabledSet];
|
|
987
|
+
projectState.disabledMcpjsonServers = nextDisabled;
|
|
988
|
+
root.projects[worktreePath] = projectState;
|
|
989
|
+
didWrite = true;
|
|
990
|
+
return root;
|
|
991
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
992
|
+
|
|
993
|
+
return { wrote: didWrite, reason: 'ok', servers, target: claudeJsonPath };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function isSpawnStartupEvent(event) {
|
|
997
|
+
if (!event || event.type !== 'system') return false;
|
|
998
|
+
return SPAWN_STARTUP_SUBTYPES.has(typeof event.subtype === 'string' ? event.subtype : '');
|
|
999
|
+
}
|
|
1000
|
+
|
|
937
1001
|
// Heuristic: does `model` look like a Claude model identifier? Powers the
|
|
938
1002
|
// preflight "stale model after CLI switch" warning in cli.js. Returning false
|
|
939
1003
|
// means "this looks wrong for Claude" — gpt-5.4 / o3-* / codex etc. Keep this
|
|
@@ -948,7 +1012,8 @@ function modelLooksFamiliar(model) {
|
|
|
948
1012
|
}
|
|
949
1013
|
|
|
950
1014
|
module.exports = {
|
|
951
|
-
|
|
1015
|
+
apiVersion: 1,
|
|
1016
|
+
name: RUNTIME_NAME,
|
|
952
1017
|
capabilities,
|
|
953
1018
|
resolveBinary,
|
|
954
1019
|
capsFile: CAPS_FILE,
|
|
@@ -956,6 +1021,7 @@ module.exports = {
|
|
|
956
1021
|
modelsCache: MODELS_CACHE,
|
|
957
1022
|
spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
|
|
958
1023
|
installHint: INSTALL_HINT,
|
|
1024
|
+
resolveInvocationOptions,
|
|
959
1025
|
buildSpawnFlags,
|
|
960
1026
|
buildArgs,
|
|
961
1027
|
buildPrompt,
|
|
@@ -964,6 +1030,8 @@ module.exports = {
|
|
|
964
1030
|
getSkillWriteTargets,
|
|
965
1031
|
getCommandRoots,
|
|
966
1032
|
getMcpConfigPaths,
|
|
1033
|
+
prepareWorkspace,
|
|
1034
|
+
isSpawnStartupEvent,
|
|
967
1035
|
getResumeSessionId,
|
|
968
1036
|
saveSession,
|
|
969
1037
|
detectPermissionGate,
|
|
@@ -981,4 +1049,5 @@ module.exports = {
|
|
|
981
1049
|
_CLAUDE_SHORTHANDS,
|
|
982
1050
|
_extractInvalidModelName,
|
|
983
1051
|
THINKING_BLOCK_TYPES,
|
|
1052
|
+
SPAWN_STARTUP_SUBTYPES,
|
|
984
1053
|
};
|
package/engine/runtimes/codex.js
CHANGED