@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.
Files changed (51) hide show
  1. package/bin/minions.js +1 -0
  2. package/dashboard/js/refresh.js +2 -1
  3. package/dashboard/js/settings.js +1 -1
  4. package/dashboard.js +37 -19
  5. package/docs/completion-reports.md +20 -1
  6. package/docs/keep-processes.md +7 -2
  7. package/docs/live-checkout-mode.md +7 -7
  8. package/docs/managed-spawn.md +14 -1
  9. package/docs/runtime-adapters.md +61 -26
  10. package/docs/skills.md +2 -0
  11. package/docs/workspace-manifests.md +1 -1
  12. package/docs/worktree-lifecycle.md +36 -0
  13. package/engine/acp-transport.js +273 -58
  14. package/engine/ado-comment.js +3 -1
  15. package/engine/agent-worker-pool.js +278 -54
  16. package/engine/cc-worker-pool.js +12 -3
  17. package/engine/cleanup.js +15 -11
  18. package/engine/cli.js +56 -28
  19. package/engine/comment-format.js +51 -14
  20. package/engine/create-pr-worktree.js +57 -79
  21. package/engine/dispatch.js +7 -2
  22. package/engine/execution-model.js +68 -0
  23. package/engine/gh-comment.js +6 -3
  24. package/engine/github.js +6 -7
  25. package/engine/keep-process-sweep.js +131 -9
  26. package/engine/lifecycle.js +126 -45
  27. package/engine/live-checkout.js +4 -2
  28. package/engine/llm.js +59 -83
  29. package/engine/managed-spawn.js +112 -5
  30. package/engine/memory-store.js +3 -1
  31. package/engine/playbook.js +4 -6
  32. package/engine/pooled-agent-process.js +512 -45
  33. package/engine/pr-action.js +6 -8
  34. package/engine/process-utils.js +154 -18
  35. package/engine/runtimes/claude.js +94 -25
  36. package/engine/runtimes/codex.js +1 -0
  37. package/engine/runtimes/contract.js +239 -0
  38. package/engine/runtimes/copilot.js +97 -9
  39. package/engine/runtimes/index.js +37 -9
  40. package/engine/shared.js +349 -82
  41. package/engine/spawn-agent.js +85 -116
  42. package/engine/spawn-phase-watchdog.js +8 -37
  43. package/engine/timeout.js +14 -10
  44. package/engine/worktree-gc.js +55 -46
  45. package/engine.js +418 -241
  46. package/package.json +1 -1
  47. package/playbooks/fix.md +20 -0
  48. package/playbooks/review.md +2 -0
  49. package/playbooks/shared-rules.md +2 -2
  50. package/prompts/cc-system.md +11 -11
  51. package/skills/check-self-authored-review-comment/SKILL.md +34 -0
@@ -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: the server moves the live-checkout changes into an
188
- // isolated worktree and restores the live checkout clean, so the commit /
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 local changes you just made in the ${proj} project. ` +
193
- `This project uses isolated worktrees, so do NOT commit, branch, or push in its live checkout. ` +
194
- `Steps: (1) call POST /api/pr-action/prepare-create-pr-worktree with {"project":"${proj}"} — the server moves your uncommitted changes into a fresh isolated worktree, restores the live checkout clean, and returns {worktreePath, branch, baseBranch}; ` +
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
- `Never git-commit/-push in the live checkout. Then show me the PR URL.`;
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
@@ -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 { return false; }
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
- out.push({ pid, ppid: Number.isInteger(ppid) ? ppid : 0, name, cmd });
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 "${script}"`,
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
- const parsed = JSON.parse(out);
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.split(/\r?\n/).map(line => {
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
- * Adapter contract (all runtimes must implement):
10
- * - name: string
11
- * - capabilities: { ... } feature flags consumed by engine code
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', 'claude'];
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
- // Stamped into every session.json this adapter writes so the pre-spawn resume
272
- // path can detect "session was produced by a different runtime" — Claude
273
- // rejects Copilot session IDs (and vice versa) with "No conversation found",
274
- // which would otherwise burn a retry slot before the post-failure cleanup at
275
- // engine.js:1195 fires. See W-mot9fwya000d09cb.
276
- const RUNTIME_NAME = 'claude';
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
- name: 'claude',
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
  };
@@ -929,6 +929,7 @@ const INSTALL_HINT = 'install Codex CLI with `npm install -g @openai/codex` or `
929
929
  const PERMISSION_BYPASS_FLAGS = ['--sandbox'];
930
930
 
931
931
  module.exports = {
932
+ apiVersion: 1,
932
933
  name: RUNTIME_NAME,
933
934
  capabilities,
934
935
  resolveBinary,