@cat-factory/executor-harness 1.100.0 → 1.104.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 +27 -0
- package/dist/agent-shared.d.ts +7 -5
- package/dist/agent-shared.js +6 -4
- package/dist/agent.js +2 -1
- package/dist/checkout-dir.d.ts +33 -0
- package/dist/checkout-dir.js +56 -0
- package/dist/coding-agent.d.ts +9 -14
- package/dist/coding-agent.js +8 -19
- package/dist/context-manifests.d.ts +69 -0
- package/dist/context-manifests.js +110 -0
- package/dist/job.d.ts +9 -12
- package/dist/job.js +4 -40
- package/dist/pi-workspace.d.ts +7 -1
- package/dist/pi-workspace.js +18 -1
- package/dist/pi.d.ts +20 -0
- package/dist/pi.js +21 -13
- package/dist/reference-screenshots.d.ts +71 -0
- package/dist/reference-screenshots.js +261 -0
- package/package.json +4 -4
- package/src/agent-shared.ts +14 -5
- package/src/agent.ts +2 -6
- package/src/checkout-dir.ts +62 -0
- package/src/coding-agent.ts +17 -21
- package/src/context-manifests.ts +156 -0
- package/src/job.ts +22 -48
- package/src/pi-workspace.ts +25 -2
- package/src/pi.ts +29 -11
- package/src/reference-screenshots.ts +295 -0
package/README.md
CHANGED
|
@@ -115,6 +115,31 @@ Bootstrap differs at the ends: it may start from an empty dir, and **resets
|
|
|
115
115
|
history to one commit and force-pushes** the default branch instead of opening a
|
|
116
116
|
PR. Blueprint **commits onto a branch** (no history reset) and returns the tree.
|
|
117
117
|
|
|
118
|
+
### Reference designs
|
|
119
|
+
|
|
120
|
+
A job body for a kind that CAPTURES views (the UI tester, or a deployment's own browser-driven kind)
|
|
121
|
+
may carry `referenceScreenshots`: the reference images the platform holds for the task, as
|
|
122
|
+
`{ url, token, files: [{ artifactId, fileName, view }], omitted: [view] }`. The harness downloads
|
|
123
|
+
each into `.cat-context/reference-screenshots/` before the agent's first turn and lists them, by view
|
|
124
|
+
name, at the end of the agent's context.
|
|
125
|
+
|
|
126
|
+
Only identities travel in the body (a design frame is a full-page PNG), and the bytes come back
|
|
127
|
+
from the backend over the SAME container session token the run already holds for the LLM proxy, so
|
|
128
|
+
this needs no extra credential. The FILE NAMES are the backend's, never derived here: the name is
|
|
129
|
+
how the agent learns the view name, and the platform pairs its capture against that name later.
|
|
130
|
+
|
|
131
|
+
`omitted` carries the views the backend's cap dropped. They are stated to the agent beside the
|
|
132
|
+
transfers that failed, since from where it stands both are a view to capture with nothing to compare
|
|
133
|
+
against. This parser keeps a higher backstop of its own against a body claiming more files than any
|
|
134
|
+
real set has, and an entry past it joins `omitted` rather than vanishing: a cap that shortened the
|
|
135
|
+
list and said nothing would be indistinguishable, on disk and in the prompt, from a design that has
|
|
136
|
+
no such screen.
|
|
137
|
+
|
|
138
|
+
The pass is IDEMPOTENT over the checkout, which matters because a coding flow re-enters its
|
|
139
|
+
workspace once per repair round: a non-empty file already on disk is counted and never re-fetched,
|
|
140
|
+
and only a view that MISSED is retried. The per-image ceiling is enforced against the declared
|
|
141
|
+
length and against the stream as it arrives, so an oversized body is refused rather than buffered.
|
|
142
|
+
|
|
118
143
|
### Skills and tool servers
|
|
119
144
|
|
|
120
145
|
A job body may carry `skills[]` (procedural playbooks) and `mcpServers[]` (MCP tool servers): the
|
|
@@ -213,6 +238,7 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
213
238
|
| `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking, and the three per-job watchdogs (max-duration, inactivity, tool-silence). |
|
|
214
239
|
| `src/jsonl-stream.ts` | The BOUNDS on a child CLI's streams, shared by both runners: `JsonlLineReader` frames its JSONL stdout while refusing to buffer a runaway record, `BoundedTail` keeps a capped tail of raw output for failure quoting. Both watchdog timers and the poll endpoints share one event loop with this parsing, so an unbounded buffer here is how a container stops answering polls with no watchdog having fired. |
|
|
215
240
|
| `src/job.ts` | Request types + validators for the job specs. |
|
|
241
|
+
| `src/context-manifests.ts` | The two manifests of FILES the backend stages into the checkout: the linked-context documents and the reference design images. Their shapes plus the defensive parse of each, sharing the basename rule that keeps a body-supplied name from escaping the directory or clobbering a repo file. Both stay job body fields, so `job.ts` remains the import site. |
|
|
216
242
|
| `src/pi.ts` | Pi provider config, non-interactive run, JSON-line event + todo-progress parsing, global `AGENTS.md` guidance. |
|
|
217
243
|
| `src/pi-reduction.ts` | Reducing a Pi event stream to what the run PRODUCED (summary, stats, diagnostics, terminal failure), FOLDED as records stream rather than over a retained array — memory is O(largest record), not O(records). The array-taking entry points offline tooling uses are defined in terms of the same reducer. |
|
|
218
244
|
| `src/tool-silence.ts` | The tool-silence watchdog (F13) and the `ToolProgressWindow` an agent stream opens, beats and closes. Separate from the phase marker on purpose: a window is only meaningful while something able to reset it is running. |
|
|
@@ -229,6 +255,7 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
229
255
|
| `src/validation-checks.ts` | Pre-PR validation: runs the job's check commands in the checkout (bounded, secret-scrubbed capture, per-command watchdog) and drives the retry-until-green loop that gates the PR. Generic: keyed off the job body, never the agent kind. |
|
|
230
256
|
| `src/reproduction-proof.ts` | Bugfix reproduction proof: runs the job's declared reproduction command against two symmetric fresh worktrees (the pre-fix tree and the final tree) and computes red-then-green from the exit codes, with a repair loop that never fails the run. Generic: keyed off the job body, never the agent kind. |
|
|
231
257
|
| `src/agent-capabilities.ts` | The agent CAPABILITIES a job body carries: the run's `skills` (a `SKILL.md` payload + resources) and its `mcpServers` (tool servers): with their defensive parsing and the per-CLI config writers (`--mcp-config` JSON for claude-code, `[mcp_servers.*]` TOML for Codex). Backend-authored data the harness only MATERIALISES: adding a skill or a tool server is a backend registration, never a harness change. |
|
|
258
|
+
| `src/reference-screenshots.ts` | The task's REFERENCE DESIGN images: downloads the manifest a capturing job body carries into `.cat-context/reference-screenshots/` (on the run's own container session token) and composes the prompt block naming each file's view. Best-effort, time-bounded and IDEMPOTENT over the checkout, so a repair round re-costs a stat rather than a transfer. A reference that is not on disk is NAMED to the agent, whether a transfer failed or the backend's cap dropped the view, because on disk an absent file and a screen the design does not have are the same thing. Backend-authored throughout, including the file names. |
|
|
232
259
|
| `src/bootstrap-mode.ts` | The repo-bootstrap MODE: clone-a-reference-or-scaffold → run the agent → refuse to push an empty tree → reinit + force-push to the pre-created target repo. |
|
|
233
260
|
| `src/agent-shared.ts` | The few helpers every agent MODE shares (effort-report folding, the capability fields forwarded to `runAgentInWorkspace`). |
|
|
234
261
|
| `src/logger.ts` | Structured logging. |
|
package/dist/agent-shared.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentJob, AgentResult, McpServerSpec, SkillSpec } from './job.js';
|
|
1
|
+
import type { AgentJob, AgentResult, McpServerSpec, ReferenceScreenshotsSpec, SkillSpec } from './job.js';
|
|
2
2
|
import type { EffortReport } from './effort.js';
|
|
3
3
|
/**
|
|
4
4
|
* Fold an agent's effort self-assessment (lifted from its sentinel file by `runAgentInWorkspace`)
|
|
@@ -7,12 +7,14 @@ import type { EffortReport } from './effort.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function mergeEffort(result: AgentResult, effortReport: EffortReport | undefined): AgentResult;
|
|
9
9
|
/**
|
|
10
|
-
* The agent-capability fields (skills
|
|
11
|
-
* {@link runAgentInWorkspace}. One helper rather than a per-flow spread, so a flow
|
|
12
|
-
* be the one that drops a kind's declared playbook
|
|
13
|
-
* (the agent simply works without it) and would only show
|
|
10
|
+
* The agent-capability fields (skills, tool servers, reference designs) every agent-running flow
|
|
11
|
+
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow spread, so a flow
|
|
12
|
+
* cannot silently be the one that drops a kind's declared playbook, tool server or reference
|
|
13
|
+
* gallery: the failure mode is invisible (the agent simply works without it) and would only show
|
|
14
|
+
* up as degraded output.
|
|
14
15
|
*/
|
|
15
16
|
export declare function agentCapabilities(job: AgentJob): {
|
|
16
17
|
skills?: SkillSpec[];
|
|
17
18
|
mcpServers?: McpServerSpec[];
|
|
19
|
+
referenceScreenshots?: ReferenceScreenshotsSpec;
|
|
18
20
|
};
|
package/dist/agent-shared.js
CHANGED
|
@@ -10,14 +10,16 @@ export function mergeEffort(result, effortReport) {
|
|
|
10
10
|
return effortReport ? { ...result, effortReport } : result;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* The agent-capability fields (skills
|
|
14
|
-
* {@link runAgentInWorkspace}. One helper rather than a per-flow spread, so a flow
|
|
15
|
-
* be the one that drops a kind's declared playbook
|
|
16
|
-
* (the agent simply works without it) and would only show
|
|
13
|
+
* The agent-capability fields (skills, tool servers, reference designs) every agent-running flow
|
|
14
|
+
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow spread, so a flow
|
|
15
|
+
* cannot silently be the one that drops a kind's declared playbook, tool server or reference
|
|
16
|
+
* gallery: the failure mode is invisible (the agent simply works without it) and would only show
|
|
17
|
+
* up as degraded output.
|
|
17
18
|
*/
|
|
18
19
|
export function agentCapabilities(job) {
|
|
19
20
|
return {
|
|
20
21
|
...(job.skills?.length ? { skills: job.skills } : {}),
|
|
21
22
|
...(job.mcpServers?.length ? { mcpServers: job.mcpServers } : {}),
|
|
23
|
+
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
22
24
|
};
|
|
23
25
|
}
|
package/dist/agent.js
CHANGED
|
@@ -9,7 +9,8 @@ import { captureRedactedOutput, redactSecrets, registerKnownSecrets } from './re
|
|
|
9
9
|
import { cloneRepo, commitAll, conflictDiff, fetchPullRequestHead, fetchReferenceBranches, headCommit, mergeBranch, prepareExistingCheckout, pushBranch, unmergedPaths, } from './git.js';
|
|
10
10
|
import { inferVcsProvider, openPullRequest } from './vcs-api.js';
|
|
11
11
|
import { applyPrDescription } from './pr-description.js';
|
|
12
|
-
import { makeDirClaimer
|
|
12
|
+
import { makeDirClaimer } from './checkout-dir.js';
|
|
13
|
+
import { noChangesReason, runCodingAgent, runMultiRepoCoding } from './coding-agent.js';
|
|
13
14
|
import { validationFailureMessage } from './validation-checks.js';
|
|
14
15
|
import { prepopulateDependencies, withDependencyNote } from './dependency-install.js';
|
|
15
16
|
import { agentCapabilities, mergeEffort } from './agent-shared.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RepoSpec } from './job.js';
|
|
2
|
+
/** Sanitise an owner/name into a safe single path segment for a sibling checkout directory. */
|
|
3
|
+
export declare function safeDirSegment(value: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* A short deterministic digest of the EXACT `owner` / `name` pair, before sanitisation. FNV-1a
|
|
6
|
+
* over `owner\0name`; the NUL separator makes it a digest of the PAIR rather than of a
|
|
7
|
+
* concatenation, so `('a', 'bc')` and `('ab', 'c')` cannot share one. Hand-rolled rather than
|
|
8
|
+
* taken from `node:crypto` because the backend needs the identical function and runs it in
|
|
9
|
+
* workerd as well as on Node.
|
|
10
|
+
*
|
|
11
|
+
* MUST stay byte-identical to the backend's `checkoutDirDigest`
|
|
12
|
+
* (`@cat-factory/server`, `agents/harnessContract.ts`); see {@link makeDirClaimer}.
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkoutDirDigest(owner: string, name: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* A sibling-directory allocator for a multi-repo run: returns the checkout directory name for a
|
|
17
|
+
* repo under the workspace root. A pure function of the pair (`owner__name__digest`), which is
|
|
18
|
+
* what lets this and the backend compute it independently with no shared ordering or state. Kept
|
|
19
|
+
* as a factory so the coding + read-only explore fan-outs share ONE scheme, and it MUST stay
|
|
20
|
+
* byte-identical to the backend's `siblingCheckoutDir` / `renderMultiRepoWorkspaceSection` in
|
|
21
|
+
* `@cat-factory/server`, which names this exact directory in the agent's prompt: the two are
|
|
22
|
+
* computed independently, so a divergent rule would point the agent at a directory that does not
|
|
23
|
+
* exist.
|
|
24
|
+
*
|
|
25
|
+
* The readable `owner__name` prefix does not identify a repo on its own, which is why the digest
|
|
26
|
+
* is there. {@link safeDirSegment} folds a whole class of characters onto `-`, so a GitLab
|
|
27
|
+
* namespace path `grp/sub` and a group literally named `grp-sub` sanitise alike; and the `__`
|
|
28
|
+
* join is ambiguous once a segment may contain `_`, which GitHub owners cannot but GitLab
|
|
29
|
+
* namespace paths can, so `('a__b', 'c')` and `('a', 'b__c')` both read as `a__b__c`. Either
|
|
30
|
+
* collision puts two legs on one directory, and the second one's clone then fails against a
|
|
31
|
+
* directory the first already filled, killing the run in the clone phase naming neither repo.
|
|
32
|
+
*/
|
|
33
|
+
export declare function makeDirClaimer(): (repo: Pick<RepoSpec, 'name' | 'owner'>) => string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The HARNESS half of the sibling-checkout-directory contract.
|
|
3
|
+
//
|
|
4
|
+
// The harness CREATES these directories; `@cat-factory/server`'s `agents/harnessContract.ts`
|
|
5
|
+
// NAMES them in the agent's prompt. The image builds from this `src/` plus typescript and may
|
|
6
|
+
// depend on no workspace package, so the two halves are computed INDEPENDENTLY and pinned against
|
|
7
|
+
// each other by `test/harness-contract.conformity.test.ts`. Extracted out of `coding-agent.ts` so
|
|
8
|
+
// the pairing sits in one small module per side rather than buried in the agent runner: the whole
|
|
9
|
+
// point of the pairing is that a reader can see both halves at once.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/** Sanitise an owner/name into a safe single path segment for a sibling checkout directory. */
|
|
12
|
+
export function safeDirSegment(value) {
|
|
13
|
+
return value.replace(/[^A-Za-z0-9._-]/g, '-') || '_';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A short deterministic digest of the EXACT `owner` / `name` pair, before sanitisation. FNV-1a
|
|
17
|
+
* over `owner\0name`; the NUL separator makes it a digest of the PAIR rather than of a
|
|
18
|
+
* concatenation, so `('a', 'bc')` and `('ab', 'c')` cannot share one. Hand-rolled rather than
|
|
19
|
+
* taken from `node:crypto` because the backend needs the identical function and runs it in
|
|
20
|
+
* workerd as well as on Node.
|
|
21
|
+
*
|
|
22
|
+
* MUST stay byte-identical to the backend's `checkoutDirDigest`
|
|
23
|
+
* (`@cat-factory/server`, `agents/harnessContract.ts`); see {@link makeDirClaimer}.
|
|
24
|
+
*/
|
|
25
|
+
export function checkoutDirDigest(owner, name) {
|
|
26
|
+
const input = `${owner}\u0000${name}`;
|
|
27
|
+
let hash = 0x811c9dc5;
|
|
28
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
29
|
+
hash ^= input.charCodeAt(i);
|
|
30
|
+
// The FNV prime (16777619) as shifts, with `>>> 0` folding the result back to uint32 every
|
|
31
|
+
// step so the arithmetic never drifts into float range and diverges between engines.
|
|
32
|
+
hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0;
|
|
33
|
+
}
|
|
34
|
+
return hash.toString(36).padStart(7, '0');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A sibling-directory allocator for a multi-repo run: returns the checkout directory name for a
|
|
38
|
+
* repo under the workspace root. A pure function of the pair (`owner__name__digest`), which is
|
|
39
|
+
* what lets this and the backend compute it independently with no shared ordering or state. Kept
|
|
40
|
+
* as a factory so the coding + read-only explore fan-outs share ONE scheme, and it MUST stay
|
|
41
|
+
* byte-identical to the backend's `siblingCheckoutDir` / `renderMultiRepoWorkspaceSection` in
|
|
42
|
+
* `@cat-factory/server`, which names this exact directory in the agent's prompt: the two are
|
|
43
|
+
* computed independently, so a divergent rule would point the agent at a directory that does not
|
|
44
|
+
* exist.
|
|
45
|
+
*
|
|
46
|
+
* The readable `owner__name` prefix does not identify a repo on its own, which is why the digest
|
|
47
|
+
* is there. {@link safeDirSegment} folds a whole class of characters onto `-`, so a GitLab
|
|
48
|
+
* namespace path `grp/sub` and a group literally named `grp-sub` sanitise alike; and the `__`
|
|
49
|
+
* join is ambiguous once a segment may contain `_`, which GitHub owners cannot but GitLab
|
|
50
|
+
* namespace paths can, so `('a__b', 'c')` and `('a', 'b__c')` both read as `a__b__c`. Either
|
|
51
|
+
* collision puts two legs on one directory, and the second one's clone then fails against a
|
|
52
|
+
* directory the first already filled, killing the run in the clone phase naming neither repo.
|
|
53
|
+
*/
|
|
54
|
+
export function makeDirClaimer() {
|
|
55
|
+
return (repo) => `${safeDirSegment(repo.owner)}__${safeDirSegment(repo.name)}__${checkoutDirDigest(repo.owner, repo.name)}`;
|
|
56
|
+
}
|
package/dist/coding-agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentJob, AgentResult, HarnessAuthFields, RepoSpec, SkillSpec, McpServerSpec } from './job.js';
|
|
1
|
+
import type { AgentJob, AgentResult, HarnessAuthFields, ReferenceScreenshotsSpec, RepoSpec, SkillSpec, McpServerSpec } from './job.js';
|
|
2
2
|
import type { HarnessCallMetric } from './pi.js';
|
|
3
3
|
import type { PiRunStats } from './pi-reduction.js';
|
|
4
4
|
import { type EffortReport } from './effort.js';
|
|
@@ -112,6 +112,14 @@ export interface CodingAgentSpec extends HarnessAuthFields {
|
|
|
112
112
|
* has already dropped anything this harness cannot serve. Absent ⇒ built-in tools only.
|
|
113
113
|
*/
|
|
114
114
|
mcpServers?: McpServerSpec[];
|
|
115
|
+
/**
|
|
116
|
+
* The task's reference design images, downloaded into `.cat-context/reference-screenshots/`
|
|
117
|
+
* before the agent's first turn. Carried on the coding path as well as the explore one because
|
|
118
|
+
* what earns a run its references is the KIND's declared `ui` image, and a deployment's own
|
|
119
|
+
* UI-facing kind may well be a coding one, and nothing here switches on which built-in it is.
|
|
120
|
+
* Absent ⇒ none (the normal case).
|
|
121
|
+
*/
|
|
122
|
+
referenceScreenshots?: ReferenceScreenshotsSpec;
|
|
115
123
|
}
|
|
116
124
|
/** The outcome of a coding agent run, before each caller maps it to its own result shape. */
|
|
117
125
|
export interface CodingAgentOutcome {
|
|
@@ -219,19 +227,6 @@ export declare function runRalphValidation(repoDir: string, cwd: string, validat
|
|
|
219
227
|
iteration?: number;
|
|
220
228
|
headSha?: string;
|
|
221
229
|
}>;
|
|
222
|
-
/** Sanitise an owner/name into a safe single path segment for a sibling checkout directory. */
|
|
223
|
-
export declare function safeDirSegment(value: string): string;
|
|
224
|
-
/**
|
|
225
|
-
* A sibling-directory allocator for a multi-repo run: returns the checkout directory name for a
|
|
226
|
-
* repo under the workspace root. Deterministic (`owner__name`) and collision-free by construction
|
|
227
|
-
* — the checkout set is deduped by `owner/name` upstream and GitHub owners contain no `_`, so the
|
|
228
|
-
* `owner__name` join is unique per repo without a stateful collision dance. Kept as a factory so
|
|
229
|
-
* the coding + read-only explore fan-outs share ONE scheme, and it MUST stay byte-identical to the
|
|
230
|
-
* backend's `siblingCheckoutDir` / `renderMultiRepoWorkspaceSection` in `@cat-factory/server`
|
|
231
|
-
* (jobBody.ts), which names this exact directory in the agent's prompt — the two are computed
|
|
232
|
-
* independently, so a divergent rule would point the agent at a directory that does not exist.
|
|
233
|
-
*/
|
|
234
|
-
export declare function makeDirClaimer(): (repo: Pick<RepoSpec, 'name' | 'owner'>) => string;
|
|
235
230
|
/**
|
|
236
231
|
* Multi-repo coding (service-connections phase 3): clone the primary repo AND every connected
|
|
237
232
|
* peer repo as SIBLING checkouts under one workspace root, run the agent ONCE with its cwd at
|
package/dist/coding-agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { runCapturedCommand } from './captured-command.js';
|
|
4
|
+
import { makeDirClaimer } from './checkout-dir.js';
|
|
4
5
|
import { branchAheadOfBase, changedFilesSinceBase, branchHasCommitsSince, cloneExistingBranch, cloneRepo, commitTrackedEdits, createBranch, excludeFromGit, fetchReferenceBranches, headCommit, listUntrackedFiles, prepareExistingCheckout, pushBranch, refreshFromBaseIfClean, remoteBranchExists, } from './git.js';
|
|
5
6
|
import { openPullRequest } from './vcs-api.js';
|
|
6
7
|
import { FOLLOW_UPS_FILENAME, FollowUpTailer } from './follow-ups.js';
|
|
@@ -208,6 +209,9 @@ export async function runCodingAgent(spec, opts = {}) {
|
|
|
208
209
|
guardLimits: spec.guardLimits,
|
|
209
210
|
...(spec.skills?.length ? { skills: spec.skills } : {}),
|
|
210
211
|
...(spec.mcpServers?.length ? { mcpServers: spec.mcpServers } : {}),
|
|
212
|
+
...(spec.referenceScreenshots
|
|
213
|
+
? { referenceScreenshots: spec.referenceScreenshots }
|
|
214
|
+
: {}),
|
|
211
215
|
}, opts);
|
|
212
216
|
let outcome;
|
|
213
217
|
try {
|
|
@@ -676,23 +680,6 @@ export async function runRalphValidation(repoDir, cwd, validation, logger, opts)
|
|
|
676
680
|
...(headSha ? { headSha } : {}),
|
|
677
681
|
};
|
|
678
682
|
}
|
|
679
|
-
/** Sanitise an owner/name into a safe single path segment for a sibling checkout directory. */
|
|
680
|
-
export function safeDirSegment(value) {
|
|
681
|
-
return value.replace(/[^A-Za-z0-9._-]/g, '-') || '_';
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* A sibling-directory allocator for a multi-repo run: returns the checkout directory name for a
|
|
685
|
-
* repo under the workspace root. Deterministic (`owner__name`) and collision-free by construction
|
|
686
|
-
* — the checkout set is deduped by `owner/name` upstream and GitHub owners contain no `_`, so the
|
|
687
|
-
* `owner__name` join is unique per repo without a stateful collision dance. Kept as a factory so
|
|
688
|
-
* the coding + read-only explore fan-outs share ONE scheme, and it MUST stay byte-identical to the
|
|
689
|
-
* backend's `siblingCheckoutDir` / `renderMultiRepoWorkspaceSection` in `@cat-factory/server`
|
|
690
|
-
* (jobBody.ts), which names this exact directory in the agent's prompt — the two are computed
|
|
691
|
-
* independently, so a divergent rule would point the agent at a directory that does not exist.
|
|
692
|
-
*/
|
|
693
|
-
export function makeDirClaimer() {
|
|
694
|
-
return (repo) => `${safeDirSegment(repo.owner)}__${safeDirSegment(repo.name)}`;
|
|
695
|
-
}
|
|
696
683
|
/**
|
|
697
684
|
* Multi-repo coding (service-connections phase 3): clone the primary repo AND every connected
|
|
698
685
|
* peer repo as SIBLING checkouts under one workspace root, run the agent ONCE with its cwd at
|
|
@@ -711,8 +698,9 @@ export async function runMultiRepoCoding(job, opts = {}) {
|
|
|
711
698
|
const peers = job.peerRepos ?? [];
|
|
712
699
|
const references = job.referenceRepos ?? [];
|
|
713
700
|
const primaryWorkBranch = job.pushBranch ?? job.newBranch ?? job.branch;
|
|
714
|
-
// Assign the sibling directory per repo via the shared deterministic allocator
|
|
715
|
-
// matching the backend prompt's `siblingCheckoutDir`), shared with the
|
|
701
|
+
// Assign the sibling directory per repo via the shared deterministic allocator
|
|
702
|
+
// (`owner__name__digest`, matching the backend prompt's `siblingCheckoutDir`), shared with the
|
|
703
|
+
// read-only explore fan-out.
|
|
716
704
|
const claimDir = makeDirClaimer();
|
|
717
705
|
const legs = [
|
|
718
706
|
{
|
|
@@ -823,6 +811,7 @@ export async function runMultiRepoCoding(job, opts = {}) {
|
|
|
823
811
|
// are properties of the AGENT KIND, not of the checkout layout.
|
|
824
812
|
...(job.skills?.length ? { skills: job.skills } : {}),
|
|
825
813
|
...(job.mcpServers?.length ? { mcpServers: job.mcpServers } : {}),
|
|
814
|
+
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
826
815
|
multiRepo: true,
|
|
827
816
|
}, opts);
|
|
828
817
|
// Commit forgotten tracked edits, then push + open a PR for each repo the run actually changed.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A linked-context file the backend prepared (requirements / RFC / PRD / tracker issue)
|
|
3
|
+
* for the harness to materialise under CONTEXT_DIR in the checkout, so the agent can read
|
|
4
|
+
* it on demand. The harness can't reach Jira/GitHub itself, so all such context is fetched
|
|
5
|
+
* and shipped here up front. `path` is sanitised to a safe basename on parse.
|
|
6
|
+
*/
|
|
7
|
+
export interface ContextFileSpec {
|
|
8
|
+
path: string;
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The REFERENCE DESIGN IMAGES the backend holds for this task, for the harness to download into
|
|
15
|
+
* `.cat-context/reference-screenshots/` before the agent runs: the directory a UI tester's prompt
|
|
16
|
+
* names and, until now, nothing wrote.
|
|
17
|
+
*
|
|
18
|
+
* A manifest rather than the bytes: a design frame is a full-page PNG, and a job body is JSON
|
|
19
|
+
* that crosses every transport and is persisted with the dispatch. The bytes come back over the
|
|
20
|
+
* SAME container session token the run already holds (`GET ${url}/<artifactId>`), so this needs
|
|
21
|
+
* no extra credential and no publicly reachable URL.
|
|
22
|
+
*
|
|
23
|
+
* `view` is what the backend's gate pairs on, and `fileName` is the name the BACKEND chose for it,
|
|
24
|
+
* never derived here. The file name is how the agent learns the view name, so deriving it in the
|
|
25
|
+
* container would let a harness image the deployment has not rolled out yet rename every view a
|
|
26
|
+
* run reports, and the pairing would come apart with nothing failing.
|
|
27
|
+
*/
|
|
28
|
+
export interface ReferenceScreenshotsSpec {
|
|
29
|
+
/** Base URL of the reference download route; the artifact id is appended as a path segment. */
|
|
30
|
+
url: string;
|
|
31
|
+
/** The run's container session token (the same one the LLM proxy is called with). */
|
|
32
|
+
token: string;
|
|
33
|
+
files: ReferenceScreenshotSpec[];
|
|
34
|
+
/**
|
|
35
|
+
* View names the task holds a reference for that this job was NOT sent a file for, because the
|
|
36
|
+
* set was capped. Stated to the agent beside the transfers that failed: from where it stands
|
|
37
|
+
* both are a view to capture with no image to compare against.
|
|
38
|
+
*
|
|
39
|
+
* Two producers, and they mean the same thing here: the BACKEND's own ceiling (the number that
|
|
40
|
+
* should ever actually bind, chosen where the precedence between an upload and a design frame
|
|
41
|
+
* is known), and this parser's backstop against a body claiming more files than any real set
|
|
42
|
+
* has. A drop with no entry here is the bug this field exists to prevent.
|
|
43
|
+
*/
|
|
44
|
+
omitted: string[];
|
|
45
|
+
}
|
|
46
|
+
/** One reference image in a {@link ReferenceScreenshotsSpec}. `fileName` is sanitised on parse. */
|
|
47
|
+
export interface ReferenceScreenshotSpec {
|
|
48
|
+
artifactId: string;
|
|
49
|
+
fileName: string;
|
|
50
|
+
view: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Sanitise a body-supplied context filename to a safe basename within CONTEXT_DIR:
|
|
54
|
+
* strip any directory part, allow only `[A-Za-z0-9._-]`, and reject empties / dotfiles
|
|
55
|
+
* / `..` so a hostile value can't escape the directory or clobber repo files.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sanitizeContextFileName(value: unknown): string | undefined;
|
|
58
|
+
/** Parse the linked-context files, dropping any malformed/unsafe entry. */
|
|
59
|
+
export declare function parseContextFiles(value: unknown): ContextFileSpec[];
|
|
60
|
+
/**
|
|
61
|
+
* Parse the reference-design manifest, or undefined when absent/unusable.
|
|
62
|
+
*
|
|
63
|
+
* The whole manifest is dropped when its transport half is unusable (no absolute http(s) URL, no
|
|
64
|
+
* token): every file would fail the same way, and one stated cause beats N identical ones. An
|
|
65
|
+
* individual entry is dropped only when it cannot name a file safely: the same basename
|
|
66
|
+
* sanitisation every context file gets, so a hostile `fileName` can neither escape the directory
|
|
67
|
+
* nor clobber a repo file.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseReferenceScreenshots(value: unknown): ReferenceScreenshotsSpec | undefined;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The two manifests of FILES the backend stages into a checkout before the agent's first turn:
|
|
3
|
+
// the linked-context documents it materialises under CONTEXT_DIR, and the reference design images
|
|
4
|
+
// it has the harness download beside them.
|
|
5
|
+
//
|
|
6
|
+
// One module because they are the same kind of thing parsed the same defensive way, and because
|
|
7
|
+
// they share the basename rule below: both name files the container writes into a directory it
|
|
8
|
+
// then points the agent at, so both are held to a value that cannot escape it or clobber a repo
|
|
9
|
+
// file. Split out of `job.ts`, which parses everything else a job body carries.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Sanitise a body-supplied context filename to a safe basename within CONTEXT_DIR:
|
|
13
|
+
* strip any directory part, allow only `[A-Za-z0-9._-]`, and reject empties / dotfiles
|
|
14
|
+
* / `..` so a hostile value can't escape the directory or clobber repo files.
|
|
15
|
+
*/
|
|
16
|
+
export function sanitizeContextFileName(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return undefined;
|
|
19
|
+
const base = value.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
20
|
+
const cleaned = base.replace(/[^A-Za-z0-9._-]/g, '');
|
|
21
|
+
if (!cleaned || cleaned === '.' || cleaned === '..' || cleaned.startsWith('.'))
|
|
22
|
+
return undefined;
|
|
23
|
+
return cleaned;
|
|
24
|
+
}
|
|
25
|
+
/** Parse the linked-context files, dropping any malformed/unsafe entry. */
|
|
26
|
+
export function parseContextFiles(value) {
|
|
27
|
+
if (!Array.isArray(value))
|
|
28
|
+
return [];
|
|
29
|
+
const files = [];
|
|
30
|
+
const used = new Set();
|
|
31
|
+
for (const entry of value) {
|
|
32
|
+
if (typeof entry !== 'object' || entry === null)
|
|
33
|
+
continue;
|
|
34
|
+
const e = entry;
|
|
35
|
+
const path = sanitizeContextFileName(e.path);
|
|
36
|
+
if (!path || used.has(path))
|
|
37
|
+
continue;
|
|
38
|
+
if (typeof e.content !== 'string')
|
|
39
|
+
continue;
|
|
40
|
+
used.add(path);
|
|
41
|
+
files.push({
|
|
42
|
+
path,
|
|
43
|
+
title: typeof e.title === 'string' ? e.title : path,
|
|
44
|
+
url: typeof e.url === 'string' ? e.url : '',
|
|
45
|
+
content: e.content,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* How many reference images one job may be handed. The backend caps the set it sends well below
|
|
52
|
+
* this, so this is the harness's own backstop against a malformed or hostile body turning the
|
|
53
|
+
* pre-run setup into an unbounded download, never the ceiling a real run meets.
|
|
54
|
+
*
|
|
55
|
+
* Hitting it is REPORTED rather than silently obeyed: an entry past the ceiling is dropped from
|
|
56
|
+
* `files` and its view named on `omitted`, so an agent facing a truncated set is still told which
|
|
57
|
+
* views to capture. A cap that shortened the list and said nothing would be indistinguishable, on
|
|
58
|
+
* disk and in the prompt, from a design that simply has no such screen.
|
|
59
|
+
*/
|
|
60
|
+
const MAX_REFERENCE_SCREENSHOTS = 40;
|
|
61
|
+
/**
|
|
62
|
+
* Parse the reference-design manifest, or undefined when absent/unusable.
|
|
63
|
+
*
|
|
64
|
+
* The whole manifest is dropped when its transport half is unusable (no absolute http(s) URL, no
|
|
65
|
+
* token): every file would fail the same way, and one stated cause beats N identical ones. An
|
|
66
|
+
* individual entry is dropped only when it cannot name a file safely: the same basename
|
|
67
|
+
* sanitisation every context file gets, so a hostile `fileName` can neither escape the directory
|
|
68
|
+
* nor clobber a repo file.
|
|
69
|
+
*/
|
|
70
|
+
export function parseReferenceScreenshots(value) {
|
|
71
|
+
if (typeof value !== 'object' || value === null)
|
|
72
|
+
return undefined;
|
|
73
|
+
const o = value;
|
|
74
|
+
const url = typeof o.url === 'string' ? o.url.trim() : '';
|
|
75
|
+
const token = typeof o.token === 'string' ? o.token : '';
|
|
76
|
+
if (!url || !token || !/^https?:\/\//i.test(url))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (!Array.isArray(o.files))
|
|
79
|
+
return undefined;
|
|
80
|
+
const files = [];
|
|
81
|
+
// The backend's own dropped views come first; anything this parser drops joins them below.
|
|
82
|
+
const omitted = Array.isArray(o.omitted)
|
|
83
|
+
? o.omitted.filter((view) => typeof view === 'string' && view.length > 0)
|
|
84
|
+
: [];
|
|
85
|
+
const used = new Set();
|
|
86
|
+
for (const entry of o.files) {
|
|
87
|
+
if (typeof entry !== 'object' || entry === null)
|
|
88
|
+
continue;
|
|
89
|
+
const e = entry;
|
|
90
|
+
const fileName = sanitizeContextFileName(e.fileName);
|
|
91
|
+
const artifactId = typeof e.artifactId === 'string' ? e.artifactId.trim() : '';
|
|
92
|
+
// The id becomes a path segment on the download URL, so it is held to the shape the platform
|
|
93
|
+
// mints rather than encoded and hoped for: anything else cannot be a real artifact anyway.
|
|
94
|
+
if (!fileName || used.has(fileName) || !/^[A-Za-z0-9_-]{1,64}$/.test(artifactId))
|
|
95
|
+
continue;
|
|
96
|
+
const view = typeof e.view === 'string' ? e.view : fileName;
|
|
97
|
+
// Past the backstop the entry is NAMED, not dropped: it stays a view the agent must capture.
|
|
98
|
+
// Checked here rather than at the top of the loop so a malformed entry is refused on its own
|
|
99
|
+
// terms (it names no usable view to report) instead of being counted against the ceiling.
|
|
100
|
+
if (files.length >= MAX_REFERENCE_SCREENSHOTS) {
|
|
101
|
+
omitted.push(view);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
used.add(fileName);
|
|
105
|
+
files.push({ artifactId, fileName, view });
|
|
106
|
+
}
|
|
107
|
+
if (!files.length && !omitted.length)
|
|
108
|
+
return undefined;
|
|
109
|
+
return { url: url.replace(/\/+$/, ''), token, files, omitted };
|
|
110
|
+
}
|
package/dist/job.d.ts
CHANGED
|
@@ -8,8 +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 ReferenceScreenshotSpec, type ReferenceScreenshotsSpec } from './context-manifests.js';
|
|
11
12
|
export type { TestSecretSpec };
|
|
12
13
|
export type { McpServerSpec, SkillResourceSpec, SkillSpec };
|
|
14
|
+
export type { ContextFileSpec, ReferenceScreenshotSpec, ReferenceScreenshotsSpec };
|
|
13
15
|
/**
|
|
14
16
|
* Per-job auth fields, shared across every job shape. The Pi harness carries the
|
|
15
17
|
* proxy base URL + a model-locked session token; the subscription harnesses
|
|
@@ -229,18 +231,6 @@ export interface AgentBootstrapSpec {
|
|
|
229
231
|
/** Scaffold from an empty directory instead of cloning `job.repo` (no reference). */
|
|
230
232
|
fromScratch?: boolean;
|
|
231
233
|
}
|
|
232
|
-
/**
|
|
233
|
-
* A linked-context file the backend prepared (requirements / RFC / PRD / tracker issue)
|
|
234
|
-
* for the harness to materialise under CONTEXT_DIR in the checkout, so the agent can read
|
|
235
|
-
* it on demand. The harness can't reach Jira/GitHub itself, so all such context is fetched
|
|
236
|
-
* and shipped here up front. `path` is sanitised to a safe basename on parse.
|
|
237
|
-
*/
|
|
238
|
-
export interface ContextFileSpec {
|
|
239
|
-
path: string;
|
|
240
|
-
title: string;
|
|
241
|
-
url: string;
|
|
242
|
-
content: string;
|
|
243
|
-
}
|
|
244
234
|
/** How an explore agent's reply is consumed. */
|
|
245
235
|
export interface AgentOutputSpec {
|
|
246
236
|
/** `prose` keeps the reply text; `structured` parses (and optionally repairs) it to JSON. */
|
|
@@ -328,6 +318,13 @@ export interface AgentJob extends HarnessAuthFields {
|
|
|
328
318
|
* The agent reads them on demand; they are kept out of any commit. Absent ⇒ none.
|
|
329
319
|
*/
|
|
330
320
|
contextFiles?: ContextFileSpec[];
|
|
321
|
+
/**
|
|
322
|
+
* The task's reference design images, downloaded into `.cat-context/reference-screenshots/`
|
|
323
|
+
* before the agent runs (see {@link ReferenceScreenshotsSpec}). Sent only for a kind that
|
|
324
|
+
* CAPTURES views and only when the task actually has references, so absent is the normal case
|
|
325
|
+
* and means the agent names its own views.
|
|
326
|
+
*/
|
|
327
|
+
referenceScreenshots?: ReferenceScreenshotsSpec;
|
|
331
328
|
/**
|
|
332
329
|
* Private package-registry auth (npm private orgs, GitHub Packages), rendered into
|
|
333
330
|
* `~/.npmrc` before the run so the checkout's installs — the agent's own and the
|
package/dist/job.js
CHANGED
|
@@ -3,6 +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, parseReferenceScreenshots, } from './context-manifests.js';
|
|
6
7
|
/** A positive finite integer, or undefined for any other input (silently ignored). */
|
|
7
8
|
function posInt(value) {
|
|
8
9
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
@@ -350,45 +351,6 @@ function parseAgentBootstrapSpec(value) {
|
|
|
350
351
|
...(o.fromScratch === true ? { fromScratch: true } : {}),
|
|
351
352
|
};
|
|
352
353
|
}
|
|
353
|
-
/**
|
|
354
|
-
* Sanitise a body-supplied context filename to a safe basename within CONTEXT_DIR:
|
|
355
|
-
* strip any directory part, allow only `[A-Za-z0-9._-]`, and reject empties / dotfiles
|
|
356
|
-
* / `..` so a hostile value can't escape the directory or clobber repo files.
|
|
357
|
-
*/
|
|
358
|
-
function sanitizeContextFileName(value) {
|
|
359
|
-
if (typeof value !== 'string')
|
|
360
|
-
return undefined;
|
|
361
|
-
const base = value.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
362
|
-
const cleaned = base.replace(/[^A-Za-z0-9._-]/g, '');
|
|
363
|
-
if (!cleaned || cleaned === '.' || cleaned === '..' || cleaned.startsWith('.'))
|
|
364
|
-
return undefined;
|
|
365
|
-
return cleaned;
|
|
366
|
-
}
|
|
367
|
-
/** Parse the linked-context files, dropping any malformed/unsafe entry. */
|
|
368
|
-
function parseContextFiles(value) {
|
|
369
|
-
if (!Array.isArray(value))
|
|
370
|
-
return [];
|
|
371
|
-
const files = [];
|
|
372
|
-
const used = new Set();
|
|
373
|
-
for (const entry of value) {
|
|
374
|
-
if (typeof entry !== 'object' || entry === null)
|
|
375
|
-
continue;
|
|
376
|
-
const e = entry;
|
|
377
|
-
const path = sanitizeContextFileName(e.path);
|
|
378
|
-
if (!path || used.has(path))
|
|
379
|
-
continue;
|
|
380
|
-
if (typeof e.content !== 'string')
|
|
381
|
-
continue;
|
|
382
|
-
used.add(path);
|
|
383
|
-
files.push({
|
|
384
|
-
path,
|
|
385
|
-
title: typeof e.title === 'string' ? e.title : path,
|
|
386
|
-
url: typeof e.url === 'string' ? e.url : '',
|
|
387
|
-
content: e.content,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
return files;
|
|
391
|
-
}
|
|
392
354
|
/** Parse the explore-mode infra stand-up spec, or undefined when absent/unrecognised. */
|
|
393
355
|
function parseAgentInfraSpec(value) {
|
|
394
356
|
if (typeof value !== 'object' || value === null)
|
|
@@ -516,6 +478,7 @@ export function parseAgentJob(input) {
|
|
|
516
478
|
referenceBranches: parseReferenceBranches(o.referenceBranches),
|
|
517
479
|
bootstrap: parseAgentBootstrapSpec(o.bootstrap),
|
|
518
480
|
contextFiles: parseContextFiles(o.contextFiles),
|
|
481
|
+
referenceScreenshots: parseReferenceScreenshots(o.referenceScreenshots),
|
|
519
482
|
packageRegistries: parsePackageRegistries(o.packageRegistries),
|
|
520
483
|
skills: parseSkillSpecs(o.skills),
|
|
521
484
|
mcpServers: parseMcpServerSpecs(o.mcpServers),
|
|
@@ -582,7 +545,7 @@ function parseAgentPrSpec(raw) {
|
|
|
582
545
|
* literal doesn't blow the complexity budget; behaviour is byte-identical (spread order preserved).
|
|
583
546
|
*/
|
|
584
547
|
function assembleAgentJob(o, mode, agentField, parts) {
|
|
585
|
-
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, packageRegistries, skills, mcpServers, testSecrets, guardLimits, validation, validationChecks, reproduction, dependencyInstall, reviewPrNumber, generatorSecrets, } = parts;
|
|
548
|
+
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, referenceScreenshots, packageRegistries, skills, mcpServers, testSecrets, guardLimits, validation, validationChecks, reproduction, dependencyInstall, reviewPrNumber, generatorSecrets, } = parts;
|
|
586
549
|
const repo = (o.repo ?? {});
|
|
587
550
|
return {
|
|
588
551
|
jobId: str(o.jobId, 'jobId'),
|
|
@@ -598,6 +561,7 @@ function assembleAgentJob(o, mode, agentField, parts) {
|
|
|
598
561
|
...(bootstrap ? { bootstrap } : {}),
|
|
599
562
|
...(output ? { output } : {}),
|
|
600
563
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
564
|
+
...(referenceScreenshots ? { referenceScreenshots } : {}),
|
|
601
565
|
...(packageRegistries.length ? { packageRegistries } : {}),
|
|
602
566
|
...(skills ? { skills } : {}),
|
|
603
567
|
...(mcpServers ? { mcpServers } : {}),
|
package/dist/pi-workspace.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RepoSpec } from './job.js';
|
|
1
|
+
import type { RepoSpec, ReferenceScreenshotsSpec } 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';
|
|
@@ -105,6 +105,12 @@ export interface AgentRunSpec {
|
|
|
105
105
|
* from AGENTS.md, so the agent reads them on demand. Absent ⇒ none.
|
|
106
106
|
*/
|
|
107
107
|
contextFiles?: ContextFileInfo[];
|
|
108
|
+
/**
|
|
109
|
+
* The task's reference design images. Downloaded into `.cat-context/reference-screenshots/`
|
|
110
|
+
* before the run and named in the agent's prompt, so a capturing agent can compare against them
|
|
111
|
+
* and use their view names. Absent ⇒ nothing is downloaded and nothing is said.
|
|
112
|
+
*/
|
|
113
|
+
referenceScreenshots?: ReferenceScreenshotsSpec;
|
|
108
114
|
/**
|
|
109
115
|
* The skills to make available for this run — a `skill` step's picked skill and/or the playbooks
|
|
110
116
|
* the running agent kind declares. Installed HARNESS-AWARE: the claude-code runner writes them
|