@cat-factory/executor-harness 1.78.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 (55) 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 +15 -1
  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/claude-stream.js +23 -0
  13. package/dist/coding-agent.d.ts +263 -0
  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-workspace.js +4 -0
  30. package/dist/pi.d.ts +475 -0
  31. package/dist/pr-description.d.ts +85 -0
  32. package/dist/pr-template.d.ts +101 -0
  33. package/dist/process-exit.d.ts +7 -0
  34. package/dist/process.d.ts +19 -0
  35. package/dist/progress-guard.d.ts +88 -0
  36. package/dist/progress.d.ts +87 -0
  37. package/dist/redact.d.ts +31 -0
  38. package/dist/reproduction-proof.d.ts +224 -0
  39. package/dist/runner.d.ts +282 -0
  40. package/dist/runner.js +3 -0
  41. package/dist/server.d.ts +3 -0
  42. package/dist/structured-output.d.ts +75 -0
  43. package/dist/subagents.d.ts +88 -0
  44. package/dist/subagents.js +74 -4
  45. package/dist/transcript-retention.d.ts +21 -0
  46. package/dist/validation-checks.d.ts +159 -0
  47. package/dist/vcs-api.d.ts +73 -0
  48. package/dist/version.d.ts +2 -0
  49. package/package.json +9 -5
  50. package/src/agent-runner.ts +21 -2
  51. package/src/claude-call-aggregator.ts +181 -32
  52. package/src/claude-stream.ts +21 -0
  53. package/src/pi-workspace.ts +4 -0
  54. package/src/runner.ts +24 -0
  55. package/src/subagents.ts +57 -3
