@cat-factory/executor-harness 1.52.2 → 1.56.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 +48 -1
- package/dist/agent-runner.js +14 -11
- package/dist/agent.js +96 -43
- package/dist/coding-agent.js +107 -18
- package/dist/frontend-infra.js +9 -2
- package/dist/job.js +48 -1
- package/dist/package-registries.js +78 -13
- package/dist/pi-workspace.js +24 -8
- package/dist/pi.js +4 -3
- package/dist/runner.js +3 -0
- package/dist/validation-checks.js +300 -0
- package/package.json +3 -3
- package/src/agent-runner.ts +25 -13
- package/src/agent.ts +107 -42
- package/src/coding-agent.ts +134 -8
- package/src/frontend-infra.ts +10 -3
- package/src/job.ts +66 -0
- package/src/package-registries.ts +95 -15
- package/src/pi-workspace.ts +27 -8
- package/src/pi.ts +4 -3
- package/src/runner.ts +29 -0
- package/src/validation-checks.ts +395 -0
package/src/job.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { HarnessCallMetric, PiRunStats } from './pi.js'
|
|
|
2
2
|
import type { HarnessKind } from './pi-workspace.js'
|
|
3
3
|
import type { FailureCause } from './failure.js'
|
|
4
4
|
import type { EffortReport } from './effort.js'
|
|
5
|
+
import type { ValidationChecksSpec, ValidationReport } from './validation-checks.js'
|
|
5
6
|
|
|
6
7
|
// The job the Worker's ContainerAgentExecutor POSTs to /run. Kept as plain
|
|
7
8
|
// types with a hand-rolled validator so the image needs no schema dependency.
|
|
@@ -174,6 +175,38 @@ function parseValidationSpec(value: unknown): ValidationSpec | undefined {
|
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Parse the optional PRE-PR VALIDATION CHECKS spec (see
|
|
180
|
+
* docs/initiatives/pre-pr-validation.md): the service's ordered `{ label, command }` pairs and
|
|
181
|
+
* the repair-round budget. Every entry needs a non-empty command; entries without one are
|
|
182
|
+
* dropped, and a spec that ends up with no usable check returns `undefined` — so a malformed
|
|
183
|
+
* body degrades to the exact pre-feature behaviour (no loop, PR opens as before) rather than
|
|
184
|
+
* failing an otherwise-good coding run. `maxAttempts` is clamped to a sane range so a bad body
|
|
185
|
+
* can't make a container loop forever.
|
|
186
|
+
*/
|
|
187
|
+
function parseValidationChecksSpec(value: unknown): ValidationChecksSpec | undefined {
|
|
188
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
189
|
+
const o = value as Record<string, unknown>
|
|
190
|
+
if (!Array.isArray(o.checks)) return undefined
|
|
191
|
+
const checks: { label: string; command: string }[] = []
|
|
192
|
+
for (const raw of o.checks) {
|
|
193
|
+
if (typeof raw !== 'object' || raw === null) continue
|
|
194
|
+
const c = raw as Record<string, unknown>
|
|
195
|
+
if (typeof c.command !== 'string' || c.command.trim() === '') continue
|
|
196
|
+
const label = typeof c.label === 'string' && c.label.trim() ? c.label.trim() : c.command
|
|
197
|
+
checks.push({ label, command: c.command })
|
|
198
|
+
}
|
|
199
|
+
if (checks.length === 0) return undefined
|
|
200
|
+
const parsed = posInt(o.maxAttempts)
|
|
201
|
+
return {
|
|
202
|
+
checks,
|
|
203
|
+
maxAttempts: Math.min(
|
|
204
|
+
parsed ?? VALIDATION_DEFAULT_MAX_ATTEMPTS,
|
|
205
|
+
VALIDATION_MAX_ATTEMPTS_CEILING,
|
|
206
|
+
),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
/**
|
|
178
211
|
* Parse the shared per-job auth fields, validating per harness: a subscription
|
|
179
212
|
* harness (`claude-code` / `codex`) requires `subscriptionToken`; the default Pi
|
|
@@ -831,8 +864,29 @@ export interface AgentJob extends HarnessAuthFields {
|
|
|
831
864
|
* agent commits + pushes. Present only for a `ralph` iteration. See {@link ValidationSpec}.
|
|
832
865
|
*/
|
|
833
866
|
validation?: ValidationSpec
|
|
867
|
+
/**
|
|
868
|
+
* Coding mode: the service's PRE-PR VALIDATION CHECKS — commands the harness runs against the
|
|
869
|
+
* checkout after the agent settles and BEFORE opening a PR, feeding a failure back to the agent
|
|
870
|
+
* until they pass or the budget is spent. Present only on a dispatch that opens a PR and whose
|
|
871
|
+
* service configured checks; absent ⇒ the run behaves exactly as before. Deliberately keyed off
|
|
872
|
+
* job DATA, not the agent kind. See {@link ValidationChecksSpec}.
|
|
873
|
+
*/
|
|
874
|
+
validationChecks?: ValidationChecksSpec
|
|
834
875
|
}
|
|
835
876
|
|
|
877
|
+
/**
|
|
878
|
+
* The ceiling the harness clamps a body-supplied `validationChecks.maxAttempts` to, and the
|
|
879
|
+
* default it applies when the body omits one.
|
|
880
|
+
*
|
|
881
|
+
* DELIBERATE DUPLICATES of `VALIDATION_MAX_ATTEMPTS_CEILING` / `VALIDATION_DEFAULT_MAX_ATTEMPTS`
|
|
882
|
+
* in `@cat-factory/contracts` — the published image takes no schema dependency, so the harness
|
|
883
|
+
* cannot import them. Keep the two in step: the API validates writes against the contracts
|
|
884
|
+
* values, so a harness clamping to a DIFFERENT ceiling would silently cap a budget an operator
|
|
885
|
+
* was allowed to save, with nothing to flag the mismatch.
|
|
886
|
+
*/
|
|
887
|
+
export const VALIDATION_MAX_ATTEMPTS_CEILING = 10
|
|
888
|
+
export const VALIDATION_DEFAULT_MAX_ATTEMPTS = 3
|
|
889
|
+
|
|
836
890
|
/** Per-job, per-knob progress-guard overrides (see {@link AgentJob.guardLimits}). */
|
|
837
891
|
export interface GuardLimitsSpec {
|
|
838
892
|
maxToolCallsWithoutEdit?: number
|
|
@@ -876,6 +930,14 @@ export interface AgentResult {
|
|
|
876
930
|
* — the failure-class artifact the orchestrator-side provisioning logs can't capture.
|
|
877
931
|
*/
|
|
878
932
|
infraSetup?: InfraSetupRecord
|
|
933
|
+
/**
|
|
934
|
+
* The PRE-PR VALIDATION report: the outcome of running the service's configured check commands
|
|
935
|
+
* against the checkout after the agent settled and before opening a PR, plus how many repair
|
|
936
|
+
* rounds the harness spent. Present on BOTH outcomes — a passing report accompanies the opened
|
|
937
|
+
* PR (the captured proof), and a failing one accompanies the run's `error` (no PR was opened).
|
|
938
|
+
* Absent when the job carried no {@link AgentJob.validationChecks}.
|
|
939
|
+
*/
|
|
940
|
+
validationReport?: ValidationReport
|
|
879
941
|
/**
|
|
880
942
|
* Preview mode: the in-container URL the built app is served at (e.g. `http://localhost:4173`).
|
|
881
943
|
* This is NOT host-reachable on its own — the container runtime publishes the serve port to an
|
|
@@ -1282,6 +1344,7 @@ export function parseAgentJob(input: unknown): AgentJob {
|
|
|
1282
1344
|
testSecrets: parseTestSecrets(o.testSecrets),
|
|
1283
1345
|
guardLimits: parseGuardLimits(o.guardLimits),
|
|
1284
1346
|
validation: parseValidationSpec(o.validation),
|
|
1347
|
+
validationChecks: parseValidationChecksSpec(o.validationChecks),
|
|
1285
1348
|
reviewPrNumber: posInt(o.reviewPrNumber),
|
|
1286
1349
|
})
|
|
1287
1350
|
assertAllowedHost(job.repo.cloneUrl, 'repo.cloneUrl')
|
|
@@ -1319,6 +1382,7 @@ interface ParsedAgentJobParts {
|
|
|
1319
1382
|
testSecrets: ReturnType<typeof parseTestSecrets>
|
|
1320
1383
|
guardLimits: ReturnType<typeof parseGuardLimits>
|
|
1321
1384
|
validation: ReturnType<typeof parseValidationSpec>
|
|
1385
|
+
validationChecks: ReturnType<typeof parseValidationChecksSpec>
|
|
1322
1386
|
reviewPrNumber: number | undefined
|
|
1323
1387
|
}
|
|
1324
1388
|
|
|
@@ -1371,6 +1435,7 @@ function assembleAgentJob(
|
|
|
1371
1435
|
testSecrets,
|
|
1372
1436
|
guardLimits,
|
|
1373
1437
|
validation,
|
|
1438
|
+
validationChecks,
|
|
1374
1439
|
reviewPrNumber,
|
|
1375
1440
|
} = parts
|
|
1376
1441
|
const repo = (o.repo ?? {}) as Record<string, unknown>
|
|
@@ -1399,6 +1464,7 @@ function assembleAgentJob(
|
|
|
1399
1464
|
...(reviewPrNumber !== undefined ? { reviewPrNumber } : {}),
|
|
1400
1465
|
...(guardLimits ? { guardLimits } : {}),
|
|
1401
1466
|
...(validation ? { validation } : {}),
|
|
1467
|
+
...(validationChecks ? { validationChecks } : {}),
|
|
1402
1468
|
}
|
|
1403
1469
|
}
|
|
1404
1470
|
|
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
import { chmod, rm, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { chmod, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import type { PackageRegistrySpec } from './job.js'
|
|
5
5
|
import { registerKnownSecrets } from './redact.js'
|
|
6
6
|
|
|
7
7
|
// Private package-registry auth for the checkout's installs (npm private orgs,
|
|
8
|
-
// GitHub Packages). The job's allowlisted entries are rendered into
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// GitHub Packages). The job's allowlisted entries are rendered into an npmrc — read by
|
|
9
|
+
// npm, pnpm and yarn v1 alike, and inherited by every child process (the agent's own
|
|
10
|
+
// shell installs and the frontend-infra stand-up's) — so the token never rides argv or
|
|
11
|
+
// the checkout.
|
|
12
|
+
//
|
|
13
|
+
// WHERE that npmrc lands depends on whether the harness process owns its HOME:
|
|
14
|
+
// - container (the default): the user `~/.npmrc`. HOME belongs to that one container, so
|
|
15
|
+
// writing it is safe and a job with NO entries CLEARS it — warm-pool containers are
|
|
16
|
+
// reused across jobs and must not leak a prior workspace's token.
|
|
17
|
+
// - shared native host process (`ambientAuth`, the local native transport): HOME is the
|
|
18
|
+
// DEVELOPER's. Writing there would overwrite their own npm config, clearing there would
|
|
19
|
+
// DELETE it, and concurrent jobs in the one process would race on the single file. Such a
|
|
20
|
+
// job gets its own npmrc under a per-job directory instead, pointed at by
|
|
21
|
+
// `npm_config_userconfig`; the developer's file is never written and never removed.
|
|
22
|
+
//
|
|
23
|
+
// Note the isolated path trades a little reach for that safety: `~/.npmrc` is read by npm, pnpm
|
|
24
|
+
// and yarn v1 alike, whereas `npm_config_userconfig` is honoured by npm and pnpm but NOT by yarn
|
|
25
|
+
// (v1 or Berry). A yarn-based checkout on the native path therefore sees only the developer's own
|
|
26
|
+
// registries, not the job's. Since the alternative is overwriting the file they actually use, the
|
|
27
|
+
// limitation stands — a yarn repo needing private-registry auth wants the container path.
|
|
14
28
|
|
|
15
|
-
/** Where the per-job npm auth lands (the user npmrc, outside any checkout). */
|
|
29
|
+
/** Where the per-job npm auth lands in a container (the user npmrc, outside any checkout). */
|
|
16
30
|
export function npmrcPath(): string {
|
|
17
31
|
return join(homedir(), '.npmrc')
|
|
18
32
|
}
|
|
19
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Per-job isolation for the rendered npmrc. Set `isolatedDir` when the harness process is
|
|
36
|
+
* SHARED across concurrent jobs and its HOME is the developer's own — i.e. the local native
|
|
37
|
+
* host-process transport, which is exactly the set of jobs carrying `ambientAuth`. Absent ⇒
|
|
38
|
+
* the container default (`~/.npmrc`).
|
|
39
|
+
*/
|
|
40
|
+
export interface PackageRegistryScope {
|
|
41
|
+
/** A per-job directory (removed with the job) to hold this job's npmrc. */
|
|
42
|
+
isolatedDir?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
20
45
|
/**
|
|
21
46
|
* Render the job's registry entries as npmrc lines: each scope routed to its
|
|
22
47
|
* registry, plus one `_authToken` credential line per distinct host.
|
|
@@ -39,20 +64,75 @@ export function renderNpmrc(entries: readonly PackageRegistrySpec[]): string {
|
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
/**
|
|
42
|
-
* Write (or clear) the
|
|
43
|
-
*
|
|
67
|
+
* Write (or clear) the job's npmrc before the agent runs, and return the env the agent's child
|
|
68
|
+
* process needs to find it (empty for the container default, which npm picks up from HOME).
|
|
69
|
+
* Tokens are registered for output redaction so a token echoed in an npm error never reaches
|
|
44
70
|
* logs or stored output.
|
|
45
71
|
*/
|
|
46
72
|
export async function configurePackageRegistries(
|
|
47
73
|
entries: readonly PackageRegistrySpec[] | undefined,
|
|
48
|
-
|
|
74
|
+
scope: PackageRegistryScope = {},
|
|
75
|
+
): Promise<Record<string, string>> {
|
|
76
|
+
const hasEntries = Boolean(entries?.length)
|
|
77
|
+
if (scope.isolatedDir) {
|
|
78
|
+
// A job with no entries needs no file at all: emitting no override leaves the developer's
|
|
79
|
+
// own `~/.npmrc` in effect (their private registries keep working) — and, crucially, leaves
|
|
80
|
+
// it ALONE. Clearing a stale file is a container concern; here nothing stale can exist,
|
|
81
|
+
// because the per-job dir is created and removed with the job.
|
|
82
|
+
if (!hasEntries) return {}
|
|
83
|
+
const path = join(scope.isolatedDir, '.npmrc')
|
|
84
|
+
await writeIsolatedNpmrc(path, entries!)
|
|
85
|
+
return { npm_config_userconfig: path }
|
|
86
|
+
}
|
|
49
87
|
const path = npmrcPath()
|
|
50
|
-
if (!
|
|
88
|
+
if (!hasEntries) {
|
|
51
89
|
await rm(path, { force: true })
|
|
52
|
-
return
|
|
90
|
+
return {}
|
|
53
91
|
}
|
|
54
|
-
registerKnownSecrets(entries
|
|
55
|
-
await writeFile(path, renderNpmrc(entries), { mode: 0o600 })
|
|
92
|
+
registerKnownSecrets(entries!.map((entry) => entry.token))
|
|
93
|
+
await writeFile(path, renderNpmrc(entries!), { mode: 0o600 })
|
|
56
94
|
// writeFile's mode only applies on create — tighten an existing file too.
|
|
57
95
|
await chmod(path, 0o600)
|
|
96
|
+
return {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Write the per-job npmrc, seeded from the developer's own `~/.npmrc` when they have one so
|
|
101
|
+
* their unrelated settings (a corporate registry, a proxy) keep working for this run. The job's
|
|
102
|
+
* lines are APPENDED, and npm resolves the last occurrence of a key, so the job's entries win on
|
|
103
|
+
* any host they both configure. Copying their file into a 0600 temp adds no exposure: an ambient
|
|
104
|
+
* run already has the developer's full file access by definition.
|
|
105
|
+
*
|
|
106
|
+
* The seeded credentials are registered for redaction alongside the job's own. The job's tokens
|
|
107
|
+
* were always registered; the developer's were not, because before this path existed their file
|
|
108
|
+
* was overwritten and no credential of theirs was in play during the run. Now that theirs is in
|
|
109
|
+
* effect, an npm error echoing one must be scrubbed on exactly the same terms.
|
|
110
|
+
*/
|
|
111
|
+
async function writeIsolatedNpmrc(
|
|
112
|
+
path: string,
|
|
113
|
+
entries: readonly PackageRegistrySpec[],
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
registerKnownSecrets(entries.map((entry) => entry.token))
|
|
116
|
+
// Best-effort: no personal npmrc (or an unreadable one) just means the job's entries stand alone.
|
|
117
|
+
const inherited = await readFile(npmrcPath(), 'utf8').catch(() => '')
|
|
118
|
+
registerKnownSecrets(npmrcCredentials(inherited))
|
|
119
|
+
const prefix = inherited && !inherited.endsWith('\n') ? `${inherited}\n` : inherited
|
|
120
|
+
await writeFile(path, `${prefix}${renderNpmrc(entries)}`, { mode: 0o600 })
|
|
121
|
+
await chmod(path, 0o600)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The credential VALUES in npmrc content: the three keys npm accepts a secret under, on any host
|
|
126
|
+
* line. Used to register a seeded (developer-owned) file's tokens for redaction. An `${ENV_VAR}`
|
|
127
|
+
* reference is not itself a secret — npm expands it at read time — so it is skipped rather than
|
|
128
|
+
* registered as a literal to scrub.
|
|
129
|
+
*/
|
|
130
|
+
export function npmrcCredentials(content: string): string[] {
|
|
131
|
+
const found: string[] = []
|
|
132
|
+
for (const line of content.split(/\r?\n/)) {
|
|
133
|
+
const match = /^\s*(?:.*:)?_(?:authToken|auth|password)\s*=\s*(.+?)\s*$/.exec(line)
|
|
134
|
+
const value = match?.[1]?.replace(/^["']|["']$/g, '')
|
|
135
|
+
if (value && !/^\$\{.*\}$/.test(value)) found.push(value)
|
|
136
|
+
}
|
|
137
|
+
return found
|
|
58
138
|
}
|
package/src/pi-workspace.ts
CHANGED
|
@@ -241,11 +241,13 @@ export async function runAgentInWorkspace(
|
|
|
241
241
|
// harness paths; kept out of the agent's commits via a local git exclude entry.
|
|
242
242
|
const contextFiles = spec.contextFiles ?? []
|
|
243
243
|
await materializeContextFiles(spec.dir, contextFiles)
|
|
244
|
-
// Repo-sourced skill (slice 2): claude-code installs it natively
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
244
|
+
// Repo-sourced skill (slice 2): claude-code installs it natively into its ISOLATED config dir,
|
|
245
|
+
// so it reads from there. Everything else reads the checkout, so materialise the skill's
|
|
246
|
+
// resources under `.cat-context/skill/` (its instructions are folded into the prompt by the
|
|
247
|
+
// backend) — Pi, codex, and AMBIENT claude-code, which has no isolated config dir to install
|
|
248
|
+
// into (the runner refuses to write a repo's skill into the developer's own `~/.claude`; see
|
|
249
|
+
// `runClaudeCode`). A resource-free skill is a no-op here.
|
|
250
|
+
if (spec.skill && !installsSkillNatively(spec)) {
|
|
249
251
|
await materializeSkillResources(spec.dir, spec.skill)
|
|
250
252
|
}
|
|
251
253
|
|
|
@@ -268,6 +270,7 @@ export async function runAgentInWorkspace(
|
|
|
268
270
|
subscriptionBaseUrl: spec.subscriptionBaseUrl,
|
|
269
271
|
...(spec.ambientAuth ? { ambientAuth: true } : {}),
|
|
270
272
|
...(spec.skill ? { skill: spec.skill } : {}),
|
|
273
|
+
...(opts.agentEnv ? { extraEnv: opts.agentEnv } : {}),
|
|
271
274
|
signal: opts.signal,
|
|
272
275
|
onActivity: opts.onActivity,
|
|
273
276
|
onProgress: opts.onProgress,
|
|
@@ -293,9 +296,11 @@ export async function runAgentInWorkspace(
|
|
|
293
296
|
// container env, which `webSearchConfigFromEnv` autodetects.
|
|
294
297
|
// The proxy vars are handed to Pi's child via `extraEnv` (not the harness's own
|
|
295
298
|
// process.env), so detection runs against the same merged view the extension sees.
|
|
296
|
-
const extraEnv: Record<string, string> =
|
|
297
|
-
? webSearchProxyEnv(proxyBaseUrl, sessionToken)
|
|
298
|
-
|
|
299
|
+
const extraEnv: Record<string, string> = {
|
|
300
|
+
...(spec.webSearchProxy ? webSearchProxyEnv(proxyBaseUrl, sessionToken) : {}),
|
|
301
|
+
// Per-job env (tester secrets, a private-registry npmrc pointer) — see `RunOptions.agentEnv`.
|
|
302
|
+
...opts.agentEnv,
|
|
303
|
+
}
|
|
299
304
|
const webSearch = webSearchConfigFromEnv({ ...process.env, ...extraEnv })
|
|
300
305
|
if (webSearch) await writeWebToolsConfig(webSearch)
|
|
301
306
|
await writeAgentsContext(spec.systemPrompt, {
|
|
@@ -325,6 +330,20 @@ export async function runAgentInWorkspace(
|
|
|
325
330
|
return withEffortReport(spec.dir, piOutcome)
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Whether the claude-code runner will install this run's repo-sourced skill natively (into the
|
|
335
|
+
* CLI's config dir) rather than the caller materialising it into the checkout. True ONLY for a
|
|
336
|
+
* leased-credential claude-code run, which gets a throwaway per-run config home. An AMBIENT run
|
|
337
|
+
* uses the developer's own `~/.claude`, which the runner will not write a repo's skill into —
|
|
338
|
+
* it would outlive the run in their personal setup, and two concurrent jobs carrying same-named
|
|
339
|
+
* skills from different repos would overwrite each other's.
|
|
340
|
+
*/
|
|
341
|
+
export function installsSkillNatively(
|
|
342
|
+
spec: Pick<AgentRunSpec, 'harness' | 'ambientAuth'>,
|
|
343
|
+
): boolean {
|
|
344
|
+
return spec.harness === 'claude-code' && !spec.ambientAuth
|
|
345
|
+
}
|
|
346
|
+
|
|
328
347
|
/**
|
|
329
348
|
* Lift the agent's effort self-assessment off its sentinel file in `dir` and fold it onto the
|
|
330
349
|
* run outcome. Shared by both harness paths so EVERY container agent's effort report is captured
|
package/src/pi.ts
CHANGED
|
@@ -250,9 +250,10 @@ export const SKILL_CONTEXT_SUBDIR = 'skill'
|
|
|
250
250
|
|
|
251
251
|
/**
|
|
252
252
|
* Materialise a repo-sourced skill's RESOURCE files under `.cat-context/skill/` in the checkout
|
|
253
|
-
* (repo-sourced Claude Skills, slice 2) — the
|
|
254
|
-
*
|
|
255
|
-
* the
|
|
253
|
+
* (repo-sourced Claude Skills, slice 2) — the path for every run that does NOT get a native
|
|
254
|
+
* install: Pi, codex, and ambient claude-code (no isolated `CLAUDE_CONFIG_DIR` to install into).
|
|
255
|
+
* Their agents read the checkout, and the skill's instructions are folded into their prompt by the
|
|
256
|
+
* backend (`renderSkillForHarness`, which keys off ambient auth as well as the harness). Resource sub-paths were sanitized at the job boundary (no traversal), so nested
|
|
256
257
|
* dirs are created as needed. Kept out of the agent's commits via the same `.cat-context/` git
|
|
257
258
|
* exclude entry. A skill with no resource bodies is a no-op.
|
|
258
259
|
*/
|
package/src/runner.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { redactSecrets } from './redact.js'
|
|
2
2
|
import type { FollowUpLine } from './follow-ups.js'
|
|
3
|
+
import type { ValidationReport } from './validation-checks.js'
|
|
3
4
|
import type { HarnessCallMetric, TodoProgress, ToolSpan } from './pi.js'
|
|
4
5
|
import { log, type Logger } from './logger.js'
|
|
5
6
|
import {
|
|
@@ -30,6 +31,13 @@ export interface RunOptions {
|
|
|
30
31
|
onSpan?: (span: ToolSpan) => void
|
|
31
32
|
/** Receives the forward-looking follow-up / question items the Coder streamed since the last poll. */
|
|
32
33
|
onFollowUp?: (items: FollowUpLine[]) => void
|
|
34
|
+
/**
|
|
35
|
+
* Receives each completed PRE-PR VALIDATION attempt the moment the harness finishes running
|
|
36
|
+
* the service's check commands, so the backend can surface the repair loop LIVE ("lint failed,
|
|
37
|
+
* repairing — attempt 2 of 3") instead of only in the terminal result. Latest-wins (NOT a drain
|
|
38
|
+
* buffer): a published attempt is final, and the loop republishes a whole new one per round.
|
|
39
|
+
*/
|
|
40
|
+
onValidationReport?: (report: ValidationReport) => void
|
|
33
41
|
/**
|
|
34
42
|
* Receives each per-call telemetry row the moment the agent's CLI stream yields it, so a
|
|
35
43
|
* run's model calls reach `llm_call_metrics` WHILE it runs rather than only in its terminal
|
|
@@ -51,6 +59,17 @@ export interface RunOptions {
|
|
|
51
59
|
onPhase?: (phase: string) => void
|
|
52
60
|
/** A per-job child logger carrying the run's correlation fields (jobId, repo, branch, …). */
|
|
53
61
|
log?: Logger
|
|
62
|
+
/**
|
|
63
|
+
* Extra environment for the agent's child process, scoped to THIS job. The CLI is spawned with
|
|
64
|
+
* `{...process.env, ...agentEnv}`, so these reach the agent and every shell tool it spawns.
|
|
65
|
+
*
|
|
66
|
+
* This is the seam for anything per-job that would otherwise be written to a process- or
|
|
67
|
+
* HOME-global (the tester's secrets, a private-registry npmrc pointer). Those globals are only
|
|
68
|
+
* per-job when the process is — true for a container, FALSE for the local native host-process
|
|
69
|
+
* transport, which serves every concurrent ambient job from one process on the developer's own
|
|
70
|
+
* HOME. Set it via `withAgentEnv`; never mutate `process.env` for a job.
|
|
71
|
+
*/
|
|
72
|
+
agentEnv?: Record<string, string>
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
export type JobState = 'running' | 'done' | 'failed'
|
|
@@ -151,6 +170,13 @@ export interface JobView<TResult extends JobResultBase = JobResultBase> {
|
|
|
151
170
|
* Absent on a job that produced output promptly (the overwhelming common case). Sticky once set.
|
|
152
171
|
*/
|
|
153
172
|
coldStart?: { atMs: number; message: string }
|
|
173
|
+
/**
|
|
174
|
+
* The LATEST completed pre-PR validation attempt (see `docs/initiatives/pre-pr-validation.md`).
|
|
175
|
+
* Unlike {@link spans}/{@link followUps} this is NOT drain-on-read: it is a whole-value latest
|
|
176
|
+
* publish, so re-reading it on a later poll is harmless and a dropped poll loses nothing (the
|
|
177
|
+
* next round republishes). Absent for a job whose service configured no checks.
|
|
178
|
+
*/
|
|
179
|
+
validationReport?: ValidationReport
|
|
154
180
|
}
|
|
155
181
|
|
|
156
182
|
interface JobEntry<TResult extends JobResultBase> extends JobView<TResult> {
|
|
@@ -414,6 +440,9 @@ export class JobRegistry<TJob = unknown, TResult extends JobResultBase = JobResu
|
|
|
414
440
|
onFollowUp: (items) => {
|
|
415
441
|
entry.followUpBuffer.push(...items)
|
|
416
442
|
},
|
|
443
|
+
onValidationReport: (report) => {
|
|
444
|
+
entry.validationReport = report
|
|
445
|
+
},
|
|
417
446
|
onCallMetric: (call) => {
|
|
418
447
|
// Stamp the job-scoped sequence on the metric OBJECT: the handler keeps the same
|
|
419
448
|
// instance for its terminal result, so both channels carry the same `seq` and the
|