@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.
- package/README.md +1 -0
- package/dist/agent-capabilities.d.ts +130 -0
- package/dist/agent-runner.d.ts +114 -0
- package/dist/agent-runner.js +50 -32
- package/dist/agent-shared.d.ts +18 -0
- package/dist/agent.d.ts +66 -0
- package/dist/bootstrap-mode.d.ts +20 -0
- package/dist/captured-command.d.ts +58 -0
- package/dist/claude-call-aggregator.d.ts +164 -0
- package/dist/claude-call-aggregator.js +123 -17
- package/dist/claude-stream.d.ts +56 -0
- package/dist/coding-agent.d.ts +252 -0
- package/dist/coding-agent.js +69 -50
- package/dist/dependency-install.d.ts +111 -0
- package/dist/effort.d.ts +19 -0
- package/dist/embed.d.ts +4 -0
- package/dist/failure.d.ts +42 -0
- package/dist/follow-ups.d.ts +28 -0
- package/dist/frontend-infra.d.ts +25 -0
- package/dist/fs-utils.d.ts +2 -0
- package/dist/git.d.ts +394 -0
- package/dist/host-markdown.d.ts +28 -0
- package/dist/inline.d.ts +10 -0
- package/dist/job.d.ts +666 -0
- package/dist/logger.d.ts +16 -0
- package/dist/onboarding-preseed.d.ts +24 -0
- package/dist/package-registries.d.ts +32 -0
- package/dist/pi-workspace.d.ts +194 -0
- package/dist/pi.d.ts +475 -0
- package/dist/pr-description.d.ts +85 -0
- package/dist/pr-template.d.ts +101 -0
- package/dist/process-exit.d.ts +7 -0
- package/dist/process.d.ts +19 -0
- package/dist/progress-guard.d.ts +88 -0
- package/dist/progress.d.ts +87 -0
- package/dist/redact.d.ts +31 -0
- package/dist/reproduction-proof.d.ts +224 -0
- package/dist/runner.d.ts +282 -0
- package/dist/server.d.ts +3 -0
- package/dist/structured-output.d.ts +75 -0
- package/dist/subagents.d.ts +88 -0
- package/dist/transcript-retention.d.ts +21 -0
- package/dist/validation-checks.d.ts +159 -0
- package/dist/vcs-api.d.ts +73 -0
- package/dist/version.d.ts +2 -0
- package/package.json +9 -5
- package/src/agent-runner.ts +54 -29
- package/src/claude-call-aggregator.ts +181 -32
- 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>;
|
package/dist/agent-runner.js
CHANGED
|
@@ -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
|
|
415
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/agent.d.ts
ADDED
|
@@ -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;
|