@cat-factory/executor-harness 1.80.0 → 1.84.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.
Files changed (49) hide show
  1. package/README.md +1 -0
  2. package/dist/agent-capabilities.d.ts +130 -0
  3. package/dist/agent-runner.d.ts +114 -0
  4. package/dist/agent-runner.js +50 -32
  5. package/dist/agent-shared.d.ts +18 -0
  6. package/dist/agent.d.ts +66 -0
  7. package/dist/bootstrap-mode.d.ts +20 -0
  8. package/dist/captured-command.d.ts +58 -0
  9. package/dist/claude-call-aggregator.d.ts +164 -0
  10. package/dist/claude-call-aggregator.js +123 -17
  11. package/dist/claude-stream.d.ts +56 -0
  12. package/dist/coding-agent.d.ts +252 -0
  13. package/dist/coding-agent.js +69 -50
  14. package/dist/dependency-install.d.ts +111 -0
  15. package/dist/effort.d.ts +19 -0
  16. package/dist/embed.d.ts +4 -0
  17. package/dist/failure.d.ts +42 -0
  18. package/dist/follow-ups.d.ts +28 -0
  19. package/dist/frontend-infra.d.ts +25 -0
  20. package/dist/fs-utils.d.ts +2 -0
  21. package/dist/git.d.ts +394 -0
  22. package/dist/host-markdown.d.ts +28 -0
  23. package/dist/inline.d.ts +10 -0
  24. package/dist/job.d.ts +666 -0
  25. package/dist/logger.d.ts +16 -0
  26. package/dist/onboarding-preseed.d.ts +24 -0
  27. package/dist/package-registries.d.ts +32 -0
  28. package/dist/pi-workspace.d.ts +194 -0
  29. package/dist/pi.d.ts +475 -0
  30. package/dist/pr-description.d.ts +85 -0
  31. package/dist/pr-template.d.ts +101 -0
  32. package/dist/process-exit.d.ts +7 -0
  33. package/dist/process.d.ts +19 -0
  34. package/dist/progress-guard.d.ts +88 -0
  35. package/dist/progress.d.ts +87 -0
  36. package/dist/redact.d.ts +31 -0
  37. package/dist/reproduction-proof.d.ts +224 -0
  38. package/dist/runner.d.ts +282 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/structured-output.d.ts +75 -0
  41. package/dist/subagents.d.ts +88 -0
  42. package/dist/transcript-retention.d.ts +21 -0
  43. package/dist/validation-checks.d.ts +159 -0
  44. package/dist/vcs-api.d.ts +73 -0
  45. package/dist/version.d.ts +2 -0
  46. package/package.json +9 -5
  47. package/src/agent-runner.ts +54 -29
  48. package/src/claude-call-aggregator.ts +181 -32
  49. package/src/coding-agent.ts +80 -49
package/README.md CHANGED
@@ -206,6 +206,7 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
206
206
  | `src/embed.ts` | Bundled assets/templates written into the workspace. |
207
207
  | `src/package-registries.ts` | Private-registry (npm) auth: renders the job's allowlisted entries into an npmrc — the user `~/.npmrc` in a container, a per-job file pointed at by `npm_config_userconfig` for a native job. |
208
208
  | `src/agent-runner.ts` | The subscription-harness runners (`runClaudeCode` / `runCodex`) — talk direct to the vendor with a leased OAuth token, lift per-turn usage/telemetry off the CLI event stream. |
209
+ | `src/claude-call-aggregator.ts` | Folds Claude Code's per-CONTENT-BLOCK `stream-json` envelopes back into the model calls they belong to (by `message.id`), reconstructs each call's request transcript, and routes subagent turns off the parent's chain. **Exported as the `./claude-call-aggregator` subpath and driven by the BACKEND too** (`runtimes/local`, for an inline step running on the developer's host `claude`), so it stays the ONE implementation — the per-envelope over-count it fixes inflated a measured 1.47M tokens to 5.53M, and both drivers have to learn that only once. That second driver is why the transcript is retained only to `MAX_TRANSCRIPT_CHARS` (stating what it stopped retaining) and why assembling bodies at all is a `bodies` switch: in a container the reconstruction is one job's memory in a box sized for it, in the backend it is per concurrent inline step in the orchestrator process. Unlike the compile-only `./embed`, this subpath is a `dist` import, which is why the package emits declarations — and why a consumer's typecheck depends on Turbo's `^build` edge having built this package first (see `tsconfig.json`'s `comment:buildOrder`). |
209
210
  | `src/transcript-retention.ts` | Lifts the CLI session transcripts (`projects/` / `sessions/`) out of the isolated, credential-bearing config home before it is deleted, and prunes them on a TTL (debugging artifact retention). |
