@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
@@ -77,6 +77,112 @@ export function createClaudeCallAggregator(handlers) {
77
77
  flush: complete,
78
78
  };
79
79
  }
80
+ /**
81
+ * How much reconstructed transcript ONE conversation may retain.
82
+ *
83
+ * The stream feeds this without limit — a tool loop that reads large files grows the history by
84
+ * every one of them — and the reconstruction is held in the driver's own process. In the container
85
+ * that is a box sized for one job; in the BACKEND it is the orchestrator, where `streamCli` already
86
+ * refuses to retain the raw stream for exactly this reason (`harnessInline.ts` →
87
+ * `OUTPUT_TAIL_RETAIN_CHARS`: "a stalled tool-using run would otherwise park hundreds of MB in the
88
+ * orchestrator process — precisely on the runs worth diagnosing").
89
+ *
90
+ * 512 KiB because that is `LlmObservabilityService.MAX_BODY_CHARS`, the point past which the store
91
+ * truncates a body anyway: retaining more can only ever be thrown away. Deliberately NOT a
92
+ * per-deployment knob — it bounds a memory fault, and a number an operator can raise is one an
93
+ * operator can raise until the process dies.
94
+ */
95
+ export const MAX_TRANSCRIPT_CHARS = 512 * 1024;
96
+ /**
97
+ * The role a turn carries when it is not a turn at all, but the note saying what stopped being
98
+ * retained. A distinct namespaced role rather than `system`, so nothing downstream can read it as a
99
+ * message that was actually sent — the same reason `seed` exists.
100
+ */
101
+ const ELIDED_ROLE = 'cat-factory:elided';
102
+ /**
103
+ * Retain the transcript up to {@link MAX_TRANSCRIPT_CHARS} and then STOP, stating what it stopped
104
+ * retaining rather than silently ending mid-conversation.
105
+ *
106
+ * Freezing the tail (rather than evicting the head) keeps the seed and the early history — the
107
+ * task, and the turns that explain what the loop is doing — and keeps each call's `promptText` a
108
+ * stable PREFIX plus a changing note, so the backend's chain delta-compresses right up to the bound
109
+ * and only then degrades to storing the (now capped) array. Evicting the head would drop the task
110
+ * itself and break the prefix property from the first eviction on.
111
+ *
112
+ * The seed is never dropped: it is what the CALLER sent, so it is bounded by the caller's own
113
+ * prompt rather than by the stream, and it is the half a reader cannot reconstruct from anything
114
+ * else.
115
+ */
116
+ function createBoundedTranscript(seed, secrets, maxChars) {
117
+ const turns = [...seed];
118
+ const sizeOf = (turn) => {
119
+ try {
120
+ return JSON.stringify(turn)?.length ?? 0;
121
+ }
122
+ catch {
123
+ // An un-serialisable turn cannot be retained at all, so charge it nothing and let the
124
+ // append below drop it on its own terms.
125
+ return Number.POSITIVE_INFINITY;
126
+ }
127
+ };
128
+ let retained = turns.reduce((n, turn) => n + sizeOf(turn), 0);
129
+ const dropped = { turns: 0, chars: 0 };
130
+ return {
131
+ append(turn) {
132
+ const size = sizeOf(turn);
133
+ if (retained + size > maxChars) {
134
+ dropped.turns += 1;
135
+ dropped.chars += Number.isFinite(size) ? size : 0;
136
+ return;
137
+ }
138
+ turns.push(turn);
139
+ retained += size;
140
+ },
141
+ snapshot() {
142
+ const encoded = dropped.turns
143
+ ? [
144
+ ...turns,
145
+ {
146
+ role: ELIDED_ROLE,
147
+ content: `${dropped.turns} later turn(s), ${dropped.chars} chars, were not retained: ` +
148
+ `this conversation reached the ${maxChars}-char reconstruction bound`,
149
+ },
150
+ ]
151
+ : turns;
152
+ return {
153
+ text: redactBody(safeSerialise(encoded), secrets),
154
+ messageCount: encoded.length,
155
+ };
156
+ },
157
+ };
158
+ }
159
+ /**
160
+ * Count the turns and assemble NO bodies — for a driver that has nowhere to put them (the backend
161
+ * with `LLM_RECORD_PROMPTS` off, where the store drops every body it is handed).
162
+ *
163
+ * `messageCount` stays real, because it is a COUNT rather than a body and every consumer of the
164
+ * metric wants it. The point is not to omit data the gate would keep; it is that serialising a
165
+ * transcript the gate is about to drop is pure cost, and the whole reason bodies travel to the
166
+ * recorder as thunks (`CLAUDE.md` → "Telemetry & agent-context observability").
167
+ */
168
+ function createCountingTranscript(seed) {
169
+ let messageCount = seed.length;
170
+ return {
171
+ append() {
172
+ messageCount += 1;
173
+ },
174
+ snapshot: () => ({ text: '', messageCount }),
175
+ };
176
+ }
177
+ /** Serialise a transcript for the store, never throwing into the stream that produced it. */
178
+ function safeSerialise(turns) {
179
+ try {
180
+ return JSON.stringify(turns) ?? '';
181
+ }
182
+ catch {
183
+ return '';
184
+ }
185
+ }
80
186
  /**
81
187
  * Assemble ONE conversation's per-call telemetry from the CLI stream: the growing request
82
188
  * transcript and the per-call token/body metrics.
@@ -88,16 +194,20 @@ export function createClaudeCallAggregator(handlers) {
88
194
  * was not sent. A subagent's conversation seeds EMPTY — its prompt was minted by the CLI and never
89
195
  * crosses this stream — which is also why its first call carries `messageCount: 0` (the backend's
90
196
  * `latestChainTip` skips those on purpose: there is no re-sendable chain to delta against).
91
- * Bodies are credential-scrubbed; they can echo the leased token.
197
+ * Bodies are credential-scrubbed; they can echo the leased token — and assembled at all only when
198
+ * {@link ClaudeStreamTelemetryOptions.bodies} says a driver has somewhere to put them. The
199
+ * transcript is bounded either way ({@link MAX_TRANSCRIPT_CHARS}).
92
200
  *
93
201
  * Deliberately does NOT touch {@link PiRunStats}: the run's tool/output counters describe whether
94
202
  * the agent ACTED at all (`agentNeverActed`), which is true of a subagent's turns whichever channel
95
203
  * ends up owning their telemetry rows. The caller accumulates them off the raw stream instead.
96
204
  */
