@cat-factory/executor-harness 1.78.0 → 1.82.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 +1 -0
- package/dist/agent-capabilities.d.ts +130 -0
- package/dist/agent-runner.d.ts +114 -0
- package/dist/agent-runner.js +15 -1
- package/dist/agent-shared.d.ts +18 -0
- package/dist/agent.d.ts +66 -0
- package/dist/bootstrap-mode.d.ts +20 -0
- package/dist/captured-command.d.ts +58 -0
- package/dist/claude-call-aggregator.d.ts +164 -0
- package/dist/claude-call-aggregator.js +123 -17
- package/dist/claude-stream.d.ts +56 -0
- package/dist/claude-stream.js +23 -0
- package/dist/coding-agent.d.ts +263 -0
- package/dist/dependency-install.d.ts +111 -0
- package/dist/effort.d.ts +19 -0
- package/dist/embed.d.ts +4 -0
- package/dist/failure.d.ts +42 -0
- package/dist/follow-ups.d.ts +28 -0
- package/dist/frontend-infra.d.ts +25 -0
- package/dist/fs-utils.d.ts +2 -0
- package/dist/git.d.ts +394 -0
- package/dist/host-markdown.d.ts +28 -0
- package/dist/inline.d.ts +10 -0
- package/dist/job.d.ts +666 -0
- package/dist/logger.d.ts +16 -0
- package/dist/onboarding-preseed.d.ts +24 -0
- package/dist/package-registries.d.ts +32 -0
- package/dist/pi-workspace.d.ts +194 -0
- package/dist/pi-workspace.js +4 -0
- package/dist/pi.d.ts +475 -0
- package/dist/pr-description.d.ts +85 -0
- package/dist/pr-template.d.ts +101 -0
- package/dist/process-exit.d.ts +7 -0
- package/dist/process.d.ts +19 -0
- package/dist/progress-guard.d.ts +88 -0
- package/dist/progress.d.ts +87 -0
- package/dist/redact.d.ts +31 -0
- package/dist/reproduction-proof.d.ts +224 -0
- package/dist/runner.d.ts +282 -0
- package/dist/runner.js +3 -0
- package/dist/server.d.ts +3 -0
- package/dist/structured-output.d.ts +75 -0
- package/dist/subagents.d.ts +88 -0
- package/dist/subagents.js +74 -4
- package/dist/transcript-retention.d.ts +21 -0
- package/dist/validation-checks.d.ts +159 -0
- package/dist/vcs-api.d.ts +73 -0
- package/dist/version.d.ts +2 -0
- package/package.json +9 -5
- package/src/agent-runner.ts +21 -2
- package/src/claude-call-aggregator.ts +181 -32
- package/src/claude-stream.ts +21 -0
- package/src/pi-workspace.ts +4 -0
- package/src/runner.ts +24 -0
- package/src/subagents.ts +57 -3
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a child process ended: its own exit code, or the signal that killed it.
|
|
3
|
+
*
|
|
4
|
+
* @example describeProcessExit(1, null) // 'exited with code 1'
|
|
5
|
+
* @example describeProcessExit(null, 'SIGKILL') // 'killed by SIGKILL'
|
|
6
|
+
*/
|
|
7
|
+
export declare function describeProcessExit(code: number | null, signal: NodeJS.Signals | null): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type Logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Terminate a child process (and its group — see {@link signalTree}): SIGTERM first, then
|
|
5
|
+
* SIGKILL after a grace period if it hasn't exited (ignored an ordinary terminate). The
|
|
6
|
+
* escalation timer is `unref()`d so it never by itself keeps the event loop alive. Safe to
|
|
7
|
+
* call more than once.
|
|
8
|
+
*
|
|
9
|
+
* An actual escalation to SIGKILL is logged at warn level: a process that ignores
|
|
10
|
+
* SIGTERM and has to be force-killed is a signal worth seeing (a wedged Pi/CLI), and
|
|
11
|
+
* was previously invisible. Pass a child logger to carry the run's `jobId`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function killChildProcess(child: ChildProcess, graceMs?: number, logger?: Logger): void;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a spawned agent CLI should be its own process-group leader so {@link killChildProcess}
|
|
16
|
+
* can reap the whole tree (its grandchildren) on abort. POSIX only; Windows has no process
|
|
17
|
+
* groups (and `detached` there spawns a new console we don't want), so it stays false.
|
|
18
|
+
*/
|
|
19
|
+
export declare const spawnDetached: boolean;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-call signal read off a streamed Pi event, or undefined if not a tool call. Exported for
|
|
3
|
+
* `runPi`'s span emitter, which reads the same event for its per-tool trace spans.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toolCallSignal(event: Record<string, unknown>): {
|
|
6
|
+
name: string;
|
|
7
|
+
isError: boolean;
|
|
8
|
+
} | undefined;
|
|
9
|
+
/** Tunable bounds for the {@link ProgressGuard}. */
|
|
10
|
+
export interface ProgressGuardLimits {
|
|
11
|
+
/**
|
|
12
|
+
* Abort once the agent has made this many NON-exploration tool calls without ever
|
|
13
|
+
* using a file-editing tool (see `FILE_EDIT_TOOLS`). The signature of the credential
|
|
14
|
+
* rabbit-hole that motivated this: probing the environment (`bash`/exec) endlessly
|
|
15
|
+
* without implementing anything. Read-only exploration (`read`/`grep`/… — see
|
|
16
|
+
* `EXPLORATION_TOOLS`) and planning (`todo`) do NOT count, so a large task that
|
|
17
|
+
* legitimately reads/searches many files before its first edit is not killed for it.
|
|
18
|
+
* Disabled when `expectsEdits` is false (e.g. the assess-only merger / Blueprinter,
|
|
19
|
+
* which legitimately edit nothing). Note this bound only guards the run UNTIL its
|
|
20
|
+
* first edit: once the agent has edited a file at all, it has demonstrably started
|
|
21
|
+
* the work, so only `maxConsecutiveErrors` guards a later stall.
|
|
22
|
+
*/
|
|
23
|
+
maxToolCallsWithoutEdit: number;
|
|
24
|
+
/**
|
|
25
|
+
* Abort after this many consecutive failing tool calls — the agent is stuck
|
|
26
|
+
* retrying an operation that keeps failing rather than making progress.
|
|
27
|
+
*/
|
|
28
|
+
maxConsecutiveErrors: number;
|
|
29
|
+
/**
|
|
30
|
+
* Abort after this many consecutive web-search/web-fetch calls with no other tool
|
|
31
|
+
* call in between. Web tools are read-only exploration (they don't count toward the
|
|
32
|
+
* no-edit bound), so without this a model could rabbit-hole on searches indefinitely
|
|
33
|
+
* without ever tripping a guard. Any non-web tool call resets the streak. Optional:
|
|
34
|
+
* defaults to {@link DEFAULT_PROGRESS_GUARD_LIMITS} when a caller builds limits
|
|
35
|
+
* without it.
|
|
36
|
+
*/
|
|
37
|
+
maxConsecutiveWebCalls?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare const DEFAULT_PROGRESS_GUARD_LIMITS: {
|
|
40
|
+
maxToolCallsWithoutEdit: number;
|
|
41
|
+
maxConsecutiveErrors: number;
|
|
42
|
+
maxConsecutiveWebCalls: number;
|
|
43
|
+
};
|
|
44
|
+
/** Read {@link ProgressGuardLimits} from the environment, falling back to the defaults. */
|
|
45
|
+
export declare function progressGuardLimitsFromEnv(env?: NodeJS.ProcessEnv): ProgressGuardLimits;
|
|
46
|
+
/**
|
|
47
|
+
* Apply per-knob overrides onto a base set of guard limits, ENFORCING loosen-only: an
|
|
48
|
+
* override can only RAISE a knob (more headroom), never lower it below the base. A
|
|
49
|
+
* larger value is more lenient for every knob (more no-edit tool calls / errors / web
|
|
50
|
+
* calls tolerated), so each result is `max(base, override)`. This is a hard guarantee,
|
|
51
|
+
* not a convention — a tuning entry (built-in or a custom kind's, which reaches this via
|
|
52
|
+
* an untrusted job body) that supplies a value TIGHTER than the base is clamped back up
|
|
53
|
+
* to the base rather than aborting a legitimately-progressing run. An absent/undefined
|
|
54
|
+
* knob keeps the base value untouched.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mergeGuardLimits(base: ProgressGuardLimits, overrides: Partial<ProgressGuardLimits> | undefined): ProgressGuardLimits;
|
|
57
|
+
/**
|
|
58
|
+
* Live anti-rabbithole guard: fed each streamed Pi event, it returns a diagnostic
|
|
59
|
+
* reason the moment a run has plainly stopped making progress, so the harness can
|
|
60
|
+
* kill Pi early instead of letting it burn the whole budget (and then surface a
|
|
61
|
+
* useful failure instead of a generic "no file changes"). Pure and incremental so
|
|
62
|
+
* it can be unit-tested over a fixed event sequence.
|
|
63
|
+
*/
|
|
64
|
+
export declare class ProgressGuard {
|
|
65
|
+
private readonly limits;
|
|
66
|
+
/** When false (assess-only runs like the merger), the no-edit bound is skipped. */
|
|
67
|
+
private readonly expectsEdits;
|
|
68
|
+
private toolCalls;
|
|
69
|
+
private edits;
|
|
70
|
+
private consecutiveErrors;
|
|
71
|
+
private consecutiveWebCalls;
|
|
72
|
+
constructor(limits: ProgressGuardLimits,
|
|
73
|
+
/** When false (assess-only runs like the merger), the no-edit bound is skipped. */
|
|
74
|
+
expectsEdits?: boolean);
|
|
75
|
+
/** Feed one parsed Pi event; returns a diagnostic reason when the run should abort, else null. */
|
|
76
|
+
observe(event: Record<string, unknown>): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Feed one already-parsed tool-call signal (name + error flag), returning a diagnostic reason
|
|
79
|
+
* when the run should abort, else null. Split out of {@link observe} so a caller whose stream
|
|
80
|
+
* is NOT Pi's `tool_execution_end` envelope — the claude-code runner, which correlates a
|
|
81
|
+
* `tool_use` block's name with its `tool_result`'s `is_error` — can drive the SAME guard logic
|
|
82
|
+
* without synthesising a fake Pi event.
|
|
83
|
+
*/
|
|
84
|
+
observeSignal(tool: {
|
|
85
|
+
name: string;
|
|
86
|
+
isError: boolean;
|
|
87
|
+
}): string | null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { TodoProgress } from './pi.js';
|
|
2
|
+
/** Statuses a plan entry can carry; anything unrecognised is treated as not-yet-started. */
|
|
3
|
+
export declare function normalizeStatus(status: unknown): 'pending' | 'in_progress' | 'completed';
|
|
4
|
+
/** Roll a label+status list up into the counts the board renders. Shared by every plan shape. */
|
|
5
|
+
export declare function toProgress(items: {
|
|
6
|
+
label: string;
|
|
7
|
+
status: ReturnType<typeof normalizeStatus>;
|
|
8
|
+
}[]): {
|
|
9
|
+
completed: number;
|
|
10
|
+
inProgress: number;
|
|
11
|
+
total: number;
|
|
12
|
+
items: {
|
|
13
|
+
label: string;
|
|
14
|
+
status: ReturnType<typeof normalizeStatus>;
|
|
15
|
+
}[];
|
|
16
|
+
};
|
|
17
|
+
/** Map a `TodoWrite` call's `todos` array onto subtask counts. Each call is a full snapshot. */
|
|
18
|
+
export declare function todosToProgress(todos: unknown): TodoProgress | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The id the CLI assigned to a just-created task, read from `TaskCreate`'s tool RESULT.
|
|
21
|
+
*
|
|
22
|
+
* `TaskCreate`'s INPUT carries only `{subject, description}` — the id is minted by the CLI and
|
|
23
|
+
* comes back on the result, so pairing a later `TaskUpdate({taskId})` to the task it created
|
|
24
|
+
* requires reading the result text. The CLI's shipped `TaskCreateOutput` is
|
|
25
|
+
* `{task: {id, subject}}`, but the parent stream's `tool_result` block carries the rendered
|
|
26
|
+
* STRING (`"Task #1 created successfully: <subject>"`), so both shapes are accepted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCreatedTaskId(content: unknown): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Tracks the parent's incremental `TaskCreate` / `TaskUpdate` plan.
|
|
31
|
+
*
|
|
32
|
+
* A `TaskCreate` is registered as pending against its tool_use id, then bound to the CLI-assigned
|
|
33
|
+
* task id when its result arrives; `TaskUpdate` moves the bound task. A create whose result is
|
|
34
|
+
* never seen (or whose id can't be parsed) still counts toward `total` under a synthetic key, so
|
|
35
|
+
* the plan size stays honest even when the pairing fails — it simply can never advance.
|
|
36
|
+
*
|
|
37
|
+
* `deleted` tombstones are dropped from the list entirely (matching `TodoWrite`'s live-tasks-only
|
|
38
|
+
* shape), so a task the agent abandons doesn't hold the bar back forever.
|
|
39
|
+
*/
|
|
40
|
+
export interface TaskPlanTracker {
|
|
41
|
+
/** Feed an `assistant` message's content blocks: registers creates + applies updates. */
|
|
42
|
+
onAssistant(content: unknown[]): void;
|
|
43
|
+
/** Feed a `user` message's content blocks: binds each create to its CLI-assigned task id. */
|
|
44
|
+
onUser(content: unknown[]): void;
|
|
45
|
+
/** The plan as progress counts, or undefined when nothing has been planned yet. */
|
|
46
|
+
progress(): TodoProgress | undefined;
|
|
47
|
+
}
|
|
48
|
+
export declare function createTaskPlanTracker(): TaskPlanTracker;
|
|
49
|
+
/**
|
|
50
|
+
* Reconcile the parent's TWO plan vocabularies (`TodoWrite` snapshots vs the incremental
|
|
51
|
+
* `TaskCreate`/`TaskUpdate` pair) into one plan. A run uses one or the other, so this is a
|
|
52
|
+
* genuine either/or: prefer whichever is further along — more `completed`, then more
|
|
53
|
+
* `inProgress`, then more `total` — else the `TodoWrite` view. Pure + total; returns whichever
|
|
54
|
+
* single input is present when only one is.
|
|
55
|
+
*
|
|
56
|
+
* This is NOT how the plan reconciles with the parallel-subagent view — those describe the same
|
|
57
|
+
* slices from two angles and are MERGED, see {@link mergeProgress}.
|
|
58
|
+
*/
|
|
59
|
+
export declare function pickProgress(todo: TodoProgress | undefined, slice: TodoProgress | undefined): TodoProgress | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* A slice label reduced to its identifying words, for pairing a plan entry with the subagent
|
|
62
|
+
* dispatched to review it. Case, punctuation and the boilerplate around the slice name all
|
|
63
|
+
* differ between the two vocabularies; the slice NAME does not.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sliceLabelKey(label: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* MERGE the parent's plan with the `SliceTracker`'s subagent-dispatch view into the single list
|
|
68
|
+
* the board renders (ADR 0027 Defect B, corrected).
|
|
69
|
+
*
|
|
70
|
+
* The two are not competing answers, they are two halves of one: the plan is the INVENTORY (it
|
|
71
|
+
* names every slice, including the ones not dispatched yet, which is the only place a `pending`
|
|
72
|
+
* slice exists at all), and the dispatch view is the live STATUS (the plan advances only when
|
|
73
|
+
* the agent remembers to update it, which it does unreliably). Picking whichever looked "further
|
|
74
|
+
* along" — the previous behaviour — made the rendered list SHRINK the moment the first subagent
|
|
75
|
+
* returned: the dispatch view won on `completed`, and it only knows the slices dispatched so far,
|
|
76
|
+
* so every queued slice vanished from the window and reappeared one at a time as it was dispatched.
|
|
77
|
+
*
|
|
78
|
+
* Pairing is by normalised label ({@link sliceLabelKey}) — exact first, then containment — and
|
|
79
|
+
* finally positionally into the leftover pending entries, in dispatch order (the agent dispatches
|
|
80
|
+
* in plan order). A dispatch that pairs with nothing is APPENDED rather than dropped, so the list
|
|
81
|
+
* is at worst a union and can never lose a slice. Statuses only ever advance, so a plan entry the
|
|
82
|
+
* agent already marked done is not walked back by a re-dispatch.
|
|
83
|
+
*
|
|
84
|
+
* Pure + total. Falls back to {@link pickProgress} when either side carries counts but no items
|
|
85
|
+
* (nothing to merge onto).
|
|
86
|
+
*/
|
|
87
|
+
export declare function mergeProgress(plan: TodoProgress | undefined, slice: TodoProgress | undefined): TodoProgress | undefined;
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Register known secret values to scrub on every subsequent redaction. */
|
|
2
|
+
export declare function registerKnownSecrets(values: readonly string[]): void;
|
|
3
|
+
/**
|
|
4
|
+
* Strip credentials out of any string before it is logged or stored. Applies the
|
|
5
|
+
* pattern rules (URL userinfo `https://user:pass@host`, `x-access-token:<token>`, bare
|
|
6
|
+
* `ghs_`/`ghp_`/`gho_`/`github_pat_` shapes, and credential-named `KEY=value` / `KEY:
|
|
7
|
+
* value` assignments) and then scrubs every supplied known-secret value plus the
|
|
8
|
+
* module-registered ones ({@link registerKnownSecrets}). Idempotent — safe to call on
|
|
9
|
+
* already-redacted text.
|
|
10
|
+
*/
|
|
11
|
+
export declare function redact(input: string, knownSecrets?: readonly string[]): string;
|
|
12
|
+
/** Pattern + registered-value redaction. Kept for callers without a per-call secret list. */
|
|
13
|
+
export declare function redactSecrets(input: string): string;
|
|
14
|
+
/** Cap on captured command output kept on an infra record (tail-biased — failures show last). */
|
|
15
|
+
export declare const MAX_CAPTURED_OUTPUT_CHARS = 16000;
|
|
16
|
+
/**
|
|
17
|
+
* Combine, redact and tail-bound captured stdout+stderr into a single stored string. Keeps
|
|
18
|
+
* the LAST {@link MAX_CAPTURED_OUTPUT_CHARS} (where a failure's error lives), prefixed with a
|
|
19
|
+
* truncation marker when trimmed. Returns undefined for empty output so a record stays sparse.
|
|
20
|
+
* Shared by the docker-compose and the frontend UI-test stand-ups.
|
|
21
|
+
*/
|
|
22
|
+
export declare function captureRedactedOutput(stdout: unknown, stderr: unknown): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The set of secret strings to scrub from a run's stderr/output. For Claude (and the
|
|
25
|
+
* Anthropic-compatible vendors GLM/Kimi/DeepSeek) the credential IS the token string,
|
|
26
|
+
* so the whole-string entry covers it. For Codex the credential is a whole `auth.json`
|
|
27
|
+
* blob, so we ALSO scrub every string value parsed out of it (access/refresh tokens,
|
|
28
|
+
* ids): a token echoed on its OWN — not as part of the whole blob — would otherwise
|
|
29
|
+
* slip past a whole-blob-only match and leak into an error message.
|
|
30
|
+
*/
|
|
31
|
+
export declare function secretsToRedact(subscriptionToken: string): string[];
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { RunOptions } from './runner.js';
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
/** The reproduction spec as it arrives on the job body. */
|
|
4
|
+
export interface ReproductionSpec {
|
|
5
|
+
/** The command that runs EXACTLY the declared reproduction test(s), as `sh -c` in the checkout. */
|
|
6
|
+
command: string;
|
|
7
|
+
/** The test file(s) that constitute the reproduction (repo-relative, already sanitized). */
|
|
8
|
+
testPaths: string[];
|
|
9
|
+
/**
|
|
10
|
+
* How many declared paths the ENGINE dropped while resolving this spec (over the cap, absolute,
|
|
11
|
+
* traversing, over-long). Echoed onto the report unchanged: a dropped path can leave the base
|
|
12
|
+
* tree without the reproduction, which greens it and reads as "the test does not capture the
|
|
13
|
+
* defect", so the omission has to travel with the verdict rather than being implied by it.
|
|
14
|
+
*/
|
|
15
|
+
omittedTestPaths?: number;
|
|
16
|
+
/** Optional command that makes a FRESH worktree runnable (a dependency install). */
|
|
17
|
+
setupCommand?: string;
|
|
18
|
+
/** How many agent+verify rounds the loop may run before it settles for `inconclusive`. */
|
|
19
|
+
maxAttempts: number;
|
|
20
|
+
}
|
|
21
|
+
/** One tree's run of the reproduction command. */
|
|
22
|
+
export interface ReproductionPhaseOutcome {
|
|
23
|
+
/** Exit code (0 = pass); 124 on watchdog timeout, 127 on spawn failure, 130 on abort. */
|
|
24
|
+
exitCode: number;
|
|
25
|
+
passed: boolean;
|
|
26
|
+
/** Bounded, secret-scrubbed tail of the command's combined stdout+stderr. */
|
|
27
|
+
outputTail?: string;
|
|
28
|
+
durationMs?: number;
|
|
29
|
+
timedOut?: boolean;
|
|
30
|
+
/** Set when THIS phase's setup command failed, so the tree never ran the check meaningfully. */
|
|
31
|
+
setupFailed?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The harness-computed reproduction report — what crosses the wire onto the step. */
|
|
34
|
+
export interface ReproductionReport {
|
|
35
|
+
/**
|
|
36
|
+
* `reproduced` — RED on the pre-fix tree, GREEN on the final tree (the only shape that is
|
|
37
|
+
* proof); `inconclusive` — every other shape, recorded honestly rather than dressed up. The
|
|
38
|
+
* harness never emits `declared_infeasible`: a conceded run dispatches no proof at all, so the
|
|
39
|
+
* ENGINE mints that one from the declaration itself.
|
|
40
|
+
*/
|
|
41
|
+
status: 'reproduced' | 'inconclusive';
|
|
42
|
+
command: string;
|
|
43
|
+
testPaths: string[];
|
|
44
|
+
omittedTestPaths?: number;
|
|
45
|
+
base?: ReproductionPhaseOutcome;
|
|
46
|
+
/** Absent when the base run settled the verdict (a green or un-runnable base). */
|
|
47
|
+
final?: ReproductionPhaseOutcome;
|
|
48
|
+
attempts: number;
|
|
49
|
+
maxAttempts: number;
|
|
50
|
+
/** For `inconclusive`: which shape was observed, in one line, for the report and the step card. */
|
|
51
|
+
note?: string;
|
|
52
|
+
at: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Per-phase output kept on the REPORT (what crosses the wire and lands in the run's persisted
|
|
56
|
+
* `detail` blob). Deliberately smaller than `MAX_CAPTURED_OUTPUT_CHARS` (`redact.ts`), which is
|
|
57
|
+
* what the AGENT sees in its repair prompt — the same split, for the same reasons, as the pre-PR
|
|
58
|
+
* validation report's tail (`validation-checks.ts`).
|
|
59
|
+
*/
|
|
60
|
+
export declare const REPRODUCTION_REPORT_TAIL_CHARS = 4000;
|
|
61
|
+
/**
|
|
62
|
+
* The ceiling the harness clamps a body-supplied `reproduction.maxAttempts` to, and the default it
|
|
63
|
+
* applies when the body omits one.
|
|
64
|
+
*
|
|
65
|
+
* DELIBERATE DUPLICATES of `REPRODUCTION_DEFAULT_MAX_ATTEMPTS` in `@cat-factory/contracts` (and of
|
|
66
|
+
* the validation loop's own ceiling) — the published image takes no schema dependency, so the
|
|
67
|
+
* harness cannot import them. Keep them in step: a harness clamping to a DIFFERENT ceiling would
|
|
68
|
+
* silently cap a budget the engine was allowed to send, with nothing to flag the mismatch.
|
|
69
|
+
*/
|
|
70
|
+
export declare const REPRODUCTION_DEFAULT_MAX_ATTEMPTS = 3;
|
|
71
|
+
export declare const REPRODUCTION_MAX_ATTEMPTS_CEILING = 10;
|
|
72
|
+
/**
|
|
73
|
+
* The per-command watchdog: the longest a single setup or check command may run before it is
|
|
74
|
+
* killed and treated as a failure, so one hung test command cannot wedge a run. Overridable via
|
|
75
|
+
* env for tests; defaults to 15 minutes, matching the validation loop's.
|
|
76
|
+
*/
|
|
77
|
+
export declare function reproductionCommandTimeoutMs(): number;
|
|
78
|
+
/**
|
|
79
|
+
* How often the proof feeds the run's inactivity watchdog. Well under the harness's own
|
|
80
|
+
* `JOB_INACTIVITY_MS` (default 10 min) so a slow install-plus-test in each of two worktrees can
|
|
81
|
+
* never look wedged. This is NOT optional: the job-level watchdog is TIGHTER than one command's
|
|
82
|
+
* own ({@link reproductionCommandTimeoutMs}, 15 min), and the harness spawns these itself rather
|
|
83
|
+
* than through the agent, so they emit no activity of their own — without the heartbeat a
|
|
84
|
+
* legitimately slow proof aborts the entire run as "inactivity" and the per-command timeout is
|
|
85
|
+
* unreachable at stock settings.
|
|
86
|
+
*/
|
|
87
|
+
export declare function reproductionHeartbeatMs(): number;
|
|
88
|
+
/**
|
|
89
|
+
* The ceiling on the WHOLE proof phase — every attempt, both trees, setup included. Overridable
|
|
90
|
+
* via env; defaults to 45 minutes.
|
|
91
|
+
*
|
|
92
|
+
* The per-command watchdog bounds one command, not the phase, and the phase multiplies: a spent
|
|
93
|
+
* budget is `maxAttempts` × two trees × (setup + check), each of which may legitimately run for
|
|
94
|
+
* {@link reproductionCommandTimeoutMs}. At stock settings that is hours of container time spent
|
|
95
|
+
* BEFORE the pre-PR validation loop has run its own rounds, and nothing else stops it — the
|
|
96
|
+
* heartbeat deliberately keeps the job-level inactivity watchdog from firing, which is exactly
|
|
97
|
+
* what removes the accidental backstop the phase would otherwise have had.
|
|
98
|
+
*
|
|
99
|
+
* Enforced at PHASE boundaries (before each tree's run, and before each repair round) rather than
|
|
100
|
+
* mid-command: a command already carries its own watchdog, so the real bound is this budget plus
|
|
101
|
+
* at most one command's timeout. Exceeding it settles `inconclusive` with a note saying so —
|
|
102
|
+
* never a run failure, exactly like every other unproven shape.
|
|
103
|
+
*/
|
|
104
|
+
export declare function reproductionTotalBudgetMs(): number;
|
|
105
|
+
/**
|
|
106
|
+
* Parse the job body's `reproduction` envelope, or `undefined` for "no proof on this job".
|
|
107
|
+
*
|
|
108
|
+
* Lenient in exactly one direction: anything malformed yields `undefined`, so the run behaves
|
|
109
|
+
* byte-for-byte as it did before this feature existed. A body that names no command has nothing
|
|
110
|
+
* to run, and inventing one would manufacture a verdict. Test paths are re-checked here even
|
|
111
|
+
* though the engine already sanitized them — this is the harness's own trust boundary, and these
|
|
112
|
+
* paths are handed to `git checkout` against a worktree.
|
|
113
|
+
*/
|
|
114
|
+
export declare function parseReproductionSpec(value: unknown): ReproductionSpec | undefined;
|
|
115
|
+
/** Everything one proof attempt needs. Every field is per-job; nothing is read from a global. */
|
|
116
|
+
export interface ReproductionProofArgs {
|
|
117
|
+
/** The agent's own checkout — the worktrees' parent clone. Never modified by the proof. */
|
|
118
|
+
dir: string;
|
|
119
|
+
/** The pre-fix tree: the branch tip captured BEFORE this pass ran (never `HEAD~1`/a base ref). */
|
|
120
|
+
baseSha: string;
|
|
121
|
+
/** The tree the pull request will open from (the checkout's HEAD, after committing). */
|
|
122
|
+
finalSha: string;
|
|
123
|
+
/** In a monorepo, the service subdirectory the commands run in (relative to each worktree). */
|
|
124
|
+
serviceDirectory?: string;
|
|
125
|
+
spec: ReproductionSpec;
|
|
126
|
+
attempt: number;
|
|
127
|
+
logger: Logger;
|
|
128
|
+
opts: RunOptions;
|
|
129
|
+
/** Epoch ms after which no further phase may START (see {@link reproductionTotalBudgetMs}). */
|
|
130
|
+
deadlineAt?: number;
|
|
131
|
+
/**
|
|
132
|
+
* The files the PRE-FIX tree already changes relative to the PR base branch, or `undefined`
|
|
133
|
+
* when that could not be determined. Consulted ONLY when the base tree comes back green — see
|
|
134
|
+
* {@link priorWorkAtBase} for why a green base is otherwise misdiagnosed.
|
|
135
|
+
*/
|
|
136
|
+
listBaseTreeChanges?: () => Promise<string[] | undefined>;
|
|
137
|
+
}
|
|
138
|
+
/** One proof attempt: the report, the full tails for a repair prompt, and whether to repair. */
|
|
139
|
+
export interface ReproductionAttempt {
|
|
140
|
+
report: ReproductionReport;
|
|
141
|
+
fullTails: Map<string, string>;
|
|
142
|
+
/**
|
|
143
|
+
* Whether spending an agent repair round on this outcome can plausibly change it. An explicit
|
|
144
|
+
* OUTPUT of the attempt rather than something re-derived from the report, because the two
|
|
145
|
+
* unrepairable shapes are known only here: a broken environment (the agent cannot change a
|
|
146
|
+
* setup command it did not declare) and a pre-fix tree that already carries this run's own
|
|
147
|
+
* earlier work (nothing is wrong with the test, so "make it exercise the defect" is bad advice).
|
|
148
|
+
*/
|
|
149
|
+
repairable: boolean;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Run ONE proof attempt: create both worktrees, run the declared check in each, and compute the
|
|
153
|
+
* verdict from the two exit codes.
|
|
154
|
+
*
|
|
155
|
+
* The base worktree is built at `baseSha` and then has the DECLARED test files checked out of
|
|
156
|
+
* `finalSha` on top — the paths only, never a whole-tree checkout, which would drag the fix across
|
|
157
|
+
* and green the base. In the RESUMED case (a prior `repro-test` step already pushed the failing
|
|
158
|
+
* test onto the shared work branch, so `baseSha` already carries it) that overlay is a no-op by
|
|
159
|
+
* construction. Doing it unconditionally is what guarantees BOTH trees run the byte-identical
|
|
160
|
+
* check, which is the claim the report makes.
|
|
161
|
+
*
|
|
162
|
+
* Keeps the run's inactivity watchdog fed for the whole attempt (see
|
|
163
|
+
* {@link reproductionHeartbeatMs}) and always tears both worktrees down, including on a throw.
|
|
164
|
+
*/
|
|
165
|
+
export declare function runReproductionProof(args: ReproductionProofArgs): Promise<ReproductionAttempt>;
|
|
166
|
+
/**
|
|
167
|
+
* The repair instruction handed to the agent after a failed verification: which tree behaved how,
|
|
168
|
+
* the captured output, and an explicit statement of the exit condition. The FULL captured tail is
|
|
169
|
+
* used here (not the report's smaller bound) — the agent needs the whole failure to act on it, and
|
|
170
|
+
* this text never leaves the container.
|
|
171
|
+
*
|
|
172
|
+
* Deliberately prescriptive about scope, for the same reason the validation loop's prompt is: a
|
|
173
|
+
* loop that lets the agent "succeed" by weakening the reproduction is worse than no loop at all,
|
|
174
|
+
* because it launders an unverified claim into a captured "fact" — the exact failure mode this
|
|
175
|
+
* whole feature exists to remove.
|
|
176
|
+
*/
|
|
177
|
+
export declare function buildReproductionRepairPrompt(report: ReproductionReport, fullTails: Map<string, string>,
|
|
178
|
+
/**
|
|
179
|
+
* New files the agent created but never `git add`ed, if the caller can tell. The proof runs
|
|
180
|
+
* against COMMITTED trees, so an unadded reproduction test is invisible to it — and to the push.
|
|
181
|
+
*/
|
|
182
|
+
untrackedFiles?: string[]): string;
|
|
183
|
+
/**
|
|
184
|
+
* The reproduction-proof LOOP: verify, and while the verification fails, is repairable and budget
|
|
185
|
+
* remains, hand the captured output back to the agent as its next instruction and verify again.
|
|
186
|
+
* Returns the LAST attempt's report.
|
|
187
|
+
*
|
|
188
|
+
* A failed verification is a REPAIR, never a run failure (the initiative's D6). Exhausting the
|
|
189
|
+
* budget degrades to `inconclusive` and the caller opens the pull request anyway — deliberately a
|
|
190
|
+
* different disposition from the pre-PR validation loop, which opens nothing. A red validation
|
|
191
|
+
* check means the WORK is broken, so refusing the PR is right; a reproduction that could not be
|
|
192
|
+
* demonstrated means the EVIDENCE is weak, which is a reviewer's call, not a machine's, and
|
|
193
|
+
* failing the run would throw away a fix that may well be correct. The report says plainly what
|
|
194
|
+
* was and was not proven.
|
|
195
|
+
*
|
|
196
|
+
* Every settled attempt — including the ones that never ran a tree — is published on the job view
|
|
197
|
+
* (a fresh `at` per publish, which the engine's change detection relies on), so the loop is
|
|
198
|
+
* observable while it runs; `onAgentPass` lets the caller fold each repair pass's
|
|
199
|
+
* stats/usage/telemetry into the run's totals.
|
|
200
|
+
*
|
|
201
|
+
* The loop is bounded twice over: by `maxAttempts` rounds, and by the wall-clock
|
|
202
|
+
* {@link reproductionTotalBudgetMs} — attempts multiply two full tree runs each, and the phase's
|
|
203
|
+
* own heartbeat deliberately stops the job-level inactivity watchdog from ever cutting it short.
|
|
204
|
+
*/
|
|
205
|
+
export declare function runReproductionLoop<TRun>(args: {
|
|
206
|
+
dir: string;
|
|
207
|
+
baseSha: string;
|
|
208
|
+
/** Re-read before every attempt: a repair pass commits, so the final tree moves. */
|
|
209
|
+
resolveFinalSha: () => Promise<string>;
|
|
210
|
+
serviceDirectory?: string;
|
|
211
|
+
spec: ReproductionSpec;
|
|
212
|
+
logger: Logger;
|
|
213
|
+
opts: RunOptions;
|
|
214
|
+
runAgentPass: (userPrompt: string) => Promise<TRun>;
|
|
215
|
+
onAgentPass?: (run: TRun) => void;
|
|
216
|
+
/** The new files left uncommitted in the checkout, folded into each repair prompt. */
|
|
217
|
+
listUncommittedNewFiles?: () => Promise<string[]>;
|
|
218
|
+
/**
|
|
219
|
+
* The files the PRE-FIX tree already changes relative to the PR base branch (see
|
|
220
|
+
* `priorWorkAtBase`). Invariant across attempts — `baseSha` never moves — so it is resolved at
|
|
221
|
+
* most once and memoised here rather than re-probed per round.
|
|
222
|
+
*/
|
|
223
|
+
listBaseTreeChanges?: () => Promise<string[] | undefined>;
|
|
224
|
+
}): Promise<ReproductionReport>;
|