210
211
  | `src/captured-command.ts` | The one way the harness runs a declared shell command on its own behalf: `sh -c` with a per-command watchdog, abort handling, conventional exit codes (124/127/130) and a scrub-then-bound output capture. Shared by both pre-PR verification phases so a fix to one cannot miss the other. |
211
212
  | `src/dependency-install.ts` | Dependency prepopulation: `prepopulateDependencies` is the ONE seam every checkout-having mode calls — it runs the service's install command before the agent's first turn, excludes what the install materialised from git so no `git add -A` can sweep a dependency tree into the PR, and builds the prompt note describing the outcome. Best-effort — every failure shape becomes a note, never a failed job. Generic — keyed off the job body, never the agent kind. |
@@ -0,0 +1,130 @@
1
+ /** One materialisable resource file of a skill. */
2
+ export interface SkillResourceSpec {
3
+ /** Path within the skill directory, e.g. `templates/report.md` (subdirs preserved, no traversal). */
4
+ relPath: string;
5
+ content: string;
6
+ }
7
+ /**
8
+ * A skill to make available for a run. Materialised HARNESS-AWARE:
9
+ * `CLAUDE_CONFIG_DIR/skills/<name>/SKILL.md` (+ resources) for the claude-code CLI to load
10
+ * natively, or `.cat-context/skill/<name>/<relPath>` for the Pi/codex checkout (their prompt
11
+ * carries the instructions). A dedicated top-level body field, never a context file.
12
+ */
13
+ export interface SkillSpec {
14
+ name: string;
15
+ description: string;
16
+ instructions: string;
17
+ resources: SkillResourceSpec[];
18
+ }
19
+ /**
20
+ * A tool server (MCP) to wire into the agent CLI for this run, with its transport and any
21
+ * credentials the backend resolved. Only the CLIs that speak MCP receive these; the backend has
22
+ * already dropped anything this harness cannot serve, so the harness never has to decide.
23
+ *
24
+ * The values here are SECRET-BEARING (`env` / `headers` carry resolved credentials), which is why
25
+ * the config files this module writes always live outside the checkout and are never logged.
26
+ */
27
+ export interface McpServerSpec {
28
+ /** The server name the CLI exposes tools under (`mcp__<id>__<tool>`). Id-safe by construction. */
29
+ id: string;
30
+ transport: 'stdio' | 'http';
31
+ command?: string;
32
+ args?: string[];
33
+ env?: Record<string, string>;
34
+ url?: string;
35
+ headers?: Record<string, string>;
36
+ /** Bare tool names the agent may call. Absent ⇒ every tool the server exposes. */
37
+ allowedTools?: string[];
38
+ /**
39
+ * Which keys of `env` / `headers` hold a RESOLVED CREDENTIAL rather than declared configuration.
40
+ * {@link mcpServerSecretValues} reads exactly these for redaction — scrubbing the whole map
41
+ * instead would turn every later occurrence of an ordinary config string into `***`.
42
+ */
43
+ secretKeys?: string[];
44
+ }
45
+ /**
46
+ * The credential values carried by a run's tool servers, for {@link registerKnownSecrets}. An MCP
47
+ * server that fails to start routinely echoes its own argv or request headers into stderr, and
48
+ * that tail reaches the step's diagnostics — so these have to be scrubbed exactly like the leased
49
+ * subscription token. Only the keys the backend MARKED as secret are read (see `secretKeys`).
50
+ */
51
+ export declare function mcpServerSecretValues(servers: readonly McpServerSpec[]): string[];
52
+ /**
53
+ * Validate the optional `skills` field. Names are de-duplicated: two skills sharing a directory
54
+ * name would overwrite each other's `SKILL.md`, leaving the agent pointed at whichever landed
55
+ * last — so the first wins and the collision is dropped rather than silently mixing two playbooks.
56
+ */
57
+ export declare function parseSkillSpecs(value: unknown): SkillSpec[] | undefined;
58
+ /**
59
+ * A safe MCP server id: it becomes a tool-name fragment AND a TOML table key.
60
+ *
61
+ * Kept byte-identical to kernel's `MCP_SERVER_ID_PATTERN` (the harness image is built from `src/`
62
+ * plus typescript alone, so it can carry no runtime dependency on a workspace package) and pinned
63
+ * against it by `test/agent-capabilities.conformity.test.ts` — the same copy-plus-pin arrangement
64
+ * `src/host-markdown.ts` uses.
65
+ */
66
+ export declare const MCP_SERVER_ID_PATTERN: RegExp;
67
+ /**
68
+ * Whether an HTTP tool server's URL may be started. Mirrors kernel's `isAllowedMcpHttpUrl` (see
69
+ * {@link MCP_SERVER_ID_PATTERN} for why it is a copy, and the conformity suite that pins it):
70
+ * `https` anywhere, plain `http` only on loopback, since the headers carry a resolved credential.
71
+ * The backend refuses the same URLs at registration — this is the boundary check, so a body that
72
+ * reached the container by any other route is held to the rule too.
73
+ */
74
+ export declare function isAllowedMcpHttpUrl(raw: string): boolean;
75
+ /** Validate the optional `mcpServers` field, dropping malformed entries and duplicate ids. */
76
+ export declare function parseMcpServerSpecs(value: unknown): McpServerSpec[] | undefined;
77
+ /**
78
+ * The `--mcp-config` document Claude Code reads: `{ "mcpServers": { "<id>": {...} } }`. An `http`
79
+ * server declares `type: "http"` with its headers; a `stdio` one declares its command/args/env.
80
+ */
81
+ export declare function claudeMcpConfig(servers: McpServerSpec[]): {
82
+ mcpServers: Record<string, Record<string, unknown>>;
83
+ };
84
+ /**
85
+ * The claude-code CLI's own tools, named so an `--allowedTools` list can never take them away.
86
+ *
87
+ * An allow-list is whole-session: it does not scope itself to MCP just because every entry we
88
+ * generate happens to be an `mcp__*` pattern. So the moment one tool server narrows its tools, the
89
+ * list has to re-grant the agent's built-in file/bash/search tools or the run is handed a narrowed
90
+ * MCP surface AND no way to read, edit or build anything.
91
+ *
92
+ * Bias this list toward OVER-inclusion. A name the CLI does not have is inert; a name it has and
93
+ * this list lacks is a tool silently removed from a run — which surfaces as an agent that cannot
94
+ * do its work, far from the registration that caused it. Historical/renamed spellings are kept for
95
+ * the same reason: the harness image is pinned per workspace, so one image faces several CLI
96
+ * versions. When the CLI gains a tool, add it here.
97
+ */
98
+ export declare const CLAUDE_BUILT_IN_TOOLS: readonly string[];
99
+ /**
100
+ * The tool-name list for `--allowedTools`: every declared server's tools in the CLI's
101
+ * `mcp__<server>__<tool>` convention, PLUS {@link CLAUDE_BUILT_IN_TOOLS}. A server with no
102
+ * restriction contributes the whole-server pattern, so an allow-list stays one entry per server.
103
+ *
104
+ * Returns undefined when NO server restricts its tools — there is then nothing to narrow, and the
105
+ * safest list is the one we never send.
106
+ *
107
+ * Whether the CLI ENFORCES this list is permission-mode dependent and not a contract we control:
108
+ * the run uses `--permission-mode bypassPermissions` (the container is the sandbox and no human is
109
+ * there to approve a call), under which an allow-list grants rather than gates. So this is written
110
+ * to be correct under BOTH readings — if the list gates, the narrowing is real and the built-ins
111
+ * survive it; if it is inert, sending it costs nothing. The always-present channel is the PROMPT,
112
+ * which states each server's permitted tool names on every harness. Treat `allowedTools` as
113
+ * scoping, not as a security boundary: a server the agent must not reach fully should not be
114
+ * wired for that kind at all.
115
+ */
116
+ export declare function claudeAllowedToolPatterns(servers: McpServerSpec[]): string[] | undefined;
117
+ /**
118
+ * The `[mcp_servers.<id>]` TOML block Codex reads from its `CODEX_HOME/config.toml`. Codex's MCP
119
+ * client is stdio-only, so an `http` server is skipped here — the backend states such a server as
120
+ * unavailable when it declares `harnesses: ['claude-code']`, and a deployment that wires an HTTP
121
+ * server for Codex gets a no-op rather than a malformed config.
122
+ */
123
+ export declare function codexMcpConfigToml(servers: McpServerSpec[]): string;
124
+ /**
125
+ * Write the Claude Code MCP config for this run and return its path, or undefined when there are
126
+ * no servers. The file is written into the caller's PER-RUN directory (an isolated config home, or
127
+ * an ambient job's own scratch dir) — never the checkout (it would land in a commit) and never a
128
+ * HOME-global path (a second concurrent job would clobber it, and it carries this job's credentials).
129
+ */
130
+ export declare function writeClaudeMcpConfig(dir: string, servers: McpServerSpec[]): Promise<string | undefined>;
@@ -0,0 +1,114 @@
1
+ import type { Logger } from './logger.js';
2
+ import { type HarnessCallMetric, type PiRunOutcome, type TodoProgress } from './pi.js';
3
+ import { type McpServerSpec, type SkillSpec } from './agent-capabilities.js';
4
+ import { type ProgressGuardLimits } from './progress-guard.js';
5
+ import { type SliceReview } from './subagents.js';
6
+ /** Which subscription harness to run (the Pi harness uses `runPi` directly). */
7
+ export type SubscriptionHarness = 'claude-code' | 'codex';
8
+ export interface SubscriptionRunOptions {
9
+ /** Prepared working directory (cloned/scaffolded by the caller). */
10
+ cwd: string;
11
+ /** Real vendor model id, e.g. `claude-opus-4-8` / `gpt-5.5-codex`. */
12
+ model: string;
13
+ /** Composed role + best-practice fragments, supplied as the system prompt. */
14
+ systemPrompt: string;
15
+ /** The concrete task prompt handed to the CLI over stdin. */
16
+ userPrompt: string;
17
+ /**
18
+ * The decrypted subscription credential: an OAuth token (claude) or auth.json blob
19
+ * (codex). Omitted when `ambientAuth` is set — the CLI uses the developer's own login.
20
+ */
21
+ subscriptionToken?: string;
22
+ /**
23
+ * Anthropic-compatible base URL for a non-Anthropic Claude-Code vendor (GLM/Kimi).
24
+ * Present ⇒ ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN; absent ⇒ CLAUDE_CODE_OAUTH_TOKEN.
25
+ */
26
+ subscriptionBaseUrl?: string;
27
+ /**
28
+ * Native local execution: run the developer's ALREADY-INSTALLED CLI with its OWN
29
+ * ambient login (`~/.claude` / `~/.codex`) — no leased credential, no isolated config
30
+ * home. Set ONLY by the local native transport (which runs the harness as a host
31
+ * process); a no-op everywhere else. The agent then runs with the user's personal
32
+ * subscription, unsandboxed, on their own machine — the explicit trade for skipping the
33
+ * container.
34
+ */
35
+ ambientAuth?: boolean;
36
+ /**
37
+ * The skills to install natively before launch. The claude-code runner writes each to
38
+ * `CLAUDE_CONFIG_DIR/skills/<name>/SKILL.md` (+ resource files) so the CLI loads them — but ONLY
39
+ * when it owns an isolated config home, i.e. NOT under `ambientAuth`. The codex runner ignores
40
+ * them outright. Every case that skips the native install reads the checkout's
41
+ * `.cat-context/skill/<name>/`, materialised by the caller.
42
+ */
43
+ skills?: SkillSpec[];
44
+ /**
45
+ * Tool servers (MCP) to wire into the CLI for this run. Written to a PER-RUN config the CLI is
46
+ * pointed at — never a HOME-global one, which a second concurrent job would clobber and which
47
+ * carries this job's credentials. Absent ⇒ the CLI's built-in tools only.
48
+ */
49
+ mcpServers?: McpServerSpec[];
50
+ /**
51
+ * Extra environment for the CLI child, scoped to this job (the tester's secrets, a
52
+ * private-registry npmrc pointer). Merged over the inherited `process.env` at spawn, so the
53
+ * agent and its shell tools see them without the harness mutating its OWN environment — which
54
+ * is shared by every concurrent job under the native host-process transport. See
55
+ * `RunOptions.agentEnv`.
56
+ */
57
+ extraEnv?: Record<string, string>;
58
+ /** Aborting this kills the CLI (the job's inactivity/max-duration watchdog). */
59
+ signal?: AbortSignal;
60
+ /**
61
+ * Fully-resolved no-progress guard limits (env defaults merged loosen-only with the kind's
62
+ * tuning + any complexity-scaled allowance). When set, the claude-code runner runs the SAME
63
+ * {@link ProgressGuard} as Pi over the CLI's tool stream and kills a run that has plainly
64
+ * stopped making progress (no-edit probing, error-retry loop, web rabbit-hole) rather than
65
+ * letting it burn the whole wall-clock budget. Omitted ⇒ the guard is disabled for this run
66
+ * (only the external watchdog bounds it), preserving the pre-guard behaviour.
67
+ */
68
+ guardLimits?: ProgressGuardLimits;
69
+ /** Whether this run is expected to edit files (false for assess-only runs); gates the no-edit bound. */
70
+ expectsEdits?: boolean;
71
+ /** Called on every chunk of CLI output, so the watchdog sees the agent is alive. */
72
+ onActivity?: () => void;
73
+ /** Called with the latest subtask counts each time the CLI updates its todo/plan list. */
74
+ onProgress?: (progress: TodoProgress) => void;
75
+ /**
76
+ * Called with the FULL set of per-slice reviews each time one lands, so the backend can persist
77
+ * a parallel review's completed work as it happens instead of only from the terminal result.
78
+ * A whole value rather than a delta: the set only grows and losing a finished slice's report to
79
+ * a dropped poll would defeat the point (see `SliceTracker.sliceReviews`).
80
+ */
81
+ onSliceReviews?: (reviews: SliceReview[]) => void;
82
+ /**
83
+ * Called with each per-call telemetry row as the CLI stream yields it, so the backend can
84
+ * record the run's model calls WHILE it runs instead of only from its terminal result. The
85
+ * same row still rides the result, so a lost poll response costs nothing.
86
+ */
87
+ onCallMetric?: (call: HarnessCallMetric) => void;
88
+ /**
89
+ * The per-job child logger (jobId/repo/branch correlation). Threaded so the retained
90
+ * session-transcript path is logged for the run when the isolated config home is torn down.
91
+ */
92
+ log?: Logger;
93
+ }
94
+ /**
95
+ * Decide how the Claude Code runner carries the composed system prompt. Small prompts ride
96
+ * `--append-system-prompt` (a real system turn, cacheable) as before; a prompt too large for a
97
+ * single argv string is instead folded into the stdin task prompt (like the Codex runner), which
98
+ * has no size ceiling. Pure so the branch is unit-testable without spawning the CLI.
99
+ */
100
+ export declare function carryClaudeSystemPrompt(systemPrompt: string, userPrompt: string): {
101
+ appendArgs: string[];
102
+ prompt: string;
103
+ folded: boolean;
104
+ };
105
+ export declare function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
106
+ /**
107
+ * Run the Codex CLI headlessly against `opts.cwd`, authenticated with the leased
108
+ * ChatGPT `auth.json` bundle written to an isolated CODEX_HOME, talking direct to
109
+ * the ChatGPT backend. Streams `codex exec --json`, mapping plan/todo updates onto
110
+ * subtask progress and the running cumulative token usage onto the outcome.
111
+ */
112
+ export declare function runCodex(opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
113
+ /** Dispatch to the configured subscription harness runner. */
114
+ export declare function runSubscriptionHarness(harness: SubscriptionHarness, opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
@@ -326,6 +326,52 @@ async function setUpClaudeMcp(servers, configHome) {
326
326
  cleanup,
327
327
  };
328
328
  }
329
+ /**
330
+ * No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
331
+ * which cannot see the CLI's internal turns. The caller remembers each `tool_use` id's name off
332
+ * the assistant turn (`rememberTool`) and hands the following user turn's content to `feedGuard`,
333
+ * which pairs each `tool_result`'s `is_error` with that name. The FIRST reason trips it: the
334
+ * diagnostic is recorded (readable via `reason()`, which the catch surfaces over the generic abort
335
+ * message) and `guardAbort` fires — folded into streamCli's signal so a tripped guard kills the CLI
336
+ * the same way the external watchdog does. Disabled when the caller supplies no limits (only the
337
+ * external watchdog then bounds the run).
338
+ *
339
+ * Split out of {@link runClaudeCode} for the per-function line budget.
340
+ */
341
+ function createClaudeProgressGuard(opts) {
342
+ const guard = opts.guardLimits
343
+ ? new ProgressGuard(opts.guardLimits, opts.expectsEdits ?? true)
344
+ : undefined;
345
+ const toolNames = new Map();
346
+ const guardAbort = new AbortController();
347
+ let guardReason;
348
+ const feedGuard = (content) => {
349
+ if (!guard || guardReason)
350
+ return;
351
+ for (const block of content) {
352
+ if (!isObject(block) || block.type !== 'tool_result')
353
+ continue;
354
+ const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
355
+ const name = id ? toolNames.get(id) : undefined;
356
+ if (id)
357
+ toolNames.delete(id);
358
+ if (!name)
359
+ continue;
360
+ const reason = guard.observeSignal({ name, isError: block.is_error === true });
361
+ if (reason) {
362
+ guardReason = reason;
363
+ guardAbort.abort();
364
+ return;
365
+ }
366
+ }
367
+ };
368
+ return {
369
+ rememberTool: (id, name) => toolNames.set(id, name),
370
+ feedGuard,
371
+ guardAbort,
372
+ reason: () => guardReason,
373
+ };
374
+ }
329
375
  export async function runClaudeCode(opts) {
330
376
  const stats = { toolCalls: 0, assistantChars: 0 };
331
377
  let summary = '';
@@ -411,37 +457,8 @@ export async function runClaudeCode(opts) {
411
457
  // `tool_use` id to feed the guard a {name,isError} signal. A tripped guard aborts the CLI via
412
458
  // `guardAbort` (folded into streamCli's signal below) and the run then fails with its
413
459
  // diagnostic. Disabled when the caller supplies no limits (only the external watchdog bounds it).
414
- const guard = opts.guardLimits
415
- ? new ProgressGuard(opts.guardLimits, opts.expectsEdits ?? true)
416
- : undefined;
417
- const toolNames = new Map();
418
- const guardAbort = new AbortController();
419
- let guardReason;
420
- // Feed a user turn's settled tool calls to the guard, pairing each `tool_result`'s `is_error`
421
- // with the name captured for its `tool_use` id on the assistant turn. The FIRST reason trips
422
- // it: record the diagnostic and abort the CLI (streamCli's close handler rejects; the catch
423
- // below surfaces `guardReason` over the generic abort message). A standalone closure so the
424
- // per-block loop doesn't nest onEvent past the readable-depth limit.
425
- const feedGuard = (content) => {
426
- if (!guard || guardReason)
427
- return;
428
- for (const block of content) {
429
- if (!isObject(block) || block.type !== 'tool_result')
430
- continue;
431
- const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
432
- const name = id ? toolNames.get(id) : undefined;
433
- if (id)
434
- toolNames.delete(id);
435
- if (!name)
436
- continue;
437
- const reason = guard.observeSignal({ name, isError: block.is_error === true });
438
- if (reason) {
439
- guardReason = reason;
440
- guardAbort.abort();
441
- return;
442
- }
443
- }
444
- };
460
+ const progressGuard = createClaudeProgressGuard(opts);
461
+ const { rememberTool, feedGuard, guardAbort } = progressGuard;
445
462
  const onEvent = (event, meta) => {
446
463
  const type = event.type;
447
464
  // A subagent's turns ride the parent's stdout tagged with the dispatch that spawned them;
@@ -463,7 +480,7 @@ export async function runClaudeCode(opts) {
463
480
  // Remember each call's name against its id so the guard can pair it with the
464
481
  // `is_error` its `tool_result` carries on the next `user` turn.
465
482
  if (typeof block.id === 'string' && typeof block.name === 'string') {
466
- toolNames.set(block.id, block.name);
483
+ rememberTool(block.id, block.name);
467
484
  }
468
485
  if (block.name === 'TodoWrite') {
469
486
  const progress = todosToProgress(block.input?.todos);
@@ -571,6 +588,7 @@ export async function runClaudeCode(opts) {
571
588
  // report is appended after them when the CLI managed to emit one before it was killed, which
572
589
  // is uncommon but is the same evidence a bad exit now carries — a guard trip is no reason to
573
590
  // discard it.
591
+ const guardReason = progressGuard.reason();
574
592
  if (guardReason) {
575
593
  const tail = err?.stderrTail;
576
594
  const report = capReport(redact(terminalReport, secrets).trim());
@@ -0,0 +1,18 @@
1
+ import type { AgentJob, AgentResult, McpServerSpec, SkillSpec } from './job.js';
2
+ import type { EffortReport } from './effort.js';
3
+ /**
4
+ * Fold an agent's effort self-assessment (lifted from its sentinel file by `runAgentInWorkspace`)
5
+ * onto its final result. Every container mode routes its result through this so the report reaches
6
+ * the backend uniformly. A run that wrote no report passes through unchanged.
7
+ */
8
+ export declare function mergeEffort(result: AgentResult, effortReport: EffortReport | undefined): AgentResult;
9
+ /**
10
+ * The agent-capability fields (skills + tool servers) every agent-running flow forwards to
11
+ * {@link runAgentInWorkspace}. One helper rather than a per-flow spread, so a flow cannot silently
12
+ * be the one that drops a kind's declared playbook or tool server — the failure mode is invisible
13
+ * (the agent simply works without it) and would only show up as degraded output.
14
+ */
15
+ export declare function agentCapabilities(job: AgentJob): {
16
+ skills?: SkillSpec[];
17
+ mcpServers?: McpServerSpec[];
18
+ };
@@ -0,0 +1,66 @@
1
+ import type { AgentJob, AgentResult, TestSecretSpec } from './job.js';
2
+ import { runCodingAgent } from './coding-agent.js';
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
+ /** Run one generic agent job end to end, dispatching on `mode`. */
16
+ export declare function handleAgent(job: AgentJob, opts?: RunOptions): Promise<AgentResult>;
17
+ /**
18
+ * Decide a preview stand-up's outcome from its result (pure, so the success/failure boundary
19
+ * is unit-tested without spawning a build). A preview must actually come up: unlike the tester's
20
+ * "test what you can" fallback, a stand-up that produced no reachable serve URL (failed build /
21
+ * server never bound) is a hard failure and its `note` becomes the failure reason. When the app
22
+ * is up but WireMock is not, the `note` rides along as a non-fatal warning.
23
+ */
24
+ export declare function buildPreviewOutcome(standUp: {
25
+ serveUrl?: string;
26
+ note?: string;
27
+ }): {
28
+ ok: true;
29
+ url: string;
30
+ note?: string;
31
+ } | {
32
+ ok: false;
33
+ error: string;
34
+ };
35
+ /**
36
+ * Build the env carrying the tester's sensitive secrets, so the agent's shell tools (spawned as
37
+ * child processes that inherit it) can read `$KEY` — the out-of-band delivery channel. Each value
38
+ * is registered for redaction so it can't leak into captured output/logs. Reserved/toolchain env
39
+ * names were already dropped at parse. No secrets ⇒ an empty env.
40
+ *
41
+ * Returned as EXPLICIT child env rather than written onto `process.env`: a process-global
42
+ * set/restore is only safe when the process runs one job, which the native host-process transport
43
+ * breaks (it serves every concurrent ambient job from one process). There, two overlapping tester
44
+ * runs would read each other's secrets, and whichever finished first would delete the other's
45
+ * mid-run. Scoping them to the spawn env makes the delivery correct under concurrency and drops
46
+ * the restore step entirely.
47
+ */
48
+ export declare function testSecretEnv(secrets: TestSecretSpec[] | undefined): Record<string, string>;
49
+ /**
50
+ * Whether a Ralph iteration ({@link AgentJob.validation} set) landed on a MULTI-REPO job (writable
51
+ * peer repos or read-only reference repos). The post-commit validation command is only wired into
52
+ * the single-repo flow, so a multi-repo run would silently skip it and degenerate the loop into a
53
+ * one-shot with no completion gate — multi-repo ralph is out of scope for v1 (see
54
+ * backend/docs/ralph-loop.md), so {@link runCodingMode} fails loudly on this instead.
55
+ */
56
+ export declare function ralphUnsupportedOnMultiRepo(job: Pick<AgentJob, 'validation' | 'peerRepos' | 'referenceRepos'>): boolean;
57
+ /**
58
+ * Assemble the {@link runCodingAgent} spec for the ordinary single-repo coding flow. Extracted
59
+ * from {@link runSingleRepoCoding} so the many optional-field spreads don't inflate that
60
+ * function's cyclomatic complexity; the mapping is a straight field copy off `job`.
61
+ *
62
+ * Exported for the `opensPr` assertion: whether a dispatch fills the repo's PR template turns on
63
+ * this one spread, and the in-place fixers reach it through the SAME function as the implementer,
64
+ * so no structural guard can tell their cases apart.
65
+ */
66
+ export declare function buildSingleRepoCodingSpec(job: AgentJob, pushBranch: string): Parameters<typeof runCodingAgent>[0];
@@ -0,0 +1,20 @@
1
+ import type { AgentJob, AgentResult } from './job.js';
2
+ import type { RunOptions } from './runner.js';
3
+ /**
4
+ * Repo-bootstrap coding flow (the bootstrapper): with a reference architecture, clone it →
5
+ * the agent adapts it in place per the instructions; without one (`fromScratch`), start from
6
+ * an empty directory → the agent scaffolds the new service. Either way the result's history
7
+ * is reset to a single commit and force-pushed to the SEPARATE, pre-created target repo's
8
+ * default branch. Diverges from the ordinary coding flow in pushing to a different repo with
9
+ * a reinitialised history rather than a work branch + PR on the cloned repo.
10
+ */
11
+ export declare function runBootstrap(job: AgentJob, opts: RunOptions): Promise<AgentResult>;
12
+ /**
13
+ * Whether the bootstrapper actually produced repository content, so a no-op run (the agent
14
+ * never reached the model / never wrote anything) is failed rather than force-pushed as an
15
+ * empty repo. With a reference architecture, "produced content" means the agent changed the
16
+ * clone; scaffolding from scratch, it means at least one file now exists in the working
17
+ * directory. (The harness writes its prompt context to Pi's global `~/.pi/agent/AGENTS.md`,
18
+ * never into `dir`, so nothing here needs to be filtered out as harness boilerplate.)
19
+ */
20
+ export declare function producedRepoContent(dir: string, hasReference: boolean, signal?: AbortSignal): Promise<boolean>;
@@ -0,0 +1,58 @@
1
+ import type { RunOptions } from './runner.js';
2
+ import type { Logger } from './logger.js';
3
+ /** What one harness-spawned command did, as both phases record it. */
4
+ export interface CapturedCommandResult {
5
+ /** Exit code (0 = pass); 124 on watchdog timeout, 127 on spawn failure, 130 on abort. */
6
+ exitCode: number;
7
+ passed: boolean;
8
+ /** Scrubbed output bounded to the caller's REPORT budget (what crosses the wire). */
9
+ outputTail?: string;
10
+ durationMs: number;
11
+ timedOut?: boolean;
12
+ /**
13
+ * The FULL scrubbed tail (up to {@link MAX_CAPTURED_OUTPUT_CHARS}) for a repair prompt. Never
14
+ * leaves the container — the agent needs the whole failure to act on it, the wire does not.
15
+ */
16
+ fullTail?: string;
17
+ }
18
+ /**
19
+ * Run ONE command as `sh -c` in `cwd`, capturing a bounded, secret-scrubbed tail of its combined
20
+ * stdout+stderr. The exit code is the verdict — computed here by the harness, never self-reported
21
+ * by the model, which is the whole point of a programmatic phase. A watchdog kills the process
22
+ * tree on timeout and an aborted run resolves non-zero, so a phase is never what blocks a job
23
+ * from settling.
24
+ *
25
+ * The child inherits the JOB's environment (`RunOptions.agentEnv` layered over the process env),
26
+ * not a mutated global: the harness spawns this itself rather than through the agent, so without
27
+ * the explicit merge a native-mode job would run without the private-registry npmrc pointer (and,
28
+ * had this been staged in `process.env`, against a sibling job's state).
29
+ *
30
+ * `logLabel`/`logFields` shape only the two warnings this runner emits itself (the watchdog kill
31
+ * and a spawn failure); the caller keeps its own start/finish logging, which knows what the
32
+ * command MEANS.
33
+ */
34
+ export declare function runCapturedCommand(args: {
35
+ cwd: string;
36
+ command: string;
37
+ timeoutMs: number;
38
+ /** Bound for {@link CapturedCommandResult.outputTail} — the caller's per-report budget. */
39
+ reportTailChars: number;
40
+ logLabel: string;
41
+ logFields?: Record<string, unknown>;
42
+ logger: Logger;
43
+ opts: RunOptions;
44
+ }): Promise<CapturedCommandResult>;
45
+ /**
46
+ * Wrap captured command output in a fenced block that the output itself cannot break out of.
47
+ *
48
+ * Every consumer of a captured tail embeds it in markdown a MODEL then reads — a repair prompt,
49
+ * the dependency-install note — and a package manager legitimately prints backticks (a linter
50
+ * quoting a template literal, a test echoing a fenced snippet from a fixture). A fixed three-tick
51
+ * fence closes on the first such run, and everything after it reads as prose: the remaining
52
+ * output, and worse, the INSTRUCTIONS that follow the block. Sizing the fence one tick longer than
53
+ * the longest run in the body is what CommonMark specifies for exactly this, so the block always
54
+ * spans the whole tail.
55
+ */
56
+ export declare function fencedOutput(text: string): string;
57
+ /** Bound an already-scrubbed output tail to what a REPORT carries, saying what it dropped. */
58
+ export declare function boundTail(scrubbed: string, maxChars: number): string;