@cat-factory/executor-harness 1.54.0 → 1.58.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 +9 -1
- package/dist/agent.js +39 -1
- package/dist/coding-agent.js +103 -18
- package/dist/job.js +48 -1
- package/dist/runner.js +3 -0
- package/dist/validation-checks.js +300 -0
- package/package.json +5 -5
- package/src/agent.ts +50 -2
- package/src/coding-agent.ts +127 -6
- package/src/job.ts +66 -0
- package/src/runner.ts +18 -0
- package/src/validation-checks.ts +395 -0
package/README.md
CHANGED
|
@@ -59,7 +59,13 @@ The implementation job (`POST /run`) is the canonical sequence:
|
|
|
59
59
|
Pi reads and concatenates both), and point Pi at the Worker's LLM proxy via
|
|
60
60
|
`~/.pi/agent/models.json` (provider `proxy`, `api: openai-completions`),
|
|
61
61
|
3. **run Pi** non-interactively (`pi -p --mode json --model proxy/<model> --approve`),
|
|
62
|
-
4. **
|
|
62
|
+
4. **validate** the checkout, when the job body carries `validationChecks` — the service's
|
|
63
|
+
configured check commands (install/lint/test/build) run with `sh -c` in the checkout, and
|
|
64
|
+
while they fail and the attempt budget remains the agent is re-run with the captured output
|
|
65
|
+
as its instruction (see [pre-PR validation](../../../docs/initiatives/pre-pr-validation.md)),
|
|
66
|
+
5. **commit, push** a branch and **open a PR**, returning `{ prUrl, branch, summary }` — but
|
|
67
|
+
ONLY if step 4 ended green. A spent budget returns an error result with the validation report
|
|
68
|
+
and opens no PR. Absent `validationChecks`, step 4 does not happen at all.
|
|
63
69
|
|
|
64
70
|
Bootstrap differs at the ends — it may start from an empty dir, and **resets
|
|
65
71
|
history to one commit and force-pushes** the default branch instead of opening a
|
|
@@ -127,6 +133,7 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
127
133
|
| `src/package-registries.ts` | Private-registry (npm) auth: renders the job's allowlisted entries into an npmrc — the user `~/.npmrc` in a container, a per-job file pointed at by `npm_config_userconfig` for a native job. |
|
|
128
134
|
| `src/agent-runner.ts` | The subscription-harness runners (`runClaudeCode` / `runCodex`) — talk direct to the vendor with a leased OAuth token, lift per-turn usage/telemetry off the CLI event stream. |
|
|
129
135
|
| `src/transcript-retention.ts` | Lifts the CLI session transcripts (`projects/` / `sessions/`) out of the isolated, credential-bearing config home before it is deleted, and prunes them on a TTL (debugging artifact retention). |
|
|
136
|
+
| `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. |
|
|
130
137
|
| `src/logger.ts` | Structured logging. |
|
|
131
138
|
|
|
132
139
|
## Runner lifecycle knobs
|
|
@@ -139,6 +146,7 @@ runner):
|
|
|
139
146
|
| `PORT` | `8080` | HTTP port the harness listens on. |
|
|
140
147
|
| `JOB_MAX_DURATION_MS` | `3600000` (60m) | Hard ceiling on a job's wall-clock time; force-fails after. |
|
|
141
148
|
| `JOB_INACTIVITY_MS` | `600000` (10m) | Kills a hung agent that produces no output for this long. |
|
|
149
|
+
| `VALIDATION_COMMAND_TIMEOUT_MS` | `900000` (15m) | Per-command watchdog for a pre-PR validation check; a timeout counts as a failure (exit 124) so one hung command can't wedge the loop. |
|
|
142
150
|
| `HARNESS_TRANSCRIPT_TTL_MS` | `259200000` (3d) | How long lifted subscription-CLI session transcripts are kept before the retention sweep prunes them. |
|
|
143
151
|
| `HARNESS_TRANSCRIPT_ROOT` | `<tmpdir>/cf-agent-transcripts` | Where retained session transcripts are moved to (one dir per run). Meaningful only on a reused (warm-pool) container; a per-run container is torn down with the job. The TTL sweep deletes only dirs it created (each carries a `.cf-retained` marker), so pointing this at a shared directory never touches unrelated content — though a dedicated dir is still recommended. An override on a different filesystem than the config home falls back to copy-then-remove. |
|
|
144
152
|
|
package/dist/agent.js
CHANGED
|
@@ -8,6 +8,7 @@ import { configurePackageRegistries } from './package-registries.js';
|
|
|
8
8
|
import { captureRedactedOutput, redactSecrets, registerKnownSecrets } from './redact.js';
|
|
9
9
|
import { cloneRepo, commitAll, conflictDiff, fetchPullRequestHead, fetchReferenceBranches, hasAgentChanges, headCommit, inferVcsProvider, mergeBranch, openPullRequest, prepareExistingCheckout, pushBranch, reinitAndPush, unmergedPaths, } from './git.js';
|
|
10
10
|
import { makeDirClaimer, noChangesReason, runCodingAgent, runMultiRepoCoding, } from './coding-agent.js';
|
|
11
|
+
import { validationFailureMessage } from './validation-checks.js';
|
|
11
12
|
import { acquireRepoCheckout, agentNeverActed, agentOutputTail, NEVER_ACTED_CAUSE, runAgentInWorkspace, unusableFinalAnswerCause, withWorkspace, } from './pi-workspace.js';
|
|
12
13
|
import { diagnosticsSuffix, resolveStructuredOutput, } from './structured-output.js';
|
|
13
14
|
import { log } from './logger.js';
|
|
@@ -802,6 +803,11 @@ function buildSingleRepoCodingSpec(job, pushBranch) {
|
|
|
802
803
|
},
|
|
803
804
|
}
|
|
804
805
|
: {}),
|
|
806
|
+
// Pre-PR validation: the service's check commands, run against the checkout BEFORE the PR
|
|
807
|
+
// opens with failures fed back to the agent (see docs/initiatives/pre-pr-validation.md).
|
|
808
|
+
// Forwarded straight off the job body — the loop is generic machinery keyed on the data, not
|
|
809
|
+
// on the agent kind.
|
|
810
|
+
...(job.validationChecks ? { validationChecks: job.validationChecks } : {}),
|
|
805
811
|
};
|
|
806
812
|
}
|
|
807
813
|
/**
|
|
@@ -812,12 +818,38 @@ function buildSingleRepoCodingSpec(job, pushBranch) {
|
|
|
812
818
|
*/
|
|
813
819
|
async function runSingleRepoCoding(job, opts) {
|
|
814
820
|
const pushBranch = job.pushBranch ?? job.newBranch ?? job.branch;
|
|
815
|
-
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, effortReport } = await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts);
|
|
821
|
+
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, validationReport, effortReport, } = await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts);
|
|
816
822
|
// Ralph loop: the harness-computed validation verdict, forwarded onto the coding result as
|
|
817
823
|
// `ralphVerdict` so the backend's `toRunResult` lifts it onto `AgentRunResult.ralphVerdict`.
|
|
818
824
|
const ralphVerdict = validation ? { ralphVerdict: validation } : {};
|
|
819
825
|
// The agent's effort self-assessment, spread onto every result path below (mirrors ralphVerdict).
|
|
820
826
|
const effort = effortReport ? { effortReport } : {};
|
|
827
|
+
// The pre-PR validation report, spread onto every result path below: on the passing path it is
|
|
828
|
+
// the captured proof the checkout was green when the PR opened; on the exhausted path it is the
|
|
829
|
+
// evidence behind the failure below. Absent when the service configured no checks.
|
|
830
|
+
const validationFields = validationReport ? { validationReport } : {};
|
|
831
|
+
// Pre-PR validation spent its attempt budget with the checkout still red. FAIL the job — do
|
|
832
|
+
// NOT open a pull request, and do not pretend the push succeeded as a deliverable. The work is
|
|
833
|
+
// still on the branch (a retry resumes on it); the report carries each failing command's exit
|
|
834
|
+
// code and captured output so the step's failure detail says exactly what broke.
|
|
835
|
+
if (validationReport && !validationReport.passed) {
|
|
836
|
+
return {
|
|
837
|
+
// The work IS on the branch (the loop only runs for a pass that produced some, and the
|
|
838
|
+
// harness pushes it) — a retry resumes on top of it. `error` is what marks the job failed;
|
|
839
|
+
// reporting `pushed: false` here would misdescribe the branch state in the harness's own
|
|
840
|
+
// result for no benefit.
|
|
841
|
+
pushed,
|
|
842
|
+
branch: pushBranch,
|
|
843
|
+
summary,
|
|
844
|
+
stats,
|
|
845
|
+
error: validationFailureMessage(validationReport),
|
|
846
|
+
failureCause: 'agent',
|
|
847
|
+
...(usage ? { usage } : {}),
|
|
848
|
+
...(callMetrics ? { callMetrics } : {}),
|
|
849
|
+
...validationFields,
|
|
850
|
+
...effort,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
821
853
|
if (!pushed) {
|
|
822
854
|
// A no-op: a failure for the implementer, a clean non-event for the fixers.
|
|
823
855
|
if (job.noChangesIsError === false) {
|
|
@@ -829,6 +861,7 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
829
861
|
...(usage ? { usage } : {}),
|
|
830
862
|
...(callMetrics ? { callMetrics } : {}),
|
|
831
863
|
...ralphVerdict,
|
|
864
|
+
...validationFields,
|
|
832
865
|
...effort,
|
|
833
866
|
};
|
|
834
867
|
}
|
|
@@ -841,6 +874,7 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
841
874
|
failureCause: 'no-changes',
|
|
842
875
|
...(usage ? { usage } : {}),
|
|
843
876
|
...(callMetrics ? { callMetrics } : {}),
|
|
877
|
+
...validationFields,
|
|
844
878
|
...effort,
|
|
845
879
|
};
|
|
846
880
|
}
|
|
@@ -874,6 +908,7 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
874
908
|
stats,
|
|
875
909
|
...(usage ? { usage } : {}),
|
|
876
910
|
...(callMetrics ? { callMetrics } : {}),
|
|
911
|
+
...validationFields,
|
|
877
912
|
...effort,
|
|
878
913
|
};
|
|
879
914
|
}
|
|
@@ -886,6 +921,7 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
886
921
|
failureCause: 'no-changes',
|
|
887
922
|
...(usage ? { usage } : {}),
|
|
888
923
|
...(callMetrics ? { callMetrics } : {}),
|
|
924
|
+
...validationFields,
|
|
889
925
|
...effort,
|
|
890
926
|
};
|
|
891
927
|
}
|
|
@@ -898,6 +934,7 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
898
934
|
...(usage ? { usage } : {}),
|
|
899
935
|
...(callMetrics ? { callMetrics } : {}),
|
|
900
936
|
...ralphVerdict,
|
|
937
|
+
...validationFields,
|
|
901
938
|
...effort,
|
|
902
939
|
};
|
|
903
940
|
}
|
|
@@ -909,6 +946,7 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
909
946
|
...(usage ? { usage } : {}),
|
|
910
947
|
...(callMetrics ? { callMetrics } : {}),
|
|
911
948
|
...ralphVerdict,
|
|
949
|
+
...validationFields,
|
|
912
950
|
...effort,
|
|
913
951
|
};
|
|
914
952
|
}
|
package/dist/coding-agent.js
CHANGED
|
@@ -8,6 +8,7 @@ import { FOLLOW_UPS_FILENAME, FollowUpTailer } from './follow-ups.js';
|
|
|
8
8
|
import { EFFORT_REPORT_FILE } from './effort.js';
|
|
9
9
|
import { acquireRepoCheckout, agentNeverActed, agentOutputTail, runAgentInWorkspace, withWorkspace, } from './pi-workspace.js';
|
|
10
10
|
import { log } from './logger.js';
|
|
11
|
+
import { runValidationLoop, } from './validation-checks.js';
|
|
11
12
|
/**
|
|
12
13
|
* How often the harness checkpoints the agent's work mid-run by pushing the branch.
|
|
13
14
|
* A per-run container can be evicted at any moment; pushing the agent's commits
|
|
@@ -128,28 +129,57 @@ export async function runCodingAgent(spec, opts = {}) {
|
|
|
128
129
|
}, followUpPollIntervalMs());
|
|
129
130
|
followUpTick.unref?.();
|
|
130
131
|
}
|
|
132
|
+
// One agent pass over this checkout, parameterised only by the prompt — so the pre-PR
|
|
133
|
+
// validation loop below can re-run the agent with a repair instruction without
|
|
134
|
+
// re-deriving (or drifting from) the dispatch's own settings.
|
|
135
|
+
const runAgentPass = (userPrompt) => runAgentInWorkspace({
|
|
136
|
+
dir: workDir,
|
|
137
|
+
systemPrompt: spec.systemPrompt,
|
|
138
|
+
userPrompt,
|
|
139
|
+
model: spec.model,
|
|
140
|
+
harness: spec.harness,
|
|
141
|
+
subscriptionToken: spec.subscriptionToken,
|
|
142
|
+
subscriptionBaseUrl: spec.subscriptionBaseUrl,
|
|
143
|
+
ambientAuth: spec.ambientAuth,
|
|
144
|
+
proxyBaseUrl: spec.proxyBaseUrl,
|
|
145
|
+
sessionToken: spec.sessionToken,
|
|
146
|
+
serviceDirectory,
|
|
147
|
+
webToolsGuidance: spec.webToolsGuidance,
|
|
148
|
+
webSearchProxy: spec.webSearchProxy,
|
|
149
|
+
guardLimits: spec.guardLimits,
|
|
150
|
+
...(spec.skill ? { skill: spec.skill } : {}),
|
|
151
|
+
}, opts);
|
|
131
152
|
let outcome;
|
|
132
153
|
try {
|
|
133
154
|
opts.onPhase?.('agent');
|
|
134
155
|
logger.info('coding-agent: running agent', { serviceDirectory });
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
let agentRun = await runAgentPass(spec.userPrompt);
|
|
157
|
+
// PRE-PR VALIDATION: run the service's configured checks against the checkout and, while
|
|
158
|
+
// they fail and budget remains, hand the captured output back to the agent and run it
|
|
159
|
+
// again. Sits BETWEEN the agent and the finalize/push/PR step so a red checkout never
|
|
160
|
+
// reaches `openPullRequest` — the whole point of the feature. Keyed purely off the job
|
|
161
|
+
// body carrying checks (no agent-kind switch); absent ⇒ this is a no-op and the flow
|
|
162
|
+
// below is byte-for-byte what it was.
|
|
163
|
+
const validationChecks = spec.validationChecks;
|
|
164
|
+
let validationReport;
|
|
165
|
+
if (validationChecks && (await producedWork(dir, spec, baseSha, resumed, opts))) {
|
|
166
|
+
validationReport = await runValidationLoop({
|
|
167
|
+
workDir,
|
|
168
|
+
spec: validationChecks,
|
|
169
|
+
logger,
|
|
170
|
+
opts,
|
|
171
|
+
runAgentPass,
|
|
172
|
+
onAgentPass: (run) => {
|
|
173
|
+
agentRun = mergeAgentPasses(agentRun, run);
|
|
174
|
+
},
|
|
175
|
+
// The checks run against the WORKING TREE, but only tracked edits are staged for the
|
|
176
|
+
// push — so a repair round can go green on a new file the PR would never contain.
|
|
177
|
+
// Name those files in the next repair prompt so the agent adds them.
|
|
178
|
+
listUncommittedNewFiles: () => listUntrackedFiles(workDir, opts.signal),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
152
181
|
outcome = await finalizeCodingRun({
|
|
182
|
+
validationReport,
|
|
153
183
|
dir,
|
|
154
184
|
spec,
|
|
155
185
|
logger,
|
|
@@ -284,7 +314,7 @@ async function prepareCodingCheckout(dir, spec, logger, opts) {
|
|
|
284
314
|
* {@link runCodingAgent} so its body stays small; returns the built {@link CodingAgentOutcome}.
|
|
285
315
|
*/
|
|
286
316
|
async function finalizeCodingRun(args) {
|
|
287
|
-
const { dir, spec, logger, opts, baseSha, resumed, workDir, checkpoint, followUpTick, followUpTailer, pushWorkOnce, inFlightPush, agentRun, } = args;
|
|
317
|
+
const { validationReport, dir, spec, logger, opts, baseSha, resumed, workDir, checkpoint, followUpTick, followUpTailer, pushWorkOnce, inFlightPush, agentRun, } = args;
|
|
288
318
|
const { signal } = opts;
|
|
289
319
|
const { summary, stats, stderrTail, usage, callMetrics, effortReport } = agentRun;
|
|
290
320
|
let outcome;
|
|
@@ -368,8 +398,63 @@ async function finalizeCodingRun(args) {
|
|
|
368
398
|
if (spec.validation) {
|
|
369
399
|
outcome.validation = await runRalphValidation(workDir, spec.validation, logger, opts);
|
|
370
400
|
}
|
|
401
|
+
// Pre-PR validation: the loop already ran (before this finalize, so a red checkout never
|
|
402
|
+
// reaches the PR-opening caller); attach its verdict for the caller to gate on and for the
|
|
403
|
+
// backend to record on the step.
|
|
404
|
+
if (validationReport)
|
|
405
|
+
outcome.validationReport = validationReport;
|
|
371
406
|
return outcome;
|
|
372
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Whether this pass produced anything worth VALIDATING — i.e. the branch advanced past its
|
|
410
|
+
* pre-run tip (or the run resumed an earlier one's pushed work). Gates the pre-PR validation
|
|
411
|
+
* loop, for two reasons: a run that changed nothing has nothing to check, and its real failure
|
|
412
|
+
* is "the agent produced no file changes" — reporting a red BASE branch instead would blame the
|
|
413
|
+
* run for a pre-existing condition it never touched (and burn the whole repair budget re-running
|
|
414
|
+
* an agent that already declined to act).
|
|
415
|
+
*
|
|
416
|
+
* Commits forgotten edits to tracked files first, exactly as {@link finalizeCodingRun} does, so
|
|
417
|
+
* an agent that edited-but-didn't-commit still counts as work. That call is idempotent, so
|
|
418
|
+
* finalize repeating it later is a no-op. Uncommitted NEW files are invisible here — but they
|
|
419
|
+
* are equally invisible to finalize, so a run whose only product is an uncommitted new file is
|
|
420
|
+
* a no-op on both paths, and the checks would have nothing to gate anyway.
|
|
421
|
+
*/
|
|
422
|
+
async function producedWork(dir, spec, baseSha, resumed, opts) {
|
|
423
|
+
await commitTrackedEdits(dir, spec.commitMessage, opts.signal);
|
|
424
|
+
return resumed || (await branchHasCommitsSince(dir, baseSha, opts.signal));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Fold a pre-PR validation REPAIR pass's run into the accumulated agent outcome, so a looped run
|
|
428
|
+
* reports what every round actually spent rather than only the first. Counts and telemetry are
|
|
429
|
+
* summed/concatenated; the single-valued fields (the summary the backend renders, the effort
|
|
430
|
+
* report, the diagnostics that judge the FINAL answer) take the LATEST pass, which is the one
|
|
431
|
+
* whose state the PR is opened from.
|
|
432
|
+
*/
|
|
433
|
+
function mergeAgentPasses(previous, next) {
|
|
434
|
+
return {
|
|
435
|
+
...next,
|
|
436
|
+
stats: {
|
|
437
|
+
toolCalls: (previous.stats?.toolCalls ?? 0) + (next.stats?.toolCalls ?? 0),
|
|
438
|
+
assistantChars: (previous.stats?.assistantChars ?? 0) + (next.stats?.assistantChars ?? 0),
|
|
439
|
+
},
|
|
440
|
+
...(previous.usage || next.usage
|
|
441
|
+
? {
|
|
442
|
+
usage: {
|
|
443
|
+
inputTokens: (previous.usage?.inputTokens ?? 0) + (next.usage?.inputTokens ?? 0),
|
|
444
|
+
outputTokens: (previous.usage?.outputTokens ?? 0) + (next.usage?.outputTokens ?? 0),
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
448
|
+
...(previous.callMetrics || next.callMetrics
|
|
449
|
+
? { callMetrics: [...(previous.callMetrics ?? []), ...(next.callMetrics ?? [])] }
|
|
450
|
+
: {}),
|
|
451
|
+
// The repair pass's own effort report wins when it wrote one; otherwise keep the first
|
|
452
|
+
// pass's rather than losing the assessment entirely.
|
|
453
|
+
...((next.effortReport ?? previous.effortReport)
|
|
454
|
+
? { effortReport: next.effortReport ?? previous.effortReport }
|
|
455
|
+
: {}),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
373
458
|
/**
|
|
374
459
|
* The Ralph-loop validation watchdog: the longest a completion command may run before it is
|
|
375
460
|
* killed and treated as a failure (a hung `pnpm test` must never block the loop forever).
|
package/dist/job.js
CHANGED
|
@@ -66,6 +66,39 @@ function parseValidationSpec(value) {
|
|
|
66
66
|
...(iteration !== undefined ? { iteration } : {}),
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse the optional PRE-PR VALIDATION CHECKS spec (see
|
|
71
|
+
* docs/initiatives/pre-pr-validation.md): the service's ordered `{ label, command }` pairs and
|
|
72
|
+
* the repair-round budget. Every entry needs a non-empty command; entries without one are
|
|
73
|
+
* dropped, and a spec that ends up with no usable check returns `undefined` — so a malformed
|
|
74
|
+
* body degrades to the exact pre-feature behaviour (no loop, PR opens as before) rather than
|
|
75
|
+
* failing an otherwise-good coding run. `maxAttempts` is clamped to a sane range so a bad body
|
|
76
|
+
* can't make a container loop forever.
|
|
77
|
+
*/
|
|
78
|
+
function parseValidationChecksSpec(value) {
|
|
79
|
+
if (typeof value !== 'object' || value === null)
|
|
80
|
+
return undefined;
|
|
81
|
+
const o = value;
|
|
82
|
+
if (!Array.isArray(o.checks))
|
|
83
|
+
return undefined;
|
|
84
|
+
const checks = [];
|
|
85
|
+
for (const raw of o.checks) {
|
|
86
|
+
if (typeof raw !== 'object' || raw === null)
|
|
87
|
+
continue;
|
|
88
|
+
const c = raw;
|
|
89
|
+
if (typeof c.command !== 'string' || c.command.trim() === '')
|
|
90
|
+
continue;
|
|
91
|
+
const label = typeof c.label === 'string' && c.label.trim() ? c.label.trim() : c.command;
|
|
92
|
+
checks.push({ label, command: c.command });
|
|
93
|
+
}
|
|
94
|
+
if (checks.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
const parsed = posInt(o.maxAttempts);
|
|
97
|
+
return {
|
|
98
|
+
checks,
|
|
99
|
+
maxAttempts: Math.min(parsed ?? VALIDATION_DEFAULT_MAX_ATTEMPTS, VALIDATION_MAX_ATTEMPTS_CEILING),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
69
102
|
/**
|
|
70
103
|
* Parse the shared per-job auth fields, validating per harness: a subscription
|
|
71
104
|
* harness (`claude-code` / `codex`) requires `subscriptionToken`; the default Pi
|
|
@@ -353,6 +386,18 @@ export function parseTestSecrets(value) {
|
|
|
353
386
|
}
|
|
354
387
|
return entries;
|
|
355
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* The ceiling the harness clamps a body-supplied `validationChecks.maxAttempts` to, and the
|
|
391
|
+
* default it applies when the body omits one.
|
|
392
|
+
*
|
|
393
|
+
* DELIBERATE DUPLICATES of `VALIDATION_MAX_ATTEMPTS_CEILING` / `VALIDATION_DEFAULT_MAX_ATTEMPTS`
|
|
394
|
+
* in `@cat-factory/contracts` — the published image takes no schema dependency, so the harness
|
|
395
|
+
* cannot import them. Keep the two in step: the API validates writes against the contracts
|
|
396
|
+
* values, so a harness clamping to a DIFFERENT ceiling would silently cap a budget an operator
|
|
397
|
+
* was allowed to save, with nothing to flag the mismatch.
|
|
398
|
+
*/
|
|
399
|
+
export const VALIDATION_MAX_ATTEMPTS_CEILING = 10;
|
|
400
|
+
export const VALIDATION_DEFAULT_MAX_ATTEMPTS = 3;
|
|
356
401
|
/** Parse the coding-mode bootstrap spec, or undefined when absent. Validates the target. */
|
|
357
402
|
function parseAgentBootstrapSpec(value) {
|
|
358
403
|
if (typeof value !== 'object' || value === null)
|
|
@@ -671,6 +716,7 @@ export function parseAgentJob(input) {
|
|
|
671
716
|
testSecrets: parseTestSecrets(o.testSecrets),
|
|
672
717
|
guardLimits: parseGuardLimits(o.guardLimits),
|
|
673
718
|
validation: parseValidationSpec(o.validation),
|
|
719
|
+
validationChecks: parseValidationChecksSpec(o.validationChecks),
|
|
674
720
|
reviewPrNumber: posInt(o.reviewPrNumber),
|
|
675
721
|
});
|
|
676
722
|
assertAllowedHost(job.repo.cloneUrl, 'repo.cloneUrl');
|
|
@@ -727,7 +773,7 @@ function parseAgentPrSpec(raw) {
|
|
|
727
773
|
* literal doesn't blow the complexity budget; behaviour is byte-identical (spread order preserved).
|
|
728
774
|
*/
|
|
729
775
|
function assembleAgentJob(o, mode, agentField, parts) {
|
|
730
|
-
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, packageRegistries, skill, testSecrets, guardLimits, validation, reviewPrNumber, } = parts;
|
|
776
|
+
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, packageRegistries, skill, testSecrets, guardLimits, validation, validationChecks, reviewPrNumber, } = parts;
|
|
731
777
|
const repo = (o.repo ?? {});
|
|
732
778
|
return {
|
|
733
779
|
jobId: str(o.jobId, 'jobId'),
|
|
@@ -754,6 +800,7 @@ function assembleAgentJob(o, mode, agentField, parts) {
|
|
|
754
800
|
...(reviewPrNumber !== undefined ? { reviewPrNumber } : {}),
|
|
755
801
|
...(guardLimits ? { guardLimits } : {}),
|
|
756
802
|
...(validation ? { validation } : {}),
|
|
803
|
+
...(validationChecks ? { validationChecks } : {}),
|
|
757
804
|
};
|
|
758
805
|
}
|
|
759
806
|
/**
|
package/dist/runner.js
CHANGED
|
@@ -213,6 +213,9 @@ export class JobRegistry {
|
|
|
213
213
|
onFollowUp: (items) => {
|
|
214
214
|
entry.followUpBuffer.push(...items);
|
|
215
215
|
},
|
|
216
|
+
onValidationReport: (report) => {
|
|
217
|
+
entry.validationReport = report;
|
|
218
|
+
},
|
|
216
219
|
onCallMetric: (call) => {
|
|
217
220
|
// Stamp the job-scoped sequence on the metric OBJECT: the handler keeps the same
|
|
218
221
|
// instance for its terminal result, so both channels carry the same `seq` and the
|