@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
@@ -0,0 +1,282 @@
1
+ import type { FollowUpLine } from './follow-ups.js';
2
+ import type { ValidationReport } from './validation-checks.js';
3
+ import type { ReproductionReport } from './reproduction-proof.js';
4
+ import type { SliceReview } from './subagents.js';
5
+ import type { HarnessCallMetric, TodoProgress, ToolSpan } from './pi.js';
6
+ import { type Logger } from './logger.js';
7
+ import { type FailureCause } from './failure.js';
8
+ /** Non-secret correlation fields a job carries on every log line (jobId, repo, branch, …). */
9
+ type LogFields = Record<string, unknown>;
10
+ /** Options threaded into the long-running git/Pi work so a watchdog can cancel it. */
11
+ export interface RunOptions {
12
+ signal?: AbortSignal;
13
+ onActivity?: () => void;
14
+ /** Receives the latest subtask counts as Pi updates its todo list. */
15
+ onProgress?: (progress: TodoProgress) => void;
16
+ /** Receives one compact {@link ToolSpan} per completed tool call (observability). */
17
+ onSpan?: (span: ToolSpan) => void;
18
+ /** Receives the forward-looking follow-up / question items the Coder streamed since the last poll. */
19
+ onFollowUp?: (items: FollowUpLine[]) => void;
20
+ /**
21
+ * Receives each completed PRE-PR VALIDATION attempt the moment the harness finishes running
22
+ * the service's check commands, so the backend can surface the repair loop LIVE ("lint failed,
23
+ * repairing — attempt 2 of 3") instead of only in the terminal result. Latest-wins (NOT a drain
24
+ * buffer): a published attempt is final, and the loop republishes a whole new one per round.
25
+ */
26
+ onValidationReport?: (report: ValidationReport) => void;
27
+ /**
28
+ * Receives each completed BUGFIX REPRODUCTION PROOF attempt the moment the harness finishes
29
+ * running the declared check against both trees, so the backend can surface a failed
30
+ * verification WHILE the repair loop still runs rather than only in the terminal result.
31
+ * Latest-wins (NOT a drain buffer), exactly like {@link onValidationReport}: a published
32
+ * attempt is final, and the loop republishes a whole new one — with a fresh `at` — per round.
33
+ */
34
+ onReproductionProof?: (report: ReproductionReport) => void;
35
+ /**
36
+ * Receives the full set of per-slice reviews a parallel review has captured, republished each
37
+ * time a slice's subagent returns. Latest-wins (NOT a drain buffer), for the same reason as
38
+ * {@link onValidationReport} but with more at stake: these carry the slices' actual review work,
39
+ * and a review whose aggregation never finishes is recoverable ONLY from what the backend
40
+ * already persisted. Absent for a job that dispatched no subagents.
41
+ */
42
+ onSliceReviews?: (reviews: SliceReview[]) => void;
43
+ /**
44
+ * Receives each per-call telemetry row the moment the agent's CLI stream yields it, so a
45
+ * run's model calls reach `llm_call_metrics` WHILE it runs rather than only in its terminal
46
+ * result. The registry stamps the call's job-scoped {@link HarnessCallMetric.seq} and buffers
47
+ * it for the next poll to drain.
48
+ *
49
+ * Call this for every metric you also put on the result — the SAME object, not a copy: the
50
+ * stamped `seq` is what lets the backend recognise the terminal write of an already-recorded
51
+ * call and skip it. A run that dies mid-flight (the container is evicted, the harness process
52
+ * is OOM-killed) never produces a terminal result, so without this its entire token spend and
53
+ * every prompt/response body are lost — exactly the run an operator most needs to inspect.
54
+ */
55
+ onCallMetric?: (call: HarnessCallMetric) => void;
56
+ /**
57
+ * Mark the coarse lifecycle phase the handler has entered (`clone` / `agent` / `push` / …).
58
+ * Drives the stuck-run breadcrumb: an inactivity kill reports WHICH phase was hung, and the
59
+ * per-phase wall-clock is logged on completion. Free-form; unknown phases just show verbatim.
60
+ */
61
+ onPhase?: (phase: string) => void;
62
+ /**
63
+ * The phase most recently marked via {@link onPhase} — the read side of the same marker, for
64
+ * work that has to TELL the backend which phase it is in rather than merely record it. Today
65
+ * that is the Pi path, whose calls are metered server-side by the LLM proxy: the harness tags
66
+ * the proxy URL with this so a repair round's spend is attributable
67
+ * (`docs/initiatives/token-burn-instrumentation.md`). Absent ⇒ no phase is carried and those
68
+ * calls land in the backend's unattributed slice.
69
+ */
70
+ currentPhase?: () => string;
71
+ /** A per-job child logger carrying the run's correlation fields (jobId, repo, branch, …). */
72
+ log?: Logger;
73
+ /**
74
+ * Extra environment for the agent's child process, scoped to THIS job. The CLI is spawned with
75
+ * `{...process.env, ...agentEnv}`, so these reach the agent and every shell tool it spawns.
76
+ *
77
+ * This is the seam for anything per-job that would otherwise be written to a process- or
78
+ * HOME-global (the tester's secrets, a private-registry npmrc pointer). Those globals are only
79
+ * per-job when the process is — true for a container, FALSE for the local native host-process
80
+ * transport, which serves every concurrent ambient job from one process on the developer's own
81
+ * HOME. Set it via `withAgentEnv`; never mutate `process.env` for a job.
82
+ */
83
+ agentEnv?: Record<string, string>;
84
+ }
85
+ export type JobState = 'running' | 'done' | 'failed';
86
+ /**
87
+ * The minimum a job result must expose: a structured `error` marks a job-level
88
+ * failure even when the HTTP run itself succeeded. Every agent result (explore /
89
+ * coding / bootstrap / conflict) satisfies this, so {@link JobRegistry} is generic
90
+ * over the result it tracks while reusing one watchdog/lifecycle.
91
+ */
92
+ export interface JobResultBase {
93
+ error?: string;
94
+ /**
95
+ * The structured reason a clean-exit result failed (set alongside `error` by a handler that
96
+ * finished but produced an unusable/failed result — no-usable-output, no-changes, …). The
97
+ * registry copies it onto the job view's `failureCause`. Absent on a watchdog/throw failure
98
+ * (the registry sets that cause itself). See {@link FailureCause}.
99
+ */
100
+ failureCause?: FailureCause;
101
+ }
102
+ /** The job view returned by GET /jobs/{id}, generic over the orchestration's result. */
103
+ export interface JobView<TResult extends JobResultBase = JobResultBase> {
104
+ id: string;
105
+ state: JobState;
106
+ startedAt: number;
107
+ /** Epoch ms of the last sign of progress (job start, or Pi output). */
108
+ heartbeatAt: number;
109
+ /**
110
+ * The coarse lifecycle phase the job is CURRENTLY in (`starting` → `clone` → `agent`
111
+ * → `push` → `done`/`failed`), so the backend can surface WHAT the container is doing
112
+ * rather than a blank "working" state — is it still cloning/preparing the checkout, or
113
+ * has the agent begun making calls? The same per-phase marker that drives the stuck-run
114
+ * breadcrumb on a failure, exposed live here while the job runs. Free-form; unknown
115
+ * phases just show verbatim. Always present (seeded `starting` at job start).
116
+ */
117
+ phase?: string;
118
+ /**
119
+ * Latest subtask progress from Pi's `todo` tool while the job runs — the
120
+ * Worker poll surfaces it to the board (e.g. "3/8 done"). Absent until Pi
121
+ * first touches its todo list (or if the model never uses it).
122
+ */
123
+ progress?: TodoProgress;
124
+ /** Present when `state === 'done'`: the orchestration's structured result. */
125
+ result?: TResult;
126
+ /** Present when `state === 'failed'`: why the job faulted (or was killed). */
127
+ error?: string;
128
+ /**
129
+ * Present when `state === 'failed'`: the STRUCTURED failure cause, so the backend can
130
+ * classify the failure without regex-matching {@link error}. Backward compatible — the
131
+ * backend prefers this and falls back to the (still-stable) `error` regex when absent.
132
+ * Container eviction is NOT represented here (the runtime facade detects that from a
133
+ * vanished container); see {@link FailureCause}.
134
+ */
135
+ failureCause?: FailureCause;
136
+ /**
137
+ * Present when `state === 'failed'`: an extended, redacted diagnostic (phase-timing
138
+ * breakdown, last-tool breadcrumb, …) distinct from the one-line {@link error}. The
139
+ * backend surfaces it as the failure `detail` on the board card. Best-effort.
140
+ */
141
+ detail?: string;
142
+ /**
143
+ * Tool spans accumulated SINCE THE LAST POLL (drain-on-read): the GET /jobs/{id}
144
+ * handler returns the spans buffered since the previous poll and clears the buffer,
145
+ * so the harness only ever holds one poll-interval's worth. Best-effort observability
146
+ * — a dropped poll response loses at most one window. Absent until a tool runs.
147
+ */
148
+ spans?: ToolSpan[];
149
+ /**
150
+ * Forward-looking follow-up / question items the Coder streamed SINCE THE LAST POLL
151
+ * (drain-on-read, exactly like {@link spans}): the GET /jobs/{id} handler returns the
152
+ * items buffered since the previous poll and clears the buffer. The backend appends them
153
+ * to the run's step so the Follow-up companion surfaces them live. Absent until the Coder
154
+ * surfaces the first one (and only on a follow-ups-enabled coding run).
155
+ */
156
+ followUps?: FollowUpLine[];
157
+ /**
158
+ * Per-model-call telemetry the agent's CLI stream yielded SINCE THE LAST POLL
159
+ * (drain-on-read, exactly like {@link spans}). The backend records these into
160
+ * `llm_call_metrics` as they arrive, so a run's token spend and prompt/response bodies are
161
+ * queryable while it is still running — and survive it dying before it can produce a
162
+ * terminal result. Each carries a job-scoped `seq` so the terminal
163
+ * {@link JobResultBase} list can re-offer the same calls without duplicating rows.
164
+ * Absent until the agent's first model call (and on the proxy-metered Pi harness, whose
165
+ * calls the LLM proxy meters directly).
166
+ */
167
+ callMetrics?: HarnessCallMetric[];
168
+ /**
169
+ * ADR 0026 D4: set when the cold-start watchdog fired — the job produced NO activity
170
+ * within {@link RunnerLimits.coldStartMs} of starting, a likely onboarding/auth wedge.
171
+ * This does NOT fail the job (the inactivity/max-duration watchdogs still own that).
172
+ *
173
+ * Legibility is via the per-job container log line emitted the moment it fires (the
174
+ * ~2-minute early signal the ADR wants), this field on the GET /jobs/{id} view for an
175
+ * operator hitting the endpoint, and — when the job goes on to fail — a sentence folded into
176
+ * {@link detail}, which is the path that reaches the run without a new field on every
177
+ * transport hop. Surfacing it on a still-RUNNING step (the early warning) remains deferred.
178
+ * Absent on a job that produced output promptly (the overwhelming common case). Sticky once set.
179
+ */
180
+ coldStart?: {
181
+ atMs: number;
182
+ message: string;
183
+ };
184
+ /**
185
+ * The LATEST completed pre-PR validation attempt (see `docs/initiatives/pre-pr-validation.md`).
186
+ * Unlike {@link spans}/{@link followUps} this is NOT drain-on-read: it is a whole-value latest
187
+ * publish, so re-reading it on a later poll is harmless and a dropped poll loses nothing (the
188
+ * next round republishes). Absent for a job whose service configured no checks.
189
+ */
190
+ validationReport?: ValidationReport;
191
+ /**
192
+ * The LATEST completed bugfix reproduction-proof attempt (see
193
+ * `docs/initiatives/bugfix-reproduction-proof.md`). Like {@link validationReport} — and unlike
194
+ * {@link spans}/{@link followUps} — this is a whole-value latest publish, not drain-on-read, so
195
+ * re-reading it on a later poll is harmless and a dropped poll loses nothing. Absent for a job
196
+ * that carried no reproduction declaration.
197
+ */
198
+ reproductionReport?: ReproductionReport;
199
+ /**
200
+ * The per-slice reviews captured so far on a parallel (subagent-fanned) review — each slice's
201
+ * label, whether its subagent returned, and its verbatim report. A whole-value latest publish
202
+ * like {@link validationReport}, not drain-on-read.
203
+ *
204
+ * This is the durable half of a PR review. The reviewer returns `slices`/`findings` only in its
205
+ * TERMINAL structured output, so before this existed a review killed mid-run (or one whose
206
+ * aggregation pass wedged) lost every finished slice and could only be re-run from zero. The
207
+ * backend persists these onto the step as they arrive, which is what a manual resume re-aggregates
208
+ * from. Absent for a job that dispatched no subagents.
209
+ */
210
+ sliceReviews?: SliceReview[];
211
+ }
212
+ /** Watchdog windows that bound every job. Tunable via the container's env. */
213
+ export interface RunnerLimits {
214
+ /** Hard ceiling on total job wall-clock before it's force-failed. */
215
+ maxDurationMs: number;
216
+ /** Force-fail the job if the agent produces no output for this long (hang guard). */
217
+ inactivityMs: number;
218
+ /**
219
+ * ADR 0026 D4: a short first-output window. If the job produces NO activity within this
220
+ * long after start, emit a structured cold-start diagnostic (a likely onboarding/auth
221
+ * wedge) — WITHOUT killing the run. Purely a legibility signal so a genuine cold-start
222
+ * wedge surfaces in a couple of minutes instead of waiting out the full inactivity
223
+ * window. Safely under the clone-inclusive phases (a large clone still streams git
224
+ * progress, which counts as activity). Set to 0 to disable.
225
+ */
226
+ coldStartMs: number;
227
+ }
228
+ export declare function loadRunnerLimits(env?: NodeJS.ProcessEnv): RunnerLimits;
229
+ /**
230
+ * Tracks background jobs by id. Keyed by the backend-supplied job id (the per-step
231
+ * job id) so a re-dispatched start re-attaches to the running job rather than starting
232
+ * a duplicate — which keeps the durable driver's retries idempotent and avoids redoing
233
+ * already-running work. Generic over the job/result shape so the same lifecycle +
234
+ * inactivity/max-duration watchdogs drive every agent run.
235
+ */
236
+ export declare class JobRegistry<TJob = unknown, TResult extends JobResultBase = JobResultBase> {
237
+ private readonly limits;
238
+ private readonly run;
239
+ private readonly describe;
240
+ private readonly jobs;
241
+ constructor(limits: RunnerLimits, run: (job: TJob, opts: RunOptions) => Promise<TResult>, describe?: (job: TJob) => LogFields);
242
+ /** Start the job for `id`, or return the existing one (idempotent re-attach). */
243
+ start(id: string, job: TJob): JobView<TResult>;
244
+ /**
245
+ * Poll the job — and DRAIN its observability buffers (drain-on-read). The GET /jobs/{id}
246
+ * handler is the sole caller, so each poll returns the spans / follow-ups / call metrics
247
+ * accumulated since the previous poll and clears them, bounding the harness buffers to one
248
+ * poll interval.
249
+ */
250
+ get(id: string): JobView<TResult> | undefined;
251
+ /**
252
+ * Abort every RUNNING job (fires each run's abort signal, which SIGTERM→SIGKILLs its
253
+ * CLI/git children via `killChildProcess`). The graceful-shutdown hook: a harness dying
254
+ * to SIGTERM must not orphan a live agent subprocess — reparented, it would keep working
255
+ * unsupervised (and, in native local mode, on the developer's own login). Returns the
256
+ * number of jobs aborted.
257
+ */
258
+ abortAll(reason: string): number;
259
+ /**
260
+ * How many jobs are still RUNNING. Graceful shutdown polls this so it can exit the moment the
261
+ * aborted jobs have actually settled (the common case: the CLI honours SIGTERM in ms) instead
262
+ * of waiting out a fixed kill-grace window.
263
+ */
264
+ runningCount(): number;
265
+ private drive;
266
+ /**
267
+ * Build the redacted one-line `error`, the structured {@link FailureCause}, and the extended
268
+ * `detail` for a failed job. Watchdog kills set their structured cause (`inactivity-timeout` /
269
+ * `max-duration`) — the backend classifies on that, so their message is a human-readable
270
+ * breadcrumb of where they hung, no longer a regex-stable phrase; a thrown error keeps its own
271
+ * message and its structured cause when tagged (a git op → `git`, an upstream API call → `api`),
272
+ * else `agent`. All strings are credential-scrubbed.
273
+ *
274
+ * `detail` is where the evidence the harness already holds but the one-line `error` has no room
275
+ * for lands: the phase breakdown, the {@link failureBreadcrumb} (last completed tool + how long
276
+ * the run had been silent), and the cold-start diagnostic when that watchdog recorded one. It is
277
+ * the only one of the three that reaches the run's failure record, so a diagnostic that isn't
278
+ * folded in here is effectively invisible outside the container log.
279
+ */
280
+ private describeFailure;
281
+ }
282
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ declare const server: import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
3
+ export { server };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Declarative definition of one structured-output kind: how to label it, what shape
3
+ * to ask the repair model for, and how to turn the agent's text into the domain value.
4
+ * `parse` returns null (or throws) when the text is unusable. Built per-job when the
5
+ * parser needs job context (e.g. a fallback service name).
6
+ */
7
+ export interface StructuredOutputSpec<T> {
8
+ /** Label for logs/telemetry, e.g. `requirements` / `blueprint` / `merger`. */
9
+ label: string;
10
+ /** Compact human description of the expected top-level JSON shape, fed to the model. */
11
+ shapeHint: string;
12
+ /** Parse the agent's text into the domain value, or null/throw when unusable. */
13
+ parse: (text: string) => T | null;
14
+ }
15
+ /** Runtime wiring to reach the LLM proxy for the repair call. */
16
+ export interface ProxyAccess {
17
+ /** Pi-harness proxy base URL; absent for subscription harnesses (no proxy repair). */
18
+ proxyBaseUrl?: string;
19
+ /**
20
+ * The backend serves the phase-tagged completions route, so the repair call can be attributed
21
+ * to {@link STRUCTURED_REPAIR_PHASE} rather than piling into the unattributed slice
22
+ * (see {@link HarnessAuthFields.proxyPhasePath}).
23
+ */
24
+ proxyPhasePath?: boolean;
25
+ /** Pi-harness proxy session token; absent for subscription harnesses. */
26
+ sessionToken?: string;
27
+ model: string;
28
+ jobId: string;
29
+ signal?: AbortSignal;
30
+ /** Carried for context (the subscription harnesses can't use the proxy for repair). */
31
+ harness?: string;
32
+ subscriptionToken?: string;
33
+ subscriptionBaseUrl?: string;
34
+ }
35
+ /** Structured diagnostics for a resolution attempt, surfaced to logs + the failure reason. */
36
+ export interface StructuredOutputDiagnostics {
37
+ /** Which attempt produced a usable value (or `none` when both failed). */
38
+ parsedOn: 'primary' | 'repair' | 'none';
39
+ /** Length of the agent's primary (Pi) output, in characters. */
40
+ primaryChars: number;
41
+ /** Whether the primary output looked token-doubled (advisory heuristic). */
42
+ looksDoubled: boolean;
43
+ /** Whether a repair call was made. */
44
+ repairAttempted: boolean;
45
+ /** Whether the repair call produced a usable value. */
46
+ repairSucceeded: boolean;
47
+ /** One-line reason the repair call itself failed (HTTP error / still-unparseable), if any. */
48
+ repairError?: string;
49
+ }
50
+ export interface StructuredOutputResult<T> {
51
+ value: T | null;
52
+ diagnostics: StructuredOutputDiagnostics;
53
+ }
54
+ /**
55
+ * Heuristic detector for the token-doubling corruption ("serviceservice",
56
+ * "observobservabilityability", `{\n{\n`). Greedy scan over a bounded prefix: at each
57
+ * position, find the longest 2..{@link MAX_DOUBLE_RUN}-char run that is immediately
58
+ * repeated and count both copies as "doubled", then measure the doubled fraction of
59
+ * the scanned text. Token-doubled text (consecutive `t t` pairs) scores near 1.0;
60
+ * normal JSON/prose scores low (only incidental short repeats). Advisory ONLY — it
61
+ * labels a failure for telemetry, it never mutates output.
62
+ */
63
+ export declare function looksTokenDoubled(text: string): {
64
+ doubled: boolean;
65
+ ratio: number;
66
+ };
67
+ /**
68
+ * Resolve a structured output: parse the agent's `primaryText` via `spec.parse`; on
69
+ * failure, make ONE structured repair call and re-parse. Returns the value (or null
70
+ * when both attempts fail) plus {@link StructuredOutputDiagnostics}. Logging side
71
+ * effects only; never throws (a repair transport error is captured in the diagnostics).
72
+ */
73
+ export declare function resolveStructuredOutput<T>(spec: StructuredOutputSpec<T>, primaryText: string, access: ProxyAccess): Promise<StructuredOutputResult<T>>;
74
+ /** Append a compact, human-readable diagnostics suffix to a no-document failure reason. */
75
+ export declare function diagnosticsSuffix(d: StructuredOutputDiagnostics): string;
@@ -0,0 +1,88 @@
1
+ import type { Logger } from './logger.js';
2
+ import { type HarnessCallMetric, type TodoProgress } from './pi.js';
3
+ /**
4
+ * How much of one slice's terminal report is kept. A slice review is prose (findings for a handful
5
+ * of files), not a transcript, so this is far above a real report while still bounding what a
6
+ * runaway subagent can push onto the step — the reports ride the job view on every poll and are
7
+ * persisted on the run.
8
+ */
9
+ export declare const SLICE_REPORT_MAX_CHARS = 24000;
10
+ /** One slice's live review, as published on the job view. Mirrors `prReviewSliceReviewSchema`. */
11
+ export interface SliceReview {
12
+ label: string;
13
+ status: 'in_progress' | 'completed';
14
+ report?: string | null;
15
+ }
16
+ /** Tracks parallel subagents seen on the parent stream to derive slice progress. */
17
+ export interface SliceTracker {
18
+ /** Feed an `assistant` message's content blocks: registers any subagent dispatches. */
19
+ onAssistant(content: unknown[]): void;
20
+ /**
21
+ * Feed a `user` message's content blocks: marks the paired subagent(s) complete AND captures
22
+ * each one's terminal report (see {@link SliceTracker.sliceReviews}).
23
+ */
24
+ onUser(content: unknown[]): void;
25
+ /**
26
+ * Every dispatched slice with its status and captured report, in dispatch order — the durable
27
+ * half of this tracker. Published as a whole value on each poll (NOT drain-on-read): the set
28
+ * only grows, and a dropped poll response must never permanently lose a finished slice's
29
+ * review, which is the entire point of capturing it. Empty when nothing was dispatched.
30
+ */
31
+ sliceReviews(): SliceReview[];
32
+ /** Whether any `Task` subagent has been dispatched (⇒ this run parallelised). */
33
+ hasSlices(): boolean;
34
+ /**
35
+ * Progress derived from the dispatched subagents (completed / in-flight / total),
36
+ * or undefined when none have been dispatched. Reconciled with the parent's own plan
37
+ * by `pickProgress` (./progress.ts) — it is NOT gated off by the presence of a plan
38
+ * (that gate was ADR 0027 Defect B: the pr-reviewer prompt writes the plan ONCE at
39
+ * grouping time and never marks it done, which used to permanently mask this signal).
40
+ */
41
+ progress(): TodoProgress | undefined;
42
+ }
43
+ /**
44
+ * @param secrets Leased-credential strings scrubbed from every captured report. A subagent can
45
+ * echo a token it saw in the checkout, and these reports are persisted on the run, so they are
46
+ * redacted on the way in rather than trusting each consumer to do it.
47
+ */
48
+ export declare function createSliceTracker(secrets?: string[]): SliceTracker;
49
+ export interface SubagentWatcherOptions {
50
+ /** Fed the heartbeat when a transcript grows, so the inactivity watchdog sees the run is alive. */
51
+ onActivity?: () => void;
52
+ /** Leased-credential strings to scrub from captured bodies (the transcripts can echo the token). */
53
+ secrets?: string[];
54
+ /** Fallback model id stamped on a subagent call whose transcript omits one. */
55
+ model?: string;
56
+ /**
57
+ * Streams each lifted subagent call to the live telemetry drain (the run's `RunOptions`
58
+ * hook). Subagent work is exactly where a long review spends most of its tokens, and it is
59
+ * the phase the parent stream goes quiet for — so without this a run killed mid-fan-out
60
+ * reports nothing at all.
61
+ */
62
+ onCallMetric?: (call: HarnessCallMetric) => void;
63
+ /** Poll cadence (ms); overridable for tests. */
64
+ intervalMs?: number;
65
+ log?: Logger;
66
+ }
67
+ export interface SubagentWatcher {
68
+ /** Do a final poll, then stop watching. Idempotent; never throws. */
69
+ stop(): Promise<void>;
70
+ /** Cumulative subagent usage lifted so far (input + output tokens). */
71
+ usage(): {
72
+ inputTokens: number;
73
+ outputTokens: number;
74
+ };
75
+ /** The per-call telemetry rows lifted from the subagent transcripts so far. */
76
+ calls(): HarnessCallMetric[];
77
+ }
78
+ /**
79
+ * Start watching `root` (the CLI's `<configHome>/projects` tree) for subagent `*.jsonl`
80
+ * transcripts — any file under a `subagents/` directory beneath it (see
81
+ * {@link findSubagentTranscripts}) — tailing each file by byte offset. New content feeds
82
+ * `onActivity` (heartbeat) and each assistant turn carrying usage is lifted into a
83
+ * {@link HarnessCallMetric} + summed into the cumulative usage. Best-effort throughout: the
84
+ * tree may not exist yet (created lazily by the CLI), a file may be mid-write, and the
85
+ * line/usage shape may change across CLI versions — every such case is swallowed so the
86
+ * watcher can only ever ADD signal, never break the run.
87
+ */
88
+ export declare function startSubagentWatcher(root: string, opts: SubagentWatcherOptions): SubagentWatcher;
@@ -0,0 +1,21 @@
1
+ import type { Logger } from './logger.js';
2
+ /**
3
+ * A marker file dropped into every retention dir THIS module creates. The pruner deletes ONLY
4
+ * dirs carrying it, so pointing `HARNESS_TRANSCRIPT_ROOT` at a shared (non-dedicated) directory
5
+ * can never `rm -rf` unrelated sibling content — we only ever sweep our own retained transcripts.
6
+ */
7
+ export declare const RETENTION_MARKER = ".cf-retained";
8
+ export interface RetainOptions {
9
+ /** A short label for the run/harness, folded into the retention log line. */
10
+ label?: string;
11
+ /** The per-job child logger, so the retained path is logged with the run's correlation fields. */
12
+ log?: Logger;
13
+ }
14
+ /**
15
+ * Move the named transcript `subdirs` out of the credential-bearing config `home` into the
16
+ * retention root (so the caller's subsequent `rm(home)` can't take them), then prune retained
17
+ * transcripts older than the TTL. Both steps are best-effort: any failure is swallowed (logged
18
+ * at debug) so this can never fail an otherwise-successful run. Returns the destination dir when
19
+ * something was retained (for logging/tests), else `undefined`.
20
+ */
21
+ export declare function retainSessionTranscripts(home: string, subdirs: string[], options?: RetainOptions): Promise<string | undefined>;
@@ -0,0 +1,159 @@
1
+ import type { RunOptions } from './runner.js';
2
+ import type { Logger } from './logger.js';
3
+ /** One configured check, as it arrives on the job body. */
4
+ export interface ValidationCheckSpec {
5
+ label: string;
6
+ command: string;
7
+ }
8
+ /** The whole validation config a job carries. */
9
+ export interface ValidationChecksSpec {
10
+ checks: ValidationCheckSpec[];
11
+ /** How many agent+check rounds the loop may run (1 = check once, no repair round). */
12
+ maxAttempts: number;
13
+ }
14
+ /** One command's outcome within an attempt. */
15
+ export interface ValidationCheckOutcome {
16
+ label: string;
17
+ command: string;
18
+ exitCode: number;
19
+ passed: boolean;
20
+ outputTail?: string;
21
+ durationMs?: number;
22
+ timedOut?: boolean;
23
+ }
24
+ /**
25
+ * One attempt's result: the REPORT that crosses the wire, plus the FULL (scrubbed, 16k) output
26
+ * tails kept in memory for the repair prompt. The two are deliberately separate — see
27
+ * {@link VALIDATION_REPORT_TAIL_CHARS}.
28
+ */
29
+ export interface ValidationAttempt {
30
+ report: ValidationReport;
31
+ /** Full scrubbed output per check label — never leaves the container. */
32
+ fullTails: Map<string, string>;
33
+ }
34
+ /**
35
+ * The ceiling the harness clamps a body-supplied `validationChecks.maxAttempts` to, and the
36
+ * default it applies when the body omits one.
37
+ *
38
+ * DELIBERATE DUPLICATES of `VALIDATION_MAX_ATTEMPTS_CEILING` / `VALIDATION_DEFAULT_MAX_ATTEMPTS`
39
+ * in `@cat-factory/contracts` — the published image takes no schema dependency, so the harness
40
+ * cannot import them. Keep the two in step: the API validates writes against the contracts
41
+ * values, so a harness clamping to a DIFFERENT ceiling would silently cap a budget an operator
42
+ * was allowed to save, with nothing to flag the mismatch.
43
+ */
44
+ export declare const VALIDATION_MAX_ATTEMPTS_CEILING = 10;
45
+ export declare const VALIDATION_DEFAULT_MAX_ATTEMPTS = 3;
46
+ /**
47
+ * Parse the optional PRE-PR VALIDATION CHECKS envelope off the job body: the service's ordered
48
+ * `{ label, command }` pairs and the repair-round budget. Every entry needs a non-empty command;
49
+ * entries without one are dropped, and a spec that ends up with no usable check returns
50
+ * `undefined` — so a malformed body degrades to the exact pre-feature behaviour (no loop, PR
51
+ * opens as before) rather than failing an otherwise-good coding run. `maxAttempts` is clamped to
52
+ * a sane range so a bad body can't make a container loop forever.
53
+ *
54
+ * Lives with the feature rather than in `job.ts` so each pre-PR verification phase owns its own
55
+ * job-body parser next to the loop that consumes it (the reproduction proof's
56
+ * `parseReproductionSpec` is the sibling); `job.ts` stays the job SHAPE plus the generic
57
+ * assembly.
58
+ */
59
+ export declare function parseValidationChecksSpec(value: unknown): ValidationChecksSpec | undefined;
60
+ /** One attempt's report — what the backend records on the step. */
61
+ export interface ValidationReport {
62
+ passed: boolean;
63
+ attempts: number;
64
+ maxAttempts: number;
65
+ outcomes: ValidationCheckOutcome[];
66
+ at: number;
67
+ }
68
+ /**
69
+ * Per-command output kept on the REPORT (what crosses the wire and lands in the run's persisted
70
+ * `detail` blob). Deliberately smaller than `MAX_CAPTURED_OUTPUT_CHARS` (`redact.ts`), which is what the
71
+ * AGENT sees in its repair prompt: the agent needs the full failure to fix it, the operator needs
72
+ * enough to recognise it, and a chatty build must not inflate every run's stored state.
73
+ */
74
+ export declare const VALIDATION_REPORT_TAIL_CHARS = 4000;
75
+ /**
76
+ * The per-command watchdog: the longest a single check may run before it is killed and treated
77
+ * as a failure, so one hung `pnpm test` cannot wedge a run. Overridable via env for tests;
78
+ * defaults to 15 minutes (matching the ralph completion command's watchdog).
79
+ */
80
+ export declare function validationCommandTimeoutMs(): number;
81
+ /**
82
+ * How often the check loop feeds the run's inactivity watchdog. Well under the harness's own
83
+ * `JOB_INACTIVITY_MS` (default 10 min) so a single slow command can never look wedged; matches
84
+ * the frontend stand-up's heartbeat, which exists for exactly the same reason. Overridable via
85
+ * env for tests, like {@link validationCommandTimeoutMs}.
86
+ */
87
+ export declare function validationHeartbeatMs(): number;
88
+ /**
89
+ * Run every configured check IN ORDER against `cwd` and build the attempt's report.
90
+ *
91
+ * Runs all of them even after one fails, rather than short-circuiting: the agent repairing the
92
+ * checkout should see every problem at once instead of rediscovering the next one on the next
93
+ * round, which is the difference between one repair round and four. (A check whose failure makes
94
+ * the rest meaningless — e.g. a failed install — still costs only the cheap downstream failures.)
95
+ *
96
+ * Keeps the run's inactivity watchdog fed for the whole attempt. These commands are exactly the
97
+ * activity-SILENT kind — a cold `install`, a full `test` run, a `build` — and the harness spawns
98
+ * them itself rather than through the agent, so they emit no activity events of their own. The
99
+ * job-level watchdog (`JOB_INACTIVITY_MS`, default 10 min) is TIGHTER than one command's own
100
+ * watchdog ({@link validationCommandTimeoutMs}, default 15 min), so without this a legitimately
101
+ * slow check would abort the entire run as "inactivity" — mislabelling a healthy build as a
102
+ * wedge, and making the per-command timeout unreachable at stock settings.
103
+ */
104
+ export declare function runValidationChecks(cwd: string, spec: ValidationChecksSpec, attempt: number, logger: Logger, opts: RunOptions): Promise<ValidationAttempt>;
105
+ /**
106
+ * The repair instruction handed to the agent after a failed attempt: the failing commands and
107
+ * their captured output, plus an explicit statement of the exit condition and the remaining
108
+ * budget. The FULL captured tail is used here (not the report's smaller bound) — the agent needs
109
+ * the whole failure to fix it, and this text never leaves the container.
110
+ *
111
+ * Deliberately prescriptive about scope: a validation loop that lets the agent "fix" the failure
112
+ * by weakening the check is worse than no loop at all, so the prompt forbids editing the
113
+ * commands' configuration to make them pass.
114
+ */
115
+ export declare function buildRepairPrompt(report: ValidationReport, fullTails: Map<string, string>,
116
+ /**
117
+ * New files the agent created but never `git add`ed, if the caller can tell. The harness only
118
+ * auto-stages TRACKED edits (`git add -u`), so an uncommitted new file is invisible to the push
119
+ * — yet fully visible to the checks, which run against the working tree. Naming them here is
120
+ * what stops the loop going green on work the pull request would not contain.
121
+ */
122
+ untrackedFiles?: string[]): string;
123
+ /**
124
+ * The pre-PR validation LOOP: run the checks, and while they fail and budget remains, hand the
125
+ * captured output back to the agent as its next instruction and check again. Returns the LAST
126
+ * attempt's report — `passed: true` means the caller may open the PR; `passed: false` means the
127
+ * budget is spent and the caller must FAIL the job with this report as the evidence, opening
128
+ * nothing.
129
+ *
130
+ * Generic by construction: it knows nothing about agent kinds, repos or PRs — only how to run
131
+ * commands in a directory and how to ask for another pass. Every input (`workDir`, `spec`,
132
+ * `opts.agentEnv`) is per-job, so two concurrent jobs on the ONE local-native host process cannot
133
+ * see each other's configuration (`validation-checks.concurrency.test.ts` pins this).
134
+ *
135
+ * `onAttempt` publishes each completed attempt on the job view so the loop is observable while it
136
+ * runs; `onAgentPass` lets the caller fold each repair pass's stats/usage/telemetry into the run's
137
+ * totals, so a 3-round loop reports what all 3 rounds actually spent.
138
+ */
139
+ export declare function runValidationLoop<TRun>(args: {
140
+ workDir: string;
141
+ spec: ValidationChecksSpec;
142
+ logger: Logger;
143
+ opts: RunOptions;
144
+ runAgentPass: (userPrompt: string) => Promise<TRun>;
145
+ onAgentPass?: (run: TRun) => void;
146
+ /**
147
+ * Optional: the new files left uncommitted in the checkout, folded into each repair prompt.
148
+ * Injected rather than read here so this module stays git-agnostic (it knows only how to run
149
+ * commands in a directory and how to ask for another pass); the coding agent, which owns the
150
+ * checkout, supplies it. A throw is swallowed — a missing warning must never fail the loop.
151
+ */
152
+ listUncommittedNewFiles?: () => Promise<string[]>;
153
+ }): Promise<ValidationReport>;
154
+ /**
155
+ * The failure message for a run whose pre-PR validation never went green: which checks failed
156
+ * (with exit codes) and the last one's captured output. Read by the operator on the step's
157
+ * failure card, so it must say what broke without needing the full report opened.
158
+ */
159
+ export declare function validationFailureMessage(report: ValidationReport): string;