@cat-factory/executor-harness 1.106.0 → 1.110.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.
@@ -0,0 +1,379 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { makeDirClaimer } from './checkout-dir.js';
4
+ import { branchAheadOfBase, branchHasCommitsSince, cloneExistingBranch, cloneRepo, commitTrackedEdits, createBranch, excludeFromGit, fetchReferenceBranches, headCommit, listUntrackedFiles, pushBranch, refreshFromBaseIfClean, remoteBranchExists, } from './git.js';
5
+ import { openPullRequest } from './vcs-api.js';
6
+ import { applyPrDescription, PR_DESCRIPTION_FILE, readPrDescription } from './pr-description.js';
7
+ import { runAgentInWorkspace, withWorkspace } from './pi-workspace.js';
8
+ import { log } from './logger.js';
9
+ import { prepopulateDependencies, withDependencyNote } from './dependency-install.js';
10
+ import { resolvePrTemplateNote, withPrTemplateNote, } from './pr-template.js';
11
+ import { noChangesReason } from './coding-agent.js';
12
+ /**
13
+ * Multi-repo coding (service-connections phase 3): clone the primary repo AND every connected
14
+ * peer repo as SIBLING checkouts under one workspace root, run the agent ONCE with its cwd at
15
+ * that root (so it makes the cross-service change coherently across all of them), then commit +
16
+ * push each repo that actually changed and open one PR per dirty repo. The task's own-service PR
17
+ * is reported as `prUrl`/`branch`; the peer PRs as `peerPullRequests`.
18
+ *
19
+ * Deliberately simpler than the single-repo {@link runCodingAgent} for the first cut: NO mid-run
20
+ * checkpoint pushes (an evicted multi-repo run re-clones on retry — the deterministic work branch
21
+ * still lets it resume any commits it managed to push at the end), NO warm-pool persistent
22
+ * checkout (always ephemeral), and NO follow-up sentinel streaming. It reuses the SAME dir-scoped
23
+ * git helpers, so the per-repo clone/commit/push/PR mechanics match the single-repo path exactly.
24
+ */
25
+ export async function runMultiRepoCoding(job, opts = {}) {
26
+ const logger = (opts.log ?? log).child({ kind: 'multi-repo', jobId: job.jobId });
27
+ const peers = job.peerRepos ?? [];
28
+ const references = job.referenceRepos ?? [];
29
+ const primaryWorkBranch = job.pushBranch ?? job.newBranch ?? job.branch;
30
+ // Assign the sibling directory per repo via the shared deterministic allocator
31
+ // (`owner__name__digest`, matching the backend prompt's `siblingCheckoutDir`), shared with the
32
+ // read-only explore fan-out.
33
+ const claimDir = makeDirClaimer();
34
+ const legs = [
35
+ {
36
+ repo: job.repo,
37
+ dirName: claimDir(job.repo),
38
+ dir: '',
39
+ cloneBranch: job.branch,
40
+ workBranch: primaryWorkBranch,
41
+ ghToken: job.ghToken,
42
+ ...(job.pr ? { pr: job.pr } : {}),
43
+ primary: true,
44
+ baseSha: '',
45
+ resumed: false,
46
+ },
47
+ ...peers.map((peer) => ({
48
+ repo: peer.repo,
49
+ dirName: claimDir(peer.repo),
50
+ dir: '',
51
+ cloneBranch: peer.repo.baseBranch,
52
+ // Coding peers always carry `newBranch` (the backend sets the shared work branch);
53
+ // fall back to the primary's for the type (read-only peers never reach this path).
54
+ workBranch: peer.newBranch ?? primaryWorkBranch,
55
+ ghToken: peer.ghToken ?? job.ghToken,
56
+ ...(peer.pr ? { pr: peer.pr } : {}),
57
+ ...(peer.frameIds?.length ? { frameIds: peer.frameIds } : {}),
58
+ primary: false,
59
+ baseSha: '',
60
+ resumed: false,
61
+ })),
62
+ // Read-only reference repos (doc-writer): cloned as siblings the agent reads but never writes.
63
+ // `workBranch` is set to the base only to satisfy the type — a read-only leg never branches or
64
+ // pushes (guarded by `readOnly` in both the clone and push phases below).
65
+ ...references.map((reference) => ({
66
+ repo: reference.repo,
67
+ dirName: claimDir(reference.repo),
68
+ dir: '',
69
+ cloneBranch: reference.repo.baseBranch,
70
+ workBranch: reference.repo.baseBranch,
71
+ ghToken: reference.ghToken ?? job.ghToken,
72
+ primary: false,
73
+ readOnly: true,
74
+ baseSha: '',
75
+ resumed: false,
76
+ })),
77
+ ];
78
+ return withWorkspace('multi', async (root) => {
79
+ // Clone (or resume) every sibling checkout under the workspace root and fetch the primary's
80
+ // reference branches. Mutates each leg's `dir`/`resumed`/`baseSha` in place.
81
+ await prepareMultiRepoCheckouts(root, legs, job, logger, opts);
82
+ // DEPENDENCY PREPOPULATION for the PRIMARY leg, exactly as the read-only multi-repo fan-out
83
+ // does it. The install is declared on ONE service frame (the primary repo's), so it runs in
84
+ // that leg's checkout and is never fanned out across peers, whose own frames declare configs
85
+ // this dispatch never resolved — running a `pnpm install` inside a Go checkout is not a
86
+ // degraded outcome, it is a wrong one. A cross-repo implementer needs its dependencies for
87
+ // the same reason a cross-repo investigator does; the note names the sibling directory
88
+ // because the agent itself stands at the workspace root.
89
+ //
90
+ // At the leg's checkout ROOT, not a `serviceDirectory` subtree: this layout applies no
91
+ // service-directory scoping anywhere (the agent runs at the root and the prompt explains the
92
+ // sibling checkouts), and a root install is the one that resolves a monorepo workspace whole.
93
+ const primaryLeg = legs.find((leg) => leg.primary);
94
+ const dependencyNote = primaryLeg
95
+ ? await prepopulateDependencies({
96
+ spec: job.dependencyInstall,
97
+ installDir: primaryLeg.dir,
98
+ repoDir: primaryLeg.dir,
99
+ agentDir: root,
100
+ logger,
101
+ opts,
102
+ })
103
+ : undefined;
104
+ // THE REPOS' OWN PR TEMPLATES: one per leg that will actually open a pull request, each named
105
+ // by its sibling directory so the agent knows which checkout's briefing takes which shape —
106
+ // the repos in a workspace need not share a template, or ship one at all. A read-only
107
+ // reference leg is excluded by construction: it carries no `pr`, so nothing publishes for it.
108
+ const prTemplate = await resolvePrTemplateNote({
109
+ targets: legs
110
+ .filter((leg) => leg.pr)
111
+ .map((leg) => ({
112
+ repoDir: leg.dir,
113
+ repoLabel: leg.dirName,
114
+ ...(leg.repo.provider ? { provider: leg.repo.provider } : {}),
115
+ })),
116
+ logger,
117
+ });
118
+ // Run the agent ONCE with its cwd at the workspace root, so it sees every sibling checkout
119
+ // and can change them coherently. No monorepo/service-directory scoping — the multi-repo
120
+ // note + the backend system-prompt section explain the layout.
121
+ opts.onPhase?.('agent');
122
+ logger.info('multi-repo: running agent', { repos: legs.map((l) => l.dirName) });
123
+ const { summary, stats, stderrTail, usage, callMetrics, effortReport } = await runAgentInWorkspace({
124
+ dir: root,
125
+ systemPrompt: job.systemPrompt,
126
+ userPrompt: withDependencyNote(withPrTemplateNote(job.userPrompt, prTemplate.note), dependencyNote),
127
+ model: job.model,
128
+ harness: job.harness,
129
+ subscriptionToken: job.subscriptionToken,
130
+ subscriptionBaseUrl: job.subscriptionBaseUrl,
131
+ ambientAuth: job.ambientAuth,
132
+ proxyBaseUrl: job.proxyBaseUrl,
133
+ proxyPhasePath: job.proxyPhasePath,
134
+ sessionToken: job.sessionToken,
135
+ webToolsGuidance: job.webToolsGuidance,
136
+ webSearchProxy: job.webSearch,
137
+ guardLimits: job.guardLimits,
138
+ ...(job.contextFiles ? { contextFiles: job.contextFiles } : {}),
139
+ // Skills + tool servers apply to a multi-repo run exactly as to a single-repo one: they
140
+ // are properties of the AGENT KIND, not of the checkout layout.
141
+ ...(job.skills?.length ? { skills: job.skills } : {}),
142
+ ...(job.mcpServers?.length ? { mcpServers: job.mcpServers } : {}),
143
+ ...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
144
+ ...(job.designImages ? { designImages: job.designImages } : {}),
145
+ multiRepo: true,
146
+ }, opts);
147
+ // Commit forgotten tracked edits, then push + open a PR for each repo the run actually changed.
148
+ const { primaryPushed, primaryPrUrl, peerPullRequests } = await pushMultiRepoLegs(legs, job, logger, opts, root, prTemplate);
149
+ const anyWork = primaryPushed || peerPullRequests.length > 0;
150
+ if (!anyWork) {
151
+ // Nothing changed in ANY repo. For the implementer this is a failure (as in the
152
+ // single-repo path); a caller that tolerates a no-op (never the implementer today)
153
+ // gets a clean non-event.
154
+ if (job.noChangesIsError === false) {
155
+ return {
156
+ pushed: false,
157
+ branch: primaryWorkBranch,
158
+ summary,
159
+ stats,
160
+ ...(usage ? { usage } : {}),
161
+ ...(callMetrics ? { callMetrics } : {}),
162
+ ...(effortReport ? { effortReport } : {}),
163
+ };
164
+ }
165
+ return {
166
+ pushed: false,
167
+ branch: primaryWorkBranch,
168
+ summary,
169
+ stats,
170
+ error: noChangesReason('the agent produced no file changes in any repository', stats, stderrTail),
171
+ failureCause: 'no-changes',
172
+ ...(usage ? { usage } : {}),
173
+ ...(callMetrics ? { callMetrics } : {}),
174
+ ...(effortReport ? { effortReport } : {}),
175
+ };
176
+ }
177
+ logger.info('multi-repo: complete', {
178
+ primaryPushed,
179
+ primaryPrUrl: primaryPrUrl ?? null,
180
+ peers: peerPullRequests.length,
181
+ });
182
+ return {
183
+ pushed: primaryPushed,
184
+ ...(primaryPrUrl ? { prUrl: primaryPrUrl } : {}),
185
+ branch: primaryWorkBranch,
186
+ ...(peerPullRequests.length ? { peerPullRequests } : {}),
187
+ summary,
188
+ stats,
189
+ ...(usage ? { usage } : {}),
190
+ ...(callMetrics ? { callMetrics } : {}),
191
+ ...(effortReport ? { effortReport } : {}),
192
+ };
193
+ });
194
+ }
195
+ /**
196
+ * Clone phase for {@link runMultiRepoCoding}: every repo into its sibling dir under the workspace
197
+ * root. Resume an existing remote work branch (an evicted retry) rather than branching off base
198
+ * again, then fetch the primary repo's reference branches. Mutates each leg's `dir`/`resumed`/
199
+ * `baseSha` in place. Extracted so the multi-repo body stays small.
200
+ */
201
+ async function prepareMultiRepoCheckouts(root, legs, job, logger, opts) {
202
+ const { signal } = opts;
203
+ opts.onPhase?.('clone');
204
+ for (const leg of legs) {
205
+ const dir = join(root, leg.dirName);
206
+ await mkdir(dir, { recursive: true });
207
+ // A read-only reference leg: clone its base branch for the agent to read, and stop there —
208
+ // no work branch, no resume, no base-refresh. It is skipped in the push phase, so it can
209
+ // never be written to. (Kept in the loop so it lands in the same workspace root as siblings.)
210
+ if (leg.readOnly) {
211
+ logger.info('multi-repo: cloning read-only reference', {
212
+ repo: leg.dirName,
213
+ cloneBranch: leg.cloneBranch,
214
+ });
215
+ await cloneRepo({
216
+ repo: { ...leg.repo, baseBranch: leg.cloneBranch },
217
+ ghToken: leg.ghToken,
218
+ dir,
219
+ signal,
220
+ });
221
+ leg.dir = dir;
222
+ continue;
223
+ }
224
+ leg.resumed = await remoteBranchExists(leg.repo.cloneUrl, leg.workBranch, leg.ghToken, signal);
225
+ if (leg.resumed) {
226
+ logger.info('multi-repo: resuming existing branch', {
227
+ repo: leg.dirName,
228
+ branch: leg.workBranch,
229
+ });
230
+ await cloneExistingBranch({
231
+ cloneUrl: leg.repo.cloneUrl,
232
+ branch: leg.workBranch,
233
+ ghToken: leg.ghToken,
234
+ dir,
235
+ signal,
236
+ });
237
+ }
238
+ else {
239
+ logger.info('multi-repo: cloning', { repo: leg.dirName, cloneBranch: leg.cloneBranch });
240
+ await cloneRepo({
241
+ repo: { ...leg.repo, baseBranch: leg.cloneBranch },
242
+ ghToken: leg.ghToken,
243
+ dir,
244
+ signal,
245
+ });
246
+ await createBranch(dir, leg.workBranch, signal);
247
+ }
248
+ leg.dir = dir;
249
+ // Exclude the agent-authored PR-description sentinel locally (as the single-repo path does)
250
+ // so the agent's own `git add` can never stage the briefing into the PR it describes.
251
+ await excludeFromGit(dir, PR_DESCRIPTION_FILE, signal);
252
+ // The branch tip before the agent runs. Captured BEFORE the resume base refresh below so
253
+ // that refresh's merge commit counts as advancement and is pushed (as in the single-repo
254
+ // path). A fresh leg produced work iff its branch advances past this; a resumed leg already
255
+ // carries prior work.
256
+ leg.baseSha = await headCommit(dir, signal);
257
+ // A resumed branch was cut from an OLDER base; merge the latest base in when the two merge
258
+ // cleanly so the agent works against current base and the peer/own PRs stay current. On a
259
+ // conflict this is a best-effort no-op (the merge gate handles a conflicting PR downstream),
260
+ // mirroring the single-repo {@link runCodingAgent} resume refresh.
261
+ if (leg.resumed) {
262
+ const refreshed = await refreshFromBaseIfClean(dir, leg.cloneBranch, leg.ghToken, signal).catch(() => false);
263
+ if (!refreshed) {
264
+ logger.info('multi-repo: resume base refresh skipped (conflict or error)', {
265
+ repo: leg.dirName,
266
+ base: leg.cloneBranch,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ // Reference branches attach to the PRIMARY repo, so fetch them into the primary sibling
272
+ // checkout's `origin/<b>` refs (best-effort per branch). The backend's reference-branches
273
+ // prompt section names the primary repo's directory to run the read commands in.
274
+ if (job.referenceBranches?.length) {
275
+ const primaryLeg = legs.find((l) => l.primary);
276
+ if (primaryLeg?.dir) {
277
+ const fetched = await fetchReferenceBranches({
278
+ dir: primaryLeg.dir,
279
+ branches: job.referenceBranches,
280
+ ghToken: primaryLeg.ghToken,
281
+ signal,
282
+ onSkip: (branch, reason) => logger.warn('multi-repo: reference branch fetch skipped', { branch, reason }),
283
+ });
284
+ logger.info('multi-repo: fetched reference branches', {
285
+ requested: job.referenceBranches.length,
286
+ fetched: fetched.length,
287
+ });
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * Push phase for {@link runMultiRepoCoding}: commit forgotten tracked edits, then push + open a PR
293
+ * for each repo the run actually changed (a repo the agent left untouched is skipped — no branch,
294
+ * no PR; a read-only reference leg is never committed or pushed). Extracted so the multi-repo body
295
+ * stays small; returns the primary's push/PR state plus the peer PRs.
296
+ */
297
+ async function pushMultiRepoLegs(legs, job, logger, opts,
298
+ /** The workspace root the agent ran in — the fallback probe for the primary's briefing. */
299
+ root,
300
+ /** Which legs' briefings are filled templates — see the `titleFromHeading` read below. */
301
+ prTemplate) {
302
+ const { signal } = opts;
303
+ opts.onPhase?.('push');
304
+ let primaryPushed = false;
305
+ let primaryPrUrl;
306
+ const peerPullRequests = [];
307
+ for (const leg of legs) {
308
+ // A read-only reference leg is never committed or pushed — the third layer of the read-only
309
+ // guarantee (the spec carries no branch/PR, and the clone phase gave it no work branch).
310
+ if (leg.readOnly)
311
+ continue;
312
+ // Lift (and remove) the agent-authored PR description for THIS repo's PR before anything
313
+ // else touches the checkout — each sibling checkout carries its own briefing for its own PR.
314
+ // The agent's cwd here is the WORKSPACE ROOT rather than any one checkout, so an agent that
315
+ // read the prompt loosely may well have written a single briefing there instead. Fall back
316
+ // to it for the PRIMARY leg only: at the root there is nothing to say which repo it
317
+ // describes, and the primary is the one the run is actually about.
318
+ //
319
+ // Per-leg `titleFromHeading`: only a leg whose OWN repo ships a template has repo-authored
320
+ // headings in its sentinel, and the legs of a workspace need not agree about that — so this
321
+ // is keyed on the leg, never on whether the run found any template at all.
322
+ const readOptions = { titleFromHeading: !prTemplate.templated.has(leg.dir) };
323
+ const agentPrDescription = (await readPrDescription(leg.dir, readOptions)) ??
324
+ (leg.primary ? await readPrDescription(root, readOptions) : undefined);
325
+ await commitTrackedEdits(leg.dir, job.commitMessage ?? leg.pr?.title ?? 'Agent changes', signal);
326
+ const advanced = await branchHasCommitsSince(leg.dir, leg.baseSha, signal);
327
+ let hasWork = advanced || leg.resumed;
328
+ if (leg.resumed && !advanced) {
329
+ const ahead = await branchAheadOfBase(leg.dir, leg.repo.baseBranch, leg.ghToken, signal);
330
+ if (ahead === false)
331
+ hasWork = false;
332
+ }
333
+ const leftover = await listUntrackedFiles(leg.dir, signal);
334
+ if (leftover.length > 0) {
335
+ logger.warn('multi-repo: uncommitted new files left behind (not pushed)', {
336
+ repo: leg.dirName,
337
+ count: leftover.length,
338
+ files: leftover.slice(0, 20),
339
+ });
340
+ }
341
+ if (!hasWork) {
342
+ logger.info('multi-repo: no changes for repo', { repo: leg.dirName });
343
+ continue;
344
+ }
345
+ await pushBranch(leg.dir, leg.workBranch, leg.ghToken, signal);
346
+ let prUrl = null;
347
+ if (leg.pr) {
348
+ prUrl = await openPullRequest({
349
+ owner: leg.repo.owner,
350
+ name: leg.repo.name,
351
+ ghToken: leg.ghToken,
352
+ head: leg.workBranch,
353
+ base: leg.repo.baseBranch,
354
+ pr: applyPrDescription(leg.pr, agentPrDescription),
355
+ // See the single-repo call site: refresh a resumed leg's already-open PR, but only
356
+ // when the text is the agent's own briefing rather than the dispatch-time fallback.
357
+ ...(agentPrDescription ? { refreshExisting: true } : {}),
358
+ apiBase: job.githubApiBase,
359
+ cloneUrl: leg.repo.cloneUrl,
360
+ ...(leg.repo.provider ? { provider: leg.repo.provider } : {}),
361
+ signal,
362
+ });
363
+ }
364
+ if (leg.primary) {
365
+ primaryPushed = true;
366
+ if (prUrl)
367
+ primaryPrUrl = prUrl;
368
+ }
369
+ else if (prUrl) {
370
+ peerPullRequests.push({
371
+ repo: `${leg.repo.owner}/${leg.repo.name}`,
372
+ ...(leg.frameIds?.length ? { frameIds: leg.frameIds } : {}),
373
+ prUrl,
374
+ branch: leg.workBranch,
375
+ });
376
+ }
377
+ }
378
+ return { primaryPushed, primaryPrUrl, peerPullRequests };
379
+ }
@@ -1,4 +1,4 @@
1
- import type { RepoSpec, ReferenceScreenshotsSpec } from './job.js';
1
+ import type { RepoSpec, ImageManifestSpec } from './job.js';
2
2
  import type { McpServerSpec, SkillSpec } from './agent-capabilities.js';
3
3
  import { type ContextFileInfo, type PiRunOutcome } from './pi.js';
4
4
  import type { PiRunStats, RunDiagnostics } from './pi-reduction.js';
@@ -110,7 +110,14 @@ export interface AgentRunSpec {
110
110
  * before the run and named in the agent's prompt, so a capturing agent can compare against them
111
111
  * and use their view names. Absent ⇒ nothing is downloaded and nothing is said.
112
112
  */
113
- referenceScreenshots?: ReferenceScreenshotsSpec;
113
+ referenceScreenshots?: ImageManifestSpec;
114
+ /**
115
+ * The PICTURES of the task's designs. Downloaded into `.cat-context/design-renders/` before the
116
+ * run; the agent's prompt (composed by the backend) already names each file and its view, so the
117
+ * only thing said here is a CORRECTION when one of them did not land. Absent ⇒ nothing is
118
+ * downloaded and nothing is said.
119
+ */
120
+ designImages?: ImageManifestSpec;
114
121
  /**
115
122
  * The skills to make available for this run — a `skill` step's picked skill and/or the playbooks
116
123
  * the running agent kind declares. Installed HARNESS-AWARE: the claude-code runner writes them
@@ -1,7 +1,7 @@
1
1
  import { mkdir, mkdtemp, readdir, rm, stat } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- import { deliverReferenceScreenshots } from './reference-screenshots.js';
4
+ import { deliverJobImages } from './job-images.js';
5
5
  import { readEffortReport } from './effort.js';
6
6
  import { log } from './logger.js';
7
7
  import { CONTEXT_DIR, materializeContextFiles, materializeSkillResources, phasedProxyBaseUrl, runPi, webSearchConfigFromEnv, webSearchProxyEnv, writeAgentsContext, writePiModelsConfig, writeWebToolsConfig, } from './pi.js';
@@ -154,7 +154,7 @@ export async function runAgentInWorkspace(spec, opts = {}) {
154
154
  // cannot report a view an earlier round successfully delivered as absent. A view that MISSED is
155
155
  // retried, which is the behaviour worth having: the next round is a fresh chance at a blob
156
156
  // backend that was briefly down.
157
- const referenceGuidance = await deliverReferenceScreenshots(spec.dir, spec.referenceScreenshots, {
157
+ const imageGuidance = await deliverJobImages(spec, {
158
158
  ...(opts.signal ? { signal: opts.signal } : {}),
159
159
  log: opts.log ?? log,
160
160
  });
@@ -180,7 +180,7 @@ export async function runAgentInWorkspace(spec, opts = {}) {
180
180
  const subOutcome = await runSubscriptionHarness(spec.harness, {
181
181
  cwd: spec.dir,
182
182
  model: spec.model,
183
- systemPrompt: `${subscriptionSystemPrompt(spec.systemPrompt, contextFiles)}${referenceGuidance}`,
183
+ systemPrompt: `${subscriptionSystemPrompt(spec.systemPrompt, contextFiles)}${imageGuidance}`,
184
184
  userPrompt: spec.userPrompt,
185
185
  ...(spec.subscriptionToken ? { subscriptionToken: spec.subscriptionToken } : {}),
186
186
  subscriptionBaseUrl: spec.subscriptionBaseUrl,
@@ -251,7 +251,7 @@ export async function runAgentInWorkspace(spec, opts = {}) {
251
251
  serviceDirectory: spec.serviceDirectory,
252
252
  contextFiles,
253
253
  hasBlueprints,
254
- ...(referenceGuidance ? { referenceGuidance } : {}),
254
+ ...(imageGuidance ? { referenceGuidance: imageGuidance } : {}),
255
255
  ...(spec.multiRepo ? { multiRepo: true } : {}),
256
256
  });
257
257
  // Pi's calls are metered server-side by the LLM proxy, which sees only an HTTP request — so
@@ -1,47 +1,15 @@
1
- import type { ReferenceScreenshotsSpec } from './job.js';
1
+ import type { ImageManifestSpec } from './job.js';
2
2
  import type { Logger } from './logger.js';
3
+ import { type ContextImageOutcome } from './context-images.js';
3
4
  /** Subdirectory of {@link CONTEXT_DIR} the reference designs are written to. */
4
5
  export declare const REFERENCE_SCREENSHOT_SUBDIR = "reference-screenshots";
5
- /** What the pass has on disk, and what it does not. */
6
- export interface ReferenceScreenshotOutcome {
7
- written: {
8
- fileName: string;
9
- view: string;
10
- }[];
11
- /**
12
- * One entry per reference that is NOT on disk, with the cause stated in `reason`. Covers both
13
- * halves of that absence, because the agent's job is the same either way (capture the view under
14
- * its own name, with nothing to compare against): a transfer that failed, and a view the cap
15
- * dropped before this container was ever asked to fetch it.
16
- */
17
- missing: {
18
- view: string;
19
- reason: string;
20
- }[];
21
- /** Where the written files live, relative to the checkout root. */
22
- dir: string;
23
- }
24
6
  /** The relative directory the references are written to (what the prompt points the agent at). */
25
7
  export declare const REFERENCE_SCREENSHOT_DIR = ".cat-context/reference-screenshots";
26
- /**
27
- * Download the manifest's images into the checkout and report what landed.
28
- *
29
- * IDEMPOTENT, and that is load-bearing rather than an optimisation: an agent flow re-enters its
30
- * workspace once per repair round, so this pass runs several times over one checkout. A file
31
- * already on disk is counted and never re-fetched, which keeps a later round from spending the
32
- * budget again AND from reporting a view as absent that pass 1 successfully delivered. A view that
33
- * MISSED is retried, since the next round is a fresh chance at whatever was transiently down.
34
- *
35
- * Never throws: references are an aid to a comparison, not a precondition for running, so a
36
- * backend outage degrades a UI run to "name your own views" (the documented fallback) rather than
37
- * failing it. Every miss is carried out on {@link ReferenceScreenshotOutcome.missing} so the caller
38
- * can say so in the prompt, which is the difference between a design the platform failed to hand
39
- * over and one that has no such screen.
40
- */
41
- export declare function materializeReferenceScreenshots(cwd: string, spec: ReferenceScreenshotsSpec, options?: {
8
+ /** Download this job's capture references. See {@link materializeContextImages}. */
9
+ export declare function materializeReferenceScreenshots(cwd: string, spec: ImageManifestSpec, options?: {
42
10
  signal?: AbortSignal;
43
11
  fetchImpl?: typeof fetch;
44
- }): Promise<ReferenceScreenshotOutcome>;
12
+ }): Promise<ContextImageOutcome>;
45
13
  /**
46
14
  * The whole delivery in one call: download the manifest (when there is one) and answer the prompt
47
15
  * block naming what landed, reporting any miss to the operator on the way.
@@ -51,7 +19,7 @@ export declare function materializeReferenceScreenshots(cwd: string, spec: Refer
51
19
  * never arrives is otherwise invisible in the run's output: the gallery simply pairs against
52
20
  * nothing, months later, with no line anywhere saying why.
53
21
  */
54
- export declare function deliverReferenceScreenshots(cwd: string, spec: ReferenceScreenshotsSpec | undefined, options: {
22
+ export declare function deliverReferenceScreenshots(cwd: string, spec: ImageManifestSpec | undefined, options: {
55
23
  signal?: AbortSignal;
56
24
  log: Logger;
57
25
  fetchImpl?: typeof fetch;
@@ -68,4 +36,4 @@ export declare function deliverReferenceScreenshots(cwd: string, spec: Reference
68
36
  * the directory could not be created at all) sends the agent looking for a path that may not even
69
37
  * exist, and reads as a platform bug at exactly the moment the platform is already degraded.
70
38
  */
71
- export declare function referenceScreenshotGuidance(outcome: ReferenceScreenshotOutcome): string;
39
+ export declare function referenceScreenshotGuidance(outcome: ContextImageOutcome): string;