@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/bin/minions.js +1 -0
- package/dashboard.js +26 -12
- package/docs/completion-reports.md +20 -1
- package/docs/runtime-adapters.md +54 -22
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +14 -0
- package/engine/acp-transport.js +63 -35
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +11 -4
- package/engine/cc-worker-pool.js +4 -2
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- 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/lifecycle.js +125 -44
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +28 -26
- 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 +78 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +95 -21
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine.js +183 -109
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
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
|
-
//
|
|
3432
|
-
// + budget + bare-mode applies to this spawn?". Engine code MUST
|
|
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.
|
|
3480
|
-
*
|
|
3481
|
-
* switch
|
|
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
|
-
*
|
|
3487
|
-
* 3. Runtime-aware default: ON
|
|
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
|
-
*
|
|
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
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
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
|
|
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,
|
package/engine/spawn-agent.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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 (
|
|
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
|
};
|