@cat-factory/executor-harness 1.80.0 → 1.82.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 (45) 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-shared.d.ts +18 -0
  5. package/dist/agent.d.ts +66 -0
  6. package/dist/bootstrap-mode.d.ts +20 -0
  7. package/dist/captured-command.d.ts +58 -0
  8. package/dist/claude-call-aggregator.d.ts +164 -0
  9. package/dist/claude-call-aggregator.js +123 -17
  10. package/dist/claude-stream.d.ts +56 -0
  11. package/dist/coding-agent.d.ts +263 -0
  12. package/dist/dependency-install.d.ts +111 -0
  13. package/dist/effort.d.ts +19 -0
  14. package/dist/embed.d.ts +4 -0
  15. package/dist/failure.d.ts +42 -0
  16. package/dist/follow-ups.d.ts +28 -0
  17. package/dist/frontend-infra.d.ts +25 -0
  18. package/dist/fs-utils.d.ts +2 -0
  19. package/dist/git.d.ts +394 -0
  20. package/dist/host-markdown.d.ts +28 -0
  21. package/dist/inline.d.ts +10 -0
  22. package/dist/job.d.ts +666 -0
  23. package/dist/logger.d.ts +16 -0
  24. package/dist/onboarding-preseed.d.ts +24 -0
  25. package/dist/package-registries.d.ts +32 -0
  26. package/dist/pi-workspace.d.ts +194 -0
  27. package/dist/pi.d.ts +475 -0
  28. package/dist/pr-description.d.ts +85 -0
  29. package/dist/pr-template.d.ts +101 -0
  30. package/dist/process-exit.d.ts +7 -0
  31. package/dist/process.d.ts +19 -0
  32. package/dist/progress-guard.d.ts +88 -0
  33. package/dist/progress.d.ts +87 -0
  34. package/dist/redact.d.ts +31 -0
  35. package/dist/reproduction-proof.d.ts +224 -0
  36. package/dist/runner.d.ts +282 -0
  37. package/dist/server.d.ts +3 -0
  38. package/dist/structured-output.d.ts +75 -0
  39. package/dist/subagents.d.ts +88 -0
  40. package/dist/transcript-retention.d.ts +21 -0
  41. package/dist/validation-checks.d.ts +159 -0
  42. package/dist/vcs-api.d.ts +73 -0
  43. package/dist/version.d.ts +2 -0
  44. package/package.json +9 -5
  45. package/src/claude-call-aggregator.ts +181 -32
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>;
@@ -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;
@@ -0,0 +1,164 @@
1
+ import type { HarnessCallMetric } from './pi.js';
2
+ /** One model call, assembled from every stream envelope that carried a piece of it. */
3
+ export interface AggregatedClaudeCall {
4
+ model?: string;
5
+ /** Every content block of the response, in arrival order. */
6
+ content: unknown[];
7
+ text: string;
8
+ reasoning: string;
9
+ stopReason: string | null;
10
+ inputTokens: number;
11
+ cacheReadTokens: number;
12
+ cacheWriteTokens: number;
13
+ outputTokens: number;
14
+ /** The `user` turns carrying this call's tool_result blocks, in arrival order. */
15
+ toolResults: unknown[][];
16
+ /** tool_use blocks across the whole response (the run's `stats.toolCalls` term). */
17
+ toolUses: number;
18
+ }
19
+ export interface ClaudeCallAggregator {
20
+ /**
21
+ * Fold one `assistant` envelope in. A new `message.id` completes the call in flight first, so
22
+ * `onCallStart` for the new call always runs after `onCall` for the previous one.
23
+ */
24
+ onAssistant(message: Record<string, unknown>): void;
25
+ /** Buffer a `user` turn's content against the call in flight (dropped when none is). */
26
+ onToolResult(content: unknown[]): void;
27
+ /** Complete the call still in flight, if any. Call once the stream has ended. */
28
+ flush(): void;
29
+ }
30
+ /**
31
+ * Assemble per-call telemetry out of Claude Code's per-block stream envelopes.
32
+ *
33
+ * `onCallStart` fires when a call's FIRST envelope arrives, which is the moment the caller must
34
+ * snapshot the prompt: the history at that point is what produced the response. `onCall` fires
35
+ * once the call is complete (a different `message.id` began, or the stream ended).
36
+ *
37
+ * Usage is merged as the MAXIMUM of each bucket across the call's envelopes rather than the last
38
+ * one seen. The envelopes carry a snapshot of the same call's usage, and which of them holds the
39
+ * final output count is a CLI detail we should not depend on; a max is right whether the value is
40
+ * repeated verbatim or grows.
41
+ *
42
+ * An envelope with no `message.id` cannot be attributed, so it is treated as a call of its own —
43
+ * the pre-aggregation behaviour, kept so a CLI build (or a transcript) that omits the id degrades
44
+ * to over-counting rather than to silently merging unrelated calls.
45
+ */
46
+ export declare function createClaudeCallAggregator(handlers: {
47
+ onCallStart?: () => void;
48
+ onCall: (call: AggregatedClaudeCall) => void;
49
+ }): ClaudeCallAggregator;
50
+ /** One turn of the reconstructed request transcript, in the proxy's chat-array shape. */
51
+ interface TranscriptTurn {
52
+ role: string;
53
+ content: unknown;
54
+ }
55
+ /**
56
+ * How much reconstructed transcript ONE conversation may retain.
57
+ *
58
+ * The stream feeds this without limit — a tool loop that reads large files grows the history by
59
+ * every one of them — and the reconstruction is held in the driver's own process. In the container
60
+ * that is a box sized for one job; in the BACKEND it is the orchestrator, where `streamCli` already
61
+ * refuses to retain the raw stream for exactly this reason (`harnessInline.ts` →
62
+ * `OUTPUT_TAIL_RETAIN_CHARS`: "a stalled tool-using run would otherwise park hundreds of MB in the
63
+ * orchestrator process — precisely on the runs worth diagnosing").
64
+ *
65
+ * 512 KiB because that is `LlmObservabilityService.MAX_BODY_CHARS`, the point past which the store
66
+ * truncates a body anyway: retaining more can only ever be thrown away. Deliberately NOT a
67
+ * per-deployment knob — it bounds a memory fault, and a number an operator can raise is one an
68
+ * operator can raise until the process dies.
69
+ */
70
+ export declare const MAX_TRANSCRIPT_CHARS: number;
71
+ /** The per-call telemetry the Claude Code stream yields, assembled behind one small surface. */
72
+ export interface ClaudeStreamTelemetry {
73
+ /** Fold an `assistant` envelope in (parent-loop turns only — see {@link isSubagentEvent}). */
74
+ onAssistant(message: Record<string, unknown>): void;
75
+ /** Fold a `user` turn's tool_result content in, against the call in flight. */
76
+ onToolResult(content: unknown[]): void;
77
+ /** Publish the call still in flight. Idempotent; safe to call on both the clean and error path. */
78
+ flush(): void;
79
+ }
80
+ /**
81
+ * Assemble ONE conversation's per-call telemetry from the CLI stream: the growing request
82
+ * transcript and the per-call token/body metrics.
83
+ *
84
+ * Owns the transcript because the two are one concern — a call's `promptText` is the transcript as
85
+ * of that call, and its turns may only be appended once the call that produced them is complete.
86
+ * `seed` is what the harness supplied and the stream therefore never shows (the system + first user
87
+ * message, or the single folded user turn), so the reconstruction never claims a system turn that
88
+ * was not sent. A subagent's conversation seeds EMPTY — its prompt was minted by the CLI and never
89
+ * crosses this stream — which is also why its first call carries `messageCount: 0` (the backend's
90
+ * `latestChainTip` skips those on purpose: there is no re-sendable chain to delta against).
91
+ * Bodies are credential-scrubbed; they can echo the leased token — and assembled at all only when
92
+ * {@link ClaudeStreamTelemetryOptions.bodies} says a driver has somewhere to put them. The
93
+ * transcript is bounded either way ({@link MAX_TRANSCRIPT_CHARS}).
94
+ *
95
+ * Deliberately does NOT touch {@link PiRunStats}: the run's tool/output counters describe whether
96
+ * the agent ACTED at all (`agentNeverActed`), which is true of a subagent's turns whichever channel
97
+ * ends up owning their telemetry rows. The caller accumulates them off the raw stream instead.
98
+ */
99
+ export declare function createClaudeStreamTelemetry(opts: ClaudeStreamTelemetryOptions): ClaudeStreamTelemetry;
100
+ /** How one conversation's per-call telemetry is assembled. */
101
+ export interface ClaudeStreamTelemetryOptions {
102
+ seed: TranscriptTurn[];
103
+ secrets: string[];
104
+ publish: (metric: HarnessCallMetric) => void;
105
+ /**
106
+ * Whether to assemble the prompt/response BODIES at all. Absent ⇒ true (the container harness,
107
+ * whose job result carries them).
108
+ *
109
+ * `false` for a driver whose store will drop them — the backend with `LLM_RECORD_PROMPTS` off —
110
+ * where reconstructing a transcript per call is pure cost. Token counts, `messageCount` and
111
+ * finish reasons are unaffected: only the bodies go.
112
+ */
113
+ bodies?: boolean;
114
+ /** Retention bound override (tests). Absent ⇒ {@link MAX_TRANSCRIPT_CHARS}. */
115
+ maxTranscriptChars?: number;
116
+ }
117
+ /**
118
+ * The dispatch (`Agent`/`Task` tool_use) id a stream envelope is tagged with, or `undefined` for a
119
+ * parent-loop turn.
120
+ *
121
+ * Claude Code streams the turns of the subagents it dispatches onto the parent's stdout, tagged
122
+ * with the tool_use id that spawned them. Those same turns are also written to the per-session
123
+ * `subagents/*.jsonl` transcripts the watcher reads, so recording both channels counted every
124
+ * subagent call twice — and splicing them into the parent's message reconstruction produced a
125
+ * `promptText` chain that interleaves several conversations and therefore matches no real request.
126
+ *
127
+ * The id is what makes the fallback below possible: concurrent subagents interleave on one stdout,
128
+ * so it is the ONLY thing separating their conversations.
129
+ */
130
+ export declare function subagentDispatchId(event: Record<string, unknown>): string | undefined;
131
+ /** Whether a stream envelope describes a SUBAGENT's turn rather than the parent loop's. */
132
+ export declare function isSubagentEvent(event: Record<string, unknown>): boolean;
133
+ /** All per-call telemetry for ONE claude-code run: the parent loop, and whoever bills the subagents. */
134
+ export interface ClaudeRunTelemetry {
135
+ /** Fold an `assistant` envelope in, routed by its dispatch tag (`undefined` ⇒ the parent loop). */
136
+ onAssistant(dispatchId: string | undefined, message: Record<string, unknown>): void;
137
+ /** Fold a `user` turn's tool_result content in, against the same conversation. */
138
+ onToolResult(dispatchId: string | undefined, content: unknown[]): void;
139
+ /** Publish every conversation's call in flight. Idempotent; safe on the clean and error paths. */
140
+ flush(): void;
141
+ /**
142
+ * Subagent turns crossed the stream AND the watcher was the channel meant to record them — so a
143
+ * watcher that captured nothing means this run's subagent rows are simply missing.
144
+ */
145
+ expectsWatcherCalls(): boolean;
146
+ }
147
+ /**
148
+ * Assemble a run's per-call telemetry, routing each envelope to the conversation it belongs to.
149
+ *
150
+ * The routing is the whole point. A subagent's turns ride the parent's stdout tagged with the
151
+ * dispatch that spawned them, and they must never join the PARENT's chain — that splice produced a
152
+ * `promptText` interleaving several conversations, matching no request that was ever sent.
153
+ *
154
+ * Who RECORDS them is a separate question, decided once per run rather than per event:
155
+ * `watcherOwnsSubagents` says a `subagents/*.jsonl` watcher will run, and it is the better source
156
+ * (it reads the settled transcript, so its usage and stop reason are final). With no watcher — an
157
+ * `ambientAuth` run has no isolated config home to watch — the tagged turns are recorded here
158
+ * instead, on per-dispatch transcripts of their own. Dropping them in that case would leave the run
159
+ * billed by neither channel, and an under-count reads as a cheap run rather than as an error.
160
+ */
161
+ export declare function createClaudeRunTelemetry(opts: ClaudeStreamTelemetryOptions & {
162
+ watcherOwnsSubagents: boolean;
163
+ }): ClaudeRunTelemetry;
164
+ export {};