@cat-factory/executor-harness 1.104.0 → 1.108.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 +10 -0
- package/dist/agent-capabilities.js +1 -1
- package/dist/agent-shared.d.ts +3 -2
- package/dist/agent-shared.js +1 -0
- package/dist/coding-agent.d.ts +9 -2
- package/dist/coding-agent.js +2 -0
- package/dist/context-images.d.ts +38 -0
- package/dist/context-images.js +207 -0
- package/dist/context-manifests.d.ts +13 -11
- package/dist/context-manifests.js +21 -14
- package/dist/design-images.d.ts +40 -0
- package/dist/design-images.js +68 -0
- package/dist/job-images.d.ts +28 -0
- package/dist/job-images.js +21 -0
- package/dist/job.d.ts +16 -4
- package/dist/job.js +5 -3
- package/dist/pi-workspace.d.ts +9 -2
- package/dist/pi-workspace.js +4 -4
- package/dist/reference-screenshots.d.ts +7 -39
- package/dist/reference-screenshots.js +12 -198
- package/package.json +6 -6
- package/src/agent-capabilities.ts +1 -1
- package/src/agent-shared.ts +4 -8
- package/src/coding-agent.ts +11 -2
- package/src/context-images.ts +235 -0
- package/src/context-manifests.ts +26 -19
- package/src/design-images.ts +82 -0
- package/src/job-images.ts +43 -0
- package/src/job.ts +24 -8
- package/src/pi-workspace.ts +13 -6
- package/src/reference-screenshots.ts +17 -223
package/dist/job.d.ts
CHANGED
|
@@ -8,10 +8,10 @@ import { type ReproductionReport, type ReproductionSpec } from './reproduction-p
|
|
|
8
8
|
import { type DependencyInstallSpec } from './dependency-install.js';
|
|
9
9
|
import { type McpServerSpec, type SkillResourceSpec, type SkillSpec } from './agent-capabilities.js';
|
|
10
10
|
import { type TestSecretSpec } from './job-env.js';
|
|
11
|
-
import { type ContextFileSpec, type
|
|
11
|
+
import { type ContextFileSpec, type ImageFileSpec, type ImageManifestSpec } from './context-manifests.js';
|
|
12
12
|
export type { TestSecretSpec };
|
|
13
13
|
export type { McpServerSpec, SkillResourceSpec, SkillSpec };
|
|
14
|
-
export type { ContextFileSpec,
|
|
14
|
+
export type { ContextFileSpec, ImageFileSpec, ImageManifestSpec };
|
|
15
15
|
/**
|
|
16
16
|
* Per-job auth fields, shared across every job shape. The Pi harness carries the
|
|
17
17
|
* proxy base URL + a model-locked session token; the subscription harnesses
|
|
@@ -320,11 +320,23 @@ export interface AgentJob extends HarnessAuthFields {
|
|
|
320
320
|
contextFiles?: ContextFileSpec[];
|
|
321
321
|
/**
|
|
322
322
|
* The task's reference design images, downloaded into `.cat-context/reference-screenshots/`
|
|
323
|
-
* before the agent runs (see {@link
|
|
323
|
+
* before the agent runs (see {@link ImageManifestSpec}). Sent only for a kind that
|
|
324
324
|
* CAPTURES views and only when the task actually has references, so absent is the normal case
|
|
325
325
|
* and means the agent names its own views.
|
|
326
326
|
*/
|
|
327
|
-
referenceScreenshots?:
|
|
327
|
+
referenceScreenshots?: ImageManifestSpec;
|
|
328
|
+
/**
|
|
329
|
+
* The PICTURES of the task's designs, for a kind that builds or plans a screen. Downloaded into
|
|
330
|
+
* `.cat-context/design-renders/` before the run; the agent's prompt (composed by the backend)
|
|
331
|
+
* names each file and its view, and the agent opens them with its own image-reading tool.
|
|
332
|
+
*
|
|
333
|
+
* The same wire shape and the same download seam as {@link AgentJob.referenceScreenshots}, and a
|
|
334
|
+
* separate field with a separate directory because the two are opposite instructions: that one
|
|
335
|
+
* names the views to CAPTURE, this one is the design to BUILD. Sent only when the backend
|
|
336
|
+
* decided this harness can read an image at all, so absent is the normal case and means the run
|
|
337
|
+
* works from the textual design description (its prompt says which).
|
|
338
|
+
*/
|
|
339
|
+
designImages?: ImageManifestSpec;
|
|
328
340
|
/**
|
|
329
341
|
* Private package-registry auth (npm private orgs, GitHub Packages), rendered into
|
|
330
342
|
* `~/.npmrc` before the run so the checkout's installs — the agent's own and the
|
package/dist/job.js
CHANGED
|
@@ -3,7 +3,7 @@ import { parseReproductionSpec, } from './reproduction-proof.js';
|
|
|
3
3
|
import { parseDependencyInstallSpec } from './dependency-install.js';
|
|
4
4
|
import { parseMcpServerSpecs, parseSkillSpecs, } from './agent-capabilities.js';
|
|
5
5
|
import { parseInfraEnv, parseSecretEnvPairs, str } from './job-env.js';
|
|
6
|
-
import { parseContextFiles,
|
|
6
|
+
import { parseContextFiles, parseImageManifest, } from './context-manifests.js';
|
|
7
7
|
/** A positive finite integer, or undefined for any other input (silently ignored). */
|
|
8
8
|
function posInt(value) {
|
|
9
9
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
@@ -478,7 +478,8 @@ export function parseAgentJob(input) {
|
|
|
478
478
|
referenceBranches: parseReferenceBranches(o.referenceBranches),
|
|
479
479
|
bootstrap: parseAgentBootstrapSpec(o.bootstrap),
|
|
480
480
|
contextFiles: parseContextFiles(o.contextFiles),
|
|
481
|
-
referenceScreenshots:
|
|
481
|
+
referenceScreenshots: parseImageManifest(o.referenceScreenshots),
|
|
482
|
+
designImages: parseImageManifest(o.designImages),
|
|
482
483
|
packageRegistries: parsePackageRegistries(o.packageRegistries),
|
|
483
484
|
skills: parseSkillSpecs(o.skills),
|
|
484
485
|
mcpServers: parseMcpServerSpecs(o.mcpServers),
|
|
@@ -545,7 +546,7 @@ function parseAgentPrSpec(raw) {
|
|
|
545
546
|
* literal doesn't blow the complexity budget; behaviour is byte-identical (spread order preserved).
|
|
546
547
|
*/
|
|
547
548
|
function assembleAgentJob(o, mode, agentField, parts) {
|
|
548
|
-
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, referenceScreenshots, packageRegistries, skills, mcpServers, testSecrets, guardLimits, validation, validationChecks, reproduction, dependencyInstall, reviewPrNumber, generatorSecrets, } = parts;
|
|
549
|
+
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, referenceScreenshots, designImages, packageRegistries, skills, mcpServers, testSecrets, guardLimits, validation, validationChecks, reproduction, dependencyInstall, reviewPrNumber, generatorSecrets, } = parts;
|
|
549
550
|
const repo = (o.repo ?? {});
|
|
550
551
|
return {
|
|
551
552
|
jobId: str(o.jobId, 'jobId'),
|
|
@@ -562,6 +563,7 @@ function assembleAgentJob(o, mode, agentField, parts) {
|
|
|
562
563
|
...(output ? { output } : {}),
|
|
563
564
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
564
565
|
...(referenceScreenshots ? { referenceScreenshots } : {}),
|
|
566
|
+
...(designImages ? { designImages } : {}),
|
|
565
567
|
...(packageRegistries.length ? { packageRegistries } : {}),
|
|
566
568
|
...(skills ? { skills } : {}),
|
|
567
569
|
...(mcpServers ? { mcpServers } : {}),
|
package/dist/pi-workspace.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RepoSpec,
|
|
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?:
|
|
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
|
package/dist/pi-workspace.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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)}${
|
|
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
|
-
...(
|
|
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 {
|
|
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
|
-
|
|
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<
|
|
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:
|
|
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:
|
|
39
|
+
export declare function referenceScreenshotGuidance(outcome: ContextImageOutcome): string;
|
|
@@ -1,130 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { CONTEXT_DIR, excludeContextDir } from './pi.js';
|
|
1
|
+
import { CONTEXT_DIR } from './pi.js';
|
|
2
|
+
import { materializeContextImages } from './context-images.js';
|
|
4
3
|
// ---------------------------------------------------------------------------
|
|
5
|
-
// REFERENCE DESIGNS on disk:
|
|
6
|
-
// `.cat-context/reference-screenshots
|
|
7
|
-
// nothing wrote.
|
|
4
|
+
// REFERENCE DESIGNS on disk for a CAPTURING kind: the images a UI tester compares its own
|
|
5
|
+
// screenshots against, in `.cat-context/reference-screenshots/` — the directory the UI-tester
|
|
6
|
+
// prompt has always named and nothing wrote.
|
|
8
7
|
//
|
|
9
|
-
// The
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// look identical on disk.
|
|
8
|
+
// The transfer itself lives in `context-images.ts`, shared with the design-picture delivery. What
|
|
9
|
+
// stays here is what makes this manifest a CAPTURE instruction: the directory, and the prompt block
|
|
10
|
+
// telling the tester to name each screenshot after the view it was handed, including the views it
|
|
11
|
+
// was handed no image for.
|
|
14
12
|
// ---------------------------------------------------------------------------
|
|
15
13
|
/** Subdirectory of {@link CONTEXT_DIR} the reference designs are written to. */
|
|
16
14
|
export const REFERENCE_SCREENSHOT_SUBDIR = 'reference-screenshots';
|
|
17
|
-
/** Per-image ceiling, matching the platform's own upload ceiling (16 MiB). */
|
|
18
|
-
const MAX_REFERENCE_BYTES = 16 * 1024 * 1024;
|
|
19
|
-
/** Per-image request timeout. */
|
|
20
|
-
const REQUEST_TIMEOUT_MS = 20_000;
|
|
21
|
-
/**
|
|
22
|
-
* Wall-clock ceiling on the WHOLE pass.
|
|
23
|
-
*
|
|
24
|
-
* Downloading is activity-silent from the watchdog's point of view (no agent stream, no output),
|
|
25
|
-
* and `JOB_INACTIVITY_MS` (10 min) is what kills a job that stops producing. Rather than heartbeat
|
|
26
|
-
* a transfer that should take seconds, the pass is bounded far below that: a slow or wedged blob
|
|
27
|
-
* backend costs the run its references (stated to the agent) instead of costing it the run.
|
|
28
|
-
*/
|
|
29
|
-
const TOTAL_BUDGET_MS = 90_000;
|
|
30
|
-
/** How many images are fetched at once. Small on purpose: this is a shared blob backend. */
|
|
31
|
-
const CONCURRENCY = 4;
|
|
32
|
-
/** The cause reported for a view the backend resolved but never sent this job a file for. */
|
|
33
|
-
const OMITTED_REASON = 'not sent to this container (reference limit)';
|
|
34
15
|
/** The relative directory the references are written to (what the prompt points the agent at). */
|
|
35
16
|
export const REFERENCE_SCREENSHOT_DIR = `${CONTEXT_DIR}/${REFERENCE_SCREENSHOT_SUBDIR}`;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* IDEMPOTENT, and that is load-bearing rather than an optimisation: an agent flow re-enters its
|
|
40
|
-
* workspace once per repair round, so this pass runs several times over one checkout. A file
|
|
41
|
-
* already on disk is counted and never re-fetched, which keeps a later round from spending the
|
|
42
|
-
* budget again AND from reporting a view as absent that pass 1 successfully delivered. A view that
|
|
43
|
-
* MISSED is retried, since the next round is a fresh chance at whatever was transiently down.
|
|
44
|
-
*
|
|
45
|
-
* Never throws: references are an aid to a comparison, not a precondition for running, so a
|
|
46
|
-
* backend outage degrades a UI run to "name your own views" (the documented fallback) rather than
|
|
47
|
-
* failing it. Every miss is carried out on {@link ReferenceScreenshotOutcome.missing} so the caller
|
|
48
|
-
* can say so in the prompt, which is the difference between a design the platform failed to hand
|
|
49
|
-
* over and one that has no such screen.
|
|
50
|
-
*/
|
|
51
|
-
export async function materializeReferenceScreenshots(cwd, spec, options = {}) {
|
|
52
|
-
const dir = join(cwd, CONTEXT_DIR, REFERENCE_SCREENSHOT_SUBDIR);
|
|
53
|
-
const outcome = {
|
|
54
|
-
written: [],
|
|
55
|
-
// The backend's own dropped views are missing before a single byte is fetched, and for a cause
|
|
56
|
-
// no transfer could have changed.
|
|
57
|
-
missing: spec.omitted.map((view) => ({ view, reason: OMITTED_REASON })),
|
|
58
|
-
dir: REFERENCE_SCREENSHOT_DIR,
|
|
59
|
-
};
|
|
60
|
-
try {
|
|
61
|
-
await mkdir(dir, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
// Nowhere to write: report every reference as missed rather than half of them, since none of
|
|
65
|
-
// them can land and the cause is the same for all.
|
|
66
|
-
for (const file of spec.files)
|
|
67
|
-
outcome.missing.push({ view: file.view, reason: describe(error) });
|
|
68
|
-
return sortByManifest(outcome, spec);
|
|
69
|
-
}
|
|
70
|
-
const deadline = Date.now() + TOTAL_BUDGET_MS;
|
|
71
|
-
const queue = [...spec.files];
|
|
72
|
-
const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
|
|
73
|
-
for (;;) {
|
|
74
|
-
const file = queue.shift();
|
|
75
|
-
if (!file)
|
|
76
|
-
return;
|
|
77
|
-
// An earlier pass over this same checkout already delivered it. Checked before the budget so
|
|
78
|
-
// a fully-delivered set costs one stat per file and no network at all, however long an
|
|
79
|
-
// earlier round took.
|
|
80
|
-
if (await alreadyOnDisk(dir, file.fileName)) {
|
|
81
|
-
outcome.written.push({ fileName: file.fileName, view: file.view });
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (Date.now() >= deadline) {
|
|
85
|
-
outcome.missing.push({ view: file.view, reason: 'reference download budget exhausted' });
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const failure = await downloadOne(dir, spec, file, options);
|
|
89
|
-
if (failure)
|
|
90
|
-
outcome.missing.push({ view: file.view, reason: failure });
|
|
91
|
-
else
|
|
92
|
-
outcome.written.push({ fileName: file.fileName, view: file.view });
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
await Promise.all(workers);
|
|
96
|
-
// Even a partial set must not reach the agent's PR (same rule as every other context file).
|
|
97
|
-
await excludeContextDir(cwd);
|
|
98
|
-
return sortByManifest(outcome, spec);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Order both lists the way the BACKEND composed the set (its own gallery order) rather than the
|
|
102
|
-
* order the transfers happened to finish in, so the list the agent reads is stable across rounds.
|
|
103
|
-
* The dropped views trail the sent ones, having no position in the manifest to sort by.
|
|
104
|
-
*/
|
|
105
|
-
function sortByManifest(outcome, spec) {
|
|
106
|
-
const rank = new Map(spec.files.map((file, index) => [file.view, index]));
|
|
107
|
-
const at = (view) => rank.get(view) ?? Number.MAX_SAFE_INTEGER;
|
|
108
|
-
outcome.written.sort((a, b) => at(a.view) - at(b.view));
|
|
109
|
-
outcome.missing.sort((a, b) => at(a.view) - at(b.view));
|
|
110
|
-
return outcome;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Whether a previous pass over this checkout already wrote this reference.
|
|
114
|
-
*
|
|
115
|
-
* Non-empty is the test, not mere existence: a zero-length file is what a half-written transfer
|
|
116
|
-
* leaves behind, and treating it as delivered would hand the agent a blank image it reads as a
|
|
117
|
-
* design with nothing on the screen (the same case {@link downloadOne} refuses to write).
|
|
118
|
-
*/
|
|
119
|
-
async function alreadyOnDisk(dir, fileName) {
|
|
120
|
-
try {
|
|
121
|
-
return (await stat(join(dir, fileName))).size > 0;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// silent-catch-ok: absence is the ordinary answer here (first pass over the checkout), and any
|
|
125
|
-
// other stat failure is answered the same way — by attempting the download.
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
17
|
+
/** Download this job's capture references. See {@link materializeContextImages}. */
|
|
18
|
+
export function materializeReferenceScreenshots(cwd, spec, options = {}) {
|
|
19
|
+
return materializeContextImages(cwd, REFERENCE_SCREENSHOT_SUBDIR, spec, options);
|
|
128
20
|
}
|
|
129
21
|
/**
|
|
130
22
|
* The whole delivery in one call: download the manifest (when there is one) and answer the prompt
|
|
@@ -148,84 +40,6 @@ export async function deliverReferenceScreenshots(cwd, spec, options) {
|
|
|
148
40
|
}
|
|
149
41
|
return referenceScreenshotGuidance(outcome);
|
|
150
42
|
}
|
|
151
|
-
/** Fetch and write one reference, answering a failure reason or undefined on success. */
|
|
152
|
-
async function downloadOne(dir, spec, file, options) {
|
|
153
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
154
|
-
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
155
|
-
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
156
|
-
try {
|
|
157
|
-
const response = await fetchImpl(`${spec.url}/${encodeURIComponent(file.artifactId)}`, {
|
|
158
|
-
headers: { authorization: `Bearer ${spec.token}` },
|
|
159
|
-
signal,
|
|
160
|
-
});
|
|
161
|
-
if (!response.ok)
|
|
162
|
-
return `HTTP ${response.status}`;
|
|
163
|
-
const bytes = await readBounded(response, MAX_REFERENCE_BYTES);
|
|
164
|
-
if (bytes === 'too-large')
|
|
165
|
-
return 'reference exceeds size limit';
|
|
166
|
-
// A zero-length body is a miss, not a file: written out it would be an image the agent opens,
|
|
167
|
-
// finds empty, and reads as a design with nothing on the screen.
|
|
168
|
-
if (!bytes.byteLength)
|
|
169
|
-
return 'empty response';
|
|
170
|
-
await writeFile(join(dir, file.fileName), bytes);
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
return describe(error);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Read a response body, refusing one that goes past `limit` WITHOUT buffering all of it first.
|
|
179
|
-
*
|
|
180
|
-
* The ceiling has to bound the transfer and not just the write. Buffering the whole body and then
|
|
181
|
-
* measuring it means an oversized (or endless) response is already resident, times the pass's
|
|
182
|
-
* concurrency, by the time it is rejected — which is the container's memory, in a run whose whole
|
|
183
|
-
* point is that it has not started working yet. So the declared length is refused up front where
|
|
184
|
-
* it is honest, and the stream is counted as it arrives and cancelled the moment it crosses the
|
|
185
|
-
* line, which is what makes a chunked or lying body cost no more than a truthful one.
|
|
186
|
-
*/
|
|
187
|
-
async function readBounded(response, limit) {
|
|
188
|
-
const declared = Number(response.headers.get('content-length'));
|
|
189
|
-
if (Number.isFinite(declared) && declared > limit)
|
|
190
|
-
return 'too-large';
|
|
191
|
-
const body = response.body;
|
|
192
|
-
if (!body) {
|
|
193
|
-
// No stream to count (a mocked or already-buffered response): fall back to measuring after the
|
|
194
|
-
// fact, which is sound because there is nothing left to stop arriving.
|
|
195
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
196
|
-
return bytes.byteLength > limit ? 'too-large' : bytes;
|
|
197
|
-
}
|
|
198
|
-
const reader = body.getReader();
|
|
199
|
-
const chunks = [];
|
|
200
|
-
let total = 0;
|
|
201
|
-
try {
|
|
202
|
-
for (;;) {
|
|
203
|
-
const { done, value } = await reader.read();
|
|
204
|
-
if (done)
|
|
205
|
-
break;
|
|
206
|
-
total += value.byteLength;
|
|
207
|
-
if (total > limit) {
|
|
208
|
-
await reader.cancel();
|
|
209
|
-
return 'too-large';
|
|
210
|
-
}
|
|
211
|
-
chunks.push(value);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
finally {
|
|
215
|
-
reader.releaseLock();
|
|
216
|
-
}
|
|
217
|
-
const bytes = new Uint8Array(total);
|
|
218
|
-
let offset = 0;
|
|
219
|
-
for (const chunk of chunks) {
|
|
220
|
-
bytes.set(chunk, offset);
|
|
221
|
-
offset += chunk.byteLength;
|
|
222
|
-
}
|
|
223
|
-
return bytes;
|
|
224
|
-
}
|
|
225
|
-
/** A one-line cause for a failed transfer (never the token, which only rides a header). */
|
|
226
|
-
function describe(error) {
|
|
227
|
-
return error instanceof Error ? error.message : String(error);
|
|
228
|
-
}
|
|
229
43
|
/**
|
|
230
44
|
* The prompt block naming what the agent was handed, or '' when the pass produced nothing at all.
|
|
231
45
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.108.0",
|
|
4
4
|
"description": "Container payload: a thin TypeScript wrapper that runs the Pi coding agent against a cloned repo and opens a PR. Runs in the Cloudflare Container (and, in local native mode, as a host process); carries no secrets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@hono/node-server": "^2.1.0",
|
|
29
|
-
"@types/node": "^26.
|
|
30
|
-
"hono": "^4.13.
|
|
29
|
+
"@types/node": "^26.2.0",
|
|
30
|
+
"hono": "^4.13.1",
|
|
31
31
|
"typescript": "7.0.2",
|
|
32
32
|
"vitest": "^4.1.10",
|
|
33
|
-
"@cat-factory/kernel": "0.
|
|
34
|
-
"@cat-factory/server": "0.
|
|
35
|
-
"@cat-factory/spend": "0.15.
|
|
33
|
+
"@cat-factory/kernel": "0.284.0",
|
|
34
|
+
"@cat-factory/server": "0.267.0",
|
|
35
|
+
"@cat-factory/spend": "0.15.63"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -p tsconfig.json",
|
|
@@ -343,7 +343,7 @@ export function parseSkillSpecs(value: unknown): SkillSpec[] | undefined {
|
|
|
343
343
|
* A member is added here in the SAME change that teaches the parser the field, never ahead of it:
|
|
344
344
|
* the whole value of the list is that it is the image's own honest answer.
|
|
345
345
|
*/
|
|
346
|
-
export const HARNESS_BODY_CAPABILITIES: readonly string[] = ['mcpServers', 'skills']
|
|
346
|
+
export const HARNESS_BODY_CAPABILITIES: readonly string[] = ['mcpServers', 'skills', 'designImages']
|
|
347
347
|
|
|
348
348
|
/**
|
|
349
349
|
* A safe MCP server id: it becomes a tool-name fragment AND a TOML table key.
|
package/src/agent-shared.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentJob,
|
|
3
|
-
AgentResult,
|
|
4
|
-
McpServerSpec,
|
|
5
|
-
ReferenceScreenshotsSpec,
|
|
6
|
-
SkillSpec,
|
|
7
|
-
} from './job.js'
|
|
1
|
+
import type { AgentJob, AgentResult, McpServerSpec, ImageManifestSpec, SkillSpec } from './job.js'
|
|
8
2
|
import type { EffortReport } from './effort.js'
|
|
9
3
|
|
|
10
4
|
// Small helpers shared by every agent MODE (explore / coding / bootstrap / preview). They live
|
|
@@ -33,11 +27,13 @@ export function mergeEffort(
|
|
|
33
27
|
export function agentCapabilities(job: AgentJob): {
|
|
34
28
|
skills?: SkillSpec[]
|
|
35
29
|
mcpServers?: McpServerSpec[]
|
|
36
|
-
referenceScreenshots?:
|
|
30
|
+
referenceScreenshots?: ImageManifestSpec
|
|
31
|
+
designImages?: ImageManifestSpec
|
|
37
32
|
} {
|
|
38
33
|
return {
|
|
39
34
|
...(job.skills?.length ? { skills: job.skills } : {}),
|
|
40
35
|
...(job.mcpServers?.length ? { mcpServers: job.mcpServers } : {}),
|
|
41
36
|
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
37
|
+
...(job.designImages ? { designImages: job.designImages } : {}),
|
|
42
38
|
}
|
|
43
39
|
}
|
package/src/coding-agent.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
HarnessAuthFields,
|
|
9
9
|
PeerRepoSpec,
|
|
10
10
|
ReferenceRepoSpec,
|
|
11
|
-
|
|
11
|
+
ImageManifestSpec,
|
|
12
12
|
RepoSpec,
|
|
13
13
|
SkillSpec,
|
|
14
14
|
McpServerSpec,
|
|
@@ -188,7 +188,14 @@ export interface CodingAgentSpec extends HarnessAuthFields {
|
|
|
188
188
|
* UI-facing kind may well be a coding one, and nothing here switches on which built-in it is.
|
|
189
189
|
* Absent ⇒ none (the normal case).
|
|
190
190
|
*/
|
|
191
|
-
referenceScreenshots?:
|
|
191
|
+
referenceScreenshots?: ImageManifestSpec
|
|
192
|
+
/**
|
|
193
|
+
* The PICTURES of the task's designs, downloaded into `.cat-context/design-renders/` before the
|
|
194
|
+
* agent's first turn. Carried here for the same reason the capture set is: what earns a run its
|
|
195
|
+
* pictures is the KIND's declared trait plus a harness that can read an image, and a coding kind
|
|
196
|
+
* is the commonest holder of both. Absent ⇒ none (the normal case).
|
|
197
|
+
*/
|
|
198
|
+
designImages?: ImageManifestSpec
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
/** The outcome of a coding agent run, before each caller maps it to its own result shape. */
|
|
@@ -470,6 +477,7 @@ export async function runCodingAgent(
|
|
|
470
477
|
...(spec.referenceScreenshots
|
|
471
478
|
? { referenceScreenshots: spec.referenceScreenshots }
|
|
472
479
|
: {}),
|
|
480
|
+
...(spec.designImages ? { designImages: spec.designImages } : {}),
|
|
473
481
|
},
|
|
474
482
|
opts,
|
|
475
483
|
)
|
|
@@ -1203,6 +1211,7 @@ export async function runMultiRepoCoding(
|
|
|
1203
1211
|
...(job.skills?.length ? { skills: job.skills } : {}),
|
|
1204
1212
|
...(job.mcpServers?.length ? { mcpServers: job.mcpServers } : {}),
|
|
1205
1213
|
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
1214
|
+
...(job.designImages ? { designImages: job.designImages } : {}),
|
|
1206
1215
|
multiRepo: true,
|
|
1207
1216
|
},
|
|
1208
1217
|
opts,
|