@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
package/dist/pi.d.ts ADDED
@@ -0,0 +1,475 @@
1
+ import type { EffortReport } from './effort.js';
2
+ import { type ProgressGuardLimits } from './progress-guard.js';
3
+ /**
4
+ * Per-completion output-token ceiling Pi requests (its model-entry `maxTokens`).
5
+ * Generous on purpose: a reasoning model (e.g. GLM-5.2) spends tokens on its
6
+ * `<think>` trace before the answer + tool calls, so a tight cap truncates it
7
+ * mid-reasoning and the agent never commits edits. It is a ceiling, not a target
8
+ * — unused output tokens are not billed and Workers AI clamps the request to the
9
+ * model's real max — so erring high is safe. Raised to 32k after a spec-writer run
10
+ * truncated an intermediate tool call at the old 16k cap; the document itself
11
+ * stopped well under it, so this is headroom for larger specs/diffs, with
12
+ * {@link runDiagnostics} flagging the rare case where even 32k is not enough.
13
+ */
14
+ export declare const PI_MAX_OUTPUT_TOKENS = 32768;
15
+ /**
16
+ * Normalise a phase label to what the backend will actually store: trimmed, lowercased,
17
+ * `[a-z0-9-]` only, bounded. `''` when the label is not a phase at all.
18
+ *
19
+ * A deliberate COPY of kernel's `normalizeCallPhase` — the container image is built from `src/`
20
+ * plus typescript alone, so the harness can carry no runtime dependency on a workspace package
21
+ * (the same constraint that forced `src/host-markdown.ts`). A copy that can drift is worse than
22
+ * no copy: if the harness rejected a label the backend would have accepted, the call would take
23
+ * the plain path and land unattributed, and if it accepted one the backend rejects it would
24
+ * spend a request on a segment destined for `''`. `test/llm-phase.conformity.test.ts` pins the
25
+ * two to identical verdicts over a corpus, so the alphabet can only be changed in both.
26
+ */
27
+ export declare function normalizeProxyPhase(phase: string | undefined): string;
28
+ /**
29
+ * Point Pi's provider at the phase-tagged completions path for the pass about to run, so the
30
+ * backend can stamp WHICH slice of the run spent each call (the agent's own loop vs a pre-PR
31
+ * validation repair round vs a reproduction-proof repair round) — see
32
+ * `docs/initiatives/token-burn-instrumentation.md`. The harness drives those loops, so it is
33
+ * the only component that knows; reconstructing the boundary downstream from wall-clock
34
+ * timestamps is exactly the brittle inference this avoids.
35
+ *
36
+ * A URL segment because the harness does not make these requests: Pi does, from a config whose
37
+ * only per-run knobs are the base URL and the token — there is no per-request header to set.
38
+ *
39
+ * `supported` is the BACKEND's declaration that it serves the phase-tagged route, carried on the
40
+ * job body exactly as `webSearch` carries "point the search tool at my `/web-search`". Without it
41
+ * this function would encode a routing shape the receiving backend may not have: a runner pool
42
+ * pins its OWN harness image (`RunnerPoolManifest`), and `LOCAL_HARNESS_IMAGE` overrides the
43
+ * recommended pin outright, so "the image and the backend are a matched set" holds for the
44
+ * Cloudflare deployment and nowhere else. An image ahead of its backend would 404 EVERY model
45
+ * call — a dead run, not degraded telemetry. Absent/false ⇒ the plain path, and the calls land
46
+ * in the backend's unattributed slice.
47
+ *
48
+ * Pure so the join is unit-testable without spawning anything.
49
+ */
50
+ export declare function phasedProxyBaseUrl(proxyBaseUrl: string, phase: string | undefined, supported: boolean | undefined): string;
51
+ /** Write the Pi provider config that routes all model calls through the proxy. */
52
+ export declare function writePiModelsConfig(opts: {
53
+ model: string;
54
+ proxyBaseUrl: string;
55
+ /** Output-token ceiling Pi may request per completion. Defaults to PI_MAX_OUTPUT_TOKENS. */
56
+ maxTokens?: number;
57
+ }): Promise<string>;
58
+ /**
59
+ * Write the composed system prompt as Pi's GLOBAL agent context
60
+ * (`~/.pi/agent/AGENTS.md`), which Pi reads automatically and concatenates with
61
+ * any `AGENTS.md`/`CLAUDE.md` the repo itself ships (global file first, then the
62
+ * ones walked up from the run cwd). Deliberately OUTSIDE the checkout (the same
63
+ * `~/.pi/agent` dir `writePiModelsConfig` already uses) so the harness's
64
+ * instructions never enter the git working tree — they can't be committed into a
65
+ * PR and they never clobber a repo's own committed `AGENTS.md`.
66
+ *
67
+ * This relies on Pi's context-file resolution: the global `~/.pi/agent/AGENTS.md`
68
+ * is loaded before the project-trust decision, so it applies in non-interactive
69
+ * (`-p`) runs without a trust prompt. That contract is pinned by `PI_VERSION` in
70
+ * the Dockerfile — revisit this if that bump changes context-file resolution.
71
+ */
72
+ export declare function writeAgentsContext(systemPrompt: string, opts?: {
73
+ webSearch?: boolean;
74
+ guidance?: string;
75
+ serviceDirectory?: string;
76
+ contextFiles?: ContextFileInfo[];
77
+ multiRepo?: boolean;
78
+ /**
79
+ * Whether the checkout actually ships a `blueprints/` folder. The blueprint orientation
80
+ * note is only appended when it does — otherwise it is ~10 lines of dead guidance (re-sent
81
+ * every turn) pointing at files that don't exist. Absent/false ⇒ the note is omitted.
82
+ */
83
+ hasBlueprints?: boolean;
84
+ }): Promise<void>;
85
+ /** Directory in the checkout where linked-context files are materialised (see CONTEXT_DIR in agents). */
86
+ export declare const CONTEXT_DIR = ".cat-context";
87
+ /** The metadata the AGENTS.md context block needs to point an agent at a materialised file. */
88
+ export interface ContextFileInfo {
89
+ path: string;
90
+ title: string;
91
+ url: string;
92
+ content: string;
93
+ }
94
+ /**
95
+ * Write the backend-prepared linked-context files into {@link CONTEXT_DIR} in the
96
+ * checkout so the agent can read them on demand, and add a LOCAL git exclude entry so
97
+ * even `git add -A` never commits them into the agent's PR. Best-effort on the exclude
98
+ * (a scaffold-from-scratch checkout has no `.git` yet — the files just stay untracked).
99
+ */
100
+ export declare function materializeContextFiles(cwd: string, files: ContextFileInfo[]): Promise<void>;
101
+ /** Subdirectory of {@link CONTEXT_DIR} where a skill's resources are materialised, per skill. */
102
+ export declare const SKILL_CONTEXT_SUBDIR = "skill";
103
+ /**
104
+ * Materialise the run's skills' RESOURCE files under `.cat-context/skill/<name>/` in the checkout
105
+ * — the path for every run that does NOT get a native install: Pi, codex, and ambient claude-code
106
+ * (no isolated `CLAUDE_CONFIG_DIR` to install into). Their agents read the checkout, and the
107
+ * skills' instructions are folded into their prompt by the backend (`renderSkillsForHarness`,
108
+ * which keys off ambient auth as well as the harness).
109
+ *
110
+ * Each skill gets its OWN subdirectory: several skills can apply to one run (a step's pick plus
111
+ * the kind's declared playbooks), and a flat directory would let two skills' `templates/report.md`
112
+ * overwrite each other — silently handing the agent the wrong template. The names were sanitized
113
+ * to a single safe path segment at the job boundary, as were the resource sub-paths (no
114
+ * traversal), so nested dirs are created as needed. Kept out of the agent's commits via the same
115
+ * `.cat-context/` git exclude entry. Skills with no resource bodies are a no-op.
116
+ */
117
+ export declare function materializeSkillResources(cwd: string, skills: {
118
+ name: string;
119
+ resources: {
120
+ relPath: string;
121
+ content: string;
122
+ }[];
123
+ }[]): Promise<void>;
124
+ /**
125
+ * The active web-search backend for the rpiv-web-tools extension. Only the
126
+ * provider id is persisted to disk: the per-provider credential (and any base URL
127
+ * — `SEARXNG_URL`, `OLLAMA_HOST`) is read by the extension straight from the
128
+ * environment, so no key is ever written to the container's filesystem.
129
+ */
130
+ export interface WebSearchConfig {
131
+ /** rpiv-web-tools provider id, e.g. `brave`, `tavily`, `exa`, `searxng`. */
132
+ provider: string;
133
+ }
134
+ /**
135
+ * Resolve the web-search configuration from the environment, or undefined when no
136
+ * provider is configured (⇒ the harness writes no rpiv-web-tools config and never
137
+ * nudges the agent towards the tools, so runs behave exactly as before). Enablement
138
+ * is CONDITIONAL on a provider being configured: if any provider's credential/URL
139
+ * env var is present, web search turns on with that provider (highest-priority one
140
+ * when several are set). `WEB_SEARCH_PROVIDER` is an explicit override that pins the
141
+ * active provider regardless of detection — but only when that provider's own
142
+ * credential/URL is also present, so a pin without a key never nudges the agent
143
+ * towards a tool that would error the moment it's called. No key passes through here
144
+ * — the extension reads each provider's own env var directly.
145
+ */
146
+ export declare function webSearchConfigFromEnv(env?: NodeJS.ProcessEnv): WebSearchConfig | undefined;
147
+ /**
148
+ * The env that points the rpiv-web-tools SearXNG provider at the backend's
149
+ * search proxy: `SEARXNG_URL` = `${proxyBaseUrl}/web-search` (the controller mounted
150
+ * under the LLM proxy's `/v1`), and `SEARXNG_API_KEY` = the per-job session token,
151
+ * which the proxy verifies exactly like the LLM proxy. Handed to Pi's child via
152
+ * `runPi`'s `extraEnv`, so the search key never has to enter the sandbox — the search
153
+ * runs server-side under the deployment's own provider key.
154
+ */
155
+ export declare function webSearchProxyEnv(proxyBaseUrl: string, sessionToken: string): {
156
+ SEARXNG_URL: string;
157
+ SEARXNG_API_KEY: string;
158
+ };
159
+ /**
160
+ * Select the active rpiv-web-tools provider by writing
161
+ * `~/.config/rpiv-web-tools/config.json` (the file the extension reads, falling
162
+ * back to `brave` when `provider` is absent). Only the provider id is written —
163
+ * credentials and base URLs come from the environment (env wins over the file in
164
+ * the extension's own resolution order), so no secret is committed to disk. Written
165
+ * 0600 to match the extension's own permissions for that path.
166
+ */
167
+ export declare function writeWebToolsConfig(config: WebSearchConfig): Promise<string>;
168
+ /** One entry of the agent's todo list — its subject and current status. */
169
+ export interface TodoItem {
170
+ /** The task's subject text, as the agent wrote it. */
171
+ label: string;
172
+ status: 'pending' | 'in_progress' | 'completed';
173
+ }
174
+ /** Live subtask progress derived from Pi's `todo` tool — e.g. "3/8 done". */
175
+ export interface TodoProgress {
176
+ /** Tasks marked completed. */
177
+ completed: number;
178
+ /** Tasks currently being worked (rpiv-todo's `in_progress` status). */
179
+ inProgress: number;
180
+ /** Total live tasks (tombstoned/deleted tasks excluded). */
181
+ total: number;
182
+ /**
183
+ * The individual live tasks (label + status), in list order — so the board can
184
+ * render the actual task list, not just the count. Absent for the simpler
185
+ * `todos[].done` fallback shape, which carries no per-task subject.
186
+ */
187
+ items?: TodoItem[];
188
+ }
189
+ /**
190
+ * One tool invocation in Pi's loop, captured for the run's observability trace.
191
+ * Metadata only (name + timing + ok) — never the tool's args or result — so the
192
+ * harness buffer stays tiny. The backend drains these on its existing job poll and
193
+ * emits them as child spans under the run trace.
194
+ */
195
+ export interface ToolSpan {
196
+ tool: string;
197
+ /** Epoch ms the tool call started (approximated as the previous tool's end). */
198
+ startedAt: number;
199
+ /** Epoch ms the tool call ended (when its `tool_execution_end` event arrived). */
200
+ endedAt: number;
201
+ ok: boolean;
202
+ }
203
+ /**
204
+ * What the agent actually did this run, independent of any file changes. Used to
205
+ * tell a genuine no-op (the agent never reached the model / never acted) apart
206
+ * from a real run, so a bootstrap that produced nothing is failed rather than
207
+ * pushed as an empty repo. `toolCalls === 0 && assistantChars === 0` is the
208
+ * signature of a run where Pi never made a successful model call.
209
+ */
210
+ export interface PiRunStats {
211
+ /** Tool calls the assistant emitted across the transcript (0 ⇒ it never acted). */
212
+ toolCalls: number;
213
+ /** Total characters of assistant text (0 ⇒ the model produced nothing). */
214
+ assistantChars: number;
215
+ }
216
+ /**
217
+ * Output-quality signals lifted from the agent's transcript, so the harness can fail
218
+ * LOUDLY on a malformed run instead of silently handing a half-baked artifact to the
219
+ * structured-output repair (which would manufacture a doc from garbage — the trap
220
+ * behind the spec-writer ⇄ companion rework loop). Two distinct invalid states, both
221
+ * seen in production from `kimi-k2.7-code`:
222
+ * - a completion that hit the output ceiling (its answer/tool call was cut off), and
223
+ * - a FINAL turn that carried no text at all (an empty `content: []` despite spending
224
+ * output tokens), so there is no answer to parse.
225
+ */
226
+ export interface RunDiagnostics {
227
+ /** Some completion ended at the output-token ceiling — its content was cut off. */
228
+ truncated: boolean;
229
+ /** The agent's FINAL completion hit the ceiling: its ANSWER (not a mid-run step) was cut off. */
230
+ finalTruncated: boolean;
231
+ /** The agent's final turn carried no text content (e.g. an empty `content: []`). */
232
+ finalAnswerEmpty: boolean;
233
+ }
234
+ /**
235
+ * One model call captured from a subscription harness's CLI event stream, shaped so
236
+ * the backend can record it into the same `llm_call_metrics` telemetry the LLM proxy
237
+ * writes for the Pi harness. The subscription harnesses (Claude Code / Codex) talk
238
+ * DIRECT to the vendor and never touch the proxy, so this is the only place their
239
+ * per-call bodies are observable. Claude Code's `stream-json --verbose` is a near-
240
+ * verbatim Anthropic Messages stream, so its calls carry full request/response
241
+ * bodies; Codex's `exec --json` only surfaces flat assistant text + per-turn tokens,
242
+ * so its rows are honestly thinner (no request transcript, no tool/command bodies).
243
+ */
244
+ export interface HarnessCallMetric {
245
+ /** The vendor model that served this call (from the CLI event), when reported. */
246
+ model?: string;
247
+ /**
248
+ * The full request as an OpenAI-style chat array (`[{role, content}, …]`),
249
+ * JSON-stringified — the growing history as of this call. Matches the proxy's
250
+ * `promptText` shape so the telemetry chain delta-compresses + renders identically.
251
+ */
252
+ promptText: string;
253
+ /** Number of messages encoded in {@link promptText} (the telemetry chain messageCount). */
254
+ messageCount: number;
255
+ /** The assistant's response text, as a plain string (`''` for a tool-only turn). */
256
+ responseText: string;
257
+ /** The reasoning/thinking trace, as a plain string (`''` when none). */
258
+ reasoningText: string;
259
+ /**
260
+ * FRESH (uncached) input tokens: exclusive of BOTH cache classes below, so the three
261
+ * are orthogonal and additive. Every producer normalises to this — reading the already
262
+ * exclusive field where the vendor reports the classes apart (Anthropic), subtracting
263
+ * the cached share where the vendor reports an inclusive prompt count (Codex/OpenAI).
264
+ */
265
+ inputTokens: number;
266
+ /** Input tokens served from the vendor's prompt cache (~0.1× base input). */
267
+ cacheReadTokens: number;
268
+ /**
269
+ * Input tokens written INTO the vendor's cache (1.25–2× base input — dearer than fresh),
270
+ * kept apart from the reads so a loop that keeps re-writing the prefix is distinguishable
271
+ * from one riding a warm cache. 0 where the CLI reports no separate write class.
272
+ */
273
+ cacheWriteTokens: number;
274
+ outputTokens: number;
275
+ /** The provider finish/stop reason when the CLI reports one (else null). */
276
+ finishReason: string | null;
277
+ /**
278
+ * This call's position in the JOB's telemetry sequence, stamped by the job registry the
279
+ * moment the call is emitted (see `RunOptions.onCallMetric`). It is what makes a call's
280
+ * recorded row id stable across the two channels that carry it: the live drain (per poll,
281
+ * so a run's telemetry is inspectable WHILE it runs) and the terminal result (the complete
282
+ * list, so a transport that doesn't drain still records everything). Both channels hold the
283
+ * SAME metric objects, so both mint the same `<jobId>-hc-<seq>` row id and the backend's
284
+ * second write of an already-recorded call is a no-op instead of a duplicate row.
285
+ *
286
+ * Absent only when a producer built a metric without emitting it live; the recorder then
287
+ * falls back to the array index, which is what it always used before streaming existed.
288
+ */
289
+ seq?: number;
290
+ /**
291
+ * The run PHASE that spent this call (`agent` / `validation-repair` / `reproduction-repair` /
292
+ * …), stamped by the job registry from the same marker the handlers set as they enter each
293
+ * phase — so the phase axis on `llm_call_metrics` comes from the component that owns the
294
+ * boundary rather than from a downstream guess
295
+ * (`docs/initiatives/token-burn-instrumentation.md`).
296
+ *
297
+ * Stamped on the SAME object as {@link seq}, so the live drain and the terminal result can
298
+ * never disagree about which phase billed a call.
299
+ */
300
+ phase?: string;
301
+ }
302
+ /**
303
+ * Publish one captured model call: append it to the run's list (which becomes the terminal
304
+ * result's `callMetrics`) AND hand the SAME object to the live stream, where the job registry
305
+ * stamps its {@link HarnessCallMetric.seq} and buffers it for the next poll to drain.
306
+ *
307
+ * Every producer goes through here rather than a bare `calls.push`, so the two channels can't
308
+ * drift: a call that reaches the terminal list but never the live stream would be invisible
309
+ * until the job ends, and one that reaches only the live stream would go unrecorded if the
310
+ * poll response were lost.
311
+ *
312
+ * A published call must be FINAL. The backend records it the moment the drain reaches it and
313
+ * IGNORES the terminal repeat (first write wins, so its stored prompt delta stays valid against
314
+ * the chain tip it was written against), which means a field mutated after publishing never
315
+ * reaches the store. A producer whose calls can still change (the cumulative-usage fallback,
316
+ * whose totals arrive with the CLI's terminal `result` event) publishes through
317
+ * {@link createCallMetricPublisher} instead, which withholds exactly those.
318
+ */
319
+ export declare function publishCallMetric(calls: HarnessCallMetric[], call: HarnessCallMetric, onCallMetric?: (call: HarnessCallMetric) => void): void;
320
+ /** Appends captured calls to a run's list, streaming each one as soon as it is final. */
321
+ export interface CallMetricPublisher {
322
+ /** Append a captured call, streaming it now unless its tokens can still be rewritten. */
323
+ publish(call: HarnessCallMetric): void;
324
+ /** Stream whatever is still withheld. Call once the run's totals are attributed. */
325
+ flush(): void;
326
+ }
327
+ /**
328
+ * A {@link publishCallMetric} wrapper for a producer whose per-call tokens may be filled in at
329
+ * the END of the run: a CLI that reports only a cumulative total leaves every turn at zero, and
330
+ * `attributeCumulativeUsage` pins the total onto the last call once the terminal `result` event
331
+ * arrives.
332
+ *
333
+ * Since a published call must be final (the backend stores it on the drain and ignores the
334
+ * terminal repeat), a call the CLI did NOT cost is appended to the list but WITHHELD from the
335
+ * live stream — otherwise it records as a zero-token row and the attributed numbers never land.
336
+ * The withholding window closes the moment any call IS costed: attribution can no longer fire, so
337
+ * everything held is final and released at once, in capture order, and every later call streams
338
+ * immediately whatever its tokens. {@link flush} covers the run that was never costed at all.
339
+ */
340
+ export declare function createCallMetricPublisher(calls: HarnessCallMetric[], onCallMetric?: (call: HarnessCallMetric) => void): CallMetricPublisher;
341
+ /** Pi's assistant summary plus {@link PiRunStats} describing what it did. */
342
+ export interface PiRunOutcome {
343
+ summary: string;
344
+ stats: PiRunStats;
345
+ /**
346
+ * Tail of Pi's stderr (credential-scrubbed), captured even on a clean exit.
347
+ * On a no-op run this is where the real cause shows up — e.g. an unreachable
348
+ * proxy or a model the upstream rejected — so the failure is diagnosable
349
+ * without shelling into the (ephemeral) container.
350
+ */
351
+ stderrTail?: string;
352
+ /**
353
+ * Token usage lifted from the agent CLI's own event stream. Reported by the
354
+ * subscription harnesses (Claude Code / Codex), whose traffic bypasses the LLM
355
+ * proxy — so the backend folds it into the leased token's rolling-window counters
356
+ * (usage-aware rotation) and telemetry. Absent for the proxy-metered Pi harness.
357
+ */
358
+ usage?: {
359
+ inputTokens: number;
360
+ outputTokens: number;
361
+ };
362
+ /**
363
+ * Per-model-call telemetry lifted from a subscription harness's CLI event stream
364
+ * (Claude Code / Codex), which the backend records into `llm_call_metrics` — the
365
+ * proxy-bypassing analogue of the per-call rows the LLM proxy writes for Pi. Absent
366
+ * for the proxy-metered Pi harness (the proxy is its metering point). See
367
+ * {@link HarnessCallMetric}.
368
+ */
369
+ callMetrics?: HarnessCallMetric[];
370
+ /** Output-quality signals (truncation / empty final answer); see {@link RunDiagnostics}. */
371
+ diagnostics?: RunDiagnostics;
372
+ /**
373
+ * The agent's effort self-assessment, lifted from its sentinel file after the run (how hard the
374
+ * work was, what reduced its effectiveness, the key obstacles). Absent when the agent wrote none.
375
+ * See {@link EffortReport}.
376
+ */
377
+ effortReport?: EffortReport;
378
+ }
379
+ export declare function parseTodoProgress(event: Record<string, unknown>): TodoProgress | undefined;
380
+ /**
381
+ * Run Pi non-interactively against `cwd` and return its assistant summary. Uses
382
+ * print + JSON mode (`-p --mode json`) with `--approve` so it runs unattended.
383
+ *
384
+ * The (untrusted) prompt is fed over stdin, never as an argv positional, so a
385
+ * prompt beginning with `-`/`--` can't be mis-parsed as a Pi CLI flag (Pi has no
386
+ * `--` end-of-options terminator, so a positional `-foo` errors as "Unknown
387
+ * option"). Pi's print mode reads the prompt from piped stdin; we write it and
388
+ * close the pipe so Pi gets an immediate EOF and proceeds (an open, never-closed
389
+ * stdin pipe would make print mode block forever waiting for EOF).
390
+ */
391
+ export declare function runPi(opts: {
392
+ cwd: string;
393
+ model: string;
394
+ userPrompt: string;
395
+ sessionToken: string;
396
+ /** Aborting this kills Pi (the job's inactivity/max-duration watchdog). */
397
+ signal?: AbortSignal;
398
+ /** Called on every chunk of Pi output, so the watchdog sees the agent is alive. */
399
+ onActivity?: () => void;
400
+ /** Called with the latest subtask counts each time Pi updates its todo list. */
401
+ onProgress?: (progress: TodoProgress) => void;
402
+ /**
403
+ * Called once per completed tool call with a compact {@link ToolSpan}. Feeds the
404
+ * run's observability trace (drained by the backend on its job poll); a no-op when
405
+ * the container payload doesn't pass it, so production behaviour is unchanged.
406
+ */
407
+ onSpan?: (span: ToolSpan) => void;
408
+ /**
409
+ * Called with every parsed Pi `--mode json` event, in stream order — the raw
410
+ * observability seam over the run. Used by offline tooling (the smoketest
411
+ * harness) to capture the full prompt/response/tool-call transcript for
412
+ * analysis; the container payload doesn't pass it, so production behaviour is
413
+ * unchanged. Throwing handlers are swallowed so a faulty observer can't break
414
+ * the run.
415
+ */
416
+ onEvent?: (event: Record<string, unknown>) => void;
417
+ /** No-progress guard bounds; defaults to the env-configured limits. */
418
+ guardLimits?: ProgressGuardLimits;
419
+ /** Whether this run is expected to edit files (false for assess-only runs like the merger). */
420
+ expectsEdits?: boolean;
421
+ /**
422
+ * Extra environment for Pi's child process, merged over `process.env` (but under the
423
+ * proxy token). Used to hand the rpiv-web-tools extension its proxy-backed SearXNG
424
+ * config (`SEARXNG_URL` / `SEARXNG_API_KEY`) without mutating the harness's own env.
425
+ */
426
+ extraEnv?: Record<string, string>;
427
+ }): Promise<PiRunOutcome>;
428
+ /**
429
+ * The terminal-failure message when Pi's run ended in a hard error (the model was
430
+ * unreachable / refused, and Pi exhausted its auto-retries), else undefined. Only
431
+ * the FINAL outcome counts: a mid-run hiccup the agent recovered from leaves a clean
432
+ * terminal `agent_end`, so it returns undefined. Scans from the end and decides on
433
+ * the first terminal signal it meets — the trailing `auto_retry_end` (its `success`
434
+ * flag) or the last `agent_end` (its `stopReason`). Pure so it is unit-testable over
435
+ * a fixed event sequence.
436
+ */
437
+ export declare function terminalRunError(stdout: string): string | undefined;
438
+ /**
439
+ * Classify a terminal run error whose text points at the LLM PROXY rejecting every model call
440
+ * (auth / quota / rate-limit) into an actionable remedy, else undefined. All model traffic goes
441
+ * through the Worker's OpenAI-compatible proxy, so a 401/402/429 surfaced in Pi's `finalError`
442
+ * means the leased provider key was refused, is out of credit, or was rate-limited — none of
443
+ * which is an agent bug. This is the first-wrap-point for Pi's own error text (per the
444
+ * error-message initiative's I6): we match it ONCE here and let the caller stamp the structured
445
+ * `llm-upstream` cause + this remedy. Pure, so it is unit-tested over fixed error strings.
446
+ */
447
+ export declare function classifyLlmUpstreamError(finalError: string): string | undefined;
448
+ /**
449
+ * Pi's assistant summary plus {@link PiRunStats}, derived from one pass over its
450
+ * output — the canonical close-of-run signal the harness uses both to report the
451
+ * answer and to detect a no-op run (the agent never acted).
452
+ */
453
+ export declare function summarizePiRun(stdout: string): PiRunOutcome;
454
+ /**
455
+ * Output-quality signals over the canonical `agent_end` transcript: whether any
456
+ * completion hit the output ceiling (its content was cut off), whether the FINAL
457
+ * completion did, and whether that final turn carried no text at all. Pure so it is
458
+ * unit-testable over a fixed event sequence. Defaults to all-false when there is no
459
+ * terminal transcript (a no-op run is already caught by {@link agentNeverActed}).
460
+ *
461
+ * `cap` is the per-completion ceiling Pi requested ({@link PI_MAX_OUTPUT_TOKENS});
462
+ * truncation is detected by an assistant message whose `usage.output` reached it,
463
+ * which is reliable even when the model reports a non-`length` stop reason (Workers
464
+ * AI labelled a cut-off tool call `tool_calls`, not `length`).
465
+ */
466
+ export declare function diagnosticsFromEvents(events: Record<string, unknown>[], cap?: number): RunDiagnostics;
467
+ /** {@link RunDiagnostics} over Pi's raw `--mode json` stdout (see {@link diagnosticsFromEvents}). */
468
+ export declare function runDiagnostics(stdout: string, cap?: number): RunDiagnostics;
469
+ /**
470
+ * Extract the assistant's final summary from Pi's JSON-lines output. Pi emits a
471
+ * terminal `agent_end` event whose `messages` is the full transcript, so the
472
+ * last assistant message there is the canonical answer. Falls back to scanning
473
+ * `message_end` events, then to a raw tail, so a schema tweak never loses output.
474
+ */
475
+ export declare function parsePiOutput(stdout: string): string;
@@ -0,0 +1,85 @@
1
+ /** The sentinel file the agent writes its PR description to (relative to the checkout root). */
2
+ export declare const PR_DESCRIPTION_FILE = ".cat-pr-description.md";
3
+ /**
4
+ * Ceiling on the agent-authored body.
5
+ *
6
+ * The engine appends its verification report to the SAME body later, and that section carries
7
+ * its own 50,000-character ceiling (`MAX_SECTION_CHARS` in kernel's `hostMarkdown`). GitHub
8
+ * rejects a body over 65,536 with a 422, and the report publisher swallows its own failures —
9
+ * so a briefing budget that does not leave the report room would surface as a report that
10
+ * silently never publishes. 15,000 + 50,000 stays under the limit with room to join them.
11
+ *
12
+ * Exported because the PR-TEMPLATE note states it to the agent (`pr-template.ts`): a filled
13
+ * template is the one briefing shape whose length is dictated by a file the agent did not write,
14
+ * so an agent that does not know the ceiling can answer a long template past it and have
15
+ * {@link capBody} cut the repo's last sections — the very failure the inline budget avoids on the
16
+ * way IN.
17
+ */
18
+ export declare const MAX_PR_BODY_CHARS = 15000;
19
+ /** Opens the engine-managed region of a PR body (kept in sync with `kernel/domain/pr-report.ts`). */
20
+ export declare const PR_REPORT_MARKER_START = "<!-- cat-factory:verification-report:start -->";
21
+ /** Closes the engine-managed region of a PR body. */
22
+ export declare const PR_REPORT_MARKER_END = "<!-- cat-factory:verification-report:end -->";
23
+ /** An agent-authored PR description: an optional title plus the briefing body. */
24
+ export interface AgentPrDescription {
25
+ title?: string;
26
+ body?: string;
27
+ }
28
+ /** How to read a sentinel. */
29
+ export interface ReadPrDescriptionOptions {
30
+ /**
31
+ * Whether a lone leading `# <title>` heading may be lifted off as the PR title (see
32
+ * {@link splitTitle}). Default `true` — that is what the description guidance asks a free-form
33
+ * briefing for.
34
+ *
35
+ * FALSE when the briefing is a FILLED TEMPLATE (`pr-template.ts`): then the headings are the
36
+ * repo's, not the agent's, and a template whose first heading is its only level-1 one — `#
37
+ * Pull Request` above a set of `##` sections, an entirely ordinary shape — would have that
38
+ * heading silently become the pull request's title, so the PR reads "Pull Request" instead of
39
+ * `<block> (<pipeline>)` and the body loses the heading the repo asked for. The heuristic below
40
+ * is sound for the shape the guidance describes and cannot be made to cover both, so the caller
41
+ * that KNOWS which shape it asked for says so.
42
+ */
43
+ titleFromHeading?: boolean;
44
+ }
45
+ /**
46
+ * Read + parse + REMOVE the agent's PR-description sentinel from `dir`. Lenient: returns
47
+ * undefined when the file is absent (the agent wrote none) or carries nothing usable. Never
48
+ * throws — a bad description must never fail an otherwise-good run; the caller falls back to
49
+ * the dispatch-time text.
50
+ *
51
+ * A SINGLE `# <title>` heading on the first line sets the PR title; everything after it is the
52
+ * body (see {@link splitTitle} for why a LONE heading is required, and
53
+ * {@link ReadPrDescriptionOptions.titleFromHeading} for the caller that must switch it off). The
54
+ * whole text is secret-scrubbed, an over-budget body is truncated WITH a visible note (a silent
55
+ * cut would read as the complete briefing), and both halves are made inert for the host.
56
+ *
57
+ * On scrubbing: `redactSecrets`'s credential-assignment rule is deliberately eager, so a
58
+ * briefing sentence like "the token: handling changed" loses its next word. That is the right
59
+ * trade for a surface this public — the rule is shared with every other redaction path, and
60
+ * narrowing it so prose reads better would weaken all of them.
61
+ */
62
+ export declare function readPrDescription(dir: string, opts?: ReadPrDescriptionOptions): Promise<AgentPrDescription | undefined>;
63
+ /**
64
+ * Fold an agent-authored description over the dispatch-time fallback the job body carries.
65
+ * Field-wise: the agent's title/body each win when present, so a body-only briefing keeps the
66
+ * backend-composed title and vice versa.
67
+ */
68
+ export declare function applyPrDescription(fallback: {
69
+ title: string;
70
+ body: string;
71
+ }, agent: AgentPrDescription | undefined): {
72
+ title: string;
73
+ body: string;
74
+ };
75
+ /**
76
+ * The body to PATCH onto an ALREADY-OPEN pull request when a resumed run produced a fresh
77
+ * briefing: the new description followed by whatever the engine's managed verification-report
78
+ * region currently holds.
79
+ *
80
+ * Carrying the region across is what makes the refresh safe. The engine re-publishes the report
81
+ * on every step settlement, so dropping it here would usually self-heal — but "usually" is not
82
+ * a property to rest the one artefact a reviewer reads on, and a run that settles no further
83
+ * step (the work is already merged, the run failed after its push) would never restore it.
84
+ */
85
+ export declare function preserveManagedSection(currentBody: string | undefined, nextBody: string): string;
@@ -0,0 +1,101 @@
1
+ import type { RepoSpec } from './job.js';
2
+ import type { Logger } from './logger.js';
3
+ /** The VCS providers a repo can live on. Bound to `RepoSpec` so the two cannot drift. */
4
+ type ProviderName = NonNullable<RepoSpec['provider']>;
5
+ /**
6
+ * How much template text is inlined into the agent's prompt.
7
+ *
8
+ * Over this, the template is NAMED rather than inlined and the agent is told to read it from the
9
+ * checkout — which it can, because the file is on disk. That is strictly better than the
10
+ * alternatives: truncating a template would have the agent fill a structure whose tail it never
11
+ * saw (silently dropping the repo's last sections), and skipping it entirely would abandon the
12
+ * feature on exactly the repos with the most demanding process.
13
+ */
14
+ export declare const MAX_INLINE_PR_TEMPLATE_CHARS = 8000;
15
+ /**
16
+ * The shared inline budget across a multi-repo run's legs. Each repo's template competes for it in
17
+ * leg order, and a leg that does not fit is NAMED rather than inlined (as above) — so a workspace
18
+ * of four template-carrying repos cannot quietly consume 32k of the agent's prompt.
19
+ */
20
+ export declare const MAX_TOTAL_INLINE_PR_TEMPLATE_CHARS = 12000;
21
+ /** A discovered pull-request template. */
22
+ export interface PrTemplate {
23
+ /** Repo-root-relative path, forward-slashed (it is prose an agent reads). */
24
+ path: string;
25
+ /**
26
+ * The template's size. ALWAYS the real one, including when {@link text} is absent: an over-budget
27
+ * template reporting `chars: 0` would read in the log exactly like an empty file, which is the
28
+ * one thing discovery treats as "no template at all".
29
+ */
30
+ chars: number;
31
+ /** The template text. Absent ⇒ over budget, so the agent is told to read {@link path} itself. */
32
+ text?: string;
33
+ }
34
+ /** One checkout to look for a template in. */
35
+ export interface PrTemplateTarget {
36
+ /** The repository checkout root (NOT a monorepo service subtree — a template is a repo fact). */
37
+ repoDir: string;
38
+ provider?: ProviderName;
39
+ /** Names this repo in the note. Omit when the run has a single checkout ("this repository"). */
40
+ repoLabel?: string;
41
+ }
42
+ /** What {@link resolvePrTemplateNote} tells the run about the templates it found. */
43
+ export interface PrTemplateResolution {
44
+ /** The prompt note, or absent when no target ships a template (which is most repos). */
45
+ note?: string;
46
+ /**
47
+ * The `repoDir`s whose briefing is a FILLED TEMPLATE. The push phase reads those sentinels with
48
+ * `titleFromHeading: false`, because the headings in them are the repo's — see
49
+ * `ReadPrDescriptionOptions.titleFromHeading`. A SET keyed by directory rather than a boolean
50
+ * because a multi-repo run's legs need not all ship a template.
51
+ */
52
+ templated: ReadonlySet<string>;
53
+ }
54
+ /**
55
+ * THE entry point: find each target's pull-request template and build the prompt note that asks
56
+ * the agent to fill it, plus the set of checkouts whose sentinel will therefore hold a filled
57
+ * template rather than a free-form briefing (see {@link PrTemplateResolution}).
58
+ *
59
+ * Pass NO targets for a dispatch that opens no pull request — an in-place fixer amending someone
60
+ * else's PR, a read-only explore run. Asking such a run to fill a template would be asking for a
61
+ * document nothing publishes.
62
+ *
63
+ * Never rejects: a template is an improvement to a PR body, so no failure reading one may cost a
64
+ * run that otherwise succeeded. An unreadable or empty template is simply no template.
65
+ */
66
+ export declare function resolvePrTemplateNote(args: {
67
+ targets: PrTemplateTarget[];
68
+ logger: Logger;
69
+ }): Promise<PrTemplateResolution>;
70
+ /**
71
+ * Locate the template in `repoDir`, probing the repo's OWN host convention first and the other
72
+ * host's second — a repo mirrored across both, or one whose provider the dispatcher did not set,
73
+ * still gets its template respected rather than falling to whichever list happened to be first.
74
+ */
75
+ export declare function discoverPrTemplate(repoDir: string, provider?: ProviderName): Promise<PrTemplate | undefined>;
76
+ /**
77
+ * The prompt note. It has to do more than show the template, because the agent has already been
78
+ * told (by the backend-composed `PR_DESCRIPTION_GUIDANCE`) to write a free-form briefing, and the
79
+ * two genuinely conflict: a template that asks for a test plan or a checklist is asking for
80
+ * exactly the "restated diff" that guidance rules out. So the note states which wins, and states
81
+ * why the template is not already applied — an agent that believes the host will merge the
82
+ * template with its text has no reason to reproduce the structure itself.
83
+ *
84
+ * The template is delimited with `fencedOutput`, the same helper every other captured-text-into-a-
85
+ * prompt path uses. Templates routinely carry fenced blocks of their own, and a fixed three-tick
86
+ * wrapper closes on the first of them — spilling the rest of the template, and the instructions
87
+ * after it, into the prompt as prose. `fencedOutput` sizes the fence one tick longer than the
88
+ * longest run in the body, which is what CommonMark specifies for exactly this, so no template can
89
+ * break out of its own block. A plain `--- BEGIN/END ---` rule would read more nicely and is
90
+ * trivially forgeable by the template's own content, which is the whole thing being defended.
91
+ */
92
+ export declare function buildPrTemplateNote(template: PrTemplate, repoLabel?: string): string;
93
+ /**
94
+ * Fold the note into a prompt. The sibling of `withDependencyNote`, and deliberately not inlined
95
+ * for the same reason: it rides EVERY agent pass, including the validation and reproduction
96
+ * REPAIR passes. Those start a fresh agent that still carries the description guidance in its
97
+ * system prompt, so one that is not also told about the template would rewrite the briefing
98
+ * free-form and undo the filled template the first pass produced.
99
+ */
100
+ export declare function withPrTemplateNote(userPrompt: string, note: string | undefined): string;
101
+ export {};