@yemi33/minions 0.1.2179 → 0.1.2180
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/README.md +7 -5
- package/bin/minions.js +15 -6
- package/dashboard/js/memory-panel.js +62 -0
- package/dashboard/js/refresh.js +18 -0
- package/dashboard/js/render-other.js +142 -1
- package/dashboard/js/render-work-items.js +18 -1
- package/dashboard/js/settings.js +23 -0
- package/dashboard/pages/engine-memory-panel.html +7 -0
- package/dashboard/pages/tools.html +8 -0
- package/dashboard.js +466 -3
- package/docs/diagnostics-memory.md +446 -0
- package/docs/harness-propagation.md +273 -0
- package/docs/human-vs-automated.md +1 -1
- package/docs/runtime-adapters.md +5 -0
- package/engine/cli.js +24 -5
- package/engine/preflight.js +265 -0
- package/engine/queries.js +192 -15
- package/engine/runtimes/claude.js +36 -0
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/copilot.js +27 -36
- package/engine/shared.js +277 -13
- package/engine/spawn-agent.js +178 -12
- package/engine.js +232 -3
- package/package.json +1 -1
package/engine/shared.js
CHANGED
|
@@ -2699,6 +2699,36 @@ const ENGINE_DEFAULTS = {
|
|
|
2699
2699
|
claudeFallbackModel: undefined,// Claude --fallback-model — Claude CLI honors this on rate-limit (429) only; engine retry on FAILURE_CLASS.MODEL_UNAVAILABLE keeps the flag passed so the CLI can swap internally
|
|
2700
2700
|
copilotFallbackModel: undefined,// W-mpg6isvy000xca4d: Copilot has no --fallback-model flag; engine retry on FAILURE_CLASS.MODEL_UNAVAILABLE OVERRIDES --model directly with this value (separate knob from claudeFallbackModel because model namespaces differ across runtimes)
|
|
2701
2701
|
copilotDisableBuiltinMcps: true, // Copilot --disable-builtin-mcps: keep github-mcp-server out so it can't bypass pull-requests.json tracking
|
|
2702
|
+
// P-7d31a06b — Claude pre-approves workspace .mcp.json servers in ~/.claude.json on every spawn into a fresh
|
|
2703
|
+
// worktree cwd. Without this, the first Claude invocation in a brand-new worktree shows a project-scoped MCP
|
|
2704
|
+
// "trust this server?" prompt that blocks until a human accepts — invisible behind --dangerously-skip-permissions
|
|
2705
|
+
// so the agent silently runs without the workspace MCPs. Pre-warming
|
|
2706
|
+
// projects.<worktreePath>.enabledMcpjsonServers (the field Claude Code actually reads — see live ~/.claude.json
|
|
2707
|
+
// shape) means the workspace MCPs are connected on first call. Best-effort: failures NEVER block dispatch.
|
|
2708
|
+
// Engine-scoped per the CLAUDE.md rule against `runtime.name === ...` branches at call sites — the runtime check
|
|
2709
|
+
// lives inside the helper (engine/spawn-agent.js#preApproveWorkspaceMcps) which no-ops for non-Claude runtimes.
|
|
2710
|
+
claudePreApproveWorkspaceMcps: true,
|
|
2711
|
+
// P-08b62d49 — propagate project-local-on-main harness dirs (uncommitted
|
|
2712
|
+
// <repo>/.claude/skills, <repo>/.copilot/commands, etc.) into the worktree
|
|
2713
|
+
// dispatch via additional `--add-dir` entries. Defaults TRUE so the
|
|
2714
|
+
// worktree-uncommitted footgun described in docs/harness-propagation.md is
|
|
2715
|
+
// closed by default. Set to false to fall back to the legacy
|
|
2716
|
+
// "only committed assets are visible" behavior.
|
|
2717
|
+
harnessPropagateProjectLocal: true,
|
|
2718
|
+
// P-49e1c8b7 — hermetic harness opt-out. When TRUE the spawned agent runs
|
|
2719
|
+
// with a known-empty harness surface around the worktree:
|
|
2720
|
+
// - `--add-dir` is exactly `[minionsDir]` (user-scope skill/command/MCP
|
|
2721
|
+
// roots from `runtime.getUserAssetDirs(...)` are dropped);
|
|
2722
|
+
// - project-local-on-main harness propagation (`harnessPropagateProjectLocal`)
|
|
2723
|
+
// is skipped — no `--project-harness-dir` flags emitted;
|
|
2724
|
+
// - Claude workspace `.mcp.json` pre-approval (`claudePreApproveWorkspaceMcps`)
|
|
2725
|
+
// is skipped.
|
|
2726
|
+
// Independent of `copilotDisableBuiltinMcps` and `copilotSuppressAgentsMd` —
|
|
2727
|
+
// those keep their existing semantics so operators can mix-and-match. Use
|
|
2728
|
+
// per-agent override `agent.hermeticHarness` for targeted runs. Resolved
|
|
2729
|
+
// through `shared.resolveAgentHermeticHarness(agent, engine)` (mirrors
|
|
2730
|
+
// `resolveAgentBareMode`).
|
|
2731
|
+
hermeticHarness: false,
|
|
2702
2732
|
copilotSuppressAgentsMd: true, // Copilot --no-custom-instructions: stop AGENTS.md auto-load from fighting Minions playbook prompts
|
|
2703
2733
|
copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
|
|
2704
2734
|
copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
|
|
@@ -3151,6 +3181,32 @@ function resolveCopilotAgentDisabledMcpServers(agent, engine) {
|
|
|
3151
3181
|
return [];
|
|
3152
3182
|
}
|
|
3153
3183
|
|
|
3184
|
+
/**
|
|
3185
|
+
* P-49e1c8b7 — Resolve whether this agent should run with a hermetic harness.
|
|
3186
|
+
* Priority (mirrors `resolveAgentBareMode`):
|
|
3187
|
+
* 1. `agent.hermeticHarness` — per-agent override (boolean)
|
|
3188
|
+
* 2. `engine.hermeticHarness` — fleet default
|
|
3189
|
+
* 3. `false` — hardcoded fallback
|
|
3190
|
+
*
|
|
3191
|
+
* Strict undefined/null check (not falsy) so a per-agent `false` correctly
|
|
3192
|
+
* overrides an engine `true`. Truthy/falsy non-bool values are coerced via
|
|
3193
|
+
* `!!` for config tolerance.
|
|
3194
|
+
*
|
|
3195
|
+
* When TRUE, engine.js (a) skips Claude workspace .mcp.json pre-approval,
|
|
3196
|
+
* (b) skips project-local-on-main `--project-harness-dir` propagation, and
|
|
3197
|
+
* (c) forwards `--hermetic-harness` to spawn-agent.js so `computeAddDirs`
|
|
3198
|
+
* returns `[minionsDir]` only (user-scope skill/command/MCP roots stripped).
|
|
3199
|
+
* Independent of `copilotDisableBuiltinMcps` and `copilotSuppressAgentsMd`.
|
|
3200
|
+
*/
|
|
3201
|
+
function resolveAgentHermeticHarness(agent, engine) {
|
|
3202
|
+
const a = agent ? agent.hermeticHarness : undefined;
|
|
3203
|
+
if (a !== undefined && a !== null) return !!a;
|
|
3204
|
+
const e = engine ? engine.hermeticHarness : undefined;
|
|
3205
|
+
if (e !== undefined && e !== null) return !!e;
|
|
3206
|
+
return false;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
|
|
3154
3210
|
// ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
|
|
3155
3211
|
//
|
|
3156
3212
|
// Pre-P-3b8e5f1d, `engine.ccModel` was the single fleet-wide model knob (it
|
|
@@ -4021,6 +4077,7 @@ const FAILURE_CLASS = {
|
|
|
4021
4077
|
MANAGED_SPAWN_HEALTHCHECK_FAILED: 'managed-spawn-healthcheck-failed', // P-7a3b1c92: at least one managed-spawn spec was spawned but failed its healthcheck within timeout_s. Engine killed the failing PIDs; siblings stay alive. Dispatch ERROR with the failing spec name + log tail surfaced in the inbox alert.
|
|
4022
4078
|
INJECTION_FLAGGED: 'injection-flagged', // F5 (W-mpeklod3000we69c): the agent set `securityFlags.injectionAttempt:true` in its completion report after spotting a prompt-injection attempt inside an <UNTRUSTED-INPUT> fence. Engine writes a security inbox note + stamps `_securityFlag` on the WI and treats the dispatch as non-retryable so a human can review the source before the agent re-runs.
|
|
4023
4079
|
LIVE_CHECKOUT_DIRTY: 'live-checkout-dirty', // P-a3f9b204 (live-checkout dispatch mode): spawnAgent ran prepareLiveCheckout against project.localPath and `git status --porcelain` reported uncommitted changes. Engine refused to spawn (it never runs `git reset`/`git clean` against the operator tree). Inbox alert lists dirty files; WI is stamped `_pendingReason: 'live_checkout_dirty'`. Non-retryable — operator must commit/stash/discard before re-dispatch.
|
|
4080
|
+
INVALID_WORKDIR: 'invalid-workdir', // P-714ef144: dispatch carried a meta.workdir override that failed validation — non-string, absolute path, drive-letter prefix, null byte, ".." segment, or post-resolve containment escape against project.localPath / worktree root. Engine refuses to spawn (the subpath would either be unreachable on disk or point outside the operator's allowed surface). Non-retryable — operator must fix the WI's meta.workdir before re-dispatch. Inbox alert lists the offending value + the resolved-vs-base mismatch.
|
|
4024
4081
|
MODEL_UNAVAILABLE: 'model-unavailable', // W-mpg6isvy000xca4d: requested model returned overloaded_error / 503 / service_unavailable. Retriable — engine swaps in the runtime-appropriate fallback model on next spawn (Claude leans on --fallback-model already plumbed; Copilot overrides --model with engine.copilotFallbackModel).
|
|
4025
4082
|
WORKSPACE_MANIFEST_REPO: 'workspace-manifest-repo-forbidden', // W-mq07avbk000m5543: dispatch routed an agent to a project/repo not present in its workspace_manifest.allowed_repos. Structural — never retryable until the manifest is widened or a different agent is chosen.
|
|
4026
4083
|
WORKSPACE_MANIFEST_TOOL: 'workspace-manifest-tool-forbidden', // W-mq07avbk000m5543: out-of-scope tool call (manifest enforcement at the runtime gate). Non-retryable as-is.
|
|
@@ -5278,21 +5335,47 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
5278
5335
|
* `LIVE_CHECKOUT_NO_LOCALPATH` if `localPath` is missing/falsy — live mode
|
|
5279
5336
|
* has no fallback because there is no neutral location to dispatch into.
|
|
5280
5337
|
*
|
|
5338
|
+
* **Workdir override (P-714ef144).** When `options.workdir` is a non-empty
|
|
5339
|
+
* relative POSIX subpath (already validated by `validateWorkItemWorkdir`),
|
|
5340
|
+
* the resolver joins it onto the spawn cwd:
|
|
5341
|
+
*
|
|
5342
|
+
* - live mode: cwd = path.join(localPath, workdir)
|
|
5343
|
+
* - read-only mode: cwd = path.join(<base>, workdir)
|
|
5344
|
+
* - mutating mode: cwd stays null (worktree doesn't exist yet); the
|
|
5345
|
+
* normalized workdir is echoed back as result.workdir so spawnAgent
|
|
5346
|
+
* can apply `shared.applyWorkdir(worktreePath, workdir)` after
|
|
5347
|
+
* `git worktree add` succeeds.
|
|
5348
|
+
*
|
|
5349
|
+
* Live and read-only branches run the post-resolve containment guard via
|
|
5350
|
+
* `applyWorkdir` and throw an Error with `code: 'INVALID_WORKDIR'` if the
|
|
5351
|
+
* resolved cwd escapes its base (defense-in-depth — validator already
|
|
5352
|
+
* rejects ".." / absolute paths, but symlink-style attacks can only be
|
|
5353
|
+
* caught post-resolve). The mutating containment check happens later in
|
|
5354
|
+
* spawnAgent, against the actual worktree path.
|
|
5355
|
+
*
|
|
5281
5356
|
* @param {{ localPath?: string|null, worktreeMode?: string|null }|null|undefined} project
|
|
5282
5357
|
* @param {string} type — work type (e.g. 'fix', 'explore', 'meeting')
|
|
5283
5358
|
* @param {string} minionsDir — MINIONS_DIR fallback anchor (ignored in live mode)
|
|
5284
|
-
* @
|
|
5285
|
-
*
|
|
5286
|
-
* - For
|
|
5287
|
-
* - For isolated
|
|
5288
|
-
*
|
|
5359
|
+
* @param {{ workdir?: string|null }} [options] — optional per-WI overrides
|
|
5360
|
+
* @returns {{ cwd: string|null, worktreeRootDir: string|null, liveMode?: boolean, workdir?: string|null }}
|
|
5361
|
+
* - For live mode (any type): { cwd: <abs localPath[/workdir]>, worktreeRootDir: null, liveMode: true }
|
|
5362
|
+
* - For isolated read-only types: { cwd: <project dir[/workdir] or MINIONS_DIR>, worktreeRootDir: null }
|
|
5363
|
+
* - For isolated code-mutating types: { cwd: null, worktreeRootDir: <project root>, workdir: <subpath|null> }
|
|
5364
|
+
* (caller defaults cwd to worktreeRootDir before worktree creation,
|
|
5365
|
+
* then runs shared.applyWorkdir(worktreePath, result.workdir) to land
|
|
5366
|
+
* the agent inside the subpackage cwd)
|
|
5289
5367
|
* The optional `liveMode` discriminator lets callers branch on one boolean
|
|
5290
5368
|
* instead of re-reading `project.worktreeMode`.
|
|
5291
5369
|
* @throws {Error} LIVE_CHECKOUT_NO_LOCALPATH (live mode, missing localPath),
|
|
5370
|
+
* INVALID_WORKDIR (live or read-only, workdir escapes base),
|
|
5292
5371
|
* WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT (isolated code-mutating),
|
|
5293
5372
|
* or WORKTREE_ROOTDIR_MISSING_BASE if neither anchor present.
|
|
5294
5373
|
*/
|
|
5295
|
-
function resolveSpawnPaths(project, type, minionsDir) {
|
|
5374
|
+
function resolveSpawnPaths(project, type, minionsDir, options) {
|
|
5375
|
+
const workdir = (options && typeof options === 'object' && typeof options.workdir === 'string' && options.workdir)
|
|
5376
|
+
? options.workdir
|
|
5377
|
+
: null;
|
|
5378
|
+
|
|
5296
5379
|
// ── Live-checkout mode short-circuit (P-a3f9b202) ──────────────────────
|
|
5297
5380
|
// Runs BEFORE the read-only / code-mutating split so live mode is the
|
|
5298
5381
|
// single decision point regardless of task type — read-only tasks in
|
|
@@ -5306,8 +5389,10 @@ function resolveSpawnPaths(project, type, minionsDir) {
|
|
|
5306
5389
|
err.code = 'LIVE_CHECKOUT_NO_LOCALPATH';
|
|
5307
5390
|
throw err;
|
|
5308
5391
|
}
|
|
5392
|
+
const baseLive = path.resolve(String(project.localPath));
|
|
5393
|
+
const liveCwd = _applyWorkdirOrThrow(baseLive, workdir);
|
|
5309
5394
|
return {
|
|
5310
|
-
cwd:
|
|
5395
|
+
cwd: liveCwd,
|
|
5311
5396
|
worktreeRootDir: null,
|
|
5312
5397
|
liveMode: true,
|
|
5313
5398
|
};
|
|
@@ -5315,14 +5400,189 @@ function resolveSpawnPaths(project, type, minionsDir) {
|
|
|
5315
5400
|
|
|
5316
5401
|
const isReadOnly = READ_ONLY_ROOT_TASK_TYPES.has(type);
|
|
5317
5402
|
if (isReadOnly) {
|
|
5318
|
-
|
|
5319
|
-
if (
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5403
|
+
let base = null;
|
|
5404
|
+
if (project?.localPath) base = path.resolve(String(project.localPath));
|
|
5405
|
+
else if (minionsDir) base = path.resolve(String(minionsDir));
|
|
5406
|
+
else {
|
|
5407
|
+
const err = new Error('Cannot resolve cwd for read-only spawn: no project.localPath and no MINIONS_DIR provided.');
|
|
5408
|
+
err.code = 'WORKTREE_ROOTDIR_MISSING_BASE';
|
|
5409
|
+
throw err;
|
|
5410
|
+
}
|
|
5411
|
+
const roCwd = _applyWorkdirOrThrow(base, workdir);
|
|
5412
|
+
return { cwd: roCwd, worktreeRootDir: null };
|
|
5323
5413
|
}
|
|
5324
5414
|
const worktreeRootDir = resolveProjectRootDir(project?.localPath, minionsDir);
|
|
5325
|
-
|
|
5415
|
+
// Mutating types: workdir is echoed back UNAPPLIED. spawnAgent calls
|
|
5416
|
+
// `shared.applyWorkdir(worktreePath, workdir)` after `git worktree add`
|
|
5417
|
+
// succeeds, which is the only time the actual base path exists on disk.
|
|
5418
|
+
return { cwd: null, worktreeRootDir, workdir };
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5421
|
+
// ── meta.workdir helpers (P-714ef144) ────────────────────────────────────
|
|
5422
|
+
//
|
|
5423
|
+
// Per-WI `meta.workdir` is a relative POSIX subpath under the spawn base
|
|
5424
|
+
// (project.localPath for read-only / live; worktree root for mutating).
|
|
5425
|
+
// When set, the engine lands the agent at <base>/<workdir> so monorepo
|
|
5426
|
+
// subpackage dispatches can use the runtime CLI's native cwd-rooted skill
|
|
5427
|
+
// discovery (`<workdir>/.claude/skills/<name>/SKILL.md`) without engine
|
|
5428
|
+
// awareness of which package is which. Cross-package skill aggregation is
|
|
5429
|
+
// explicitly out of scope (per PRD open_question) — when workdir is set,
|
|
5430
|
+
// the harness propagation block in engine.js also CLIPS its candidate
|
|
5431
|
+
// `--project-harness-dir` set to dirs that live under the workdir mirror
|
|
5432
|
+
// in `project.localPath`, so a `packages/foo` dispatch never sees
|
|
5433
|
+
// `packages/bar/.claude/skills/...`.
|
|
5434
|
+
//
|
|
5435
|
+
// Three pure helpers, all unit-tested in test/unit/work-item-workdir.test.js:
|
|
5436
|
+
//
|
|
5437
|
+
// 1. validateWorkItemWorkdir(value) — input validation for the
|
|
5438
|
+
// dashboard.js POST /api/work-items handler. Returns
|
|
5439
|
+
// `{ valid: true, value: null }` for empty/unset (back-compat
|
|
5440
|
+
// default), `{ valid: true, value: <normalized POSIX subpath> }`
|
|
5441
|
+
// for a clean subpath, or `{ valid: false, error: <human reason> }`
|
|
5442
|
+
// for any rejection. The normalized form has backslashes folded to
|
|
5443
|
+
// `/`, trims surrounding whitespace, drops leading `./` and trailing
|
|
5444
|
+
// `/`, and rejects `..` segments, absolute paths (POSIX or Windows
|
|
5445
|
+
// drive-letter), null bytes, and non-string types.
|
|
5446
|
+
//
|
|
5447
|
+
// 2. applyWorkdir(base, workdir) — pure join + containment guard.
|
|
5448
|
+
// Returns `{ cwd: <resolved absolute path>, error?: <reason> }`. The
|
|
5449
|
+
// `error` field is set when the resolved cwd would escape `base`
|
|
5450
|
+
// (defense-in-depth against symlink-style attacks; the validator
|
|
5451
|
+
// already rejects bare `..` / absolute strings). `base` MUST be
|
|
5452
|
+
// absolute. Empty/null workdir returns the base unchanged.
|
|
5453
|
+
//
|
|
5454
|
+
// 3. filterProjectHarnessDirsForWorkdir(dirs, opts) — clips the
|
|
5455
|
+
// candidate `--project-harness-dir` set to entries under the resolved
|
|
5456
|
+
// cwd anchor in `project.localPath`, excluding entries under the
|
|
5457
|
+
// worktree mirror (already discoverable via the agent's cwd). When
|
|
5458
|
+
// `workdir` is null/empty the behavior matches P-08b62d49 unchanged
|
|
5459
|
+
// (filter to project.localPath, exclude worktree).
|
|
5460
|
+
//
|
|
5461
|
+
// The private `_applyWorkdirOrThrow(base, workdir)` is the throw-on-error
|
|
5462
|
+
// adapter used inside resolveSpawnPaths so live/read-only callers don't
|
|
5463
|
+
// need to forward the `error` field.
|
|
5464
|
+
|
|
5465
|
+
function _applyWorkdirOrThrow(base, workdir) {
|
|
5466
|
+
if (!workdir) return base;
|
|
5467
|
+
const r = applyWorkdir(base, workdir);
|
|
5468
|
+
if (r.error) {
|
|
5469
|
+
const err = new Error(r.error);
|
|
5470
|
+
err.code = 'INVALID_WORKDIR';
|
|
5471
|
+
err.workdir = workdir;
|
|
5472
|
+
err.base = base;
|
|
5473
|
+
throw err;
|
|
5474
|
+
}
|
|
5475
|
+
return r.cwd;
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
function validateWorkItemWorkdir(value) {
|
|
5479
|
+
// Unset / empty → null (back-compat default, today's project-root behavior)
|
|
5480
|
+
if (value === undefined || value === null) return { valid: true, value: null };
|
|
5481
|
+
if (typeof value !== 'string') {
|
|
5482
|
+
return { valid: false, error: 'meta.workdir must be a string (got ' + typeof value + ')' };
|
|
5483
|
+
}
|
|
5484
|
+
const trimmed = value.trim();
|
|
5485
|
+
if (!trimmed) return { valid: true, value: null };
|
|
5486
|
+
|
|
5487
|
+
// Null bytes — common path-injection signal; refuse defensively.
|
|
5488
|
+
if (trimmed.indexOf('\u0000') !== -1) {
|
|
5489
|
+
return { valid: false, error: 'meta.workdir must not contain null bytes' };
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
// Windows drive-letter prefix (e.g. "C:\\Windows", "D:foo") — even when
|
|
5493
|
+
// the path looks "inside" the project, it's an absolute reference.
|
|
5494
|
+
if (/^[A-Za-z]:[\\/]?/.test(trimmed)) {
|
|
5495
|
+
return { valid: false, error: 'meta.workdir must be a relative POSIX subpath, not a Windows drive path (got "' + value + '")' };
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
// POSIX absolute / UNC / backslash-absolute.
|
|
5499
|
+
if (path.isAbsolute(trimmed) || trimmed.startsWith('/') || trimmed.startsWith('\\')) {
|
|
5500
|
+
return { valid: false, error: 'meta.workdir must be a relative path, not absolute (got "' + value + '")' };
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
// ".." segments — pre-resolve guard. applyWorkdir runs the post-resolve
|
|
5504
|
+
// containment check as defense-in-depth, but rejecting `..` here gives
|
|
5505
|
+
// operators a clearer error message at the dashboard validation site.
|
|
5506
|
+
const segments = trimmed.split(/[\\/]+/);
|
|
5507
|
+
if (segments.some(s => s === '..')) {
|
|
5508
|
+
return { valid: false, error: 'meta.workdir may not contain ".." segments (escape attempt blocked)' };
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
// Normalize: backslashes → forward slashes, drop leading "./", strip
|
|
5512
|
+
// trailing slashes. Preserve "." segments (filtered out by path.join later).
|
|
5513
|
+
let normalized = trimmed.replace(/\\/g, '/');
|
|
5514
|
+
while (normalized.startsWith('./')) normalized = normalized.slice(2);
|
|
5515
|
+
normalized = normalized.replace(/\/+$/, '');
|
|
5516
|
+
if (!normalized) return { valid: true, value: null };
|
|
5517
|
+
|
|
5518
|
+
return { valid: true, value: normalized };
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
function applyWorkdir(base, workdir) {
|
|
5522
|
+
if (workdir === null || workdir === undefined || workdir === '') {
|
|
5523
|
+
return { cwd: base };
|
|
5524
|
+
}
|
|
5525
|
+
if (typeof workdir !== 'string') {
|
|
5526
|
+
return { cwd: base, error: 'workdir must be a string (got ' + typeof workdir + ')' };
|
|
5527
|
+
}
|
|
5528
|
+
// Refuse absolute workdir even if it'd technically resolve inside — only
|
|
5529
|
+
// relative subpaths are allowed. This catches the "operator pasted an
|
|
5530
|
+
// absolute path" authoring error before path.join silently swaps the base.
|
|
5531
|
+
if (path.isAbsolute(workdir) || /^[A-Za-z]:[\\/]?/.test(workdir) || workdir.startsWith('/') || workdir.startsWith('\\')) {
|
|
5532
|
+
return { cwd: base, error: 'workdir must be relative, not absolute (got "' + workdir + '")' };
|
|
5533
|
+
}
|
|
5534
|
+
const baseAbs = path.resolve(String(base));
|
|
5535
|
+
const joined = path.resolve(baseAbs, workdir);
|
|
5536
|
+
// Containment guard: resolved cwd must equal base or live strictly inside.
|
|
5537
|
+
if (joined !== baseAbs && !joined.startsWith(baseAbs + path.sep)) {
|
|
5538
|
+
return {
|
|
5539
|
+
cwd: baseAbs,
|
|
5540
|
+
error: 'workdir resolved outside base (containment escape): "' + workdir + '" → "' + joined + '" not inside "' + baseAbs + '"',
|
|
5541
|
+
};
|
|
5542
|
+
}
|
|
5543
|
+
return { cwd: joined };
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
function filterProjectHarnessDirsForWorkdir(dirs, opts) {
|
|
5547
|
+
if (!Array.isArray(dirs) || dirs.length === 0) return [];
|
|
5548
|
+
const projectLocalPath = opts && opts.projectLocalPath;
|
|
5549
|
+
const worktreePath = opts && opts.worktreePath;
|
|
5550
|
+
const workdir = opts && opts.workdir;
|
|
5551
|
+
if (!projectLocalPath || !worktreePath) return [];
|
|
5552
|
+
|
|
5553
|
+
const projectLocalAbs = path.resolve(String(projectLocalPath));
|
|
5554
|
+
const worktreeAbs = path.resolve(String(worktreePath));
|
|
5555
|
+
|
|
5556
|
+
// When workdir is set, the harness propagation anchors shift to the
|
|
5557
|
+
// subpath mirror in BOTH the project root and the worktree root, so a
|
|
5558
|
+
// `packages/foo` dispatch only sees `<projectLocal>/packages/foo/...`
|
|
5559
|
+
// harness dirs (its actual cwd in the operator's main checkout) and
|
|
5560
|
+
// excludes `<worktree>/packages/foo/...` (already discoverable via cwd).
|
|
5561
|
+
let projectAnchor = projectLocalAbs;
|
|
5562
|
+
let worktreeAnchor = worktreeAbs;
|
|
5563
|
+
if (workdir && typeof workdir === 'string' && workdir) {
|
|
5564
|
+
const pr = applyWorkdir(projectLocalAbs, workdir);
|
|
5565
|
+
const wr = applyWorkdir(worktreeAbs, workdir);
|
|
5566
|
+
// If either join fails containment, drop everything — better empty than wrong.
|
|
5567
|
+
if (pr.error || wr.error) return [];
|
|
5568
|
+
projectAnchor = pr.cwd;
|
|
5569
|
+
worktreeAnchor = wr.cwd;
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
const seen = new Set();
|
|
5573
|
+
const out = [];
|
|
5574
|
+
for (const dir of dirs) {
|
|
5575
|
+
if (typeof dir !== 'string' || !dir) continue;
|
|
5576
|
+
const abs = path.resolve(dir);
|
|
5577
|
+
if (seen.has(abs)) continue;
|
|
5578
|
+
seen.add(abs);
|
|
5579
|
+
const insideProject = abs === projectAnchor || abs.startsWith(projectAnchor + path.sep);
|
|
5580
|
+
if (!insideProject) continue;
|
|
5581
|
+
const insideWorktree = abs === worktreeAnchor || abs.startsWith(worktreeAnchor + path.sep);
|
|
5582
|
+
if (insideWorktree) continue;
|
|
5583
|
+
out.push(abs);
|
|
5584
|
+
}
|
|
5585
|
+
return out;
|
|
5326
5586
|
}
|
|
5327
5587
|
|
|
5328
5588
|
// ── HTTP Origin Allowlist & Security Headers ─────────────────────────────────
|
|
@@ -7805,6 +8065,7 @@ module.exports = {
|
|
|
7805
8065
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
|
|
7806
8066
|
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
7807
8067
|
resolveCopilotAgentDisabledMcpServers,
|
|
8068
|
+
resolveAgentHermeticHarness,
|
|
7808
8069
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
7809
8070
|
applyCcWorkerPoolForceOnMigration,
|
|
7810
8071
|
runtimeConfigWarnings,
|
|
@@ -7897,6 +8158,9 @@ module.exports = {
|
|
|
7897
8158
|
assertWorktreeOutsideProject,
|
|
7898
8159
|
resolveProjectRootDir,
|
|
7899
8160
|
resolveSpawnPaths,
|
|
8161
|
+
validateWorkItemWorkdir,
|
|
8162
|
+
applyWorkdir,
|
|
8163
|
+
filterProjectHarnessDirsForWorkdir,
|
|
7900
8164
|
READ_ONLY_ROOT_TASK_TYPES,
|
|
7901
8165
|
isLiveCommandCenterPath,
|
|
7902
8166
|
describeCcProtectedPaths,
|
package/engine/spawn-agent.js
CHANGED
|
@@ -55,6 +55,15 @@ function parseSpawnArgs(argv) {
|
|
|
55
55
|
let runtimeName = 'claude';
|
|
56
56
|
const opts = {};
|
|
57
57
|
const passthrough = [];
|
|
58
|
+
// P-08b62d49 — accumulating array of project-local-on-main harness dirs that
|
|
59
|
+
// engine.js wants `computeAddDirs` to surface as additional `--add-dir`
|
|
60
|
+
// entries. Multiple `--project-harness-dir <path>` flags accumulate in
|
|
61
|
+
// declaration order; the dedup/exists filter happens inside `computeAddDirs`.
|
|
62
|
+
const projectHarnessDirs = [];
|
|
63
|
+
// P-49e1c8b7 — hermetic harness opt-out (boolean). Set by engine.js when
|
|
64
|
+
// shared.resolveAgentHermeticHarness(agent, engine) is true; propagates to
|
|
65
|
+
// computeAddDirs which then returns [minionsDir] only.
|
|
66
|
+
let hermetic = false;
|
|
58
67
|
|
|
59
68
|
for (let i = 2; i < args.length; i++) {
|
|
60
69
|
const a = args[i];
|
|
@@ -77,6 +86,17 @@ function parseSpawnArgs(argv) {
|
|
|
77
86
|
case '--disable-builtin-mcps': opts.disableBuiltinMcps = true; break;
|
|
78
87
|
case '--no-custom-instructions': opts.suppressAgentsMd = true; break;
|
|
79
88
|
case '--enable-reasoning-summaries': opts.reasoningSummaries = true; break;
|
|
89
|
+
case '--project-harness-dir': {
|
|
90
|
+
const v = peek();
|
|
91
|
+
if (typeof v === 'string' && v) projectHarnessDirs.push(v);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
// P-49e1c8b7 — hermetic harness opt-out. When set, computeAddDirs in
|
|
95
|
+
// main() returns [minionsDir] only (user-scope asset dirs from
|
|
96
|
+
// runtime.getUserAssetDirs AND any forwarded --project-harness-dir
|
|
97
|
+
// entries are dropped). Engine.js gates this flag via
|
|
98
|
+
// shared.resolveAgentHermeticHarness(agent, engine).
|
|
99
|
+
case '--hermetic-harness': hermetic = true; break;
|
|
80
100
|
// LEGACY: dropped — the runtime adapter emits its own permission flag.
|
|
81
101
|
// Pre-P-2a6d9c4f engine.js still passes `--permission-mode bypassPermissions`;
|
|
82
102
|
// letting it through would duplicate the permission flag for Claude.
|
|
@@ -85,7 +105,7 @@ function parseSpawnArgs(argv) {
|
|
|
85
105
|
}
|
|
86
106
|
}
|
|
87
107
|
|
|
88
|
-
return { promptFile, sysPromptFile, runtimeName, opts, passthrough };
|
|
108
|
+
return { promptFile, sysPromptFile, runtimeName, opts, passthrough, projectHarnessDirs, hermetic };
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
/**
|
|
@@ -299,18 +319,39 @@ function createParentPipeForwarder(stream, outputPath, prefix = '') {
|
|
|
299
319
|
|
|
300
320
|
/**
|
|
301
321
|
* Build the `--add-dir` list passed to the runtime CLI. Pure: takes
|
|
302
|
-
* `{ runtime, minionsDir, homeDir, exists }`
|
|
303
|
-
* array of dirs the agent should be able to
|
|
322
|
+
* `{ runtime, minionsDir, homeDir, projectHarnessDirs, hermetic, exists }`
|
|
323
|
+
* and returns an ordered, deduped array of dirs the agent should be able to
|
|
324
|
+
* read from outside its worktree.
|
|
325
|
+
*
|
|
326
|
+
* Order:
|
|
327
|
+
* 1. minionsDir (always first, so playbooks/system-prompt are reachable).
|
|
328
|
+
* 2. Every existing dir from `runtime.getUserAssetDirs({ homeDir })` —
|
|
329
|
+
* user-scope skill/command/MCP roots (`~/.claude`, `~/.copilot`,
|
|
330
|
+
* `~/.agents`, …) so worktree-bound agents inherit user-installed
|
|
331
|
+
* harnesses.
|
|
332
|
+
* 3. P-08b62d49 — Every existing dir in `projectHarnessDirs` (engine.js
|
|
333
|
+
* passes the union of `queries.getProjectHarnesses(project).{skills,
|
|
334
|
+
* commands}` dirs that live in `project.localPath` but NOT under the
|
|
335
|
+
* worktree, so uncommitted `<repo>/.claude/skills/…` etc. on the main
|
|
336
|
+
* checkout are reachable from worktree dispatches). See
|
|
337
|
+
* `docs/harness-propagation.md` → "The worktree-uncommitted footgun".
|
|
304
338
|
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
339
|
+
* Non-existent dirs are dropped at every layer — Claude CLI rejects unknown
|
|
340
|
+
* `--add-dir` entries. The dedup compares resolved paths so we never emit a
|
|
341
|
+
* dir twice (e.g. when a runtime asset dir IS the minions repo in unusual
|
|
342
|
+
* setups, or when a project-harness dir duplicates a user-asset dir).
|
|
343
|
+
*
|
|
344
|
+
* P-49e1c8b7 — When `hermetic === true`, the result is exactly `[minionsDir]`:
|
|
345
|
+
* both the runtime adapter's user-asset dirs AND `projectHarnessDirs` are
|
|
346
|
+
* dropped so the spawned agent runs with a known-empty harness surface.
|
|
347
|
+
* Engine.js sets this via `shared.resolveAgentHermeticHarness(agent, engine)`
|
|
348
|
+
* and forwards `--hermetic-harness` to spawn-agent so the flag survives the
|
|
349
|
+
* subprocess hop.
|
|
310
350
|
*
|
|
311
351
|
* `exists` is injectable for tests; defaults to `fs.existsSync`.
|
|
312
352
|
*/
|
|
313
|
-
function computeAddDirs({ runtime, minionsDir, homeDir, exists = fs.existsSync } = {}) {
|
|
353
|
+
function computeAddDirs({ runtime, minionsDir, homeDir, projectHarnessDirs, hermetic, exists = fs.existsSync } = {}) {
|
|
354
|
+
if (hermetic) return [minionsDir];
|
|
314
355
|
const out = [minionsDir];
|
|
315
356
|
const seen = new Set([path.resolve(minionsDir)]);
|
|
316
357
|
const assetDirs = typeof runtime?.getUserAssetDirs === 'function'
|
|
@@ -324,9 +365,121 @@ function computeAddDirs({ runtime, minionsDir, homeDir, exists = fs.existsSync }
|
|
|
324
365
|
out.push(d);
|
|
325
366
|
seen.add(resolved);
|
|
326
367
|
}
|
|
368
|
+
if (Array.isArray(projectHarnessDirs)) {
|
|
369
|
+
for (const d of projectHarnessDirs) {
|
|
370
|
+
if (!d) continue;
|
|
371
|
+
const resolved = path.resolve(d);
|
|
372
|
+
if (seen.has(resolved)) continue;
|
|
373
|
+
if (!exists(d)) continue;
|
|
374
|
+
out.push(d);
|
|
375
|
+
seen.add(resolved);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
327
378
|
return out;
|
|
328
379
|
}
|
|
329
380
|
|
|
381
|
+
/**
|
|
382
|
+
* P-7d31a06b — Pre-approve a worktree's `.mcp.json` servers in `~/.claude.json`
|
|
383
|
+
* so Claude Code's first invocation in a fresh worktree cwd doesn't show the
|
|
384
|
+
* project-scoped "trust this MCP server?" prompt (which `--dangerously-skip-permissions`
|
|
385
|
+
* silently suppresses → the agent runs without the workspace MCPs).
|
|
386
|
+
*
|
|
387
|
+
* Writes to `projects[worktreePath].enabledMcpjsonServers` (the field Claude
|
|
388
|
+
* Code actually reads — confirmed against the live `~/.claude.json` schema:
|
|
389
|
+
* `enabledMcpjsonServers` / `disabledMcpjsonServers` are siblings of
|
|
390
|
+
* `mcpServers`, indexed by absolute project path). Notes on the PRD wording
|
|
391
|
+
* "mcpServers.approved": that's the intent label; `enabledMcpjsonServers` is
|
|
392
|
+
* the on-disk reality.
|
|
393
|
+
*
|
|
394
|
+
* Pure-ish: all I/O (fs reads, mutator) is injected for tests. Best-effort —
|
|
395
|
+
* callers MUST swallow errors and never block dispatch on this.
|
|
396
|
+
*
|
|
397
|
+
* Returns `{ wrote: boolean, reason: string, servers: string[] }`:
|
|
398
|
+
* - reason ∈ { 'ok', 'flag-off', 'not-claude', 'no-worktree',
|
|
399
|
+
* 'no-workspace-mcp', 'invalid-workspace-mcp', 'no-servers' }
|
|
400
|
+
* - servers: keys discovered in the workspace `.mcp.json` (regardless of write)
|
|
401
|
+
*
|
|
402
|
+
* Args:
|
|
403
|
+
* runtimeName — `runtime.name` from `resolveRuntime(...)`. Helper no-ops if !== 'claude'.
|
|
404
|
+
* worktreePath — absolute path to the agent's cwd (the worktree). Used as both the file location
|
|
405
|
+
* (`<worktreePath>/.mcp.json`) and the `projects[...]` key in `~/.claude.json`.
|
|
406
|
+
* homeDir — `os.homedir()`. Injected for test isolation; helper does NOT call os.homedir directly.
|
|
407
|
+
* engineConfig — config.engine object; reads `claudePreApproveWorkspaceMcps` (default true via ENGINE_DEFAULTS).
|
|
408
|
+
* mutateJsonFileLocked — injected file-locked mutator (engine/shared.js#mutateJsonFileLocked).
|
|
409
|
+
* existsSync, readFileSync — injectable fs primitives (default to node:fs).
|
|
410
|
+
*/
|
|
411
|
+
function preApproveWorkspaceMcps({
|
|
412
|
+
runtimeName,
|
|
413
|
+
worktreePath,
|
|
414
|
+
homeDir,
|
|
415
|
+
engineConfig,
|
|
416
|
+
mutateJsonFileLocked,
|
|
417
|
+
existsSync = fs.existsSync,
|
|
418
|
+
readFileSync = fs.readFileSync,
|
|
419
|
+
} = {}) {
|
|
420
|
+
if (engineConfig && engineConfig.claudePreApproveWorkspaceMcps === false) {
|
|
421
|
+
return { wrote: false, reason: 'flag-off', servers: [] };
|
|
422
|
+
}
|
|
423
|
+
if (runtimeName !== 'claude') {
|
|
424
|
+
return { wrote: false, reason: 'not-claude', servers: [] };
|
|
425
|
+
}
|
|
426
|
+
if (!worktreePath || typeof worktreePath !== 'string') {
|
|
427
|
+
return { wrote: false, reason: 'no-worktree', servers: [] };
|
|
428
|
+
}
|
|
429
|
+
const workspaceMcpPath = path.join(worktreePath, '.mcp.json');
|
|
430
|
+
if (!existsSync(workspaceMcpPath)) {
|
|
431
|
+
return { wrote: false, reason: 'no-workspace-mcp', servers: [] };
|
|
432
|
+
}
|
|
433
|
+
let workspaceMcp;
|
|
434
|
+
try {
|
|
435
|
+
workspaceMcp = JSON.parse(readFileSync(workspaceMcpPath, 'utf8'));
|
|
436
|
+
} catch {
|
|
437
|
+
return { wrote: false, reason: 'invalid-workspace-mcp', servers: [] };
|
|
438
|
+
}
|
|
439
|
+
const serversObj = workspaceMcp && typeof workspaceMcp === 'object' ? workspaceMcp.mcpServers : null;
|
|
440
|
+
const servers = serversObj && typeof serversObj === 'object' && !Array.isArray(serversObj)
|
|
441
|
+
? Object.keys(serversObj).filter((k) => typeof k === 'string' && k.length > 0)
|
|
442
|
+
: [];
|
|
443
|
+
if (servers.length === 0) {
|
|
444
|
+
return { wrote: false, reason: 'no-servers', servers: [] };
|
|
445
|
+
}
|
|
446
|
+
if (!homeDir || typeof mutateJsonFileLocked !== 'function') {
|
|
447
|
+
return { wrote: false, reason: 'no-home-or-mutator', servers };
|
|
448
|
+
}
|
|
449
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
450
|
+
let didWrite = false;
|
|
451
|
+
mutateJsonFileLocked(claudeJsonPath, (data) => {
|
|
452
|
+
const root = data && typeof data === 'object' ? data : {};
|
|
453
|
+
if (!root.projects || typeof root.projects !== 'object') root.projects = {};
|
|
454
|
+
const projectKey = worktreePath;
|
|
455
|
+
const proj = root.projects[projectKey] && typeof root.projects[projectKey] === 'object'
|
|
456
|
+
? root.projects[projectKey]
|
|
457
|
+
: {};
|
|
458
|
+
// enabledMcpjsonServers is Claude's actual on-disk approval field
|
|
459
|
+
const enabled = Array.isArray(proj.enabledMcpjsonServers) ? proj.enabledMcpjsonServers.slice() : [];
|
|
460
|
+
const disabled = Array.isArray(proj.disabledMcpjsonServers) ? proj.disabledMcpjsonServers.slice() : [];
|
|
461
|
+
const enabledSet = new Set(enabled.filter((s) => typeof s === 'string'));
|
|
462
|
+
let mutated = false;
|
|
463
|
+
for (const name of servers) {
|
|
464
|
+
if (!enabledSet.has(name)) {
|
|
465
|
+
enabledSet.add(name);
|
|
466
|
+
mutated = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// If a server was previously explicitly disabled, lift the disabled mark when we
|
|
470
|
+
// re-approve it — otherwise Claude treats the disabled entry as authoritative.
|
|
471
|
+
const nextDisabled = disabled.filter((s) => typeof s === 'string' && !servers.includes(s));
|
|
472
|
+
if (nextDisabled.length !== disabled.length) mutated = true;
|
|
473
|
+
if (!mutated) return data;
|
|
474
|
+
proj.enabledMcpjsonServers = [...enabledSet];
|
|
475
|
+
proj.disabledMcpjsonServers = nextDisabled;
|
|
476
|
+
root.projects[projectKey] = proj;
|
|
477
|
+
didWrite = true;
|
|
478
|
+
return root;
|
|
479
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
480
|
+
return { wrote: didWrite, reason: 'ok', servers };
|
|
481
|
+
}
|
|
482
|
+
|
|
330
483
|
// ─── Main script execution ──────────────────────────────────────────────────
|
|
331
484
|
|
|
332
485
|
function _installHint(name, runtime) {
|
|
@@ -345,7 +498,7 @@ function main() {
|
|
|
345
498
|
console.error('Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [--runtime <name>] [args...]');
|
|
346
499
|
process.exit(1);
|
|
347
500
|
}
|
|
348
|
-
const { promptFile, sysPromptFile, runtimeName, opts, passthrough } = parsed;
|
|
501
|
+
const { promptFile, sysPromptFile, runtimeName, opts, passthrough, projectHarnessDirs, hermetic } = parsed;
|
|
349
502
|
|
|
350
503
|
const env = cleanChildEnv();
|
|
351
504
|
injectAdoTokenEnvForRepoHost(env);
|
|
@@ -382,8 +535,21 @@ function main() {
|
|
|
382
535
|
// Skill discovery dirs — agents run with CWD set to an external repo
|
|
383
536
|
// worktree, so runtime-native global assets would otherwise be invisible.
|
|
384
537
|
// The adapter owns both where those assets live and how to surface them.
|
|
538
|
+
// P-08b62d49: engine.js also forwards uncommitted project-local harness
|
|
539
|
+
// dirs from the operator's main checkout via `--project-harness-dir <path>`
|
|
540
|
+
// (one per dir, accumulated in `projectHarnessDirs`) so worktree-bound
|
|
541
|
+
// agents inherit them too. See docs/harness-propagation.md.
|
|
542
|
+
// P-49e1c8b7: when engine.js passes `--hermetic-harness`, computeAddDirs
|
|
543
|
+
// returns [minionsDir] only — both user-asset dirs and project harness
|
|
544
|
+
// dirs are dropped for a known-empty harness surface.
|
|
385
545
|
const minionsDir = path.resolve(__dirname, '..');
|
|
386
|
-
const addDirs = computeAddDirs({
|
|
546
|
+
const addDirs = computeAddDirs({
|
|
547
|
+
runtime,
|
|
548
|
+
minionsDir,
|
|
549
|
+
homeDir: os.homedir(),
|
|
550
|
+
projectHarnessDirs,
|
|
551
|
+
hermetic,
|
|
552
|
+
});
|
|
387
553
|
|
|
388
554
|
let resolved;
|
|
389
555
|
try { resolved = runtime.resolveBinary({ env }); }
|
|
@@ -639,6 +805,6 @@ function main() {
|
|
|
639
805
|
});
|
|
640
806
|
}
|
|
641
807
|
|
|
642
|
-
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, computeAddDirs, createParentPipeForwarder, assertStaleHeadOk };
|
|
808
|
+
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, computeAddDirs, preApproveWorkspaceMcps, createParentPipeForwarder, assertStaleHeadOk };
|
|
643
809
|
|
|
644
810
|
if (require.main === module) main();
|