@@ -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 {};
package/dist/runner.js CHANGED
@@ -220,6 +220,9 @@ export class JobRegistry {
220
220
  onValidationReport: (report) => {
221
221
  entry.validationReport = report;
222
222
  },
223
+ onSliceReviews: (reviews) => {
224
+ entry.sliceReviews = reviews;
225
+ },
223
226
  onReproductionProof: (report) => {
224
227
  entry.reproductionReport = report;
225
228
  },
@@ -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;
package/dist/subagents.js CHANGED
@@ -1,9 +1,62 @@
1
1
  import { readdir, stat } from 'node:fs/promises';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
- import { claudeAssistantContent, claudeCallUsage, isObject, redactBody, SUBAGENT_TOOL_NAMES, } from './claude-stream.js';
4
+ import { claudeAssistantContent, claudeCallUsage, claudeToolResultText, isObject, redactBody, SUBAGENT_TOOL_NAMES, } from './claude-stream.js';
5
5
  import { publishCallMetric } from './pi.js';
6
- export function createSliceTracker() {
6
+ // ADR 0026 D2.1 + D3, corrected by ADR 0027. When the Claude Code CLI reviews a large PR
7
+ // it fans the work out across parallel `Task` subagents. Two things then go dark to the
8
+ // harness, which only reads the PARENT process's stream-json stdout:
9
+ //
10
+ // - the parent stream falls quiet for the whole (potentially 15+ minute) parallel
11
+ // review, so the inactivity heartbeat freezes and a healthy run looks wedged (P3);
12
+ // - every subagent's token spend is written to a SEPARATE `subagents/*.jsonl`
13
+ // transcript under the CLI's config home and never reaches the parent stream, so
14
+ // the run's telemetry reports ~0 tokens while hundreds of thousands are spent (P3).
15
+ //
16
+ // This module closes both without disabling the (context-bounding, ADR-0023-wanted)
17
+ // subagent parallelism:
18
+ //
19
+ // - {@link createSliceTracker} derives the slice plan + per-slice progress from the
20
+ // PARENT stream alone — the subagent-dispatch tool_use and its terminal tool_result
21
+ // DO appear there (only the subagent's intermediate turns don't), so slices/progress
22
+ // need no file watching (D2.1). `pickProgress` (./progress.ts) reconciles it with the
23
+ // parent's own plan (ADR 0027 Defect B);
24
+ // - {@link startSubagentWatcher} tails the `subagents/*.jsonl` transcripts for the
25
+ // heartbeat (any new bytes ⇒ `onActivity`) and sums each subagent turn's usage into
26
+ // the run's telemetry (D3).
27
+ //
28
+ // The CLI does NOT write those transcripts to `<configHome>/subagents` (the location ADR
29
+ // 0026 assumed, which never exists — ADR 0027 Defect A). It writes them PER SESSION under
30
+ // `<configHome>/projects/<encoded-cwd>/<session-uuid>/subagents/agent-*.jsonl`, and the
31
+ // session-uuid dir isn't known before the CLI mints it — so the watcher is pointed at the
32
+ // `projects` root and DISCOVERS the `subagents/` dir by walking (see
33
+ // {@link findSubagentTranscripts}).
34
+ //
35
+ // Both degrade gracefully in the sense that a missing directory, an unreadable file, or an
36
+ // unparseable line is swallowed rather than failing the run — the CLI's subagent transcript layout
37
+ // is not a stable contract. But note what that costs SINCE the per-call fold landed: the parent
38
+ // loop's telemetry now filters the subagent turns the CLI tags onto its stdout (they were being
39
+ // counted twice and spliced into the parent's message chain), so when this watcher is wired and
40
+ // yields nothing, the run's subagent calls are recorded by NEITHER channel. `runClaudeCode` warns
41
+ // on exactly that shape, and an `ambientAuth` run — which has no config home to watch, so no
42
+ // watcher — keeps recording them off the parent stream instead
43
+ // (`createSubagentStreamTelemetry`). Do not "simplify" that fallback away.
44
+ // ---------------------------------------------------------------------------
45
+ // Slice / progress tracking off the PARENT stream (D2.1)
46
+ // ---------------------------------------------------------------------------
47
+ /**
48
+ * How much of one slice's terminal report is kept. A slice review is prose (findings for a handful
49
+ * of files), not a transcript, so this is far above a real report while still bounding what a
50
+ * runaway subagent can push onto the step — the reports ride the job view on every poll and are
51
+ * persisted on the run.
52
+ */
53
+ export const SLICE_REPORT_MAX_CHARS = 24_000;
54
+ /**
55
+ * @param secrets Leased-credential strings scrubbed from every captured report. A subagent can
56
+ * echo a token it saw in the checkout, and these reports are persisted on the run, so they are
57
+ * redacted on the way in rather than trusting each consumer to do it.
58
+ */
59
+ export function createSliceTracker(secrets = []) {
7
60
  // Insertion-ordered so the progress `items` render in dispatch order.
8
61
  const slices = new Map();
9
62
  return {
@@ -33,10 +86,27 @@ export function createSliceTracker() {
33
86
  continue;
34
87
  const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
35
88
  const slice = id ? slices.get(id) : undefined;
36
- if (slice)
37
- slice.done = true;
89
+ if (!slice)
90
+ continue;
91
+ slice.done = true;
92
+ // The report is captured here or nowhere: this `tool_result` is the only place the
93
+ // subagent's findings appear on the parent stream, and the next poll may be the last one
94
+ // this job ever answers.
95
+ const report = redactBody(claudeToolResultText(block), secrets).trim();
96
+ if (report)
97
+ slice.report = report.slice(0, SLICE_REPORT_MAX_CHARS);
38
98
  }
39
99
  },
100
+ sliceReviews() {
101
+ return [...slices.values()].map((s) => ({
102
+ label: s.description,
103
+ status: (s.done ? 'completed' : 'in_progress'),
104
+ // A slice that finished but whose result carried no readable text is reported as
105
+ // completed with a null report rather than being dropped: a resume must still know it
106
+ // does not need re-reviewing, and silently omitting it would send it round again.
107
+ report: s.report ?? null,
108
+ }));
109
+ },
40
110
  hasSlices() {
41
111
  return slices.size > 0;
42
112
  },
@@ -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>;