97
205
  export function createClaudeStreamTelemetry(opts) {
98
- const messages = [...opts.seed];
99
- let callPrompt = '';
100
- let callMessageCount = 0;
206
+ const bodies = opts.bodies ?? true;
207
+ const transcript = bodies
208
+ ? createBoundedTranscript(opts.seed, opts.secrets, opts.maxTranscriptChars ?? MAX_TRANSCRIPT_CHARS)
209
+ : createCountingTranscript(opts.seed);
210
+ let sent = { text: '', messageCount: 0 };
101
211
  // The aggregator IS the surface: the transcript and metric work happens in its callbacks, so
102
212
  // there is nothing to wrap it in.
103
213
  return createClaudeCallAggregator({
@@ -105,16 +215,15 @@ export function createClaudeStreamTelemetry(opts) {
105
215
  // produced the response, and later envelopes of the same call must not see the turns it
106
216
  // went on to add.
107
217
  onCallStart: () => {
108
- callPrompt = redactBody(JSON.stringify(messages), opts.secrets);
109
- callMessageCount = messages.length;
218
+ sent = transcript.snapshot();
110
219
  },
111
220
  onCall: (call) => {
112
221
  opts.publish({
113
222
  ...(call.model ? { model: call.model } : {}),
114
- promptText: callPrompt,
115
- messageCount: callMessageCount,
116
- responseText: redactBody(call.text, opts.secrets),
117
- reasoningText: redactBody(call.reasoning, opts.secrets),
223
+ promptText: sent.text,
224
+ messageCount: sent.messageCount,
225
+ responseText: bodies ? redactBody(call.text, opts.secrets) : '',
226
+ reasoningText: bodies ? redactBody(call.reasoning, opts.secrets) : '',
118
227
  inputTokens: call.inputTokens,
119
228
  cacheReadTokens: call.cacheReadTokens,
120
229
  cacheWriteTokens: call.cacheWriteTokens,
@@ -123,9 +232,9 @@ export function createClaudeStreamTelemetry(opts) {
123
232
  });
124
233
  // Appended only now, so each call's prompt stays a strict prefix of the next and the
125
234
  // backend's telemetry chain delta-compresses cleanly.
126
- messages.push({ role: 'assistant', content: call.content });
235
+ transcript.append({ role: 'assistant', content: call.content });
127
236
  for (const result of call.toolResults)
128
- messages.push({ role: 'tool', content: result });
237
+ transcript.append({ role: 'tool', content: result });
129
238
  },
130
239
  });
131
240
  }
@@ -171,11 +280,8 @@ function createSubagentStreamTelemetry(opts) {
171
280
  let telemetry = perDispatch.get(dispatchId);
172
281
  if (!telemetry) {
173
282
  // Seeded EMPTY: the CLI minted this subagent's prompt and it never crossed the stream.
174
- telemetry = createClaudeStreamTelemetry({
175
- seed: [],
176
- secrets: opts.secrets,
177
- publish: opts.publish,
178
- });
283
+ // Each dispatch gets its OWN retention bound, since each is its own conversation.
284
+ telemetry = createClaudeStreamTelemetry({ ...opts, seed: [] });
179
285
  perDispatch.set(dispatchId, telemetry);
180
286
  }
181
287
  return telemetry;
@@ -0,0 +1,56 @@
1
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
2
+ /**
3
+ * The tool names the Claude Code CLI dispatches a parallel subagent under. `Agent` is what the
4
+ * shipped schema declares (`AgentInput` in `sdk-tools.d.ts`, carrying `description` / `prompt` /
5
+ * `subagent_type`); `Task` is the older name for the same dispatch. Both are matched because the
6
+ * harness runs against whatever CLI the image happens to bundle, and matching only the old name
7
+ * is what left a CLI 2.1.x pr-review reporting no slices at all.
8
+ *
9
+ * Note the asymmetry: keeping the legacy `Task` here is the one place a CLI rename could produce a
10
+ * FALSE signal rather than merely no signal — if a future build were to name a plain task-list
11
+ * tool `Task`, its writes would be counted as in-flight slices. We accept that because no shipped
12
+ * build does (the incremental plan tool is `TaskCreate`/`TaskUpdate`, tracked separately in
13
+ * `progress.ts`), and dropping legacy coverage is the more likely regression.
14
+ *
15
+ * Lives here rather than in `subagents.ts` because BOTH the slice tracker and the no-progress
16
+ * guard (`pi.ts`, which `subagents.ts` imports — so it cannot import back) must agree on what a
17
+ * subagent dispatch looks like.
18
+ */
19
+ export declare const SUBAGENT_TOOL_NAMES: Set<string>;
20
+ export declare function numberOf(value: unknown): number;
21
+ /** Scrub any leased-credential occurrences from a telemetry body (no-op when none). */
22
+ export declare function redactBody(text: string, secrets: string[]): string;
23
+ /** Pull the text + reasoning out of a Claude `assistant` message's content blocks. */
24
+ export declare function claudeAssistantContent(content: unknown[]): {
25
+ text: string;
26
+ reasoning: string;
27
+ toolUses: number;
28
+ };
29
+ /**
30
+ * The text a `tool_result` block carries. The CLI writes it either as a bare string or as an
31
+ * array of content blocks (the shape a subagent's terminal report arrives in), so both are read
32
+ * here rather than at each call site. Non-text blocks (an image a tool returned) contribute
33
+ * nothing. Returns '' when the block carries no readable text.
34
+ *
35
+ * This is what makes a parallel subagent's work observable to the harness at all: the parent
36
+ * stream shows a subagent's dispatch and its terminal `tool_result` and nothing in between, so
37
+ * this text is the ONLY place its findings surface outside its own untailed transcript.
38
+ */
39
+ export declare function claudeToolResultText(block: Record<string, unknown>): string;
40
+ /**
41
+ * Per-CALL token usage off a Claude `assistant` message's `usage` (this turn only, not
42
+ * the cumulative `result` total).
43
+ *
44
+ * Anthropic reports all three input classes SEPARATELY and `input_tokens` is already
45
+ * exclusive of both caches, so the three fields here are orthogonal and additive:
46
+ * total input = `inputTokens + cacheReadTokens + cacheWriteTokens`. Do NOT re-lump the
47
+ * reads and the writes — a cache write costs 1.25–2× base input while a read costs ~0.1×,
48
+ * so a turn that keeps invalidating the prefix and one that rides a warm cache are
49
+ * indistinguishable once they are summed.
50
+ */
51
+ export declare function claudeCallUsage(raw: unknown): {
52
+ inputTokens: number;
53
+ cacheReadTokens: number;
54
+ cacheWriteTokens: number;
55
+ outputTokens: number;
56
+ };
@@ -0,0 +1,263 @@
1
+ import type { AgentJob, AgentResult, HarnessAuthFields, RepoSpec, SkillSpec, McpServerSpec } from './job.js';
2
+ import type { HarnessCallMetric, PiRunStats } from './pi.js';
3
+ import { type EffortReport } from './effort.js';
4
+ import { type AgentPrDescription } from './pr-description.js';
5
+ import type { ProgressGuardLimits } from './progress-guard.js';
6
+ import type { RunOptions } from './runner.js';
7
+ import { type Logger } from './logger.js';
8
+ import { type ValidationChecksSpec, type ValidationReport } from './validation-checks.js';
9
+ import { type ReproductionReport, type ReproductionSpec } from './reproduction-proof.js';
10
+ import { type DependencyInstallSpec } from './dependency-install.js';
11
+ /** What a coding agent run needs: where to clone, what to run, where to push. */
12
+ export interface CodingAgentSpec extends HarnessAuthFields {
13
+ /** Short label for the temp dir + log lines (e.g. 'impl', 'ci-fix'). */
14
+ kind: string;
15
+ /** The job id, threaded into every log line for end-to-end tracing. */
16
+ jobId: string;
17
+ repo: RepoSpec;
18
+ /** Branch to clone and check out as the starting point. */
19
+ cloneBranch: string;
20
+ /** A fresh branch to create off the clone before running; omit to work directly on `cloneBranch`. */
21
+ newBranch?: string;
22
+ /** Branch the produced change is pushed to. */
23
+ pushBranch: string;
24
+ ghToken: string;
25
+ /** Composed role + best-practice fragments; written to Pi's global AGENTS.md context. */
26
+ systemPrompt: string;
27
+ /** The concrete task prompt handed to Pi. */
28
+ userPrompt: string;
29
+ model: string;
30
+ /** Commit message for any work the agent left uncommitted. */
31
+ commitMessage: string;
32
+ /** Per-kind web-search guidance (backend-composed); surfaced only when web search is on. */
33
+ webToolsGuidance?: string;
34
+ /** Enable proxy-backed web search for this run (see {@link AgentRunSpec.webSearchProxy}). */
35
+ webSearchProxy?: boolean;
36
+ /** Backend serves the phase-tagged completions route (see {@link AgentRunSpec.proxyPhasePath}). */
37
+ proxyPhasePath?: boolean;
38
+ /** Per-knob progress-guard overrides (loosen-only), set per agent kind by the backend. */
39
+ guardLimits?: Partial<ProgressGuardLimits>;
40
+ /**
41
+ * Reuse a stable per-repo checkout (clean-sweep + fetch + switch branch) instead of a
42
+ * fresh clone into a throwaway temp dir. Set only by the local warm-pool transport
43
+ * (its containers are reused across runs); absent everywhere else.
44
+ */
45
+ persistentCheckout?: boolean;
46
+ /**
47
+ * Tail the Coder's follow-up sentinel file ({@link FOLLOW_UPS_FILENAME}) and stream the
48
+ * forward-looking items it surfaces out on the job view (the Follow-up companion). Set
49
+ * only for the implementer (`coder`) dispatch; absent ⇒ no tailing (e.g. the CI-fixer).
50
+ */
51
+ streamFollowUps?: boolean;
52
+ /**
53
+ * Whether this dispatch OPENS a pull request (the caller passes `pr` to `openPullRequest`).
54
+ * Set, the harness looks for the repo's own pull-request template and asks the agent to fill it
55
+ * (see `pr-template.ts`). Absent for a dispatch that amends someone else's PR (the in-place
56
+ * fixers) — a template filled for a pull request nothing opens is wasted prompt and, worse,
57
+ * would have a CI-fixer rewrite the implementer's already-published description.
58
+ */
59
+ opensPr?: boolean;
60
+ /**
61
+ * READ-ONLY reference branches of THIS repo (the apriori-branches reference mode): fetched
62
+ * into `origin/<b>` after the checkout so the agent can inspect them but never commits to
63
+ * them. Best-effort per branch. Absent/empty ⇒ none fetched.
64
+ */
65
+ referenceBranches?: string[];
66
+ /**
67
+ * Ralph loop: run this programmatic completion command in the checkout AFTER the agent
68
+ * commits + pushes, capturing its exit code + a bounded output tail (the loop's exit
69
+ * condition — computed by the harness, never the model). Absent for every non-`ralph` run.
70
+ */
71
+ validation?: {
72
+ command: string;
73
+ iteration?: number;
74
+ };
75
+ /**
76
+ * PRE-PR VALIDATION: the service's configured check commands + repair-round budget. When set,
77
+ * the harness runs them against the checkout after the agent settles and, while they fail and
78
+ * budget remains, re-runs the agent with the captured output as its instruction. A red checkout
79
+ * at the end means the caller opens NO pull request and fails the job. Set only for a dispatch
80
+ * that opens a PR and whose service configured checks; absent everywhere else. See
81
+ * `docs/initiatives/pre-pr-validation.md`.
82
+ */
83
+ validationChecks?: ValidationChecksSpec;
84
+ /**
85
+ * DEPENDENCY PREPOPULATION: the service's install command, run against the checkout BEFORE the
86
+ * agent's first turn so it works against a tree whose dependencies are present. Best-effort —
87
+ * a failure becomes a note in the agent's prompt, never a failed run. Absent ⇒ no install
88
+ * phase. See `docs/initiatives/agent-dependency-prepopulation.md`.
89
+ */
90
+ dependencyInstall?: DependencyInstallSpec;
91
+ /**
92
+ * BUGFIX REPRODUCTION PROOF: the run's declared reproduction command + test files. When set, the
93
+ * harness runs that command against the pre-fix tree AND the tree the PR will open from, feeding
94
+ * a failed verification back to the agent while budget remains, and attaches the verdict to the
95
+ * outcome. Unlike {@link validationChecks} it NEVER gates the pull request — an unproven
96
+ * reproduction is weak evidence, which is a reviewer's call, not a machine's. Set only for a
97
+ * dispatch that opens a PR and whose run carries a declaration. See
98
+ * `docs/initiatives/bugfix-reproduction-proof.md`.
99
+ */
100
+ reproduction?: ReproductionSpec;
101
+ /**
102
+ * The skills to make available for this run — a `skill` step's pick and/or the running kind's
103
+ * declared playbooks. Threaded into {@link runAgentInWorkspace}, which installs them
104
+ * harness-aware: natively under the ISOLATED `CLAUDE_CONFIG_DIR` for a leased-credential
105
+ * claude-code run, `.cat-context/skill/<name>/` for everything else (Pi, codex, and ambient
106
+ * claude-code, which has no isolated config dir). Absent ⇒ no skills.
107
+ */
108
+ skills?: SkillSpec[];
109
+ /**
110
+ * Tool servers (MCP) to wire into the agent CLI for this run. Forwarded verbatim — the backend
111
+ * has already dropped anything this harness cannot serve. Absent ⇒ built-in tools only.
112
+ */
113
+ mcpServers?: McpServerSpec[];
114
+ }
115
+ /** The outcome of a coding agent run, before each caller maps it to its own result shape. */
116
+ export interface CodingAgentOutcome {
117
+ /** Whether the branch carries work and was therefore pushed (new commits, or resumed prior work). */
118
+ pushed: boolean;
119
+ /** Whether the run resumed an existing remote branch (prior work already pushed). */
120
+ resumed: boolean;
121
+ summary: string;
122
+ stats: PiRunStats;
123
+ stderrTail?: string;
124
+ /** Token usage from a subscription harness's CLI stream (absent for Pi). */
125
+ usage?: {
126
+ inputTokens: number;
127
+ outputTokens: number;
128
+ };
129
+ /** Per-model-call telemetry from a subscription harness's CLI stream (absent for Pi). */
130
+ callMetrics?: HarnessCallMetric[];
131
+ /** The agent's effort self-assessment, lifted from its sentinel file (absent when it wrote none). */
132
+ effortReport?: EffortReport;
133
+ /**
134
+ * The agent-authored PR description, lifted from its sentinel file (absent when it wrote none).
135
+ * The PR-opening caller folds it over the dispatch-time title/body via {@link applyPrDescription};
136
+ * absent means the fallback text, unchanged.
137
+ */
138
+ prDescription?: AgentPrDescription;
139
+ /**
140
+ * Ralph loop: the verdict of the post-commit validation command (whether it exited 0, the
141
+ * exit code, and a bounded/redacted output tail). Present only when {@link CodingAgentSpec.validation}
142
+ * was set. The exit code is the loop's authoritative completion signal.
143
+ */
144
+ validation?: {
145
+ validationPassed: boolean;
146
+ exitCode: number;
147
+ validationOutputTail?: string;
148
+ iteration?: number;
149
+ /** The work-branch HEAD the command was judged against (absent when it could not be read). */
150
+ headSha?: string;
151
+ };
152
+ /**
153
+ * The pre-PR validation loop's LAST attempt (present only when {@link CodingAgentSpec.validationChecks}
154
+ * was set). `passed: false` means the attempt budget was spent with the checkout still red —
155
+ * the caller must open no PR and fail the job with this as the evidence.
156
+ */
157
+ validationReport?: ValidationReport;
158
+ /**
159
+ * The bugfix reproduction proof's LAST attempt (present only when
160
+ * {@link CodingAgentSpec.reproduction} was set). Evidence, never a gate: `inconclusive` is
161
+ * attached to a perfectly successful run and the PR still opens.
162
+ */
163
+ reproductionReport?: ReproductionReport;
164
+ }
165
+ /**
166
+ * Clone (or RESUME an existing branch) → write context → run Pi → push the branch
167
+ * iff it carries work. The agent commits its OWN work (it alone knows which files
168
+ * belong vs scratch/artifacts it created), so the harness never blanket-stages:
169
+ * {@link commitTrackedEdits} is only a safety net for forgotten edits to ALREADY
170
+ * tracked files, and the run is judged a no-op only when the branch never advanced
171
+ * past its pre-run tip ({@link branchHasCommitsSince}). The harness owns push + PR;
172
+ * it checkpoints (pushes) periodically so an evicted run's commits survive and a
173
+ * retry resumes on them. Returns the run's summary/stats, whether it pushed, and
174
+ * whether it resumed; callers decide what to do after a push (open a PR, or nothing).
175
+ */
176
+ export declare function runCodingAgent(spec: CodingAgentSpec, opts?: RunOptions): Promise<CodingAgentOutcome>;
177
+ /**
178
+ * The Ralph-loop validation watchdog: the longest a completion command may run before it is
179
+ * killed and treated as a failure (a hung `pnpm test` must never block the loop forever).
180
+ * Overridable via env for tests; defaults to 15 minutes.
181
+ */
182
+ export declare function ralphValidationTimeoutMs(): number;
183
+ /**
184
+ * How often the Ralph validation feeds the run's inactivity watchdog while its command runs.
185
+ * The command is exactly the activity-SILENT kind — a full `pnpm test`, a cold install-then-build
186
+ * — and the harness spawns it ITSELF rather than through the agent, so it emits no activity
187
+ * events of its own. `JOB_INACTIVITY_MS` (default 10 min) is TIGHTER than the command's own
188
+ * watchdog ({@link ralphValidationTimeoutMs}, default 15 min), so without this heartbeat any
189
+ * validation running past 10 minutes aborted the whole iteration as "inactivity" — mislabelling
190
+ * a healthy test suite as a wedge, and making the 15-minute watchdog unreachable at stock
191
+ * settings. The two sibling harness-run phases (pre-PR validation, reproduction proof) have
192
+ * always fed it; this one did not. Overridable via env for tests.
193
+ */
194
+ export declare function ralphHeartbeatMs(): number;
195
+ /**
196
+ * Bound on the validation output tail that crosses the wire. Deliberately smaller than
197
+ * `MAX_CAPTURED_OUTPUT_CHARS` (`redact.ts`) and equal to the two sibling phases' budgets, for
198
+ * the same reason: this tail is persisted on the step (and on EVERY iteration of the attempt
199
+ * log) inside the run's `detail` JSON blob, which is re-serialized on every step-progress write.
200
+ */
201
+ export declare const RALPH_VALIDATION_TAIL_CHARS = 4000;
202
+ /**
203
+ * Ralph loop: run the programmatic completion command in the checkout and return its exit
204
+ * code, a bounded + redacted tail of its output, and the work branch's HEAD it ran against.
205
+ * The EXIT CODE is the loop's authoritative done signal (0 = the criterion is met) — computed
206
+ * here by the harness, never self-reported by the model, which is the whole point of a
207
+ * programmatic exit condition. The command runs INSIDE the sandboxed run container (the same
208
+ * trust boundary as the coding agent) — there is no host/backend execution.
209
+ *
210
+ * The spawn itself goes through {@link runCapturedCommand}, the ONE seam for a harness-run
211
+ * command, rather than the near-verbatim copy this used to be. That copy had drifted in two
212
+ * ways the seam exists to prevent: it scrubbed secrets AFTER the rolling truncation with no
213
+ * margin (so a credential straddling the cut lost its `KEY=` prefix and survived redaction as
214
+ * an unrecognised partial, on a tail that reaches the step, the notification and the SPA), and
215
+ * it published the full 16k capture where both siblings deliberately bound the wire tail.
216
+ *
217
+ * `headSha` is what lets the engine tell a loop that is iterating from one that is merely
218
+ * repeating: two consecutive failing iterations against an unchanged head means the agent
219
+ * committed nothing, and the loop is ended early instead of spending the rest of its budget.
220
+ * Best-effort — a head that cannot be read is simply omitted, and the engine's check fails open.
221
+ */
222
+ export declare function runRalphValidation(repoDir: string, cwd: string, validation: {
223
+ command: string;
224
+ iteration?: number;
225
+ }, logger: Logger, opts: RunOptions): Promise<{
226
+ validationPassed: boolean;
227
+ exitCode: number;
228
+ validationOutputTail?: string;
229
+ iteration?: number;
230
+ headSha?: string;
231
+ }>;
232
+ /** Sanitise an owner/name into a safe single path segment for a sibling checkout directory. */
233
+ export declare function safeDirSegment(value: string): string;
234
+ /**
235
+ * A sibling-directory allocator for a multi-repo run: returns the checkout directory name for a
236
+ * repo under the workspace root. Deterministic (`owner__name`) and collision-free by construction
237
+ * — the checkout set is deduped by `owner/name` upstream and GitHub owners contain no `_`, so the
238
+ * `owner__name` join is unique per repo without a stateful collision dance. Kept as a factory so
239
+ * the coding + read-only explore fan-outs share ONE scheme, and it MUST stay byte-identical to the
240
+ * backend's `siblingCheckoutDir` / `renderMultiRepoWorkspaceSection` in `@cat-factory/server`
241
+ * (jobBody.ts), which names this exact directory in the agent's prompt — the two are computed
242
+ * independently, so a divergent rule would point the agent at a directory that does not exist.
243
+ */
244
+ export declare function makeDirClaimer(): (repo: Pick<RepoSpec, 'name' | 'owner'>) => string;
245
+ /**
246
+ * Multi-repo coding (service-connections phase 3): clone the primary repo AND every connected
247
+ * peer repo as SIBLING checkouts under one workspace root, run the agent ONCE with its cwd at
248
+ * that root (so it makes the cross-service change coherently across all of them), then commit +
249
+ * push each repo that actually changed and open one PR per dirty repo. The task's own-service PR
250
+ * is reported as `prUrl`/`branch`; the peer PRs as `peerPullRequests`.
251
+ *
252
+ * Deliberately simpler than the single-repo {@link runCodingAgent} for the first cut: NO mid-run
253
+ * checkpoint pushes (an evicted multi-repo run re-clones on retry — the deterministic work branch
254
+ * still lets it resume any commits it managed to push at the end), NO warm-pool persistent
255
+ * checkout (always ephemeral), and NO follow-up sentinel streaming. It reuses the SAME dir-scoped
256
+ * git helpers, so the per-repo clone/commit/push/PR mechanics match the single-repo path exactly.
257
+ */
258
+ export declare function runMultiRepoCoding(job: AgentJob, opts?: RunOptions): Promise<AgentResult>;
259
+ /**
260
+ * The "no changes" reason both coding agents report: a caller-supplied lead phrase
261
+ * plus the shared "never acted" cause and a credential-scrubbed tail of Pi's stderr.
262
+ */
263
+ export declare function noChangesReason(lead: string, stats: PiRunStats, stderrTail: string | undefined): string;
@@ -0,0 +1,111 @@
1
+ import { type RunOptions } from './runner.js';
2
+ import type { Logger } from './logger.js';
3
+ /** The dependency-install phase as it arrives on the job body. */
4
+ export interface DependencyInstallSpec {
5
+ /** The shell command, run as `sh -c` in the checkout (the service directory for a monorepo). */
6
+ command: string;
7
+ }
8
+ /** What the install did — folded into the agent's prompt, never a verdict about the run. */
9
+ export interface DependencyInstallOutcome {
10
+ command: string;
11
+ exitCode: number;
12
+ passed: boolean;
13
+ /** Scrubbed, bounded tail of the combined output. Only kept for a FAILED install. */
14
+ outputTail?: string;
15
+ durationMs: number;
16
+ timedOut?: boolean;
17
+ }
18
+ /**
19
+ * How much of a failed install's output the agent is shown. Smaller than the validation loop's
20
+ * repair budget (16k) on purpose: a repair prompt has to carry the whole failure because fixing
21
+ * it IS the task, whereas this note only has to let the agent decide whether to install
22
+ * something itself. The tail is where a package manager puts its actual error.
23
+ */
24
+ export declare const DEPENDENCY_INSTALL_TAIL_CHARS = 4000;
25
+ /**
26
+ * The per-install watchdog: the longest the install may run before it is killed and reported as
27
+ * failed. Generous (20 min at the defaults) because a cold monorepo install on a slow registry
28
+ * legitimately takes many minutes.
29
+ *
30
+ * DERIVED from the configured job ceiling rather than hardcoded against the default one, the same
31
+ * way `git.ts` derives its per-command timeout from the configured inactivity window: a constant
32
+ * sized against a default silently breaks its own invariant the moment an operator changes that
33
+ * default. An explicit `DEPENDENCY_INSTALL_TIMEOUT_MS` is honoured but still CLAMPED — the point
34
+ * of the share is that no configuration lets setup eat the run, and an override that could exceed
35
+ * the job's own ceiling would only ever be killed later by a watchdog that fails the whole job
36
+ * instead of degrading to a note.
37
+ */
38
+ export declare function dependencyInstallTimeoutMs(env?: NodeJS.ProcessEnv): number;
39
+ /**
40
+ * How often the install feeds the run's inactivity watchdog. Well under `JOB_INACTIVITY_MS`
41
+ * (default 10 min); matches the validation loop's and the frontend stand-up's heartbeat, which
42
+ * exist for exactly the same reason.
43
+ */
44
+ export declare function dependencyInstallHeartbeatMs(): number;
45
+ /**
46
+ * Parse the optional DEPENDENCY INSTALL envelope off the job body. A missing/blank command
47
+ * returns `undefined`, so a malformed body degrades to the exact pre-feature behaviour (no
48
+ * install phase, the agent starts against the bare clone) rather than failing a good run.
49
+ *
50
+ * Lives with the feature rather than in `job.ts`, following the same rule the two pre-PR
51
+ * verification phases do: each phase owns its own job-body parser next to the code that consumes
52
+ * it, and `job.ts` stays the job SHAPE plus the generic assembly.
53
+ */
54
+ export declare function parseDependencyInstallSpec(value: unknown): DependencyInstallSpec | undefined;
55
+ /**
56
+ * Run the declared install against `cwd` and return what happened. Never throws and never fails
57
+ * the job: every failure shape ({@link runCapturedCommand} maps a timeout to 124, a spawn error
58
+ * to 127, an abort to 130) comes back as a non-zero outcome the caller turns into a prompt note.
59
+ *
60
+ * The output tail is kept ONLY for a failure. A successful install prints tens of thousands of
61
+ * uninteresting lines, and the agent needs to know that it succeeded, not what it resolved.
62
+ */
63
+ export declare function runDependencyInstall(args: {
64
+ cwd: string;
65
+ spec: DependencyInstallSpec;
66
+ logger: Logger;
67
+ opts: RunOptions;
68
+ }): Promise<DependencyInstallOutcome>;
69
+ /**
70
+ * THE entry point: run the phase for a mode that has a checkout, and hand back the note to fold
71
+ * into the agent's prompt (or `undefined` when the service declared no install, which is every
72
+ * dispatch today that never configured one).
73
+ *
74
+ * Everything a caller could get wrong lives here rather than at six call sites: the phase marker,
75
+ * the best-effort run, keeping the installed tree out of the agent's commits, and naming WHERE
76
+ * the install ran when that is not where the agent will be standing. A mode supplies only its
77
+ * three directories.
78
+ */
79
+ export declare function prepopulateDependencies(args: {
80
+ spec: DependencyInstallSpec | undefined;
81
+ /** Where the install runs: the service subtree for a monorepo, else the checkout root. */
82
+ installDir: string;
83
+ /** The git checkout whose local excludes protect the agent's commits from the installed tree. */
84
+ repoDir: string;
85
+ /** The agent's own working directory, which names the install location when the two differ. */
86
+ agentDir: string;
87
+ logger: Logger;
88
+ opts: RunOptions;
89
+ }): Promise<string | undefined>;
90
+ /**
91
+ * Fold the note into a prompt. Trivial, and deliberately not inlined: it is applied on EVERY
92
+ * agent pass — including the validation and reproduction REPAIR passes, which start a fresh
93
+ * agent that would otherwise never learn the tree is already installed and would spend a repair
94
+ * round reinstalling it.
95
+ */
96
+ export declare function withDependencyNote(userPrompt: string, note: string | undefined): string;
97
+ /**
98
+ * The note folded into the agent's prompt describing the checkout it is about to work in.
99
+ *
100
+ * Stated in BOTH directions on purpose. On success the agent is told the tree is ready, which is
101
+ * what stops it spending turns re-running an install that already ran (and, on a repo whose
102
+ * install is slow, spending most of its budget there). On failure it is told plainly what failed
103
+ * and that it may install what it needs itself — an agent that merely finds no `node_modules` and
104
+ * no explanation concludes the environment is offline and works around a gap that isn't there.
105
+ *
106
+ * `scope` names the checkout the install ran in and is set ONLY when that is not the agent's own
107
+ * working directory — the multi-repo layout runs the agent at the workspace root while the install
108
+ * belongs to the primary service's sibling directory. Saying "this checkout" there would point the
109
+ * agent at a root that has no dependency tree of its own.
110
+ */
111
+ export declare function buildDependencyInstallNote(outcome: DependencyInstallOutcome, scope?: string): string;
@@ -0,0 +1,19 @@
1
+ /** The sentinel file the agent writes its effort self-assessment to (relative to its cwd). */
2
+ export declare const EFFORT_REPORT_FILE = ".cat-effort.json";
3
+ /** A container agent's self-assessment of the work it just did. */
4
+ export interface EffortReport {
5
+ /** How hard the work was: 1 (trivial) .. 10 (extremely hard). */
6
+ difficulty: number;
7
+ /** One or two sentences on how hard/easy the work was and why. */
8
+ summary?: string;
9
+ /** What reduced the agent's effectiveness. */
10
+ reducedEffectiveness?: string;
11
+ /** The key obstacles the agent hit. */
12
+ obstacles?: string[];
13
+ }
14
+ /**
15
+ * Read + parse + REMOVE the agent's effort sentinel file from `cwd`. Lenient: returns undefined
16
+ * when the file is absent (the agent wrote none), unreadable, not JSON, or carries nothing
17
+ * meaningful. Never throws — a malformed self-report must never fail an otherwise-good run.
18
+ */
19
+ export declare function readEffortReport(cwd: string): Promise<EffortReport | undefined>;
@@ -0,0 +1,4 @@
1
+ export { PI_MAX_OUTPUT_TOKENS, writePiModelsConfig, writeAgentsContext, runPi, summarizePiRun, parsePiOutput, parseTodoProgress, terminalRunError, type PiRunOutcome, type PiRunStats, type TodoItem, type TodoProgress, } from './pi.js';
2
+ export { DEFAULT_PROGRESS_GUARD_LIMITS, progressGuardLimitsFromEnv, type ProgressGuardLimits, } from './progress-guard.js';
3
+ export { cloneRepo, createBranch, changedPathsFromPorcelain, hasAgentChanges, redactSecrets, } from './git.js';
4
+ export type { RepoSpec } from './job.js';