@cat-factory/executor-harness 1.80.0 → 1.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/agent-capabilities.d.ts +130 -0
- package/dist/agent-runner.d.ts +114 -0
- package/dist/agent-runner.js +50 -32
- package/dist/agent-shared.d.ts +18 -0
- package/dist/agent.d.ts +66 -0
- package/dist/bootstrap-mode.d.ts +20 -0
- package/dist/captured-command.d.ts +58 -0
- package/dist/claude-call-aggregator.d.ts +164 -0
- package/dist/claude-call-aggregator.js +123 -17
- package/dist/claude-stream.d.ts +56 -0
- package/dist/coding-agent.d.ts +252 -0
- package/dist/coding-agent.js +69 -50
- package/dist/dependency-install.d.ts +111 -0
- package/dist/effort.d.ts +19 -0
- package/dist/embed.d.ts +4 -0
- package/dist/failure.d.ts +42 -0
- package/dist/follow-ups.d.ts +28 -0
- package/dist/frontend-infra.d.ts +25 -0
- package/dist/fs-utils.d.ts +2 -0
- package/dist/git.d.ts +394 -0
- package/dist/host-markdown.d.ts +28 -0
- package/dist/inline.d.ts +10 -0
- package/dist/job.d.ts +666 -0
- package/dist/logger.d.ts +16 -0
- package/dist/onboarding-preseed.d.ts +24 -0
- package/dist/package-registries.d.ts +32 -0
- package/dist/pi-workspace.d.ts +194 -0
- package/dist/pi.d.ts +475 -0
- package/dist/pr-description.d.ts +85 -0
- package/dist/pr-template.d.ts +101 -0
- package/dist/process-exit.d.ts +7 -0
- package/dist/process.d.ts +19 -0
- package/dist/progress-guard.d.ts +88 -0
- package/dist/progress.d.ts +87 -0
- package/dist/redact.d.ts +31 -0
- package/dist/reproduction-proof.d.ts +224 -0
- package/dist/runner.d.ts +282 -0
- package/dist/server.d.ts +3 -0
- package/dist/structured-output.d.ts +75 -0
- package/dist/subagents.d.ts +88 -0
- package/dist/transcript-retention.d.ts +21 -0
- package/dist/validation-checks.d.ts +159 -0
- package/dist/vcs-api.d.ts +73 -0
- package/dist/version.d.ts +2 -0
- package/package.json +9 -5
- package/src/agent-runner.ts +54 -29
- package/src/claude-call-aggregator.ts +181 -32
- package/src/coding-agent.ts +80 -49
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { HarnessCallMetric } from './pi.js';
|
|
2
|
+
/** One model call, assembled from every stream envelope that carried a piece of it. */
|
|
3
|
+
export interface AggregatedClaudeCall {
|
|
4
|
+
model?: string;
|
|
5
|
+
/** Every content block of the response, in arrival order. */
|
|
6
|
+
content: unknown[];
|
|
7
|
+
text: string;
|
|
8
|
+
reasoning: string;
|
|
9
|
+
stopReason: string | null;
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
cacheReadTokens: number;
|
|
12
|
+
cacheWriteTokens: number;
|
|
13
|
+
outputTokens: number;
|
|
14
|
+
/** The `user` turns carrying this call's tool_result blocks, in arrival order. */
|
|
15
|
+
toolResults: unknown[][];
|
|
16
|
+
/** tool_use blocks across the whole response (the run's `stats.toolCalls` term). */
|
|
17
|
+
toolUses: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ClaudeCallAggregator {
|
|
20
|
+
/**
|
|
21
|
+
* Fold one `assistant` envelope in. A new `message.id` completes the call in flight first, so
|
|
22
|
+
* `onCallStart` for the new call always runs after `onCall` for the previous one.
|
|
23
|
+
*/
|
|
24
|
+
onAssistant(message: Record<string, unknown>): void;
|
|
25
|
+
/** Buffer a `user` turn's content against the call in flight (dropped when none is). */
|
|
26
|
+
onToolResult(content: unknown[]): void;
|
|
27
|
+
/** Complete the call still in flight, if any. Call once the stream has ended. */
|
|
28
|
+
flush(): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Assemble per-call telemetry out of Claude Code's per-block stream envelopes.
|
|
32
|
+
*
|
|
33
|
+
* `onCallStart` fires when a call's FIRST envelope arrives, which is the moment the caller must
|
|
34
|
+
* snapshot the prompt: the history at that point is what produced the response. `onCall` fires
|
|
35
|
+
* once the call is complete (a different `message.id` began, or the stream ended).
|
|
36
|
+
*
|
|
37
|
+
* Usage is merged as the MAXIMUM of each bucket across the call's envelopes rather than the last
|
|
38
|
+
* one seen. The envelopes carry a snapshot of the same call's usage, and which of them holds the
|
|
39
|
+
* final output count is a CLI detail we should not depend on; a max is right whether the value is
|
|
40
|
+
* repeated verbatim or grows.
|
|
41
|
+
*
|
|
42
|
+
* An envelope with no `message.id` cannot be attributed, so it is treated as a call of its own —
|
|
43
|
+
* the pre-aggregation behaviour, kept so a CLI build (or a transcript) that omits the id degrades
|
|
44
|
+
* to over-counting rather than to silently merging unrelated calls.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createClaudeCallAggregator(handlers: {
|
|
47
|
+
onCallStart?: () => void;
|
|
48
|
+
onCall: (call: AggregatedClaudeCall) => void;
|
|
49
|
+
}): ClaudeCallAggregator;
|
|
50
|
+
/** One turn of the reconstructed request transcript, in the proxy's chat-array shape. */
|
|
51
|
+
interface TranscriptTurn {
|
|
52
|
+
role: string;
|
|
53
|
+
content: unknown;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How much reconstructed transcript ONE conversation may retain.
|
|
57
|
+
*
|
|
58
|
+
* The stream feeds this without limit — a tool loop that reads large files grows the history by
|
|
59
|
+
* every one of them — and the reconstruction is held in the driver's own process. In the container
|
|
60
|
+
* that is a box sized for one job; in the BACKEND it is the orchestrator, where `streamCli` already
|
|
61
|
+
* refuses to retain the raw stream for exactly this reason (`harnessInline.ts` →
|
|
62
|
+
* `OUTPUT_TAIL_RETAIN_CHARS`: "a stalled tool-using run would otherwise park hundreds of MB in the
|
|
63
|
+
* orchestrator process — precisely on the runs worth diagnosing").
|
|
64
|
+
*
|
|
65
|
+
* 512 KiB because that is `LlmObservabilityService.MAX_BODY_CHARS`, the point past which the store
|
|
66
|
+
* truncates a body anyway: retaining more can only ever be thrown away. Deliberately NOT a
|
|
67
|
+
* per-deployment knob — it bounds a memory fault, and a number an operator can raise is one an
|
|
68
|
+
* operator can raise until the process dies.
|
|
69
|
+
*/
|
|
70
|
+
export declare const MAX_TRANSCRIPT_CHARS: number;
|
|
71
|
+
/** The per-call telemetry the Claude Code stream yields, assembled behind one small surface. */
|
|
72
|
+
export interface ClaudeStreamTelemetry {
|
|
73
|
+
/** Fold an `assistant` envelope in (parent-loop turns only — see {@link isSubagentEvent}). */
|
|
74
|
+
onAssistant(message: Record<string, unknown>): void;
|
|
75
|
+
/** Fold a `user` turn's tool_result content in, against the call in flight. */
|
|
76
|
+
onToolResult(content: unknown[]): void;
|
|
77
|
+
/** Publish the call still in flight. Idempotent; safe to call on both the clean and error path. */
|
|
78
|
+
flush(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Assemble ONE conversation's per-call telemetry from the CLI stream: the growing request
|
|
82
|
+
* transcript and the per-call token/body metrics.
|
|
83
|
+
*
|
|
84
|
+
* Owns the transcript because the two are one concern — a call's `promptText` is the transcript as
|
|
85
|
+
* of that call, and its turns may only be appended once the call that produced them is complete.
|
|
86
|
+
* `seed` is what the harness supplied and the stream therefore never shows (the system + first user
|
|
87
|
+
* message, or the single folded user turn), so the reconstruction never claims a system turn that
|
|
88
|
+
* was not sent. A subagent's conversation seeds EMPTY — its prompt was minted by the CLI and never
|
|
89
|
+
* crosses this stream — which is also why its first call carries `messageCount: 0` (the backend's
|
|
90
|
+
* `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 — and assembled at all only when
|
|
92
|
+
* {@link ClaudeStreamTelemetryOptions.bodies} says a driver has somewhere to put them. The
|
|
93
|
+
* transcript is bounded either way ({@link MAX_TRANSCRIPT_CHARS}).
|
|
94
|
+
*
|
|
95
|
+
* Deliberately does NOT touch {@link PiRunStats}: the run's tool/output counters describe whether
|
|
96
|
+
* the agent ACTED at all (`agentNeverActed`), which is true of a subagent's turns whichever channel
|
|
97
|
+
* ends up owning their telemetry rows. The caller accumulates them off the raw stream instead.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createClaudeStreamTelemetry(opts: ClaudeStreamTelemetryOptions): ClaudeStreamTelemetry;
|
|
100
|
+
/** How one conversation's per-call telemetry is assembled. */
|
|
101
|
+
export interface ClaudeStreamTelemetryOptions {
|
|
102
|
+
seed: TranscriptTurn[];
|
|
103
|
+
secrets: string[];
|
|
104
|
+
publish: (metric: HarnessCallMetric) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Whether to assemble the prompt/response BODIES at all. Absent ⇒ true (the container harness,
|
|
107
|
+
* whose job result carries them).
|
|
108
|
+
*
|
|
109
|
+
* `false` for a driver whose store will drop them — the backend with `LLM_RECORD_PROMPTS` off —
|
|
110
|
+
* where reconstructing a transcript per call is pure cost. Token counts, `messageCount` and
|
|
111
|
+
* finish reasons are unaffected: only the bodies go.
|
|
112
|
+
*/
|
|
113
|
+
bodies?: boolean;
|
|
114
|
+
/** Retention bound override (tests). Absent ⇒ {@link MAX_TRANSCRIPT_CHARS}. */
|
|
115
|
+
maxTranscriptChars?: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The dispatch (`Agent`/`Task` tool_use) id a stream envelope is tagged with, or `undefined` for a
|
|
119
|
+
* parent-loop turn.
|
|
120
|
+
*
|
|
121
|
+
* Claude Code streams the turns of the subagents it dispatches onto the parent's stdout, tagged
|
|
122
|
+
* with the tool_use id that spawned them. Those same turns are also written to the per-session
|
|
123
|
+
* `subagents/*.jsonl` transcripts the watcher reads, so recording both channels counted every
|
|
124
|
+
* subagent call twice — and splicing them into the parent's message reconstruction produced a
|
|
125
|
+
* `promptText` chain that interleaves several conversations and therefore matches no real request.
|
|
126
|
+
*
|
|
127
|
+
* The id is what makes the fallback below possible: concurrent subagents interleave on one stdout,
|
|
128
|
+
* so it is the ONLY thing separating their conversations.
|
|
129
|
+
*/
|
|
130
|
+
export declare function subagentDispatchId(event: Record<string, unknown>): string | undefined;
|
|
131
|
+
/** Whether a stream envelope describes a SUBAGENT's turn rather than the parent loop's. */
|
|
132
|
+
export declare function isSubagentEvent(event: Record<string, unknown>): boolean;
|
|
133
|
+
/** All per-call telemetry for ONE claude-code run: the parent loop, and whoever bills the subagents. */
|
|
134
|
+
export interface ClaudeRunTelemetry {
|
|
135
|
+
/** Fold an `assistant` envelope in, routed by its dispatch tag (`undefined` ⇒ the parent loop). */
|
|
136
|
+
onAssistant(dispatchId: string | undefined, message: Record<string, unknown>): void;
|
|
137
|
+
/** Fold a `user` turn's tool_result content in, against the same conversation. */
|
|
138
|
+
onToolResult(dispatchId: string | undefined, content: unknown[]): void;
|
|
139
|
+
/** Publish every conversation's call in flight. Idempotent; safe on the clean and error paths. */
|
|
140
|
+
flush(): void;
|
|
141
|
+
/**
|
|
142
|
+
* Subagent turns crossed the stream AND the watcher was the channel meant to record them — so a
|
|
143
|
+
* watcher that captured nothing means this run's subagent rows are simply missing.
|
|
144
|
+
*/
|
|
145
|
+
expectsWatcherCalls(): boolean;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Assemble a run's per-call telemetry, routing each envelope to the conversation it belongs to.
|
|
149
|
+
*
|
|
150
|
+
* The routing is the whole point. A subagent's turns ride the parent's stdout tagged with the
|
|
151
|
+
* dispatch that spawned them, and they must never join the PARENT's chain — that splice produced a
|
|
152
|
+
* `promptText` interleaving several conversations, matching no request that was ever sent.
|
|
153
|
+
*
|
|
154
|
+
* Who RECORDS them is a separate question, decided once per run rather than per event:
|
|
155
|
+
* `watcherOwnsSubagents` says a `subagents/*.jsonl` watcher will run, and it is the better source
|
|
156
|
+
* (it reads the settled transcript, so its usage and stop reason are final). With no watcher — an
|
|
157
|
+
* `ambientAuth` run has no isolated config home to watch — the tagged turns are recorded here
|
|
158
|
+
* instead, on per-dispatch transcripts of their own. Dropping them in that case would leave the run
|
|
159
|
+
* billed by neither channel, and an under-count reads as a cheap run rather than as an error.
|
|
160
|
+
*/
|
|
161
|
+
export declare function createClaudeRunTelemetry(opts: ClaudeStreamTelemetryOptions & {
|
|
162
|
+
watcherOwnsSubagents: boolean;
|
|
163
|
+
}): ClaudeRunTelemetry;
|
|
164
|
+
export {};
|
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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:
|
|
115
|
-
messageCount:
|
|
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
|
-
|
|
235
|
+
transcript.append({ role: 'assistant', content: call.content });
|
|
127
236
|
for (const result of call.toolResults)
|
|
128
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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,252 @@
|
|
|
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
|
+
export declare function runCodingAgent(spec: CodingAgentSpec, opts?: RunOptions): Promise<CodingAgentOutcome>;
|
|
166
|
+
/**
|
|
167
|
+
* The Ralph-loop validation watchdog: the longest a completion command may run before it is
|
|
168
|
+
* killed and treated as a failure (a hung `pnpm test` must never block the loop forever).
|
|
169
|
+
* Overridable via env for tests; defaults to 15 minutes.
|
|
170
|
+
*/
|
|
171
|
+
export declare function ralphValidationTimeoutMs(): number;
|
|
172
|
+
/**
|
|
173
|
+
* How often the Ralph validation feeds the run's inactivity watchdog while its command runs.
|
|
174
|
+
* The command is exactly the activity-SILENT kind — a full `pnpm test`, a cold install-then-build
|
|
175
|
+
* — and the harness spawns it ITSELF rather than through the agent, so it emits no activity
|
|
176
|
+
* events of its own. `JOB_INACTIVITY_MS` (default 10 min) is TIGHTER than the command's own
|
|
177
|
+
* watchdog ({@link ralphValidationTimeoutMs}, default 15 min), so without this heartbeat any
|
|
178
|
+
* validation running past 10 minutes aborted the whole iteration as "inactivity" — mislabelling
|
|
179
|
+
* a healthy test suite as a wedge, and making the 15-minute watchdog unreachable at stock
|
|
180
|
+
* settings. The two sibling harness-run phases (pre-PR validation, reproduction proof) have
|
|
181
|
+
* always fed it; this one did not. Overridable via env for tests.
|
|
182
|
+
*/
|
|
183
|
+
export declare function ralphHeartbeatMs(): number;
|
|
184
|
+
/**
|
|
185
|
+
* Bound on the validation output tail that crosses the wire. Deliberately smaller than
|
|
186
|
+
* `MAX_CAPTURED_OUTPUT_CHARS` (`redact.ts`) and equal to the two sibling phases' budgets, for
|
|
187
|
+
* the same reason: this tail is persisted on the step (and on EVERY iteration of the attempt
|
|
188
|
+
* log) inside the run's `detail` JSON blob, which is re-serialized on every step-progress write.
|
|
189
|
+
*/
|
|
190
|
+
export declare const RALPH_VALIDATION_TAIL_CHARS = 4000;
|
|
191
|
+
/**
|
|
192
|
+
* Ralph loop: run the programmatic completion command in the checkout and return its exit
|
|
193
|
+
* code, a bounded + redacted tail of its output, and the work branch's HEAD it ran against.
|
|
194
|
+
* The EXIT CODE is the loop's authoritative done signal (0 = the criterion is met) — computed
|
|
195
|
+
* here by the harness, never self-reported by the model, which is the whole point of a
|
|
196
|
+
* programmatic exit condition. The command runs INSIDE the sandboxed run container (the same
|
|
197
|
+
* trust boundary as the coding agent) — there is no host/backend execution.
|
|
198
|
+
*
|
|
199
|
+
* The spawn itself goes through {@link runCapturedCommand}, the ONE seam for a harness-run
|
|
200
|
+
* command, rather than the near-verbatim copy this used to be. That copy had drifted in two
|
|
201
|
+
* ways the seam exists to prevent: it scrubbed secrets AFTER the rolling truncation with no
|
|
202
|
+
* margin (so a credential straddling the cut lost its `KEY=` prefix and survived redaction as
|
|
203
|
+
* an unrecognised partial, on a tail that reaches the step, the notification and the SPA), and
|
|
204
|
+
* it published the full 16k capture where both siblings deliberately bound the wire tail.
|
|
205
|
+
*
|
|
206
|
+
* `headSha` is what lets the engine tell a loop that is iterating from one that is merely
|
|
207
|
+
* repeating: two consecutive failing iterations against an unchanged head means the agent
|
|
208
|
+
* committed nothing, and the loop is ended early instead of spending the rest of its budget.
|
|
209
|
+
* Best-effort — a head that cannot be read is simply omitted, and the engine's check fails open.
|
|
210
|
+
*/
|
|
211
|
+
export declare function runRalphValidation(repoDir: string, cwd: string, validation: {
|
|
212
|
+
command: string;
|
|
213
|
+
iteration?: number;
|
|
214
|
+
}, logger: Logger, opts: RunOptions): Promise<{
|
|
215
|
+
validationPassed: boolean;
|
|
216
|
+
exitCode: number;
|
|
217
|
+
validationOutputTail?: string;
|
|
218
|
+
iteration?: number;
|
|
219
|
+
headSha?: string;
|
|
220
|
+
}>;
|
|
221
|
+
/** Sanitise an owner/name into a safe single path segment for a sibling checkout directory. */
|
|
222
|
+
export declare function safeDirSegment(value: string): string;
|
|
223
|
+
/**
|
|
224
|
+
* A sibling-directory allocator for a multi-repo run: returns the checkout directory name for a
|
|
225
|
+
* repo under the workspace root. Deterministic (`owner__name`) and collision-free by construction
|
|
226
|
+
* — the checkout set is deduped by `owner/name` upstream and GitHub owners contain no `_`, so the
|
|
227
|
+
* `owner__name` join is unique per repo without a stateful collision dance. Kept as a factory so
|
|
228
|
+
* the coding + read-only explore fan-outs share ONE scheme, and it MUST stay byte-identical to the
|
|
229
|
+
* backend's `siblingCheckoutDir` / `renderMultiRepoWorkspaceSection` in `@cat-factory/server`
|
|
230
|
+
* (jobBody.ts), which names this exact directory in the agent's prompt — the two are computed
|
|
231
|
+
* independently, so a divergent rule would point the agent at a directory that does not exist.
|
|
232
|
+
*/
|
|
233
|
+
export declare function makeDirClaimer(): (repo: Pick<RepoSpec, 'name' | 'owner'>) => string;
|
|
234
|
+
/**
|
|
235
|
+
* Multi-repo coding (service-connections phase 3): clone the primary repo AND every connected
|
|
236
|
+
* peer repo as SIBLING checkouts under one workspace root, run the agent ONCE with its cwd at
|
|
237
|
+
* that root (so it makes the cross-service change coherently across all of them), then commit +
|
|
238
|
+
* push each repo that actually changed and open one PR per dirty repo. The task's own-service PR
|
|
239
|
+
* is reported as `prUrl`/`branch`; the peer PRs as `peerPullRequests`.
|
|
240
|
+
*
|
|
241
|
+
* Deliberately simpler than the single-repo {@link runCodingAgent} for the first cut: NO mid-run
|
|
242
|
+
* checkpoint pushes (an evicted multi-repo run re-clones on retry — the deterministic work branch
|
|
243
|
+
* still lets it resume any commits it managed to push at the end), NO warm-pool persistent
|
|
244
|
+
* checkout (always ephemeral), and NO follow-up sentinel streaming. It reuses the SAME dir-scoped
|
|
245
|
+
* git helpers, so the per-repo clone/commit/push/PR mechanics match the single-repo path exactly.
|
|
246
|
+
*/
|
|
247
|
+
export declare function runMultiRepoCoding(job: AgentJob, opts?: RunOptions): Promise<AgentResult>;
|
|
248
|
+
/**
|
|
249
|
+
* The "no changes" reason both coding agents report: a caller-supplied lead phrase
|
|
250
|
+
* plus the shared "never acted" cause and a credential-scrubbed tail of Pi's stderr.
|
|
251
|
+
*/
|
|
252
|
+
export declare function noChangesReason(lead: string, stats: PiRunStats, stderrTail: string | undefined): string;
|