@yemi33/minions 0.1.2382 → 0.1.2383

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/engine/shared.js CHANGED
@@ -3428,9 +3428,9 @@ function resolvePollFlag(engineCfg, granularKey, legacyMacroKey) {
3428
3428
 
3429
3429
  // ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
3430
3430
  //
3431
- // Six helpers that are the single source of truth for "which CLI runtime + model
3432
- // + budget + bare-mode applies to this spawn?". Engine code MUST go through
3433
- // these — never read `agent.cli`, `engine.defaultCli`, etc. directly. Future
3431
+ // Runtime helpers are the single source of truth for "which CLI runtime + model
3432
+ // + tool ceiling + budget + bare-mode applies to this spawn?". Engine code MUST
3433
+ // go through these — never read `agent.cli`, `engine.defaultCli`, etc. directly. Future
3434
3434
  // agents adding new resolution rules should extend these helpers, not bypass
3435
3435
  // them.
3436
3436
  //
@@ -3476,28 +3476,27 @@ function resolveCcCli(engine) {
3476
3476
  /**
3477
3477
  * Resolve whether the Command Center / doc-chat path should route through the
3478
3478
  * persistent ACP worker pool. Priority:
3479
- * 1. CC runtime guard pool spawns `copilot --acp` (Agent Client Protocol).
3480
- * Claude Code does not implement ACP, and `_invokeCcStream` would silently
3481
- * switch a Claude operator's CC turns to copilot if the pool ran for them.
3482
- * Refuse the pool when CC runtime is not copilot, even if explicit-true is
3483
- * set. Future runtimes that don't implement ACP fall into the same bucket.
3479
+ * 1. Runtime capability guard. Refuse the pool unless the selected adapter
3480
+ * declares `acpWorkerPool`, even if explicit-true is set, so pooling can
3481
+ * never switch the configured harness.
3484
3482
  * (W-mphlriic00095f69)
3485
3483
  * 2. `engine.ccUseWorkerPool` explicit true/false — operator override wins
3486
- * *within* the copilot runtime
3487
- * 3. Runtime-aware default: ON when CC runtime resolves to `copilot`
3488
- * (Copilot's cold-spawn is 18-21s on Windows — pool turns subsequent turns
3489
- * from 47-140s back into a few seconds).
3484
+ * within an ACP-capable runtime.
3485
+ * 3. Runtime-aware default: ON for any ACP-capable adapter.
3490
3486
  * 4. ENGINE_DEFAULTS.ccUseWorkerPool — final fallback
3491
3487
  *
3492
3488
  * Strict boolean check on the override so a literal `false` opts out even on
3493
- * Copilot, matching the "treat empty/null/undefined as unset" semantics used
3489
+ * ACP runtimes, matching the "treat empty/null/undefined as unset" semantics used
3494
3490
  * throughout the resolve* helpers for boolean flags.
3495
3491
  */
3496
3492
  function resolveCcUseWorkerPool(engine) {
3497
- // Guard 1 (W-mphlriic00095f69): pool transport is ACP-only. If CC runtime
3498
- // isn't copilot, the pool cannot run regardless of operator opt-in — don't
3499
- // silently switch their runtime to copilot.
3500
- if (resolveCcCli(engine) !== 'copilot') return false;
3493
+ let runtime;
3494
+ try {
3495
+ runtime = require('./runtimes').resolveRuntime(resolveCcCli(engine));
3496
+ } catch {
3497
+ return false;
3498
+ }
3499
+ if (runtime?.capabilities?.acpWorkerPool !== true) return false;
3501
3500
  if (engine && (engine.ccUseWorkerPool === true || engine.ccUseWorkerPool === false)) {
3502
3501
  return engine.ccUseWorkerPool;
3503
3502
  }
@@ -3511,7 +3510,7 @@ function resolveCcUseWorkerPool(engine) {
3511
3510
  * adapter (as returned by `resolveRuntime(resolveAgentCli(agent, engine))`)
3512
3511
  * rather than re-deriving it, since callers on the agent-spawn path already
3513
3512
  * have it in hand. Priority:
3514
- * 1. Capability guard — pool transport is ACP-only (`copilot --acp`). Hard
3513
+ * 1. Capability guard — pool transport is ACP-only. Hard
3515
3514
  * false whenever `runtime.capabilities.acpWorkerPool` isn't `true`,
3516
3515
  * regardless of operator override, so a future non-ACP runtime can never
3517
3516
  * be silently routed through the pool (same rationale as
@@ -3585,6 +3584,17 @@ function resolveCcModel(engine) {
3585
3584
  return undefined;
3586
3585
  }
3587
3586
 
3587
+ /**
3588
+ * Resolve the fleet tool ceiling for an agent spawn.
3589
+ *
3590
+ * `config.claude.allowedTools` predates runtime adapters but has always applied
3591
+ * to every selected runtime. Keep that cross-runtime contract centralized here
3592
+ * until the field is migrated to a runtime-neutral config namespace.
3593
+ */
3594
+ function resolveAgentAllowedTools(config) {
3595
+ return config?.claude?.allowedTools;
3596
+ }
3597
+
3588
3598
  /**
3589
3599
  * Resolve the per-spawn USD budget cap. Priority:
3590
3600
  * 1. `agent.maxBudgetUsd` — per-agent override
@@ -6043,7 +6053,7 @@ function validateProjectPath(pathStr, options = {}) {
6043
6053
 
6044
6054
  function parseSkillFrontmatter(content, filename) {
6045
6055
  let name = filename.replace('.md', '');
6046
- let trigger = '', description = '', project = 'any', author = '', created = '', allowedTools = '';
6056
+ let trigger = '', description = '', project = 'any', author = '', created = '', allowedTools = '', scope = 'minions';
6047
6057
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
6048
6058
  if (fmMatch) {
6049
6059
  const fm = fmMatch[1];
@@ -6055,8 +6065,52 @@ function parseSkillFrontmatter(content, filename) {
6055
6065
  author = m('author');
6056
6066
  created = m('created');
6057
6067
  allowedTools = m('allowed-tools');
6068
+ scope = m('scope') || scope;
6058
6069
  }
6059
- return { name, trigger, description, project, author, created, allowedTools };
6070
+ return { name, trigger, description, project, author, created, allowedTools, scope };
6071
+ }
6072
+
6073
+ function syncBundledPersonalSkills(skillsDir, opts = {}) {
6074
+ const result = { created: [], updated: [], unchanged: [] };
6075
+ if (!skillsDir || !fs.existsSync(skillsDir)) return result;
6076
+
6077
+ const homeDir = opts.homeDir || os.homedir();
6078
+ const { listRuntimes, resolveRuntime } = require('./runtimes');
6079
+ const personalRoots = new Set();
6080
+ for (const runtimeName of listRuntimes()) {
6081
+ const runtime = resolveRuntime(runtimeName);
6082
+ if (typeof runtime.getSkillWriteTargets !== 'function') continue;
6083
+ const personal = runtime.getSkillWriteTargets({ homeDir }).personal;
6084
+ if (typeof personal === 'string' && personal.trim()) {
6085
+ personalRoots.add(path.resolve(personal));
6086
+ }
6087
+ }
6088
+
6089
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true })
6090
+ .filter(entry => entry.isDirectory())
6091
+ .sort((a, b) => a.name.localeCompare(b.name));
6092
+ for (const entry of entries) {
6093
+ const sourcePath = path.join(skillsDir, entry.name, 'SKILL.md');
6094
+ if (!fs.existsSync(sourcePath)) continue;
6095
+ const content = fs.readFileSync(sourcePath, 'utf8');
6096
+ const skill = parseSkillFrontmatter(content, `${entry.name}.md`);
6097
+ if (skill.scope !== 'minions') continue;
6098
+ const skillName = String(skill.name || entry.name).replace(/[^a-z0-9-]/g, '-');
6099
+ if (!skillName) throw new Error(`Bundled skill has an invalid name: ${sourcePath}`);
6100
+
6101
+ for (const personalRoot of personalRoots) {
6102
+ const targetPath = path.join(personalRoot, skillName, 'SKILL.md');
6103
+ const exists = fs.existsSync(targetPath);
6104
+ if (exists && fs.readFileSync(targetPath, 'utf8') === content) {
6105
+ result.unchanged.push(targetPath);
6106
+ continue;
6107
+ }
6108
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
6109
+ fs.copyFileSync(sourcePath, targetPath);
6110
+ result[exists ? 'updated' : 'created'].push(targetPath);
6111
+ }
6112
+ }
6113
+ return result;
6060
6114
  }
6061
6115
 
6062
6116
  // ── PR → PRD Links ────────────────────────────────────────────────────────────
@@ -7740,6 +7794,7 @@ function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
7740
7794
  evidence.lastReviewedAt = pr?.lastReviewedAt || '';
7741
7795
  evidence.reviewedAt = review.reviewedAt || '';
7742
7796
  evidence.reviewNote = review.note || pr?.reviewNote || '';
7797
+ evidence.findingId = prReviewFindingIdentity(pr);
7743
7798
  // #2979 — same rationale as BUILD_FAILURE: review feedback fingerprints
7744
7799
  // were sticky across force-push because reviewStatus / reviewedAt /
7745
7800
  // reviewNote don't change when the author rebases. Adding the head SHA
@@ -7751,6 +7806,23 @@ function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
7751
7806
  return crypto.createHash('sha1').update(JSON.stringify(evidence)).digest('hex').slice(0, 16);
7752
7807
  }
7753
7808
 
7809
+ function prReviewFindingIdentity(pr) {
7810
+ const review = pr?.minionsReview || {};
7811
+ const threads = Array.isArray(review.threads)
7812
+ ? review.threads.map(value => String(value)).filter(Boolean).sort()
7813
+ : [];
7814
+ const evidence = {
7815
+ pr: pr?.id || pr?.url || '',
7816
+ sourceItem: review.sourceItem || '',
7817
+ dispatchId: review.dispatchId || '',
7818
+ reviewer: review.reviewer || '',
7819
+ reviewedAt: review.reviewedAt || pr?.lastReviewedAt || '',
7820
+ threads,
7821
+ note: review.note || pr?.reviewNote || '',
7822
+ };
7823
+ return `review-${crypto.createHash('sha1').update(JSON.stringify(evidence)).digest('hex').slice(0, 16)}`;
7824
+ }
7825
+
7754
7826
  function getPrNoOpFixRecord(pr, cause) {
7755
7827
  if (!pr || !cause || !pr._noOpFixes || typeof pr._noOpFixes !== 'object') return null;
7756
7828
  const record = pr._noOpFixes[cause];
@@ -8510,7 +8582,7 @@ module.exports = {
8510
8582
  ENGINE_DEFAULTS,
8511
8583
  resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
8512
8584
  resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentUseWorkerPool, resolveAgentAcpPoolSize, resolveAgentModel, resolveCcModel,
8513
- resolveAgentMaxBudget, resolveAgentBareMode, resolvePropagateClaudeMdForNonClaudeRuntimes,
8585
+ resolveAgentAllowedTools, resolveAgentMaxBudget, resolveAgentBareMode, resolvePropagateClaudeMdForNonClaudeRuntimes,
8514
8586
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
8515
8587
  runtimeConfigWarnings,
8516
8588
  projectWorkSourceWarnings,
@@ -8648,6 +8720,7 @@ module.exports = {
8648
8720
  PR_FIX_CAUSE,
8649
8721
  getPrFixAutomationCause,
8650
8722
  prFixEvidenceFingerprint,
8723
+ prReviewFindingIdentity,
8651
8724
  prMergeConflictGuardKey,
8652
8725
  resetMergeConflictStateOnRetarget,
8653
8726
  getPrNoOpFixRecord,
@@ -8655,6 +8728,7 @@ module.exports = {
8655
8728
  getPrPausedCauses,
8656
8729
  isBuildFixIneffectivePaused,
8657
8730
  parseSkillFrontmatter,
8731
+ syncBundledPersonalSkills,
8658
8732
  sleepMs,
8659
8733
  clearWorktreeFailureCache,
8660
8734
  removeWorktree,
@@ -32,12 +32,18 @@
32
32
  */
33
33
 
34
34
  const fs = require('fs');
35
- const os = require('os');
36
35
  const path = require('path');
37
36
  const { runFile, cleanChildEnv, killGracefully, killImmediate, killByPidsImmediate, listProcessDescendants, ts, readFileNoFollow } = require('./shared');
38
- const { resolveRuntime } = require('./runtimes');
37
+ const {
38
+ resolveRuntime,
39
+ RUNTIME_OPTIONS_FLAG,
40
+ RUNTIME_IMAGES_FILE_OPTION,
41
+ decodeRuntimeOptions,
42
+ prepareWorkspace: prepareRuntimeWorkspace,
43
+ } = require('./runtimes');
39
44
  const { acquireAdoTokenSync, isLikelyAdoToken } = require('./ado-token');
40
45
  const keepProcessSweep = require('./keep-process-sweep');
46
+ const MAX_RUNTIME_IMAGES_FILE_BYTES = 32 * 1024 * 1024;
41
47
 
42
48
  // ─── Pure helpers (exported for tests) ──────────────────────────────────────
43
49
 
@@ -76,17 +82,7 @@ function parseSpawnArgs(argv) {
76
82
  case '--disable-builtin-mcps': opts.disableBuiltinMcps = true; break;
77
83
  case '--no-custom-instructions': opts.suppressAgentsMd = true; break;
78
84
  case '--enable-reasoning-summaries': opts.reasoningSummaries = true; break;
79
- case '--project-harness-dir': {
80
- const v = peek();
81
- if (typeof v === 'string' && v) projectHarnessDirs.push(v);
82
- break;
83
- }
84
- // P-49e1c8b7 — hermetic harness opt-out. When set, computeAddDirs in
85
- // main() returns [minionsDir] only (user-scope asset dirs from
86
- // runtime.getUserAssetDirs AND any forwarded --project-harness-dir
87
- // entries are dropped). Engine.js gates this flag via
88
- // shared.resolveAgentHermeticHarness(agent, engine).
89
- case '--hermetic-harness': hermetic = true; break;
85
+ case RUNTIME_OPTIONS_FLAG: Object.assign(opts, decodeRuntimeOptions(peek())); break;
90
86
  // LEGACY: dropped — the runtime adapter emits its own permission flag.
91
87
  // Pre-P-2a6d9c4f engine.js still passes `--permission-mode bypassPermissions`;
92
88
  // letting it through would duplicate the permission flag for Claude.
@@ -168,6 +164,41 @@ function injectAdoTokenEnvForRepoHost(env, opts) {
168
164
  return injectAdoTokenEnv(env, opts);
169
165
  }
170
166
 
167
+ function hydrateRuntimeOptionFiles(opts, {
168
+ promptFile,
169
+ readFile = readFileNoFollow,
170
+ lstatSync = fs.lstatSync,
171
+ unlinkSync = fs.unlinkSync,
172
+ } = {}) {
173
+ const imagesFile = opts?.[RUNTIME_IMAGES_FILE_OPTION];
174
+ if (!imagesFile) return opts;
175
+ if (typeof imagesFile !== 'string' || !promptFile) {
176
+ throw new Error('Invalid runtime images sidecar path');
177
+ }
178
+ const resolvedFile = path.resolve(imagesFile);
179
+ const optionsDir = path.resolve(path.dirname(promptFile));
180
+ if (path.dirname(resolvedFile) !== optionsDir) {
181
+ throw new Error('Runtime images sidecar must be beside the prompt file');
182
+ }
183
+ const stat = lstatSync(resolvedFile);
184
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_RUNTIME_IMAGES_FILE_BYTES) {
185
+ throw new Error('Invalid runtime images sidecar file');
186
+ }
187
+ let images;
188
+ try {
189
+ images = JSON.parse(readFile(resolvedFile, 'utf8'));
190
+ } catch (err) {
191
+ throw new Error(`Invalid runtime images sidecar payload: ${err.message}`);
192
+ }
193
+ if (!Array.isArray(images)) {
194
+ throw new Error('Invalid runtime images sidecar payload: expected an array');
195
+ }
196
+ opts.images = images;
197
+ delete opts[RUNTIME_IMAGES_FILE_OPTION];
198
+ try { unlinkSync(resolvedFile); } catch {}
199
+ return opts;
200
+ }
201
+
171
202
  function formatProcessExitSentinel(exitCode, signal) {
172
203
  return `\n[process-exit] code=${exitCode}${signal ? ` signal=${signal}` : ''}\n`;
173
204
  }
@@ -307,105 +338,16 @@ function createParentPipeForwarder(stream, outputPath, prefix = '') {
307
338
  }
308
339
 
309
340
  /**
310
- * P-7d31a06b Pre-approve a worktree's `.mcp.json` servers in `~/.claude.json`
311
- * so Claude Code's first invocation in a fresh worktree cwd doesn't show the
312
- * project-scoped "trust this MCP server?" prompt (which `--dangerously-skip-permissions`
313
- * silently suppresses → the agent runs without the workspace MCPs).
314
- *
315
- * Writes to `projects[worktreePath].enabledMcpjsonServers` (the field Claude
316
- * Code actually reads — confirmed against the live `~/.claude.json` schema:
317
- * `enabledMcpjsonServers` / `disabledMcpjsonServers` are siblings of
318
- * `mcpServers`, indexed by absolute project path). Notes on the PRD wording
319
- * "mcpServers.approved": that's the intent label; `enabledMcpjsonServers` is
320
- * the on-disk reality.
321
- *
322
- * Pure-ish: all I/O (fs reads, mutator) is injected for tests. Best-effort —
323
- * callers MUST swallow errors and never block dispatch on this.
324
- *
325
- * Returns `{ wrote: boolean, reason: string, servers: string[] }`:
326
- * - reason ∈ { 'ok', 'flag-off', 'not-claude', 'no-worktree',
327
- * 'no-workspace-mcp', 'invalid-workspace-mcp', 'no-servers' }
328
- * - servers: keys discovered in the workspace `.mcp.json` (regardless of write)
329
- *
330
- * Args:
331
- * runtimeName — `runtime.name` from `resolveRuntime(...)`. Helper no-ops if !== 'claude'.
332
- * worktreePath — absolute path to the agent's cwd (the worktree). Used as both the file location
333
- * (`<worktreePath>/.mcp.json`) and the `projects[...]` key in `~/.claude.json`.
334
- * homeDir — `os.homedir()`. Injected for test isolation; helper does NOT call os.homedir directly.
335
- * engineConfig — config.engine object; reads `claudePreApproveWorkspaceMcps` (default true via ENGINE_DEFAULTS).
336
- * mutateJsonFileLocked — injected file-locked mutator (engine/shared.js#mutateJsonFileLocked).
337
- * existsSync, readFileSync — injectable fs primitives (default to node:fs).
341
+ * Backward-compatible wrapper for the old helper name. Runtime-owned workspace
342
+ * preparation now lives behind the adapter contract; the spawn wrapper does
343
+ * not know which harness needs which config file mutation.
338
344
  */
339
- function preApproveWorkspaceMcps({
340
- runtimeName,
341
- worktreePath,
342
- homeDir,
343
- engineConfig,
344
- mutateJsonFileLocked,
345
- existsSync = fs.existsSync,
346
- readFileSync = fs.readFileSync,
347
- } = {}) {
348
- if (engineConfig && engineConfig.claudePreApproveWorkspaceMcps === false) {
349
- return { wrote: false, reason: 'flag-off', servers: [] };
350
- }
351
- if (runtimeName !== 'claude') {
352
- return { wrote: false, reason: 'not-claude', servers: [] };
353
- }
354
- if (!worktreePath || typeof worktreePath !== 'string') {
355
- return { wrote: false, reason: 'no-worktree', servers: [] };
345
+ function preApproveWorkspaceMcps({ runtimeName, ...context } = {}) {
346
+ const result = prepareRuntimeWorkspace(resolveRuntime(runtimeName), context);
347
+ if (result?.reason === 'unsupported-runtime') {
348
+ return { ...result, reason: 'not-claude' };
356
349
  }
357
- const workspaceMcpPath = path.join(worktreePath, '.mcp.json');
358
- if (!existsSync(workspaceMcpPath)) {
359
- return { wrote: false, reason: 'no-workspace-mcp', servers: [] };
360
- }
361
- let workspaceMcp;
362
- try {
363
- workspaceMcp = JSON.parse(readFileSync(workspaceMcpPath, 'utf8'));
364
- } catch {
365
- return { wrote: false, reason: 'invalid-workspace-mcp', servers: [] };
366
- }
367
- const serversObj = workspaceMcp && typeof workspaceMcp === 'object' ? workspaceMcp.mcpServers : null;
368
- const servers = serversObj && typeof serversObj === 'object' && !Array.isArray(serversObj)
369
- ? Object.keys(serversObj).filter((k) => typeof k === 'string' && k.length > 0)
370
- : [];
371
- if (servers.length === 0) {
372
- return { wrote: false, reason: 'no-servers', servers: [] };
373
- }
374
- if (!homeDir || typeof mutateJsonFileLocked !== 'function') {
375
- return { wrote: false, reason: 'no-home-or-mutator', servers };
376
- }
377
- const claudeJsonPath = path.join(homeDir, '.claude.json');
378
- let didWrite = false;
379
- mutateJsonFileLocked(claudeJsonPath, (data) => {
380
- const root = data && typeof data === 'object' ? data : {};
381
- if (!root.projects || typeof root.projects !== 'object') root.projects = {};
382
- const projectKey = worktreePath;
383
- const proj = root.projects[projectKey] && typeof root.projects[projectKey] === 'object'
384
- ? root.projects[projectKey]
385
- : {};
386
- // enabledMcpjsonServers is Claude's actual on-disk approval field
387
- const enabled = Array.isArray(proj.enabledMcpjsonServers) ? proj.enabledMcpjsonServers.slice() : [];
388
- const disabled = Array.isArray(proj.disabledMcpjsonServers) ? proj.disabledMcpjsonServers.slice() : [];
389
- const enabledSet = new Set(enabled.filter((s) => typeof s === 'string'));
390
- let mutated = false;
391
- for (const name of servers) {
392
- if (!enabledSet.has(name)) {
393
- enabledSet.add(name);
394
- mutated = true;
395
- }
396
- }
397
- // If a server was previously explicitly disabled, lift the disabled mark when we
398
- // re-approve it — otherwise Claude treats the disabled entry as authoritative.
399
- const nextDisabled = disabled.filter((s) => typeof s === 'string' && !servers.includes(s));
400
- if (nextDisabled.length !== disabled.length) mutated = true;
401
- if (!mutated) return data;
402
- proj.enabledMcpjsonServers = [...enabledSet];
403
- proj.disabledMcpjsonServers = nextDisabled;
404
- root.projects[projectKey] = proj;
405
- didWrite = true;
406
- return root;
407
- }, { defaultValue: {}, skipWriteIfUnchanged: true });
408
- return { wrote: didWrite, reason: 'ok', servers };
350
+ return result;
409
351
  }
410
352
 
411
353
  // ─── Main script execution ──────────────────────────────────────────────────
@@ -421,7 +363,13 @@ function _installHint(name, runtime) {
421
363
  }
422
364
 
423
365
  function main() {
424
- const parsed = parseSpawnArgs(process.argv);
366
+ let parsed;
367
+ try {
368
+ parsed = parseSpawnArgs(process.argv);
369
+ } catch (err) {
370
+ console.error(`FATAL: ${err.message}`);
371
+ process.exit(78);
372
+ }
425
373
  if (!parsed) {
426
374
  console.error('Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [--runtime <name>] [args...]');
427
375
  process.exit(1);
@@ -445,12 +393,18 @@ function main() {
445
393
  // (Windows non-junction reads don't follow symlinks the same way POSIX does).
446
394
  const promptText = readFileNoFollow(promptFile, 'utf8');
447
395
  const sysPromptText = readFileNoFollow(sysPromptFile, 'utf8');
396
+ try {
397
+ hydrateRuntimeOptionFiles(opts, { promptFile });
398
+ } catch (err) {
399
+ console.error(`FATAL: ${err.message}`);
400
+ process.exit(78);
401
+ }
448
402
 
449
403
  // Sys prompt tmp file — only when (a) NOT resuming and (b) the adapter
450
404
  // accepts a system prompt as a separate file. For runtimes that bake the
451
405
  // system prompt into the user prompt (e.g. Copilot), sysPromptText is
452
406
  // already merged inside `runtime.buildPrompt(prompt, sys)`.
453
- const isResume = opts.sessionId != null;
407
+ const isResume = typeof opts.sessionId === 'string' && opts.sessionId.trim() !== '';
454
408
  const sysTmpPath = sysPromptFile + '.tmp';
455
409
  const wantsSystemPromptFile = typeof runtime.usesSystemPromptFile === 'function'
456
410
  ? runtime.usesSystemPromptFile({ isResume, opts })
@@ -714,6 +668,18 @@ function main() {
714
668
  });
715
669
  }
716
670
 
717
- module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, preApproveWorkspaceMcps, createParentPipeForwarder, assertStaleHeadOk };
671
+ module.exports = {
672
+ parseSpawnArgs,
673
+ buildSpawnInvocation,
674
+ normalizeRuntimeExit,
675
+ shouldInjectAdoTokenEnv,
676
+ injectAdoTokenEnv,
677
+ injectAdoTokenEnvForRepoHost,
678
+ writeProcessExitSentinel,
679
+ preApproveWorkspaceMcps,
680
+ createParentPipeForwarder,
681
+ assertStaleHeadOk,
682
+ hydrateRuntimeOptionFiles, // exported for testing
683
+ };
718
684
 
719
685
  if (require.main === module) main();
@@ -36,6 +36,7 @@ const fs = require('fs');
36
36
  const path = require('path');
37
37
  const shared = require('./shared');
38
38
  const queries = require('./queries');
39
+ const { resolveRuntime, isSpawnStartupEvent } = require('./runtimes');
39
40
  const { ENGINE_DEFAULTS, FAILURE_CLASS } = shared;
40
41
  const AGENTS_DIR = queries.AGENTS_DIR;
41
42
  const log = (level, msg) => shared.log ? shared.log(level, msg) : console.log(`[${level}] ${msg}`);
@@ -43,24 +44,6 @@ const log = (level, msg) => shared.log ? shared.log(level, msg) : console.log(`[
43
44
  // Lazy require to break circular dep with dispatch.js (which lazy-requires engine.js).
44
45
  function dispatch() { return require('./dispatch'); }
45
46
 
46
- // ── Per-runtime startup-only event filters ──────────────────────────────────
47
- //
48
- // Anything NOT in these sets is treated as "real task activity" and the
49
- // watchdog stands down. These match the runtimes/*.js KNOWN_EVENT_TYPES
50
- // startup section — keep in sync.
51
-
52
- const COPILOT_STARTUP_TYPES = new Set([
53
- 'session.mcp_server_status_changed',
54
- 'session.mcp_servers_loaded',
55
- 'session.skills_loaded',
56
- 'session.tools_updated',
57
- 'session.info',
58
- ]);
59
-
60
- // Claude stream-json prints {"type":"system","subtype":"init|hook_started|hook_response"}
61
- // during startup before any assistant/user/tool_use/result events fire.
62
- const CLAUDE_STARTUP_SUBTYPES = new Set(['init', 'hook_started', 'hook_response']);
63
-
64
47
  // Header line that engine.js writes at spawn: "[<iso>] pid: <n>"
65
48
  const HEADER_LINE_RE = /^\[[^\]]+\]\s+(?:pid:|spawn-failed)/;
66
49
 
@@ -79,6 +62,12 @@ const HEADER_LINE_RE = /^\[[^\]]+\]\s+(?:pid:|spawn-failed)/;
79
62
  */
80
63
  function hasRealTaskActivity(tailText, runtimeName) {
81
64
  if (!tailText) return false;
65
+ let runtime;
66
+ try {
67
+ runtime = resolveRuntime(runtimeName);
68
+ } catch {
69
+ return true;
70
+ }
82
71
  const lines = String(tailText).split(/\r?\n/);
83
72
  for (const rawLine of lines) {
84
73
  const line = rawLine.trim();
@@ -100,23 +89,7 @@ function hasRealTaskActivity(tailText, runtimeName) {
100
89
  // Object with no `type` — unusual; treat as activity to stay safe.
101
90
  return true;
102
91
  }
103
- if (runtimeName === 'copilot') {
104
- if (COPILOT_STARTUP_TYPES.has(type)) continue;
105
- // Anything else from Copilot is real (assistant.*, tool.*, result,
106
- // session.task_complete, user.message, function).
107
- return true;
108
- }
109
- if (runtimeName === 'claude' || !runtimeName) {
110
- // Claude stream-json: only {type:'system', subtype:'init|hook_*'} is startup.
111
- if (type === 'system') {
112
- const sub = typeof evt.subtype === 'string' ? evt.subtype : '';
113
- if (CLAUDE_STARTUP_SUBTYPES.has(sub)) continue;
114
- // Other `system` subtypes (e.g. errors, banners) — treat as activity.
115
- return true;
116
- }
117
- return true;
118
- }
119
- // Unknown runtime — be conservative.
92
+ if (isSpawnStartupEvent(runtime, evt)) continue;
120
93
  return true;
121
94
  }
122
95
  return false;
@@ -266,6 +239,4 @@ function _defaultTailReader(filePath) {
266
239
  module.exports = {
267
240
  checkSpawnPhaseStalls,
268
241
  hasRealTaskActivity, // exported for testing
269
- COPILOT_STARTUP_TYPES, // exported for testing
270
- CLAUDE_STARTUP_SUBTYPES, // exported for testing
271
242
  };