@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
@@ -42,6 +42,64 @@ function followUpPollIntervalMs() {
42
42
  * retry resumes on them. Returns the run's summary/stats, whether it pushed, and
43
43
  * whether it resumed; callers decide what to do after a push (open a PR, or nothing).
44
44
  */
45
+ /**
46
+ * The work-branch push machinery for one coding run: a single coalesced push plus the periodic
47
+ * checkpoint that keeps mid-run commits durable. Split out of {@link runCodingAgent} for the
48
+ * per-function line budget; the caller owns the interval's lifetime (it clears `checkpoint`).
49
+ *
50
+ * Serialize all pushes to the work branch through a single in-flight promise. A checkpoint tick
51
+ * and the final push (or two slow checkpoint ticks) must never run `git push` to the same branch
52
+ * concurrently: overlapping pushes race on the remote ref and can make a push fail with a
53
+ * ref-lock / non-fast-forward error — which, on the FINAL push, would fail the whole run even
54
+ * though the work is committed. `pushWorkOnce` coalesces concurrent callers onto one push and only
55
+ * pushes once the branch has advanced past `baseSha`.
56
+ *
57
+ * Only push once the branch has advanced past its pre-run tip: pushing while it still sits at
58
+ * `baseSha` would create the work branch at the base commit (a zero-diff branch), which a later
59
+ * retry would see via `remoteBranchExists` and treat as resumable work — then fail to open a PR
60
+ * ("no commits between base and head"). So a run that never commits leaves NO branch behind,
61
+ * preserving the clean no-op outcome.
62
+ */
63
+ function createWorkBranchPusher(args) {
64
+ const { dir, spec, baseSha, logger, signal } = args;
65
+ let pushInFlight = null;
66
+ const pushWorkOnce = () => {
67
+ if (pushInFlight)
68
+ return pushInFlight;
69
+ pushInFlight = (async () => {
70
+ if (!(await branchHasCommitsSince(dir, baseSha, signal)))
71
+ return;
72
+ await pushBranch(dir, spec.pushBranch, spec.ghToken, signal);
73
+ })().finally(() => {
74
+ pushInFlight = null;
75
+ });
76
+ return pushInFlight;
77
+ };
78
+ // Read the in-flight push, if any. A function (with an explicit return type) so the
79
+ // value isn't subject to the caller's straight-line narrowing — `pushInFlight` is
80
+ // only ever assigned inside closures, which flow analysis can't observe.
81
+ const inFlightPush = () => pushInFlight;
82
+ // Checkpoint the agent's committed work to the branch periodically so an eviction
83
+ // mid-run doesn't lose it (a retry then resumes from the pushed commits). The
84
+ // agent commits its own work; this only PUSHES already-committed commits, so it
85
+ // never races the agent's staging. Best-effort: a failed checkpoint is skipped.
86
+ // Surface checkpoint-push failures at warn with a running count: a checkpoint losing
87
+ // a race is harmless once, but a steadily-climbing count means mid-run work is NOT
88
+ // being durably checkpointed, so an eviction would lose it — previously invisible at
89
+ // info level. Still best-effort: a failed checkpoint never fails the run.
90
+ let checkpointFailures = 0;
91
+ const checkpoint = setInterval(() => {
92
+ pushWorkOnce().catch((err) => {
93
+ checkpointFailures++;
94
+ logger.warn('coding-agent: checkpoint push failed', {
95
+ reason: err instanceof Error ? err.message : String(err),
96
+ checkpointFailures,
97
+ });
98
+ });
99
+ }, checkpointIntervalMs());
100
+ checkpoint.unref?.();
101
+ return { pushWorkOnce, inFlightPush, checkpoint };
102
+ }
45
103
  export async function runCodingAgent(spec, opts = {}) {
46
104
  const { signal } = opts;
47
105
  // The registry already binds jobId/repo/branch; add the coding kind + the push branch
@@ -51,56 +109,17 @@ export async function runCodingAgent(spec, opts = {}) {
51
109
  // Clone (or resume) the checkout, fetch any read-only reference branches, and capture the
52
110
  // pre-run branch tip. See {@link prepareCodingCheckout} for the resume-safety invariants.
53
111
  const { resumed, baseSha } = await prepareCodingCheckout(dir, spec, logger, opts);
54
- // Serialize all pushes to the work branch through a single in-flight promise.
55
- // A checkpoint tick and the final push (or two slow checkpoint ticks) must never
56
- // run `git push` to the same branch concurrently: overlapping pushes race on the
57
- // remote ref and can make a push fail with a ref-lock / non-fast-forward error —
58
- // which, on the FINAL push, would fail the whole run even though the work is
59
- // committed. `pushWorkOnce` coalesces concurrent callers onto one push and only
60
- // pushes once the branch has advanced past `baseSha` (see below).
61
- //
62
- // Only push once the branch has advanced past its pre-run tip: pushing while it
63
- // still sits at `baseSha` would create the work branch at the base commit (a
64
- // zero-diff branch), which a later retry would see via `remoteBranchExists` and
65
- // treat as resumable work — then fail to open a PR ("no commits between base and
66
- // head"). So a run that never commits leaves NO branch behind, preserving the
67
- // clean no-op outcome.
68
- let pushInFlight = null;
69
- const pushWorkOnce = () => {
70
- if (pushInFlight)
71
- return pushInFlight;
72
- pushInFlight = (async () => {
73
- if (!(await branchHasCommitsSince(dir, baseSha, signal)))
74
- return;
75
- await pushBranch(dir, spec.pushBranch, spec.ghToken, signal);
76
- })().finally(() => {
77
- pushInFlight = null;
78
- });
79
- return pushInFlight;
80
- };
81
- // Read the in-flight push, if any. A function (with an explicit return type) so the
82
- // value isn't subject to the caller's straight-line narrowing — `pushInFlight` is
83
- // only ever assigned inside closures, which flow analysis can't observe.
84
- const inFlightPush = () => pushInFlight;
85
- // Checkpoint the agent's committed work to the branch periodically so an eviction
86
- // mid-run doesn't lose it (a retry then resumes from the pushed commits). The
87
- // agent commits its own work; this only PUSHES already-committed commits, so it
88
- // never races the agent's staging. Best-effort: a failed checkpoint is skipped.
89
- // Surface checkpoint-push failures at warn with a running count: a checkpoint losing
90
- // a race is harmless once, but a steadily-climbing count means mid-run work is NOT
91
- // being durably checkpointed, so an eviction would lose it — previously invisible at
92
- // info level. Still best-effort: a failed checkpoint never fails the run.
93
- let checkpointFailures = 0;
94
- const checkpoint = setInterval(() => {
95
- pushWorkOnce().catch((err) => {
96
- checkpointFailures++;
97
- logger.warn('coding-agent: checkpoint push failed', {
98
- reason: err instanceof Error ? err.message : String(err),
99
- checkpointFailures,
100
- });
101
- });
102
- }, checkpointIntervalMs());
103
- checkpoint.unref?.();
112
+ // The work-branch push machinery: one coalesced in-flight push plus the periodic
113
+ // checkpoint that keeps mid-run commits durable across an eviction. Lifted into
114
+ // {@link createWorkBranchPusher} so this callback stays within the per-function line budget;
115
+ // the invariants it upholds are documented there.
116
+ const { pushWorkOnce, inFlightPush, checkpoint } = createWorkBranchPusher({
117
+ dir,
118
+ spec,
119
+ baseSha,
120
+ logger,
121
+ signal,
122
+ });
104
123
  // In a monorepo the service lives in a subdirectory: run Pi with its cwd set to
105
124
  // that subtree (git stays rooted at `dir` so commits/pushes still cover the whole
106
125
  // checkout). Created if missing so a coder scaffolding a brand-new service into an
@@ -0,0 +1,111 @@
1
+ import { type RunOptions } from './runner.js';
2
+ import type { Logger } from './logger.js';
3
+ /** The dependency-install phase as it arrives on the job body. */
4
+ export interface DependencyInstallSpec {
5
+ /** The shell command, run as `sh -c` in the checkout (the service directory for a monorepo). */
6
+ command: string;
7
+ }
8
+ /** What the install did — folded into the agent's prompt, never a verdict about the run. */
9
+ export interface DependencyInstallOutcome {
10
+ command: string;
11
+ exitCode: number;
12
+ passed: boolean;
13
+ /** Scrubbed, bounded tail of the combined output. Only kept for a FAILED install. */
14
+ outputTail?: string;
15
+ durationMs: number;
16
+ timedOut?: boolean;
17
+ }
18
+ /**
19
+ * How much of a failed install's output the agent is shown. Smaller than the validation loop's
20
+ * repair budget (16k) on purpose: a repair prompt has to carry the whole failure because fixing
21
+ * it IS the task, whereas this note only has to let the agent decide whether to install
22
+ * something itself. The tail is where a package manager puts its actual error.
23
+ */
24
+ export declare const DEPENDENCY_INSTALL_TAIL_CHARS = 4000;
25
+ /**
26
+ * The per-install watchdog: the longest the install may run before it is killed and reported as
27
+ * failed. Generous (20 min at the defaults) because a cold monorepo install on a slow registry
28
+ * legitimately takes many minutes.
29
+ *
30
+ * DERIVED from the configured job ceiling rather than hardcoded against the default one, the same
31
+ * way `git.ts` derives its per-command timeout from the configured inactivity window: a constant
32
+ * sized against a default silently breaks its own invariant the moment an operator changes that
33
+ * default. An explicit `DEPENDENCY_INSTALL_TIMEOUT_MS` is honoured but still CLAMPED — the point
34
+ * of the share is that no configuration lets setup eat the run, and an override that could exceed
35
+ * the job's own ceiling would only ever be killed later by a watchdog that fails the whole job
36
+ * instead of degrading to a note.
37
+ */
38
+ export declare function dependencyInstallTimeoutMs(env?: NodeJS.ProcessEnv): number;
39
+ /**
40
+ * How often the install feeds the run's inactivity watchdog. Well under `JOB_INACTIVITY_MS`
41
+ * (default 10 min); matches the validation loop's and the frontend stand-up's heartbeat, which
42
+ * exist for exactly the same reason.
43
+ */
44
+ export declare function dependencyInstallHeartbeatMs(): number;
45
+ /**
46
+ * Parse the optional DEPENDENCY INSTALL envelope off the job body. A missing/blank command
47
+ * returns `undefined`, so a malformed body degrades to the exact pre-feature behaviour (no
48
+ * install phase, the agent starts against the bare clone) rather than failing a good run.
49
+ *
50
+ * Lives with the feature rather than in `job.ts`, following the same rule the two pre-PR
51
+ * verification phases do: each phase owns its own job-body parser next to the code that consumes
52
+ * it, and `job.ts` stays the job SHAPE plus the generic assembly.
53
+ */
54
+ export declare function parseDependencyInstallSpec(value: unknown): DependencyInstallSpec | undefined;
55
+ /**
56
+ * Run the declared install against `cwd` and return what happened. Never throws and never fails
57
+ * the job: every failure shape ({@link runCapturedCommand} maps a timeout to 124, a spawn error
58
+ * to 127, an abort to 130) comes back as a non-zero outcome the caller turns into a prompt note.
59
+ *
60
+ * The output tail is kept ONLY for a failure. A successful install prints tens of thousands of
61
+ * uninteresting lines, and the agent needs to know that it succeeded, not what it resolved.
62
+ */
63
+ export declare function runDependencyInstall(args: {
64
+ cwd: string;
65
+ spec: DependencyInstallSpec;
66
+ logger: Logger;
67
+ opts: RunOptions;
68
+ }): Promise<DependencyInstallOutcome>;
69
+ /**
70
+ * THE entry point: run the phase for a mode that has a checkout, and hand back the note to fold
71
+ * into the agent's prompt (or `undefined` when the service declared no install, which is every
72
+ * dispatch today that never configured one).
73
+ *
74
+ * Everything a caller could get wrong lives here rather than at six call sites: the phase marker,
75
+ * the best-effort run, keeping the installed tree out of the agent's commits, and naming WHERE
76
+ * the install ran when that is not where the agent will be standing. A mode supplies only its
77
+ * three directories.
78
+ */
79
+ export declare function prepopulateDependencies(args: {
80
+ spec: DependencyInstallSpec | undefined;
81
+ /** Where the install runs: the service subtree for a monorepo, else the checkout root. */
82
+ installDir: string;
83
+ /** The git checkout whose local excludes protect the agent's commits from the installed tree. */
84
+ repoDir: string;
85
+ /** The agent's own working directory, which names the install location when the two differ. */
86
+ agentDir: string;
87
+ logger: Logger;
88
+ opts: RunOptions;
89
+ }): Promise<string | undefined>;
90
+ /**
91
+ * Fold the note into a prompt. Trivial, and deliberately not inlined: it is applied on EVERY
92
+ * agent pass — including the validation and reproduction REPAIR passes, which start a fresh
93
+ * agent that would otherwise never learn the tree is already installed and would spend a repair
94
+ * round reinstalling it.
95
+ */
96
+ export declare function withDependencyNote(userPrompt: string, note: string | undefined): string;
97
+ /**
98
+ * The note folded into the agent's prompt describing the checkout it is about to work in.
99
+ *
100
+ * Stated in BOTH directions on purpose. On success the agent is told the tree is ready, which is
101
+ * what stops it spending turns re-running an install that already ran (and, on a repo whose
102
+ * install is slow, spending most of its budget there). On failure it is told plainly what failed
103
+ * and that it may install what it needs itself — an agent that merely finds no `node_modules` and
104
+ * no explanation concludes the environment is offline and works around a gap that isn't there.
105
+ *
106
+ * `scope` names the checkout the install ran in and is set ONLY when that is not the agent's own
107
+ * working directory — the multi-repo layout runs the agent at the workspace root while the install
108
+ * belongs to the primary service's sibling directory. Saying "this checkout" there would point the
109
+ * agent at a root that has no dependency tree of its own.
110
+ */
111
+ export declare function buildDependencyInstallNote(outcome: DependencyInstallOutcome, scope?: string): string;
@@ -0,0 +1,19 @@
1
+ /** The sentinel file the agent writes its effort self-assessment to (relative to its cwd). */
2
+ export declare const EFFORT_REPORT_FILE = ".cat-effort.json";
3
+ /** A container agent's self-assessment of the work it just did. */
4
+ export interface EffortReport {
5
+ /** How hard the work was: 1 (trivial) .. 10 (extremely hard). */
6
+ difficulty: number;
7
+ /** One or two sentences on how hard/easy the work was and why. */
8
+ summary?: string;
9
+ /** What reduced the agent's effectiveness. */
10
+ reducedEffectiveness?: string;
11
+ /** The key obstacles the agent hit. */
12
+ obstacles?: string[];
13
+ }
14
+ /**
15
+ * Read + parse + REMOVE the agent's effort sentinel file from `cwd`. Lenient: returns undefined
16
+ * when the file is absent (the agent wrote none), unreadable, not JSON, or carries nothing
17
+ * meaningful. Never throws — a malformed self-report must never fail an otherwise-good run.
18
+ */
19
+ export declare function readEffortReport(cwd: string): Promise<EffortReport | undefined>;
@@ -0,0 +1,4 @@
1
+ export { PI_MAX_OUTPUT_TOKENS, writePiModelsConfig, writeAgentsContext, runPi, summarizePiRun, parsePiOutput, parseTodoProgress, terminalRunError, type PiRunOutcome, type PiRunStats, type TodoItem, type TodoProgress, } from './pi.js';
2
+ export { DEFAULT_PROGRESS_GUARD_LIMITS, progressGuardLimitsFromEnv, type ProgressGuardLimits, } from './progress-guard.js';
3
+ export { cloneRepo, createBranch, changedPathsFromPorcelain, hasAgentChanges, redactSecrets, } from './git.js';
4
+ export type { RepoSpec } from './job.js';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The structured reason a harness job failed, surfaced on the job view's `failureCause`.
3
+ * Covers only HARNESS-owned failures — container eviction is detected by the runtime facade
4
+ * (a vanished container → `(container evicted or crashed)`), never set here.
5
+ *
6
+ * - `inactivity-timeout` — the inactivity watchdog fired (no agent output for the window).
7
+ * - `max-duration` — the overall wall-clock cap fired.
8
+ * - `agent` — the agent ran but produced an unusable/failed result, or threw.
9
+ * - `git` — a git operation failed (clone/push/merge/PR).
10
+ * - `api` — an upstream API call failed (e.g. the GitHub/GitLab PR/MR REST call).
11
+ * - `llm-upstream` — the model provider rejected every call (auth/quota/rate-limit) and Pi
12
+ * exhausted its retries, so the run never produced a result.
13
+ * - `no-usable-output` — the agent finished but returned no usable report / structured output.
14
+ * - `no-changes` — a coding agent finished without producing any change to push.
15
+ */
16
+ export type FailureCause = 'inactivity-timeout' | 'max-duration' | 'agent' | 'git' | 'api' | 'llm-upstream' | 'no-usable-output' | 'no-changes';
17
+ /**
18
+ * A thrown failure that carries a structured {@link FailureCause}, so a `git` / `api`
19
+ * operation that fails deep in a helper surfaces its real cause instead of being flattened
20
+ * to the generic `agent` in the registry's catch. The watchdog kills set their cause from
21
+ * `killReason` and never throw this; anything else thrown without a cause stays `agent`.
22
+ */
23
+ export declare class HarnessFailure extends Error {
24
+ readonly failureCause: FailureCause;
25
+ constructor(failureCause: FailureCause, message: string);
26
+ }
27
+ /** The structured cause a thrown error carries, or undefined for a plain/agent error. */
28
+ export declare function failureCauseOf(err: unknown): FailureCause | undefined;
29
+ /**
30
+ * The inactivity-watchdog abort message PREFIX. Human-readable only now — the backend reads the
31
+ * structured `inactivity-timeout` {@link FailureCause}, not this phrase (the string fallback was
32
+ * deleted in error-message coverage I5), so it is free to change. The caller appends a `(likely
33
+ * hung ...)` diagnostic clause (phase + last tool) after this, so the prefix deliberately stops
34
+ * before the parenthetical (see `runner.ts` drive catch).
35
+ */
36
+ export declare function inactivityAbortMessage(inactivityMs: number): string;
37
+ /**
38
+ * The max-duration-watchdog abort message. Human-readable only now — the backend reads the
39
+ * structured `max-duration` {@link FailureCause}, not this phrase (the string fallback was deleted
40
+ * in error-message coverage I5), so it is free to change.
41
+ */
42
+ export declare function maxDurationAbortMessage(maxDurationMs: number): string;
@@ -0,0 +1,28 @@
1
+ import { type Logger } from './logger.js';
2
+ /** The sentinel file the Coder appends items to, relative to its working directory. */
3
+ export declare const FOLLOW_UPS_FILENAME = ".cat-follow-ups.jsonl";
4
+ /** One streamed item the Coder surfaced. Mirrors the backend's `streamedFollowUpSchema`. */
5
+ export interface FollowUpLine {
6
+ kind: 'follow_up' | 'question';
7
+ title: string;
8
+ detail: string;
9
+ suggestedAction?: string;
10
+ }
11
+ /**
12
+ * Tails an append-only JSONL sentinel file, yielding only the NEW complete lines on each
13
+ * {@link poll}. Tracks how many characters have been consumed so a partially-written
14
+ * trailing line (no newline yet) is held back until it completes. Tolerant: a malformed
15
+ * line is skipped, a missing file yields nothing — surfacing follow-ups must never
16
+ * disturb the coding run.
17
+ */
18
+ export declare class FollowUpTailer {
19
+ private readonly filePath;
20
+ private readonly onItems;
21
+ private readonly logger;
22
+ private consumed;
23
+ /** Running count of complete-but-unparsable lines, so silent drops become visible. */
24
+ private skipped;
25
+ constructor(filePath: string, onItems: (items: FollowUpLine[]) => void, logger?: Logger);
26
+ /** Read any new complete lines and emit the coerced items. Best-effort; never throws. */
27
+ poll(): Promise<void>;
28
+ }
@@ -0,0 +1,25 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import type { FrontendInfraSpec, InfraSetupRecord } from './job.js';
3
+ import type { RunOptions } from './runner.js';
4
+ import { type Logger } from './logger.js';
5
+ export interface FrontendStandUp {
6
+ /** The processes to terminate on teardown (WireMock + the served app). */
7
+ processes: ChildProcess[];
8
+ /** The URL the built app is served at, when it came up. Folded into the agent prompt. */
9
+ serveUrl?: string;
10
+ /** A problem note folded into the agent prompt (a failed build / server that never bound). */
11
+ note?: string;
12
+ /** The captured (redacted, bounded) stand-up record surfaced on the Tester step. */
13
+ record: InfraSetupRecord;
14
+ }
15
+ /** The install command for a package manager (an explicit `install` overrides this). */
16
+ export declare function installCommand(spec: FrontendInfraSpec): string[];
17
+ /**
18
+ * Build the frontend, start WireMock, serve the built app and health-check both. Best-effort,
19
+ * like the docker-compose stand-up: a failed build / server that never binds is surfaced to
20
+ * the agent as a prompt note (and captured on the record) rather than failing the job — the
21
+ * agent then reports the gap as a concern. Every path returns the processes to tear down.
22
+ */
23
+ export declare function standUpFrontend(dir: string, infra: FrontendInfraSpec, run: Pick<RunOptions, 'signal' | 'onActivity' | 'agentEnv'>, logger?: Logger): Promise<FrontendStandUp>;
24
+ /** Terminate the frontend stand-up's processes (WireMock + the served app). Best-effort. */
25
+ export declare function tearDownFrontend(processes: ChildProcess[], logger?: Logger): Promise<void>;
@@ -0,0 +1,2 @@
1
+ /** Whether `path` exists (a file or directory), swallowing ENOENT (and any stat error). */
2
+ export declare function pathExists(path: string): Promise<boolean>;