@cat-factory/executor-harness 1.132.3 → 1.135.0
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 +49 -0
- package/dist/agent-capabilities.d.ts +21 -24
- package/dist/agent-capabilities.js +22 -50
- package/dist/agent-env.d.ts +17 -0
- package/dist/agent-env.js +47 -0
- package/dist/agent-runner.d.ts +18 -2
- package/dist/agent-runner.js +29 -231
- package/dist/agent-shared.d.ts +14 -5
- package/dist/agent-shared.js +14 -5
- package/dist/agent.d.ts +0 -11
- package/dist/agent.js +7 -138
- package/dist/captured-command.d.ts +1 -1
- package/dist/captured-command.js +3 -2
- package/dist/claude-cli.d.ts +90 -0
- package/dist/claude-cli.js +181 -0
- package/dist/claude-home.d.ts +41 -0
- package/dist/claude-home.js +159 -0
- package/dist/coding-agent.d.ts +35 -0
- package/dist/coding-agent.js +213 -41
- package/dist/docker-status.d.ts +89 -0
- package/dist/docker-status.js +147 -0
- package/dist/frontend-infra.js +4 -3
- package/dist/git.d.ts +48 -5
- package/dist/git.js +93 -26
- package/dist/guard-driver.d.ts +71 -0
- package/dist/guard-driver.js +171 -0
- package/dist/harness-server.js +13 -0
- package/dist/infra-standup.d.ts +69 -0
- package/dist/infra-standup.js +182 -0
- package/dist/job.d.ts +10 -0
- package/dist/multi-repo-coding.d.ts +17 -0
- package/dist/multi-repo-coding.js +61 -16
- package/dist/pi-workspace.d.ts +11 -0
- package/dist/pi-workspace.js +126 -57
- package/dist/pi.d.ts +8 -0
- package/dist/pi.js +16 -9
- package/dist/progress-guard.d.ts +56 -10
- package/dist/progress-guard.js +84 -22
- package/dist/runner.d.ts +1 -1
- package/dist/salvage.d.ts +180 -0
- package/dist/salvage.js +289 -0
- package/dist/workspace-probe.d.ts +85 -0
- package/dist/workspace-probe.js +124 -0
- package/package.json +4 -4
- package/src/agent-capabilities.ts +25 -51
- package/src/agent-env.ts +49 -0
- package/src/agent-runner.ts +40 -267
- package/src/agent-shared.ts +16 -5
- package/src/agent.ts +7 -164
- package/src/captured-command.ts +3 -2
- package/src/claude-cli.ts +217 -0
- package/src/claude-home.ts +233 -0
- package/src/coding-agent.ts +252 -44
- package/src/docker-status.ts +201 -0
- package/src/frontend-infra.ts +4 -3
- package/src/git.ts +104 -26
- package/src/guard-driver.ts +203 -0
- package/src/harness-server.ts +13 -0
- package/src/infra-standup.ts +218 -0
- package/src/job.ts +10 -0
- package/src/multi-repo-coding.ts +65 -16
- package/src/pi-workspace.ts +161 -57
- package/src/pi.ts +27 -12
- package/src/progress-guard.ts +110 -34
- package/src/runner.ts +1 -1
- package/src/salvage.ts +407 -0
- package/src/workspace-probe.ts +155 -0
package/dist/agent-runner.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { join } from 'node:path';
|
|
5
3
|
import { claudeAssistantContent, isObject, numberOf, redactBody } from './claude-stream.js';
|
|
6
4
|
import { claudeUsage, unaccountedUsageCall } from './usage-attribution.js';
|
|
7
5
|
import { createClaudeRunTelemetry, subagentDispatchId, } from './claude-call-aggregator.js';
|
|
@@ -9,18 +7,19 @@ import { ToolCallTracker, recordClaudeToolResults, } from './tool-trajectory.js'
|
|
|
9
7
|
import { log } from './logger.js';
|
|
10
8
|
import { NO_TOOL_WINDOW } from './tool-silence.js';
|
|
11
9
|
import { publishCallMetric, } from './pi.js';
|
|
12
|
-
import {
|
|
10
|
+
import { observeClaudeMcpInit, } from './agent-capabilities.js';
|
|
11
|
+
import { openClaudeRunHome } from './claude-home.js';
|
|
13
12
|
import { codexImageGapNote, createCodexHome, disposeCodexHome } from './codex-home.js';
|
|
14
|
-
import {
|
|
13
|
+
import { createClaudeProgressGuard } from './guard-driver.js';
|
|
15
14
|
import { BoundedTail, JsonlLineReader } from './jsonl-stream.js';
|
|
16
15
|
import { killChildProcess, spawnDetached } from './process.js';
|
|
16
|
+
import { agentChildEnv } from './agent-env.js';
|
|
17
17
|
import { abortReasonOf } from './failure.js';
|
|
18
18
|
import { describeProcessExit } from './process-exit.js';
|
|
19
|
-
import { redact,
|
|
19
|
+
import { redact, secretsToRedact } from './redact.js';
|
|
20
20
|
import { createSliceTracker, startSubagentWatcher } from './subagents.js';
|
|
21
21
|
import { createTaskPlanTracker, mergeProgress, normalizeStatus, pickProgress, toProgress, todosToProgress, } from './progress.js';
|
|
22
|
-
import {
|
|
23
|
-
import { retainSessionTranscripts } from './transcript-retention.js';
|
|
22
|
+
import { assertClaudeToolsCurrent, claudeCliArgs, CLAUDE_TOOL_SET } from './claude-cli.js';
|
|
24
23
|
/**
|
|
25
24
|
* Drive one CLI subprocess to completion, streaming LF-framed JSONL from stdout
|
|
26
25
|
* through `onEvent`. Mirrors `runPi`'s lifecycle: prompt over stdin (out-of-band,
|
|
@@ -42,7 +41,7 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
42
41
|
}
|
|
43
42
|
const child = spawn(command, args, {
|
|
44
43
|
cwd: opts.cwd,
|
|
45
|
-
env:
|
|
44
|
+
env: agentChildEnv(env),
|
|
46
45
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
46
|
// Own process group (POSIX) so killChildProcess reaps the CLI's grandchildren too.
|
|
48
47
|
detached: spawnDetached,
|
|
@@ -255,80 +254,6 @@ export function carryClaudeSystemPrompt(systemPrompt, userPrompt) {
|
|
|
255
254
|
// ---------------------------------------------------------------------------
|
|
256
255
|
// Claude Code
|
|
257
256
|
// ---------------------------------------------------------------------------
|
|
258
|
-
/**
|
|
259
|
-
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
260
|
-
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
261
|
-
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
262
|
-
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
263
|
-
* onto the summary + usage.
|
|
264
|
-
*/
|
|
265
|
-
/**
|
|
266
|
-
* Write a repo-sourced skill as a NATIVE Claude Code skill under `<skillsRoot>/<name>/`: a
|
|
267
|
-
* `SKILL.md` (YAML frontmatter `name`/`description` + the instructions body, the format the CLI
|
|
268
|
-
* expects) plus every resource file at its path within the skill directory. Resource sub-paths
|
|
269
|
-
* were sanitized at the job boundary (no traversal), so nested dirs are created as needed.
|
|
270
|
-
*
|
|
271
|
-
* The frontmatter `name`/`description` values are emitted as JSON-encoded (double-quoted) YAML
|
|
272
|
-
* scalars, not bare plain scalars: an author's description routinely contains `: ` (colon-space)
|
|
273
|
-
* or a leading YAML indicator (`#`, `-`, `[`, `{`, `"`, …), which is invalid as a plain scalar and
|
|
274
|
-
* would make the CLI fail to parse the frontmatter and silently skip the skill. A JSON string is a
|
|
275
|
-
* valid YAML double-quoted scalar, so quoting makes the manifest robust to arbitrary text.
|
|
276
|
-
*/
|
|
277
|
-
async function writeNativeSkill(skillsRoot, skill) {
|
|
278
|
-
const dir = join(skillsRoot, skill.name);
|
|
279
|
-
await mkdir(dir, { recursive: true });
|
|
280
|
-
const name = JSON.stringify(skill.name);
|
|
281
|
-
const description = JSON.stringify(skill.description.replace(/\r?\n/g, ' '));
|
|
282
|
-
const frontmatter = `---\nname: ${name}\ndescription: ${description}\n---\n`;
|
|
283
|
-
await writeFile(join(dir, 'SKILL.md'), `${frontmatter}\n${skill.instructions}\n`, 'utf8');
|
|
284
|
-
for (const resource of skill.resources) {
|
|
285
|
-
const dest = join(dir, resource.relPath);
|
|
286
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
287
|
-
await writeFile(dest, resource.content, 'utf8');
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Prepare the Claude Code CLI's MCP wiring for one run: write the servers to a PER-RUN config and
|
|
292
|
-
* return the argv that points the CLI at it, plus the cleanup for a directory we had to mint.
|
|
293
|
-
*
|
|
294
|
-
* Two decisions live here. `--strict-mcp-config` makes that file the ONLY source of servers, so an
|
|
295
|
-
* ambient run on a developer's own machine can never silently hand the agent their personal ones.
|
|
296
|
-
* And `--allowedTools` is passed ONLY when a server actually narrows its tools — an allow-list is
|
|
297
|
-
* whole-session, not MCP-scoped, so `claudeAllowedToolPatterns` re-grants the CLI's built-in
|
|
298
|
-
* file/bash tools in the same list; see it for why that holds whichever way the run's permission
|
|
299
|
-
* mode treats an allow-list.
|
|
300
|
-
*
|
|
301
|
-
* The config carries this job's resolved credentials, so it goes in the isolated config home when
|
|
302
|
-
* we own one and a throwaway per-JOB directory otherwise — never the checkout (it would land in a
|
|
303
|
-
* commit) and never a shared HOME path (a concurrent job would clobber it).
|
|
304
|
-
*/
|
|
305
|
-
async function setUpClaudeMcp(servers, configHome) {
|
|
306
|
-
const noop = { args: [], cleanup: async () => { } };
|
|
307
|
-
if (!servers?.length)
|
|
308
|
-
return noop;
|
|
309
|
-
// Before anything can spawn: a failing MCP server echoes its own argv/headers into stderr, and
|
|
310
|
-
// that tail is carried onto the step's diagnostics.
|
|
311
|
-
registerKnownSecrets(mcpServerSecretValues(servers));
|
|
312
|
-
const home = configHome ?? (await mkdtemp(join(tmpdir(), 'cf-claude-mcp-')));
|
|
313
|
-
const owned = home === configHome ? undefined : home;
|
|
314
|
-
const cleanup = async () => {
|
|
315
|
-
if (owned)
|
|
316
|
-
await rm(owned, { recursive: true, force: true }).catch(() => { });
|
|
317
|
-
};
|
|
318
|
-
const configPath = await writeClaudeMcpConfig(home, servers);
|
|
319
|
-
if (!configPath)
|
|
320
|
-
return { args: [], cleanup };
|
|
321
|
-
const allowedTools = claudeAllowedToolPatterns(servers);
|
|
322
|
-
return {
|
|
323
|
-
args: [
|
|
324
|
-
'--mcp-config',
|
|
325
|
-
configPath,
|
|
326
|
-
'--strict-mcp-config',
|
|
327
|
-
...(allowedTools?.length ? ['--allowedTools', allowedTools.join(',')] : []),
|
|
328
|
-
],
|
|
329
|
-
cleanup,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
257
|
/**
|
|
333
258
|
* The LIVE publishers of a claude-code run: everything the stream has revealed so far that the
|
|
334
259
|
* backend should see before the run ends, rather than only in its terminal result.
|
|
@@ -383,52 +308,6 @@ function reportToolServerStartup(event, onToolServers) {
|
|
|
383
308
|
if (observed)
|
|
384
309
|
onToolServers(observed);
|
|
385
310
|
}
|
|
386
|
-
/**
|
|
387
|
-
* No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
388
|
-
* which cannot see the CLI's internal turns. The caller remembers each `tool_use` id's name off
|
|
389
|
-
* the assistant turn (`rememberTool`) and hands the following user turn's content to `feedGuard`,
|
|
390
|
-
* which pairs each `tool_result`'s `is_error` with that name. The FIRST reason trips it: the
|
|
391
|
-
* diagnostic is recorded (readable via `reason()`, which the catch surfaces over the generic abort
|
|
392
|
-
* message) and `guardAbort` fires — folded into streamCli's signal so a tripped guard kills the CLI
|
|
393
|
-
* the same way the external watchdog does. Disabled when the caller supplies no limits (only the
|
|
394
|
-
* external watchdog then bounds the run).
|
|
395
|
-
*
|
|
396
|
-
* Split out of {@link runClaudeCode} for the per-function line budget.
|
|
397
|
-
*/
|
|
398
|
-
function createClaudeProgressGuard(opts) {
|
|
399
|
-
const guard = opts.guardLimits
|
|
400
|
-
? new ProgressGuard(opts.guardLimits, opts.expectsEdits ?? true)
|
|
401
|
-
: undefined;
|
|
402
|
-
const toolNames = new Map();
|
|
403
|
-
const guardAbort = new AbortController();
|
|
404
|
-
let guardReason;
|
|
405
|
-
const feedGuard = (content) => {
|
|
406
|
-
if (!guard || guardReason)
|
|
407
|
-
return;
|
|
408
|
-
for (const block of content) {
|
|
409
|
-
if (!isObject(block) || block.type !== 'tool_result')
|
|
410
|
-
continue;
|
|
411
|
-
const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
|
|
412
|
-
const name = id ? toolNames.get(id) : undefined;
|
|
413
|
-
if (id)
|
|
414
|
-
toolNames.delete(id);
|
|
415
|
-
if (!name)
|
|
416
|
-
continue;
|
|
417
|
-
const reason = guard.observeSignal({ name, isError: block.is_error === true });
|
|
418
|
-
if (reason) {
|
|
419
|
-
guardReason = reason;
|
|
420
|
-
guardAbort.abort();
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
return {
|
|
426
|
-
rememberTool: (id, name) => toolNames.set(id, name),
|
|
427
|
-
feedGuard,
|
|
428
|
-
guardAbort,
|
|
429
|
-
reason: () => guardReason,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
311
|
/**
|
|
433
312
|
* The run's TRAJECTORY, on the claude-code stream: each `tool_use` block paired with the
|
|
434
313
|
* `tool_result` that answers it on the following user turn, numbered and captured (scrubbed +
|
|
@@ -520,6 +399,13 @@ function openClaudeCallCapture(opts, stream) {
|
|
|
520
399
|
},
|
|
521
400
|
};
|
|
522
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
404
|
+
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
405
|
+
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
406
|
+
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
407
|
+
* onto the summary + usage.
|
|
408
|
+
*/
|
|
523
409
|
export async function runClaudeCode(opts) {
|
|
524
410
|
const stats = { toolCalls: 0, assistantChars: 0 };
|
|
525
411
|
let summary = '';
|
|
@@ -536,6 +422,9 @@ export async function runClaudeCode(opts) {
|
|
|
536
422
|
bytes: Buffer.byteLength(opts.systemPrompt, 'utf8'),
|
|
537
423
|
});
|
|
538
424
|
}
|
|
425
|
+
// The built-in tools this run declares, named ONCE: the same list rides `--tools` and the
|
|
426
|
+
// `--allowedTools` re-grant, which is additive rather than inert (see `claudeAllowedToolPatterns`).
|
|
427
|
+
const tools = CLAUDE_TOOL_SET;
|
|
539
428
|
const secrets = opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [];
|
|
540
429
|
const capture = openClaudeCallCapture(opts, { prompt, folded, secrets });
|
|
541
430
|
const telemetry = capture.telemetry;
|
|
@@ -577,6 +466,9 @@ export async function runClaudeCode(opts) {
|
|
|
577
466
|
const onEvent = (event, meta) => {
|
|
578
467
|
const type = event.type;
|
|
579
468
|
reportToolServerStartup(event, opts.onToolServers);
|
|
469
|
+
// The same startup event answers what the CLI granted of what we asked for; a capability it
|
|
470
|
+
// named no tool for is a silent capability loss otherwise (see `assertClaudeToolsCurrent`).
|
|
471
|
+
assertClaudeToolsCurrent(event, tools, opts.log);
|
|
580
472
|
// A subagent's turns ride the parent's stdout tagged with the dispatch that spawned them;
|
|
581
473
|
// `telemetry` routes them off the parent's chain (and decides who bills them). Progress, slice
|
|
582
474
|
// tracking, the guard and `stats` below deliberately see EVERY event: a subagent grinding on
|
|
@@ -638,7 +530,7 @@ export async function runClaudeCode(opts) {
|
|
|
638
530
|
terminalReport = claudeResultReport(event) || terminalReport;
|
|
639
531
|
}
|
|
640
532
|
};
|
|
641
|
-
const home = await openClaudeRunHome(opts);
|
|
533
|
+
const home = await openClaudeRunHome(opts, tools);
|
|
642
534
|
const { configHome } = home;
|
|
643
535
|
// ADR 0026 D3 (path corrected by ADR 0027 Defect A): while the run is live, tail the CLI's
|
|
644
536
|
// subagent `*.jsonl` transcripts so a parallel-subagent review keeps the inactivity
|
|
@@ -668,22 +560,7 @@ export async function runClaudeCode(opts) {
|
|
|
668
560
|
try {
|
|
669
561
|
const { stderrTail } = await streamCli({
|
|
670
562
|
command: 'claude',
|
|
671
|
-
args:
|
|
672
|
-
'-p',
|
|
673
|
-
'--output-format',
|
|
674
|
-
'stream-json',
|
|
675
|
-
'--verbose',
|
|
676
|
-
// The per-run container IS the sandbox, and the run is fully headless (no one
|
|
677
|
-
// to approve a tool call) — so bypass permissions entirely. `acceptEdits`
|
|
678
|
-
// would auto-accept file edits but still gate Bash, which in `-p` mode is then
|
|
679
|
-
// denied, leaving the agent unable to run builds/tests/git to verify its work.
|
|
680
|
-
'--permission-mode',
|
|
681
|
-
'bypassPermissions',
|
|
682
|
-
'--model',
|
|
683
|
-
opts.model,
|
|
684
|
-
...home.mcpArgs,
|
|
685
|
-
...appendArgs,
|
|
686
|
-
],
|
|
563
|
+
args: claudeCliArgs({ model: opts.model, tools, mcpArgs: home.mcpArgs, appendArgs }),
|
|
687
564
|
}, prompt, { ...opts, signal: runSignal }, home.env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
|
|
688
565
|
// The stream has ended, so the last call has no successor envelope to complete it.
|
|
689
566
|
telemetry.flush();
|
|
@@ -732,91 +609,6 @@ export async function runClaudeCode(opts) {
|
|
|
732
609
|
await home.dispose();
|
|
733
610
|
}
|
|
734
611
|
}
|
|
735
|
-
async function openClaudeRunHome(opts) {
|
|
736
|
-
// Native (ambient) mode: run the developer's installed `claude` with its OWN login —
|
|
737
|
-
// no isolated config home, no injected credential, no onboarding pre-seed. Otherwise,
|
|
738
|
-
// Claude Code persists user config/credentials under its config dir; point that at an
|
|
739
|
-
// isolated, per-run temp dir OUTSIDE the cloned checkout (`opts.cwd`). Otherwise the
|
|
740
|
-
// agents that finish with `git add -A` (blueprint/requirements/bootstrap) could stage a
|
|
741
|
-
// stray `.claude/` directory — and any cached credential in it — into the pushed branch.
|
|
742
|
-
// Mirrors the Codex CODEX_HOME isolation below; removed by `dispose`.
|
|
743
|
-
if (!opts.ambientAuth && !opts.subscriptionToken) {
|
|
744
|
-
throw new Error('claude-code harness requires a subscription token (or ambientAuth)');
|
|
745
|
-
}
|
|
746
|
-
const configHome = opts.ambientAuth ? undefined : await mkdtemp(join(tmpdir(), 'cf-claude-'));
|
|
747
|
-
// The config dir is brand-new every run, so Claude Code would otherwise treat this
|
|
748
|
-
// as a first launch and BLOCK on the interactive onboarding / "trust this folder" /
|
|
749
|
-
// bypass-permissions acknowledgement prompts — which never get answered headlessly,
|
|
750
|
-
// hanging the job until the watchdog kills it. Pre-seed the config that marks those
|
|
751
|
-
// as already accepted so `-p` starts straight into the run. Best-effort: written
|
|
752
|
-
// before the CLI starts; unknown keys are harmless if a CLI version ignores them.
|
|
753
|
-
// (Ambient mode skips this — the developer's own config is already onboarded.)
|
|
754
|
-
// ADR 0026 D4: assert the pinned onboarding keys landed and log them with the CLI
|
|
755
|
-
// version, so a future first-run gate this set doesn't cover (which looks identical to
|
|
756
|
-
// a healthy-but-quiet subagent start) is diffable when the cold-start watchdog fires.
|
|
757
|
-
if (configHome) {
|
|
758
|
-
await writeOnboardingPreseed(configHome);
|
|
759
|
-
await assertOnboardingKeysCurrent(configHome, process.env.CLAUDE_CLI_VERSION, opts.log);
|
|
760
|
-
}
|
|
761
|
-
// Skills: install each as a native skill under the config dir's `skills/<name>/` so the CLI
|
|
762
|
-
// discovers and can invoke it. ONLY into the isolated per-run config home — never the
|
|
763
|
-
// developer's own `~/.claude` (ambient/native mode), where it would persist in their personal
|
|
764
|
-
// setup after the run and two concurrent jobs carrying same-named skills would clobber each
|
|
765
|
-
// other. An ambient run reads the skills from the checkout instead (`.cat-context/skill/<name>/`,
|
|
766
|
-
// materialised by the caller). Best-effort: a write failure must not wedge the run — the prompt
|
|
767
|
-
// still names the skills.
|
|
768
|
-
if (configHome) {
|
|
769
|
-
for (const skill of opts.skills ?? []) {
|
|
770
|
-
await writeNativeSkill(join(configHome, 'skills'), skill).catch(() => { });
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
// Tool servers (MCP): the CLI is pointed at a per-run config rather than discovering an ambient
|
|
774
|
-
// one. See `setUpClaudeMcp` for why that matters and what has to be cleaned up afterwards.
|
|
775
|
-
const mcp = await setUpClaudeMcp(opts.mcpServers, configHome);
|
|
776
|
-
return {
|
|
777
|
-
configHome,
|
|
778
|
-
mcpArgs: mcp.args,
|
|
779
|
-
env: buildClaudeEnv(opts, configHome),
|
|
780
|
-
dispose: async () => {
|
|
781
|
-
// The ambient-mode MCP config dir (credential-bearing) never outlives the run.
|
|
782
|
-
await mcp.cleanup();
|
|
783
|
-
if (!configHome)
|
|
784
|
-
return;
|
|
785
|
-
// Lift the CLI session transcripts (`projects/`) out for short-lived retention BEFORE the
|
|
786
|
-
// home is deleted — the credential lives at the home root, never in `projects/`, so this
|
|
787
|
-
// keeps the debugging artifact without leaking the token. Best-effort; never throws.
|
|
788
|
-
await retainSessionTranscripts(configHome, ['projects'], {
|
|
789
|
-
label: 'claude-code',
|
|
790
|
-
...(opts.log ? { log: opts.log } : {}),
|
|
791
|
-
});
|
|
792
|
-
// Never leave the config dir (and any cached credential) on disk past the run.
|
|
793
|
-
await rm(configHome, { recursive: true, force: true }).catch(() => { });
|
|
794
|
-
},
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Build the child-process env for the `claude` CLI: an isolated config home plus subscription
|
|
799
|
-
* auth (Anthropic OAuth token, or an Anthropic-compatible base URL + auth token for a
|
|
800
|
-
* non-Anthropic Claude-Code vendor like GLM/Kimi/DeepSeek), or an empty env in ambient mode
|
|
801
|
-
* (the developer's own logged-in `~/.claude` is used). Extracted from {@link runClaudeCode} to
|
|
802
|
-
* keep its cyclomatic complexity down; behaviour is a straight move of the original expression.
|
|
803
|
-
*/
|
|
804
|
-
function buildClaudeEnv(opts, configHome) {
|
|
805
|
-
// The job-scoped env rides along in BOTH modes; the credential/config vars below are what
|
|
806
|
-
// ambient mode drops (the developer's own logged-in `~/.claude` is used instead).
|
|
807
|
-
if (opts.ambientAuth)
|
|
808
|
-
return { ...opts.extraEnv };
|
|
809
|
-
return {
|
|
810
|
-
...opts.extraEnv,
|
|
811
|
-
CLAUDE_CONFIG_DIR: configHome,
|
|
812
|
-
...(opts.subscriptionBaseUrl
|
|
813
|
-
? {
|
|
814
|
-
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
815
|
-
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken,
|
|
816
|
-
}
|
|
817
|
-
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken }),
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
612
|
/**
|
|
821
613
|
* Merge the parent-loop telemetry with the subagents' out-of-band usage + per-call metrics into
|
|
822
614
|
* the run outcome. INVARIANT (do not "fix" this into a double count): the run total is the parent
|
|
@@ -969,6 +761,12 @@ export async function runCodex(opts) {
|
|
|
969
761
|
'exec',
|
|
970
762
|
'--json',
|
|
971
763
|
'--skip-git-repo-check',
|
|
764
|
+
// No `--tools` analogue here, and its absence is a FINDING rather than an oversight:
|
|
765
|
+
// codex has no flag that declares a built-in tool set, because it has no set to choose
|
|
766
|
+
// from. Its surface is shell + apply_patch + the plan tool, and the optional extras are
|
|
767
|
+
// individual `CODEX_HOME/config.toml` switches the harness already sets deliberately
|
|
768
|
+
// (`[features] image_generation`, see `codex-home.ts`). So there is nothing here that
|
|
769
|
+
// silently drifts with a CLI version the way claude-code's headless default did.
|
|
972
770
|
// The per-run container IS the sandbox; let Codex write files and reach the
|
|
973
771
|
// vendor unrestricted, with no approval prompts (the run is headless).
|
|
974
772
|
'--dangerously-bypass-approvals-and-sandbox',
|
package/dist/agent-shared.d.ts
CHANGED
|
@@ -7,11 +7,18 @@ import type { EffortReport } from './effort.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function mergeEffort(result: AgentResult, effortReport: EffortReport | undefined): AgentResult;
|
|
9
9
|
/**
|
|
10
|
-
* The agent-capability fields (skills, tool servers, reference designs) every
|
|
11
|
-
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
12
|
-
* cannot silently be the one that drops a kind's declared playbook, tool server
|
|
13
|
-
* gallery: the failure mode is invisible (the agent simply works without
|
|
14
|
-
* up as degraded output.
|
|
10
|
+
* The agent-capability fields (skills, tool servers, reference designs, web research) every
|
|
11
|
+
* agent-running flow forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
12
|
+
* spread, so a flow cannot silently be the one that drops a kind's declared playbook, tool server,
|
|
13
|
+
* reference gallery or web access: the failure mode is invisible (the agent simply works without
|
|
14
|
+
* it) and would only show up as degraded output.
|
|
15
|
+
*
|
|
16
|
+
* Web research joined the helper after the conflict-resolver and bootstrap flows were found to be
|
|
17
|
+
* forwarding neither half of it: both build their own spec literal, and the two web fields were
|
|
18
|
+
* hand-written at the four sites that remembered them. That is exactly the drift this helper
|
|
19
|
+
* exists to make unrepresentable, so they are read here rather than at each call site. Both halves
|
|
20
|
+
* travel together on purpose: the guidance NAMES the tools, so a flow carrying one without the
|
|
21
|
+
* other either describes tools the run was never given or hands it tools nothing introduced.
|
|
15
22
|
*/
|
|
16
23
|
export declare function agentCapabilities(job: AgentJob): {
|
|
17
24
|
skills?: SkillSpec[];
|
|
@@ -19,4 +26,6 @@ export declare function agentCapabilities(job: AgentJob): {
|
|
|
19
26
|
generateImages?: boolean;
|
|
20
27
|
referenceScreenshots?: ImageManifestSpec;
|
|
21
28
|
designImages?: ImageManifestSpec;
|
|
29
|
+
webSearchProxy?: boolean;
|
|
30
|
+
webToolsGuidance?: string;
|
|
22
31
|
};
|
package/dist/agent-shared.js
CHANGED
|
@@ -10,11 +10,18 @@ export function mergeEffort(result, effortReport) {
|
|
|
10
10
|
return effortReport ? { ...result, effortReport } : result;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* The agent-capability fields (skills, tool servers, reference designs) every
|
|
14
|
-
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
15
|
-
* cannot silently be the one that drops a kind's declared playbook, tool server
|
|
16
|
-
* gallery: the failure mode is invisible (the agent simply works without
|
|
17
|
-
* up as degraded output.
|
|
13
|
+
* The agent-capability fields (skills, tool servers, reference designs, web research) every
|
|
14
|
+
* agent-running flow forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
15
|
+
* spread, so a flow cannot silently be the one that drops a kind's declared playbook, tool server,
|
|
16
|
+
* reference gallery or web access: the failure mode is invisible (the agent simply works without
|
|
17
|
+
* it) and would only show up as degraded output.
|
|
18
|
+
*
|
|
19
|
+
* Web research joined the helper after the conflict-resolver and bootstrap flows were found to be
|
|
20
|
+
* forwarding neither half of it: both build their own spec literal, and the two web fields were
|
|
21
|
+
* hand-written at the four sites that remembered them. That is exactly the drift this helper
|
|
22
|
+
* exists to make unrepresentable, so they are read here rather than at each call site. Both halves
|
|
23
|
+
* travel together on purpose: the guidance NAMES the tools, so a flow carrying one without the
|
|
24
|
+
* other either describes tools the run was never given or hands it tools nothing introduced.
|
|
18
25
|
*/
|
|
19
26
|
export function agentCapabilities(job) {
|
|
20
27
|
return {
|
|
@@ -23,5 +30,7 @@ export function agentCapabilities(job) {
|
|
|
23
30
|
...(job.generateImages ? { generateImages: true } : {}),
|
|
24
31
|
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
25
32
|
...(job.designImages ? { designImages: job.designImages } : {}),
|
|
33
|
+
...(job.webSearch ? { webSearchProxy: true } : {}),
|
|
34
|
+
...(job.webToolsGuidance ? { webToolsGuidance: job.webToolsGuidance } : {}),
|
|
26
35
|
};
|
|
27
36
|
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import type { AgentJob, AgentResult, TestSecretSpec } from './job.js';
|
|
2
2
|
import { runCodingAgent } from './coding-agent.js';
|
|
3
3
|
import type { RunOptions } from './runner.js';
|
|
4
|
-
/**
|
|
5
|
-
* Build the dynamic infra notes appended to the agent's user prompt from a stand-up outcome.
|
|
6
|
-
* A stand-up problem (a failed build / compose) is flagged as a concern to test around; a
|
|
7
|
-
* frontend serve URL points the UI tester at the app that was just built + served and pre-empts
|
|
8
|
-
* a live-backend CORS failure being mis-reported as an app defect. Pure (no IO) so the exact
|
|
9
|
-
* wording + ordering is unit-tested; returns the notes in order (problem first, serve URL next).
|
|
10
|
-
*/
|
|
11
|
-
export declare function buildInfraNotes(managed: {
|
|
12
|
-
note?: string;
|
|
13
|
-
serveUrl?: string;
|
|
14
|
-
}): string[];
|
|
15
4
|
/** Run one generic agent job end to end, dispatching on `mode`. */
|
|
16
5
|
export declare function handleAgent(job: AgentJob, opts?: RunOptions): Promise<AgentResult>;
|
|
17
6
|
/**
|
package/dist/agent.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// The preview mode drives the frontend stand-up directly rather than through `manageInfra`:
|
|
5
|
+
// its serve/WireMock children outlive the job on purpose, so it wants no cleanup handle.
|
|
6
6
|
import { standUpFrontend, tearDownFrontend } from './frontend-infra.js';
|
|
7
|
+
import { buildInfraNotes, manageInfra } from './infra-standup.js';
|
|
7
8
|
import { artifactUploadEnv } from './artifact-upload.js';
|
|
8
9
|
import { configurePackageRegistries } from './package-registries.js';
|
|
9
|
-
import {
|
|
10
|
+
import { registerKnownSecrets } from './redact.js';
|
|
10
11
|
import { cloneRepo, commitAll, conflictDiff, fetchPullRequestHead, fetchReferenceBranches, headCommit, mergeBranch, prepareExistingCheckout, pushBranch, unmergedPaths, } from './git.js';
|
|
11
12
|
import { inferVcsProvider, openPullRequest } from './vcs-api.js';
|
|
12
13
|
import { applyPrDescription } from './pr-description.js';
|
|
@@ -36,132 +37,6 @@ import { log } from './logger.js';
|
|
|
36
37
|
// target repo. These are the deliberate, documented exceptions — do NOT grow this into a
|
|
37
38
|
// general `if (job.someFlag)` dispatch; anything that doesn't need a checkout belongs in
|
|
38
39
|
// backend pre/post-ops. See backend/docs/custom-agents.md.
|
|
39
|
-
const exec = promisify(execFile);
|
|
40
|
-
/**
|
|
41
|
-
* Bring the service's docker-compose dependencies up (local infra only). Best-effort:
|
|
42
|
-
* runs `docker compose -f <path> up -d --wait` in the checkout. A missing Docker daemon
|
|
43
|
-
* or a compose failure is logged and surfaced to the agent (as a prompt note) rather
|
|
44
|
-
* than failing the job — the agent can still run unit-level tests and report what it
|
|
45
|
-
* could. A no-op for ephemeral / no-infra / no-compose-path runs.
|
|
46
|
-
*
|
|
47
|
-
* Whether it succeeds or fails, the (redacted, bounded) command output is captured into a
|
|
48
|
-
* {@link InfraSetupRecord} returned alongside the prompt `note`, so the backend can surface
|
|
49
|
-
* the in-container dependency stand-up logs on the Tester step — the failure-class artifact
|
|
50
|
-
* the orchestrator-side provisioning logs can't see.
|
|
51
|
-
*/
|
|
52
|
-
async function standUpInfra(dir, infra, signal, logger) {
|
|
53
|
-
if (infra.environment !== 'local' || infra.noInfraDependencies || !infra.composePath) {
|
|
54
|
-
return { started: false };
|
|
55
|
-
}
|
|
56
|
-
const startedAt = Date.now();
|
|
57
|
-
try {
|
|
58
|
-
logger.info('agent(explore): standing up infra', { composePath: infra.composePath });
|
|
59
|
-
// Raise maxBuffer well above the 1MB default so a chatty compose stand-up can't fail the
|
|
60
|
-
// (best-effort) infra step with ENOBUFS; the captured output is tail-bounded on storage.
|
|
61
|
-
const { stdout, stderr } = await exec('docker', ['compose', '-f', infra.composePath, 'up', '-d', '--wait'], { cwd: dir, signal, timeout: 5 * 60_000, maxBuffer: 16 * 1024 * 1024 });
|
|
62
|
-
const logs = captureRedactedOutput(stdout, stderr);
|
|
63
|
-
return {
|
|
64
|
-
started: true,
|
|
65
|
-
record: {
|
|
66
|
-
started: true,
|
|
67
|
-
composePath: infra.composePath,
|
|
68
|
-
at: Date.now(),
|
|
69
|
-
durationMs: Date.now() - startedAt,
|
|
70
|
-
...(logs ? { logs } : {}),
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
const note = err instanceof Error ? err.message : String(err);
|
|
76
|
-
logger.warn('agent(explore): infra stand-up failed', { error: note });
|
|
77
|
-
// `execFile` rejections carry the partial stdout/stderr on the error object — capture them
|
|
78
|
-
// so the stored logs explain the failure (a port clash, a pull-auth error, an exited
|
|
79
|
-
// dependency), not just the one-line exit message.
|
|
80
|
-
const e = err;
|
|
81
|
-
const logs = captureRedactedOutput(e.stdout, e.stderr);
|
|
82
|
-
return {
|
|
83
|
-
started: false,
|
|
84
|
-
note,
|
|
85
|
-
record: {
|
|
86
|
-
started: false,
|
|
87
|
-
composePath: infra.composePath,
|
|
88
|
-
at: Date.now(),
|
|
89
|
-
durationMs: Date.now() - startedAt,
|
|
90
|
-
error: redactSecrets(note),
|
|
91
|
-
...(logs ? { logs } : {}),
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Stand the run's infra up and return a single cleanup handle, dispatching on the spec's
|
|
98
|
-
* `kind`: the frontend UI-test flow (`kind: 'frontend'`) builds/serves the app + WireMock as
|
|
99
|
-
* processes (torn down by killing them); the default backend-service flow stands the
|
|
100
|
-
* docker-compose stack up (torn down with `docker compose down`). Unifying the two here keeps
|
|
101
|
-
* `runExploreMode` free of the branch and guarantees the matching teardown runs in its finally.
|
|
102
|
-
*
|
|
103
|
-
* `dir` is the clone ROOT; `workDir` is the service subtree (equal to `dir` when the run is not
|
|
104
|
-
* monorepo-scoped). The docker-compose stand-up runs at the root (its `composePath` is
|
|
105
|
-
* repo-relative), but the FRONTEND stand-up runs in `workDir`: a monorepo frontend's
|
|
106
|
-
* `package.json` / `outputDir` / `mocks/` all live under the service subtree, so installing,
|
|
107
|
-
* building, serving and seeding WireMock from the root would target the wrong directory.
|
|
108
|
-
*/
|
|
109
|
-
async function manageInfra(dir, workDir, infra, opts, logger) {
|
|
110
|
-
if (infra.kind === 'frontend') {
|
|
111
|
-
// `onActivity` feeds the inactivity watchdog through the frontend build/serve stand-up,
|
|
112
|
-
// which (unlike docker-compose's 5-min-capped `up`) can run past the inactivity window.
|
|
113
|
-
// Runs in `workDir` so a monorepo frontend builds/serves from its own package subtree.
|
|
114
|
-
const fe = await standUpFrontend(workDir, infra, opts, logger);
|
|
115
|
-
return {
|
|
116
|
-
...(fe.note ? { note: fe.note } : {}),
|
|
117
|
-
...(fe.serveUrl ? { serveUrl: fe.serveUrl } : {}),
|
|
118
|
-
record: fe.record,
|
|
119
|
-
cleanup: () => tearDownFrontend(fe.processes, logger),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
const standUp = await standUpInfra(dir, infra, opts.signal, logger);
|
|
123
|
-
return {
|
|
124
|
-
...(standUp.note ? { note: standUp.note } : {}),
|
|
125
|
-
...(standUp.record ? { record: standUp.record } : {}),
|
|
126
|
-
cleanup: () => tearDownInfra(dir, infra),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Build the dynamic infra notes appended to the agent's user prompt from a stand-up outcome.
|
|
131
|
-
* A stand-up problem (a failed build / compose) is flagged as a concern to test around; a
|
|
132
|
-
* frontend serve URL points the UI tester at the app that was just built + served and pre-empts
|
|
133
|
-
* a live-backend CORS failure being mis-reported as an app defect. Pure (no IO) so the exact
|
|
134
|
-
* wording + ordering is unit-tested; returns the notes in order (problem first, serve URL next).
|
|
135
|
-
*/
|
|
136
|
-
export function buildInfraNotes(managed) {
|
|
137
|
-
const notes = [];
|
|
138
|
-
if (managed.note) {
|
|
139
|
-
notes.push(`standing the infra up reported a problem (${managed.note}). Test what you can and ` +
|
|
140
|
-
`flag any dependency-related gaps as concerns.`);
|
|
141
|
-
}
|
|
142
|
-
if (managed.serveUrl) {
|
|
143
|
-
notes.push(`The frontend under test is built and served at ${managed.serveUrl}, with its other ` +
|
|
144
|
-
`backend upstreams handled by WireMock. Drive your UI tests against ${managed.serveUrl}. ` +
|
|
145
|
-
`If a call to a live backend fails with a CORS / cross-origin error, that is an infra ` +
|
|
146
|
-
`gap (the backend must allow the ${managed.serveUrl} origin), not an app defect — flag ` +
|
|
147
|
-
`it as a concern rather than a failing test.`);
|
|
148
|
-
}
|
|
149
|
-
return notes;
|
|
150
|
-
}
|
|
151
|
-
/** Tear the docker-compose dependencies down (best-effort; a no-op when none were started). */
|
|
152
|
-
async function tearDownInfra(dir, infra) {
|
|
153
|
-
if (infra.environment !== 'local' || infra.noInfraDependencies || !infra.composePath)
|
|
154
|
-
return;
|
|
155
|
-
try {
|
|
156
|
-
await exec('docker', ['compose', '-f', infra.composePath, 'down', '-v'], {
|
|
157
|
-
cwd: dir,
|
|
158
|
-
timeout: 2 * 60_000,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
// The container is ephemeral and torn down with the run anyway — ignore.
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
40
|
/**
|
|
166
41
|
* Parse an agent's final reply into the structured JSON `custom`, shared by the explore and
|
|
167
42
|
* coding structured-output paths. With repair enabled (default) a malformed reply gets ONE
|
|
@@ -262,9 +137,9 @@ export async function handleAgent(job, opts = {}) {
|
|
|
262
137
|
}
|
|
263
138
|
/**
|
|
264
139
|
* Layer extra child-process env onto a job's {@link RunOptions}. The agent CLI is spawned with
|
|
265
|
-
* `
|
|
266
|
-
*
|
|
267
|
-
*
|
|
140
|
+
* `agentChildEnv(agentEnv)`, so this is how per-job values reach the agent (and the shell tools it
|
|
141
|
+
* spawns) WITHOUT mutating the harness's own `process.env` — which is shared by every concurrent
|
|
142
|
+
* job when the harness runs as a native host process. Empty `env` ⇒ `opts` unchanged.
|
|
268
143
|
*/
|
|
269
144
|
function withAgentEnv(opts, env) {
|
|
270
145
|
if (Object.keys(env).length === 0)
|
|
@@ -541,8 +416,6 @@ async function runExploreMode(job, opts) {
|
|
|
541
416
|
// Read-only: it inspects and reports, making no edits — so the no-progress
|
|
542
417
|
// guard's no-edit bound must not fire on its legitimately edit-free run.
|
|
543
418
|
expectsEdits: false,
|
|
544
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
545
|
-
webSearchProxy: job.webSearch,
|
|
546
419
|
contextFiles: job.contextFiles,
|
|
547
420
|
guardLimits: job.guardLimits,
|
|
548
421
|
...agentCapabilities(job),
|
|
@@ -750,8 +623,6 @@ async function runMultiRepoExplore(job, opts) {
|
|
|
750
623
|
sessionToken: job.sessionToken,
|
|
751
624
|
// Read-only: no edits expected, so the no-progress guard's no-edit bound must not fire.
|
|
752
625
|
expectsEdits: false,
|
|
753
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
754
|
-
webSearchProxy: job.webSearch,
|
|
755
626
|
...(job.contextFiles ? { contextFiles: job.contextFiles } : {}),
|
|
756
627
|
guardLimits: job.guardLimits,
|
|
757
628
|
...agentCapabilities(job),
|
|
@@ -856,8 +727,6 @@ export function buildSingleRepoCodingSpec(job, pushBranch) {
|
|
|
856
727
|
proxyPhasePath: job.proxyPhasePath,
|
|
857
728
|
sessionToken: job.sessionToken,
|
|
858
729
|
commitMessage: job.commitMessage ?? job.pr?.title ?? 'Agent changes',
|
|
859
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
860
|
-
webSearchProxy: job.webSearch,
|
|
861
730
|
guardLimits: job.guardLimits,
|
|
862
731
|
...(job.persistentCheckout ? { persistentCheckout: true } : {}),
|
|
863
732
|
...(job.streamFollowUps ? { streamFollowUps: true } : {}),
|
|
@@ -22,7 +22,7 @@ export interface CapturedCommandResult {
|
|
|
22
22
|
* tree on timeout and an aborted run resolves non-zero, so a phase is never what blocks a job
|
|
23
23
|
* from settling.
|
|
24
24
|
*
|
|
25
|
-
* The child inherits the JOB's environment (`RunOptions.agentEnv` layered over
|
|
25
|
+
* The child inherits the JOB's environment (`RunOptions.agentEnv` layered over `agentChildEnv`),
|
|
26
26
|
* not a mutated global: the harness spawns this itself rather than through the agent, so without
|
|
27
27
|
* the explicit merge a native-mode job would run without the private-registry npmrc pointer (and,
|
|
28
28
|
* had this been staged in `process.env`, against a sibling job's state).
|
package/dist/captured-command.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { killChildProcess, spawnDetached } from './process.js';
|
|
3
|
+
import { agentChildEnv } from './agent-env.js';
|
|
3
4
|
import { MAX_CAPTURED_OUTPUT_CHARS, redactSecrets } from './redact.js';
|
|
4
5
|
// The ONE way the harness runs a declared shell command on its own behalf (rather than through
|
|
5
6
|
// the agent) and keeps a bounded, secret-scrubbed record of what it printed.
|
|
@@ -33,7 +34,7 @@ const CAPTURE_MARGIN_CHARS = 512;
|
|
|
33
34
|
* tree on timeout and an aborted run resolves non-zero, so a phase is never what blocks a job
|
|
34
35
|
* from settling.
|
|
35
36
|
*
|
|
36
|
-
* The child inherits the JOB's environment (`RunOptions.agentEnv` layered over
|
|
37
|
+
* The child inherits the JOB's environment (`RunOptions.agentEnv` layered over `agentChildEnv`),
|
|
37
38
|
* not a mutated global: the harness spawns this itself rather than through the agent, so without
|
|
38
39
|
* the explicit merge a native-mode job would run without the private-registry npmrc pointer (and,
|
|
39
40
|
* had this been staged in `process.env`, against a sibling job's state).
|
|
@@ -53,7 +54,7 @@ export async function runCapturedCommand(args) {
|
|
|
53
54
|
cwd,
|
|
54
55
|
detached: spawnDetached,
|
|
55
56
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
56
|
-
env:
|
|
57
|
+
env: agentChildEnv(opts.agentEnv),
|
|
57
58
|
});
|
|
58
59
|
// Keep only the tail (plus the scrub margin); guard against unbounded buffering on a chatty
|
|
59
60
|
// command.
|