@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/src/agent-env.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The environment the harness hands to everything it spawns INTO the agent's checkout: the agent
|
|
2
|
+
// CLI itself, the captured commands (dependency prepopulation, validation checks, the reproduction
|
|
3
|
+
// proof) and the frontend build/serve.
|
|
4
|
+
//
|
|
5
|
+
// The rule this exists for: the harness process and the agent's checkout are two different
|
|
6
|
+
// programs, and a few of the harness's own environment variables are actively wrong for the
|
|
7
|
+
// second. `NODE_ENV=production` is the one that bit: npm reads it as `omit=dev`, so `npm install`
|
|
8
|
+
// in a checkout silently skips devDependencies, leaving the agent with no test runner, no linter
|
|
9
|
+
// and no build tool. One measured coder run spent six of its forty budgeted tool calls
|
|
10
|
+
// discovering and undoing that (install, `npm ls`, `npm config get omit`, reinstall with
|
|
11
|
+
// `--include=dev`, re-check the bin directory, approve an install script) — all of it caused by a
|
|
12
|
+
// variable the platform set, on a project the platform knows nothing about.
|
|
13
|
+
//
|
|
14
|
+
// Stripping it at THIS seam rather than in the image is what makes it true everywhere: the
|
|
15
|
+
// container gets `NODE_ENV=production` from `entrypoint.sh` (so the harness itself still runs in
|
|
16
|
+
// production mode) and the native host transport sets the same variable on the harness process it
|
|
17
|
+
// spawns, so an image-only fix would have left the developer's own machine leaking it.
|
|
18
|
+
//
|
|
19
|
+
// Per-job env NEVER goes through `process.env` (CLAUDE.md, "Harness rules"): the native transport
|
|
20
|
+
// serves every concurrent `ambientAuth` job from one long-lived process, so a mutation here would
|
|
21
|
+
// be a cross-job leak. This function only READS the process env and returns a fresh object.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Variables of the HARNESS PROCESS that must not reach the agent's checkout.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately short, and it stays short: the bar is a variable whose value is a fact about the
|
|
27
|
+
* harness that a tool in the checkout will silently act on. It is not a sandbox (an agent can set
|
|
28
|
+
* whatever it likes in its own shell) and not a secret filter (the harness holds per-job secrets
|
|
29
|
+
* in `agentEnv`, never in `process.env`).
|
|
30
|
+
*/
|
|
31
|
+
export const HARNESS_ONLY_ENV_NAMES: readonly string[] = ['NODE_ENV']
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The child env for a command run in the agent's checkout: the harness's own environment minus
|
|
35
|
+
* {@link HARNESS_ONLY_ENV_NAMES}, with each layer merged over it in order.
|
|
36
|
+
*
|
|
37
|
+
* A layer may still SET a stripped name — a job that explicitly asks for `NODE_ENV` gets it. The
|
|
38
|
+
* strip removes what was merely INHERITED, which is the thing nobody chose.
|
|
39
|
+
*/
|
|
40
|
+
export function agentChildEnv(
|
|
41
|
+
...layers: (Record<string, string | undefined> | undefined)[]
|
|
42
|
+
): NodeJS.ProcessEnv {
|
|
43
|
+
const env: NodeJS.ProcessEnv = { ...process.env }
|
|
44
|
+
for (const name of HARNESS_ONLY_ENV_NAMES) delete env[name]
|
|
45
|
+
for (const layer of layers) {
|
|
46
|
+
if (layer) Object.assign(env, layer)
|
|
47
|
+
}
|
|
48
|
+
return env
|
|
49
|
+
}
|
package/src/agent-runner.ts
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 {
|
|
@@ -25,21 +23,22 @@ import {
|
|
|
25
23
|
} from './pi.js'
|
|
26
24
|
import type { PiRunStats } from './pi-reduction.js'
|
|
27
25
|
import {
|
|
28
|
-
claudeAllowedToolPatterns,
|
|
29
|
-
mcpServerSecretValues,
|
|
30
26
|
observeClaudeMcpInit,
|
|
31
|
-
writeClaudeMcpConfig,
|
|
32
27
|
type McpServerSpec,
|
|
33
28
|
type ObservedMcpServer,
|
|
34
29
|
type SkillSpec,
|
|
35
30
|
} from './agent-capabilities.js'
|
|
31
|
+
import { openClaudeRunHome } from './claude-home.js'
|
|
36
32
|
import { codexImageGapNote, createCodexHome, disposeCodexHome } from './codex-home.js'
|
|
37
|
-
import {
|
|
33
|
+
import type { ProgressGuardLimits } from './progress-guard.js'
|
|
34
|
+
import { createClaudeProgressGuard } from './guard-driver.js'
|
|
35
|
+
import type { WorkspaceProbe } from './workspace-probe.js'
|
|
38
36
|
import { BoundedTail, JsonlLineReader } from './jsonl-stream.js'
|
|
39
37
|
import { killChildProcess, spawnDetached } from './process.js'
|
|
38
|
+
import { agentChildEnv } from './agent-env.js'
|
|
40
39
|
import { abortReasonOf } from './failure.js'
|
|
41
40
|
import { describeProcessExit } from './process-exit.js'
|
|
42
|
-
import { redact,
|
|
41
|
+
import { redact, secretsToRedact } from './redact.js'
|
|
43
42
|
import { createSliceTracker, startSubagentWatcher, type SliceReview } from './subagents.js'
|
|
44
43
|
import {
|
|
45
44
|
createTaskPlanTracker,
|
|
@@ -49,8 +48,7 @@ import {
|
|
|
49
48
|
toProgress,
|
|
50
49
|
todosToProgress,
|
|
51
50
|
} from './progress.js'
|
|
52
|
-
import {
|
|
53
|
-
import { retainSessionTranscripts } from './transcript-retention.js'
|
|
51
|
+
import { assertClaudeToolsCurrent, claudeCliArgs, CLAUDE_TOOL_SET } from './claude-cli.js'
|
|
54
52
|
|
|
55
53
|
// The alternate (subscription) harness runners. The Pi harness reaches models
|
|
56
54
|
// through the LLM proxy with a model-locked session token; the Claude Code and
|
|
@@ -129,7 +127,7 @@ export interface SubscriptionRunOptions {
|
|
|
129
127
|
generateImages?: boolean
|
|
130
128
|
/**
|
|
131
129
|
* Extra environment for the CLI child, scoped to this job (the tester's secrets, a
|
|
132
|
-
* private-registry npmrc pointer). Merged over the inherited
|
|
130
|
+
* private-registry npmrc pointer). Merged over the inherited env at spawn (`agentChildEnv`), so the
|
|
133
131
|
* agent and its shell tools see them without the harness mutating its OWN environment — which
|
|
134
132
|
* is shared by every concurrent job under the native host-process transport. See
|
|
135
133
|
* `RunOptions.agentEnv`.
|
|
@@ -148,6 +146,14 @@ export interface SubscriptionRunOptions {
|
|
|
148
146
|
guardLimits?: ProgressGuardLimits
|
|
149
147
|
/** Whether this run is expected to edit files (false for assess-only runs); gates the no-edit bound. */
|
|
150
148
|
expectsEdits?: boolean
|
|
149
|
+
/**
|
|
150
|
+
* Probes the working tree for evidence the agent changed the repository. The guard's no-edit
|
|
151
|
+
* bound asks that question and can only see TOOL NAMES, so an agent writing files through
|
|
152
|
+
* `bash` reads as making no edits at all; this is what settles it before anything is killed.
|
|
153
|
+
* Injected so the guard stays pure, and consulted at most once per run (only when the bound is
|
|
154
|
+
* about to abort). Omitted ⇒ the bound falls back to its tool-name-only judgement.
|
|
155
|
+
*/
|
|
156
|
+
workspaceProbe?: WorkspaceProbe
|
|
151
157
|
/** Called on every chunk of CLI output, so the watchdog sees the agent is alive. */
|
|
152
158
|
onActivity?: () => void
|
|
153
159
|
/** Called with the latest subtask counts each time the CLI updates its todo/plan list. */
|
|
@@ -234,7 +240,7 @@ function streamCli(
|
|
|
234
240
|
}
|
|
235
241
|
const child = spawn(command, args, {
|
|
236
242
|
cwd: opts.cwd,
|
|
237
|
-
env:
|
|
243
|
+
env: agentChildEnv(env),
|
|
238
244
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
239
245
|
// Own process group (POSIX) so killChildProcess reaps the CLI's grandchildren too.
|
|
240
246
|
detached: spawnDetached,
|
|
@@ -470,82 +476,6 @@ export function carryClaudeSystemPrompt(
|
|
|
470
476
|
// Claude Code
|
|
471
477
|
// ---------------------------------------------------------------------------
|
|
472
478
|
|
|
473
|
-
/**
|
|
474
|
-
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
475
|
-
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
476
|
-
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
477
|
-
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
478
|
-
* onto the summary + usage.
|
|
479
|
-
*/
|
|
480
|
-
/**
|
|
481
|
-
* Write a repo-sourced skill as a NATIVE Claude Code skill under `<skillsRoot>/<name>/`: a
|
|
482
|
-
* `SKILL.md` (YAML frontmatter `name`/`description` + the instructions body, the format the CLI
|
|
483
|
-
* expects) plus every resource file at its path within the skill directory. Resource sub-paths
|
|
484
|
-
* were sanitized at the job boundary (no traversal), so nested dirs are created as needed.
|
|
485
|
-
*
|
|
486
|
-
* The frontmatter `name`/`description` values are emitted as JSON-encoded (double-quoted) YAML
|
|
487
|
-
* scalars, not bare plain scalars: an author's description routinely contains `: ` (colon-space)
|
|
488
|
-
* or a leading YAML indicator (`#`, `-`, `[`, `{`, `"`, …), which is invalid as a plain scalar and
|
|
489
|
-
* would make the CLI fail to parse the frontmatter and silently skip the skill. A JSON string is a
|
|
490
|
-
* valid YAML double-quoted scalar, so quoting makes the manifest robust to arbitrary text.
|
|
491
|
-
*/
|
|
492
|
-
async function writeNativeSkill(skillsRoot: string, skill: SkillSpec): Promise<void> {
|
|
493
|
-
const dir = join(skillsRoot, skill.name)
|
|
494
|
-
await mkdir(dir, { recursive: true })
|
|
495
|
-
const name = JSON.stringify(skill.name)
|
|
496
|
-
const description = JSON.stringify(skill.description.replace(/\r?\n/g, ' '))
|
|
497
|
-
const frontmatter = `---\nname: ${name}\ndescription: ${description}\n---\n`
|
|
498
|
-
await writeFile(join(dir, 'SKILL.md'), `${frontmatter}\n${skill.instructions}\n`, 'utf8')
|
|
499
|
-
for (const resource of skill.resources) {
|
|
500
|
-
const dest = join(dir, resource.relPath)
|
|
501
|
-
await mkdir(dirname(dest), { recursive: true })
|
|
502
|
-
await writeFile(dest, resource.content, 'utf8')
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Prepare the Claude Code CLI's MCP wiring for one run: write the servers to a PER-RUN config and
|
|
508
|
-
* return the argv that points the CLI at it, plus the cleanup for a directory we had to mint.
|
|
509
|
-
*
|
|
510
|
-
* Two decisions live here. `--strict-mcp-config` makes that file the ONLY source of servers, so an
|
|
511
|
-
* ambient run on a developer's own machine can never silently hand the agent their personal ones.
|
|
512
|
-
* And `--allowedTools` is passed ONLY when a server actually narrows its tools — an allow-list is
|
|
513
|
-
* whole-session, not MCP-scoped, so `claudeAllowedToolPatterns` re-grants the CLI's built-in
|
|
514
|
-
* file/bash tools in the same list; see it for why that holds whichever way the run's permission
|
|
515
|
-
* mode treats an allow-list.
|
|
516
|
-
*
|
|
517
|
-
* The config carries this job's resolved credentials, so it goes in the isolated config home when
|
|
518
|
-
* we own one and a throwaway per-JOB directory otherwise — never the checkout (it would land in a
|
|
519
|
-
* commit) and never a shared HOME path (a concurrent job would clobber it).
|
|
520
|
-
*/
|
|
521
|
-
async function setUpClaudeMcp(
|
|
522
|
-
servers: McpServerSpec[] | undefined,
|
|
523
|
-
configHome: string | undefined,
|
|
524
|
-
): Promise<{ args: string[]; cleanup: () => Promise<void> }> {
|
|
525
|
-
const noop = { args: [], cleanup: async () => {} }
|
|
526
|
-
if (!servers?.length) return noop
|
|
527
|
-
// Before anything can spawn: a failing MCP server echoes its own argv/headers into stderr, and
|
|
528
|
-
// that tail is carried onto the step's diagnostics.
|
|
529
|
-
registerKnownSecrets(mcpServerSecretValues(servers))
|
|
530
|
-
const home = configHome ?? (await mkdtemp(join(tmpdir(), 'cf-claude-mcp-')))
|
|
531
|
-
const owned = home === configHome ? undefined : home
|
|
532
|
-
const cleanup = async (): Promise<void> => {
|
|
533
|
-
if (owned) await rm(owned, { recursive: true, force: true }).catch(() => {})
|
|
534
|
-
}
|
|
535
|
-
const configPath = await writeClaudeMcpConfig(home, servers)
|
|
536
|
-
if (!configPath) return { args: [], cleanup }
|
|
537
|
-
const allowedTools = claudeAllowedToolPatterns(servers)
|
|
538
|
-
return {
|
|
539
|
-
args: [
|
|
540
|
-
'--mcp-config',
|
|
541
|
-
configPath,
|
|
542
|
-
'--strict-mcp-config',
|
|
543
|
-
...(allowedTools?.length ? ['--allowedTools', allowedTools.join(',')] : []),
|
|
544
|
-
],
|
|
545
|
-
cleanup,
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
479
|
/**
|
|
550
480
|
* The LIVE publishers of a claude-code run: everything the stream has revealed so far that the
|
|
551
481
|
* backend should see before the run ends, rather than only in its terminal result.
|
|
@@ -607,56 +537,6 @@ function reportToolServerStartup(
|
|
|
607
537
|
if (observed) onToolServers(observed)
|
|
608
538
|
}
|
|
609
539
|
|
|
610
|
-
/**
|
|
611
|
-
* No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
612
|
-
* which cannot see the CLI's internal turns. The caller remembers each `tool_use` id's name off
|
|
613
|
-
* the assistant turn (`rememberTool`) and hands the following user turn's content to `feedGuard`,
|
|
614
|
-
* which pairs each `tool_result`'s `is_error` with that name. The FIRST reason trips it: the
|
|
615
|
-
* diagnostic is recorded (readable via `reason()`, which the catch surfaces over the generic abort
|
|
616
|
-
* message) and `guardAbort` fires — folded into streamCli's signal so a tripped guard kills the CLI
|
|
617
|
-
* the same way the external watchdog does. Disabled when the caller supplies no limits (only the
|
|
618
|
-
* external watchdog then bounds the run).
|
|
619
|
-
*
|
|
620
|
-
* Split out of {@link runClaudeCode} for the per-function line budget.
|
|
621
|
-
*/
|
|
622
|
-
function createClaudeProgressGuard(opts: SubscriptionRunOptions): {
|
|
623
|
-
rememberTool: (id: string, name: string) => void
|
|
624
|
-
feedGuard: (content: unknown[]) => void
|
|
625
|
-
guardAbort: AbortController
|
|
626
|
-
reason: () => string | undefined
|
|
627
|
-
} {
|
|
628
|
-
const guard = opts.guardLimits
|
|
629
|
-
? new ProgressGuard(opts.guardLimits, opts.expectsEdits ?? true)
|
|
630
|
-
: undefined
|
|
631
|
-
const toolNames = new Map<string, string>()
|
|
632
|
-
const guardAbort = new AbortController()
|
|
633
|
-
let guardReason: string | undefined
|
|
634
|
-
|
|
635
|
-
const feedGuard = (content: unknown[]): void => {
|
|
636
|
-
if (!guard || guardReason) return
|
|
637
|
-
for (const block of content) {
|
|
638
|
-
if (!isObject(block) || block.type !== 'tool_result') continue
|
|
639
|
-
const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined
|
|
640
|
-
const name = id ? toolNames.get(id) : undefined
|
|
641
|
-
if (id) toolNames.delete(id)
|
|
642
|
-
if (!name) continue
|
|
643
|
-
const reason = guard.observeSignal({ name, isError: block.is_error === true })
|
|
644
|
-
if (reason) {
|
|
645
|
-
guardReason = reason
|
|
646
|
-
guardAbort.abort()
|
|
647
|
-
return
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
return {
|
|
653
|
-
rememberTool: (id, name) => toolNames.set(id, name),
|
|
654
|
-
feedGuard,
|
|
655
|
-
guardAbort,
|
|
656
|
-
reason: () => guardReason,
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
540
|
/**
|
|
661
541
|
* The run's TRAJECTORY, on the claude-code stream: each `tool_use` block paired with the
|
|
662
542
|
* `tool_result` that answers it on the following user turn, numbered and captured (scrubbed +
|
|
@@ -775,6 +655,13 @@ function openClaudeCallCapture(
|
|
|
775
655
|
}
|
|
776
656
|
}
|
|
777
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
660
|
+
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
661
|
+
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
662
|
+
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
663
|
+
* onto the summary + usage.
|
|
664
|
+
*/
|
|
778
665
|
export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRunOutcome> {
|
|
779
666
|
const stats: PiRunStats = { toolCalls: 0, assistantChars: 0 }
|
|
780
667
|
let summary = ''
|
|
@@ -793,6 +680,10 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
793
680
|
})
|
|
794
681
|
}
|
|
795
682
|
|
|
683
|
+
// The built-in tools this run declares, named ONCE: the same list rides `--tools` and the
|
|
684
|
+
// `--allowedTools` re-grant, which is additive rather than inert (see `claudeAllowedToolPatterns`).
|
|
685
|
+
const tools = CLAUDE_TOOL_SET
|
|
686
|
+
|
|
796
687
|
const secrets = opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : []
|
|
797
688
|
const capture = openClaudeCallCapture(opts, { prompt, folded, secrets })
|
|
798
689
|
const telemetry = capture.telemetry
|
|
@@ -837,6 +728,9 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
837
728
|
const onEvent = (event: Record<string, unknown>, meta?: { final?: boolean }): void => {
|
|
838
729
|
const type = event.type
|
|
839
730
|
reportToolServerStartup(event, opts.onToolServers)
|
|
731
|
+
// The same startup event answers what the CLI granted of what we asked for; a capability it
|
|
732
|
+
// named no tool for is a silent capability loss otherwise (see `assertClaudeToolsCurrent`).
|
|
733
|
+
assertClaudeToolsCurrent(event, tools, opts.log)
|
|
840
734
|
// A subagent's turns ride the parent's stdout tagged with the dispatch that spawned them;
|
|
841
735
|
// `telemetry` routes them off the parent's chain (and decides who bills them). Progress, slice
|
|
842
736
|
// tracking, the guard and `stats` below deliberately see EVERY event: a subagent grinding on
|
|
@@ -892,7 +786,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
892
786
|
}
|
|
893
787
|
}
|
|
894
788
|
|
|
895
|
-
const home = await openClaudeRunHome(opts)
|
|
789
|
+
const home = await openClaudeRunHome(opts, tools)
|
|
896
790
|
const { configHome } = home
|
|
897
791
|
|
|
898
792
|
// ADR 0026 D3 (path corrected by ADR 0027 Defect A): while the run is live, tail the CLI's
|
|
@@ -926,22 +820,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
926
820
|
const { stderrTail } = await streamCli(
|
|
927
821
|
{
|
|
928
822
|
command: 'claude',
|
|
929
|
-
args:
|
|
930
|
-
'-p',
|
|
931
|
-
'--output-format',
|
|
932
|
-
'stream-json',
|
|
933
|
-
'--verbose',
|
|
934
|
-
// The per-run container IS the sandbox, and the run is fully headless (no one
|
|
935
|
-
// to approve a tool call) — so bypass permissions entirely. `acceptEdits`
|
|
936
|
-
// would auto-accept file edits but still gate Bash, which in `-p` mode is then
|
|
937
|
-
// denied, leaving the agent unable to run builds/tests/git to verify its work.
|
|
938
|
-
'--permission-mode',
|
|
939
|
-
'bypassPermissions',
|
|
940
|
-
'--model',
|
|
941
|
-
opts.model,
|
|
942
|
-
...home.mcpArgs,
|
|
943
|
-
...appendArgs,
|
|
944
|
-
],
|
|
823
|
+
args: claudeCliArgs({ model: opts.model, tools, mcpArgs: home.mcpArgs, appendArgs }),
|
|
945
824
|
},
|
|
946
825
|
prompt,
|
|
947
826
|
{ ...opts, signal: runSignal },
|
|
@@ -998,118 +877,6 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
998
877
|
}
|
|
999
878
|
}
|
|
1000
879
|
|
|
1001
|
-
/**
|
|
1002
|
-
* The isolated, per-run home the `claude` CLI runs against: a temp config dir OUTSIDE the cloned
|
|
1003
|
-
* checkout, pre-seeded past the first-launch prompts, carrying the run's native skills and MCP
|
|
1004
|
-
* config, plus the child env pointing the CLI at it. {@link ClaudeRunHome.dispose} is the other
|
|
1005
|
-
* half of the same concern — the leased credential must never outlive the run — so acquisition
|
|
1006
|
-
* and teardown are defined together rather than split across a `finally` forty lines away.
|
|
1007
|
-
*
|
|
1008
|
-
* Ambient (native) mode has NO home: the developer's installed CLI uses its own `~/.claude`
|
|
1009
|
-
* login, so nothing is created, nothing is pre-seeded, and `dispose` only clears the MCP config.
|
|
1010
|
-
*/
|
|
1011
|
-
interface ClaudeRunHome {
|
|
1012
|
-
/** The per-run config dir; `undefined` in ambient mode (the developer's own login is used). */
|
|
1013
|
-
configHome: string | undefined
|
|
1014
|
-
/** The CLI argv selecting the run's tool servers; empty when it has none. */
|
|
1015
|
-
mcpArgs: string[]
|
|
1016
|
-
/** The child-process env (see {@link buildClaudeEnv}). */
|
|
1017
|
-
env: Record<string, string>
|
|
1018
|
-
dispose: () => Promise<void>
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
async function openClaudeRunHome(opts: SubscriptionRunOptions): Promise<ClaudeRunHome> {
|
|
1022
|
-
// Native (ambient) mode: run the developer's installed `claude` with its OWN login —
|
|
1023
|
-
// no isolated config home, no injected credential, no onboarding pre-seed. Otherwise,
|
|
1024
|
-
// Claude Code persists user config/credentials under its config dir; point that at an
|
|
1025
|
-
// isolated, per-run temp dir OUTSIDE the cloned checkout (`opts.cwd`). Otherwise the
|
|
1026
|
-
// agents that finish with `git add -A` (blueprint/requirements/bootstrap) could stage a
|
|
1027
|
-
// stray `.claude/` directory — and any cached credential in it — into the pushed branch.
|
|
1028
|
-
// Mirrors the Codex CODEX_HOME isolation below; removed by `dispose`.
|
|
1029
|
-
if (!opts.ambientAuth && !opts.subscriptionToken) {
|
|
1030
|
-
throw new Error('claude-code harness requires a subscription token (or ambientAuth)')
|
|
1031
|
-
}
|
|
1032
|
-
const configHome = opts.ambientAuth ? undefined : await mkdtemp(join(tmpdir(), 'cf-claude-'))
|
|
1033
|
-
|
|
1034
|
-
// The config dir is brand-new every run, so Claude Code would otherwise treat this
|
|
1035
|
-
// as a first launch and BLOCK on the interactive onboarding / "trust this folder" /
|
|
1036
|
-
// bypass-permissions acknowledgement prompts — which never get answered headlessly,
|
|
1037
|
-
// hanging the job until the watchdog kills it. Pre-seed the config that marks those
|
|
1038
|
-
// as already accepted so `-p` starts straight into the run. Best-effort: written
|
|
1039
|
-
// before the CLI starts; unknown keys are harmless if a CLI version ignores them.
|
|
1040
|
-
// (Ambient mode skips this — the developer's own config is already onboarded.)
|
|
1041
|
-
// ADR 0026 D4: assert the pinned onboarding keys landed and log them with the CLI
|
|
1042
|
-
// version, so a future first-run gate this set doesn't cover (which looks identical to
|
|
1043
|
-
// a healthy-but-quiet subagent start) is diffable when the cold-start watchdog fires.
|
|
1044
|
-
if (configHome) {
|
|
1045
|
-
await writeOnboardingPreseed(configHome)
|
|
1046
|
-
await assertOnboardingKeysCurrent(configHome, process.env.CLAUDE_CLI_VERSION, opts.log)
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// Skills: install each as a native skill under the config dir's `skills/<name>/` so the CLI
|
|
1050
|
-
// discovers and can invoke it. ONLY into the isolated per-run config home — never the
|
|
1051
|
-
// developer's own `~/.claude` (ambient/native mode), where it would persist in their personal
|
|
1052
|
-
// setup after the run and two concurrent jobs carrying same-named skills would clobber each
|
|
1053
|
-
// other. An ambient run reads the skills from the checkout instead (`.cat-context/skill/<name>/`,
|
|
1054
|
-
// materialised by the caller). Best-effort: a write failure must not wedge the run — the prompt
|
|
1055
|
-
// still names the skills.
|
|
1056
|
-
if (configHome) {
|
|
1057
|
-
for (const skill of opts.skills ?? []) {
|
|
1058
|
-
await writeNativeSkill(join(configHome, 'skills'), skill).catch(() => {})
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Tool servers (MCP): the CLI is pointed at a per-run config rather than discovering an ambient
|
|
1063
|
-
// one. See `setUpClaudeMcp` for why that matters and what has to be cleaned up afterwards.
|
|
1064
|
-
const mcp = await setUpClaudeMcp(opts.mcpServers, configHome)
|
|
1065
|
-
|
|
1066
|
-
return {
|
|
1067
|
-
configHome,
|
|
1068
|
-
mcpArgs: mcp.args,
|
|
1069
|
-
env: buildClaudeEnv(opts, configHome),
|
|
1070
|
-
dispose: async () => {
|
|
1071
|
-
// The ambient-mode MCP config dir (credential-bearing) never outlives the run.
|
|
1072
|
-
await mcp.cleanup()
|
|
1073
|
-
if (!configHome) return
|
|
1074
|
-
// Lift the CLI session transcripts (`projects/`) out for short-lived retention BEFORE the
|
|
1075
|
-
// home is deleted — the credential lives at the home root, never in `projects/`, so this
|
|
1076
|
-
// keeps the debugging artifact without leaking the token. Best-effort; never throws.
|
|
1077
|
-
await retainSessionTranscripts(configHome, ['projects'], {
|
|
1078
|
-
label: 'claude-code',
|
|
1079
|
-
...(opts.log ? { log: opts.log } : {}),
|
|
1080
|
-
})
|
|
1081
|
-
// Never leave the config dir (and any cached credential) on disk past the run.
|
|
1082
|
-
await rm(configHome, { recursive: true, force: true }).catch(() => {})
|
|
1083
|
-
},
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
/**
|
|
1088
|
-
* Build the child-process env for the `claude` CLI: an isolated config home plus subscription
|
|
1089
|
-
* auth (Anthropic OAuth token, or an Anthropic-compatible base URL + auth token for a
|
|
1090
|
-
* non-Anthropic Claude-Code vendor like GLM/Kimi/DeepSeek), or an empty env in ambient mode
|
|
1091
|
-
* (the developer's own logged-in `~/.claude` is used). Extracted from {@link runClaudeCode} to
|
|
1092
|
-
* keep its cyclomatic complexity down; behaviour is a straight move of the original expression.
|
|
1093
|
-
*/
|
|
1094
|
-
function buildClaudeEnv(
|
|
1095
|
-
opts: SubscriptionRunOptions,
|
|
1096
|
-
configHome: string | undefined,
|
|
1097
|
-
): Record<string, string> {
|
|
1098
|
-
// The job-scoped env rides along in BOTH modes; the credential/config vars below are what
|
|
1099
|
-
// ambient mode drops (the developer's own logged-in `~/.claude` is used instead).
|
|
1100
|
-
if (opts.ambientAuth) return { ...opts.extraEnv }
|
|
1101
|
-
return {
|
|
1102
|
-
...opts.extraEnv,
|
|
1103
|
-
CLAUDE_CONFIG_DIR: configHome!,
|
|
1104
|
-
...(opts.subscriptionBaseUrl
|
|
1105
|
-
? {
|
|
1106
|
-
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
1107
|
-
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken!,
|
|
1108
|
-
}
|
|
1109
|
-
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken! }),
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
880
|
/**
|
|
1114
881
|
* Merge the parent-loop telemetry with the subagents' out-of-band usage + per-call metrics into
|
|
1115
882
|
* the run outcome. INVARIANT (do not "fix" this into a double count): the run total is the parent
|
|
@@ -1296,6 +1063,12 @@ export async function runCodex(opts: SubscriptionRunOptions): Promise<PiRunOutco
|
|
|
1296
1063
|
'exec',
|
|
1297
1064
|
'--json',
|
|
1298
1065
|
'--skip-git-repo-check',
|
|
1066
|
+
// No `--tools` analogue here, and its absence is a FINDING rather than an oversight:
|
|
1067
|
+
// codex has no flag that declares a built-in tool set, because it has no set to choose
|
|
1068
|
+
// from. Its surface is shell + apply_patch + the plan tool, and the optional extras are
|
|
1069
|
+
// individual `CODEX_HOME/config.toml` switches the harness already sets deliberately
|
|
1070
|
+
// (`[features] image_generation`, see `codex-home.ts`). So there is nothing here that
|
|
1071
|
+
// silently drifts with a CLI version the way claude-code's headless default did.
|
|
1299
1072
|
// The per-run container IS the sandbox; let Codex write files and reach the
|
|
1300
1073
|
// vendor unrestricted, with no approval prompts (the run is headless).
|
|
1301
1074
|
'--dangerously-bypass-approvals-and-sandbox',
|
package/src/agent-shared.ts
CHANGED
|
@@ -18,11 +18,18 @@ export function mergeEffort(
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The agent-capability fields (skills, tool servers, reference designs) every
|
|
22
|
-
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
23
|
-
* cannot silently be the one that drops a kind's declared playbook, tool server
|
|
24
|
-
* gallery: the failure mode is invisible (the agent simply works without
|
|
25
|
-
* up as degraded output.
|
|
21
|
+
* The agent-capability fields (skills, tool servers, reference designs, web research) every
|
|
22
|
+
* agent-running flow forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
23
|
+
* spread, so a flow cannot silently be the one that drops a kind's declared playbook, tool server,
|
|
24
|
+
* reference gallery or web access: the failure mode is invisible (the agent simply works without
|
|
25
|
+
* it) and would only show up as degraded output.
|
|
26
|
+
*
|
|
27
|
+
* Web research joined the helper after the conflict-resolver and bootstrap flows were found to be
|
|
28
|
+
* forwarding neither half of it: both build their own spec literal, and the two web fields were
|
|
29
|
+
* hand-written at the four sites that remembered them. That is exactly the drift this helper
|
|
30
|
+
* exists to make unrepresentable, so they are read here rather than at each call site. Both halves
|
|
31
|
+
* travel together on purpose: the guidance NAMES the tools, so a flow carrying one without the
|
|
32
|
+
* other either describes tools the run was never given or hands it tools nothing introduced.
|
|
26
33
|
*/
|
|
27
34
|
export function agentCapabilities(job: AgentJob): {
|
|
28
35
|
skills?: SkillSpec[]
|
|
@@ -30,6 +37,8 @@ export function agentCapabilities(job: AgentJob): {
|
|
|
30
37
|
generateImages?: boolean
|
|
31
38
|
referenceScreenshots?: ImageManifestSpec
|
|
32
39
|
designImages?: ImageManifestSpec
|
|
40
|
+
webSearchProxy?: boolean
|
|
41
|
+
webToolsGuidance?: string
|
|
33
42
|
} {
|
|
34
43
|
return {
|
|
35
44
|
...(job.skills?.length ? { skills: job.skills } : {}),
|
|
@@ -37,5 +46,7 @@ export function agentCapabilities(job: AgentJob): {
|
|
|
37
46
|
...(job.generateImages ? { generateImages: true } : {}),
|
|
38
47
|
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
39
48
|
...(job.designImages ? { designImages: job.designImages } : {}),
|
|
49
|
+
...(job.webSearch ? { webSearchProxy: true } : {}),
|
|
50
|
+
...(job.webToolsGuidance ? { webToolsGuidance: job.webToolsGuidance } : {}),
|
|
40
51
|
}
|
|
41
52
|
}
|