Skip to content

Commit 4772cd6

Browse files
committed
feat(benchmark): add v3 invariant repair suite
1 parent 40f7ec3 commit 4772cd6

57 files changed

Lines changed: 614 additions & 17 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/harness-native-benchmark-suite.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ failure first, then measure the bounded single repair attempt before the Hidden
1111

1212
The separately versioned v2 corpus preserves all five v1 tasks and adds `configuration-semantic-repair`: public
1313
verification checks the requested settings values, while the Hidden Oracle checks the exact intended JSON document.
14-
The corpus is selected explicitly; no existing suite identity is mutated.
14+
The separately versioned v3 corpus preserves v2 and adds `recursive-feedback-invariant-repair` (a bounded feedback
15+
repair whose Oracle also protects an unrelated source file) and `nested-configuration-invariant-repair` (publicly
16+
checks two nested settings while the Oracle protects the complete configuration document). The corpus is selected
17+
explicitly; no existing suite identity is mutated.
1518

1619
## Run
1720

@@ -113,13 +116,13 @@ npm.cmd run benchmark:harness-native-suite -- `
113116
--output-root D:\Benchmarks\agentpatchcheck-native-v1-dry-run `
114117
--model <model-id> `
115118
--provider-profile <openai-responses|deepseek-chat> `
116-
--suite-version <v1|v2> `
119+
--suite-version <v1|v2|v3> `
117120
--dry-run
118121
```
119122

120123
## Limits and boundaries
121124

122-
- The v1 corpus uses five independent managed workspaces and requires a patch from each task; v2 uses six.
125+
- The v1/v2/v3 corpora use five/six/eight independent managed workspaces respectively and require a patch from each task.
123126
- The multi-file task allows only exact replacements in two existing files; it verifies both public prefixes and exact
124127
final content through a Hidden Oracle.
125128
- The recursive task requires bounded cross-directory discovery before two nested source-file replacements; its Hidden
@@ -149,8 +152,8 @@ precomputed percentages so a consumer cannot accidentally mix model transport fa
149152
- `providerFailureTasks` is separate from `agentExecutionFailureTasks`; neither should be silently counted as a semantic
150153
patch failure.
151154

152-
The v1/v2 suites contain five/six tasks respectively and are integration fixtures, not statistically meaningful quality
153-
scores. Future versioned corpora must grow the task denominator before publishing rates.
155+
The v1/v2/v3 suites contain five/six/eight tasks respectively and are integration fixtures, not statistically meaningful
156+
quality scores. Future versioned corpora must grow the task denominator before publishing rates.
154157

155158
The first retained v2 quality sample is documented in
156159
[`harness-native-quality-baseline-v2.md`](./harness-native-quality-baseline-v2.md). It records only comparable-run

‎scripts/run-harness-native-benchmark-repetitions.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const scriptDirectory = dirname(fileURLToPath(import.meta.url));
1313
const suiteScript = join(scriptDirectory, "run-harness-native-benchmark-suite.mjs");
1414
const modelPattern = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/u;
1515
const providerProfiles = new Set(["openai-responses", "deepseek-chat"]);
16-
const suiteVersions = new Set(["v1", "v2"]);
16+
const suiteVersions = new Set(["v1", "v2", "v3"]);
1717
const maxRuns = 20;
1818

1919
function parseArguments(argv) {
@@ -43,11 +43,11 @@ function parseArguments(argv) {
4343
}
4444
if (outputRoot === undefined || model === undefined || runs === undefined)
4545
throw new Error(
46-
"Usage: npm run benchmark:harness-native-repetitions -- --output-root <new-directory> --runs <2-20> --model <model> [--provider-profile <profile>] [--suite-version <v1|v2>]",
46+
"Usage: npm run benchmark:harness-native-repetitions -- --output-root <new-directory> --runs <2-20> --model <model> [--provider-profile <profile>] [--suite-version <v1|v2|v3>]",
4747
);
4848
if (!modelPattern.test(model)) throw new Error("model must be a valid 1-128 character model identifier.");
4949
if (!providerProfiles.has(providerProfile)) throw new Error("provider-profile must be one of: openai-responses, deepseek-chat.");
50-
if (!suiteVersions.has(suiteVersion)) throw new Error("suite-version must be one of: v1, v2.");
50+
if (!suiteVersions.has(suiteVersion)) throw new Error("suite-version must be one of: v1, v2, v3.");
5151
if (!Number.isSafeInteger(runs) || runs < 2 || runs > maxRuns) throw new Error(`runs must be an integer from 2 to ${maxRuns}.`);
5252
return { outputRoot, model, providerProfile, suiteVersion, runs };
5353
}

‎scripts/run-harness-native-benchmark-suite.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const providerProfiles = {
2929
requiredEnvironment: "DEEPSEEK_API_KEY",
3030
},
3131
};
32-
const suiteVersions = new Set(["v1", "v2"]);
32+
const suiteVersions = new Set(["v1", "v2", "v3"]);
3333

3434
function getProviderProfile(profileId) {
3535
const profile = providerProfiles[profileId];
@@ -64,11 +64,11 @@ function parseArguments(argv) {
6464
}
6565
if (outputRoot === undefined || model === undefined)
6666
throw new Error(
67-
"Usage: npm run benchmark:harness-native-suite -- --output-root <new-directory> --model <model> [--provider-profile <profile>] [--suite-version <v1|v2>] [--dry-run]",
67+
"Usage: npm run benchmark:harness-native-suite -- --output-root <new-directory> --model <model> [--provider-profile <profile>] [--suite-version <v1|v2|v3>] [--dry-run]",
6868
);
6969
if (!modelPattern.test(model)) throw new Error("model must be a valid 1-128 character model identifier.");
7070
getProviderProfile(providerProfile);
71-
if (!suiteVersions.has(suiteVersion)) throw new Error("suite-version must be one of: v1, v2.");
71+
if (!suiteVersions.has(suiteVersion)) throw new Error("suite-version must be one of: v1, v2, v3.");
7272
return { outputRoot, model, providerProfile, suiteVersion, dryRun };
7373
}
7474

‎src/agentpatchcheck/evidence-bundle.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ export function createEvidenceBundle(options: {
9898
}): EvidenceBundle {
9999
const createdAt = (options.createdAt ?? new Date()).toISOString();
100100
const promptSha256 = createHash("sha256").update(options.policy.prompt, "utf8").digest("hex");
101+
const publicVerificationRepairInstruction =
102+
options.policy.publicVerificationRepairInstruction === null
103+
? null
104+
: {
105+
length: options.policy.publicVerificationRepairInstruction.length,
106+
sha256: createHash("sha256")
107+
.update(options.policy.publicVerificationRepairInstruction, "utf8")
108+
.digest("hex"),
109+
};
101110
const agent = redactAgentExecution(options.execution.agent, options.policy.prompt);
102111
const patch = redactPatchSnapshot(options.execution.patch, options.policy.prompt);
103112
const commandVerification = redactCommandVerification(options.execution.commandVerification, options.policy.prompt);
@@ -113,6 +122,7 @@ export function createEvidenceBundle(options: {
113122
worktreeRoot: options.policy.worktreeRoot,
114123
promptLength: options.policy.prompt.length,
115124
promptSha256,
125+
publicVerificationRepairInstruction,
116126
codexExecutable: options.policy.codexExecutable ?? null,
117127
agentAdapter: options.policy.agentAdapter,
118128
model: options.policy.model ?? null,

‎src/agentpatchcheck/execute.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export async function executeAgentPatchCheck(
115115
const initialAgent = await runAgentSafely(resolvedDependencies.runAgent, policy, workspace.path, {
116116
phase: "initial",
117117
publicVerificationFeedback: null,
118+
repairInstruction: null,
118119
});
119120
let agent = initialAgent;
120121
let commandVerification = await runVerificationSafely(resolvedDependencies.runVerification, policy, workspace.path);
@@ -132,7 +133,11 @@ export async function executeAgentPatchCheck(
132133
resolvedDependencies.runAgent,
133134
{ ...policy, timeoutMs: remainingAgentBudgetMs },
134135
workspace.path,
135-
{ phase: "public-verification-repair", publicVerificationFeedback: feedback },
136+
{
137+
phase: "public-verification-repair",
138+
publicVerificationFeedback: feedback,
139+
repairInstruction: policy.publicVerificationRepairInstruction,
140+
},
136141
)
137142
: failedAgentExecution(policy, "Harness-native public verification repair budget was exhausted.");
138143
const attempts: AgentExecutionAttempt[] = [

‎src/agentpatchcheck/harness-native-runtime.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,11 @@ export async function runHarnessNativeRuntime(options: {
391391
repairContext?: RepairContext;
392392
}): Promise<HarnessNativeRuntimeResult> {
393393
const startedAt = Date.now();
394-
const repairContext = options.repairContext ?? { phase: "initial", publicVerificationFeedback: null };
394+
const repairContext = options.repairContext ?? {
395+
phase: "initial",
396+
publicVerificationFeedback: null,
397+
repairInstruction: null,
398+
};
395399
const trajectory: HarnessNativeTrajectoryStep[] = [];
396400
const observations: string[] = [];
397401
const session = options.provider.createSession?.() ?? {

‎src/agentpatchcheck/model-provider.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ function requestInput(context: ModelProviderContext): string {
173173
context.repairContext.phase === "initial"
174174
? "None."
175175
: JSON.stringify(context.repairContext.publicVerificationFeedback);
176-
return `${phaseInstruction}\n\nTask instructions:\n${context.prompt}\n\nPublic verification feedback:\n${feedback}\n\nObservations:\n${context.observations.join("\n---\n")}`;
176+
const repairInstruction =
177+
context.repairContext.phase === "public-verification-repair" &&
178+
typeof context.repairContext.repairInstruction === "string"
179+
? `\n\nHarness-owned targeted repair instruction:\n${context.repairContext.repairInstruction}`
180+
: "";
181+
return `${phaseInstruction}${repairInstruction}\n\nTask instructions:\n${context.prompt}\n\nPublic verification feedback:\n${feedback}\n\nObservations:\n${context.observations.join("\n---\n")}`;
177182
}
178183

179184
function selectedTools(tools: readonly HarnessNativeToolName[]) {

‎src/agentpatchcheck/task-policy.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { validateVerificationPolicy } from "./verification-policy";
2727
export const DEFAULT_TASK_TIMEOUT_MS = 15 * 60 * 1_000;
2828
export const MAX_TASK_TIMEOUT_MS = 60 * 60 * 1_000;
2929
export const MAX_TASK_PROMPT_LENGTH = 16_000;
30+
export const MAX_PUBLIC_VERIFICATION_REPAIR_INSTRUCTION_LENGTH = 2_048;
3031

3132
const SANDBOXES = new Set<AgentPatchCheckSandbox>(["read-only", "workspace-write"]);
3233
const MODEL_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/;
@@ -147,6 +148,17 @@ function normalizePrompt(prompt: string): string {
147148
return prompt;
148149
}
149150

151+
function normalizePublicVerificationRepairInstruction(instruction: string | undefined): string | null {
152+
if (instruction === undefined) return null;
153+
assertNoNullBytes(instruction, "Public verification repair instruction");
154+
if (!instruction.trim()) throw new Error("Public verification repair instruction must not be empty.");
155+
if (instruction.length > MAX_PUBLIC_VERIFICATION_REPAIR_INSTRUCTION_LENGTH)
156+
throw new Error(
157+
`Public verification repair instruction must not exceed ${MAX_PUBLIC_VERIFICATION_REPAIR_INSTRUCTION_LENGTH} characters.`,
158+
);
159+
return instruction;
160+
}
161+
150162
function normalizePatchExpectation(expectation: PatchExpectation | undefined): PatchExpectation {
151163
const normalized = expectation ?? "changes-required";
152164
if (!PATCH_EXPECTATIONS.has(normalized)) {
@@ -353,6 +365,9 @@ export async function validateTaskPolicy(input: TaskPolicyInput): Promise<TaskPo
353365
baseCommit,
354366
worktreeRoot,
355367
prompt: normalizePrompt(input.prompt),
368+
publicVerificationRepairInstruction: normalizePublicVerificationRepairInstruction(
369+
input.publicVerificationRepairInstruction,
370+
),
356371
runId: normalizeOptionalRunId(input.runId),
357372
codexExecutable: normalizeOptionalExecutable(input.codexExecutable),
358373
agentAdapter,

‎src/agentpatchcheck/task-spec.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const taskSpecSchema = z
1212
repositoryRoot: z.string(),
1313
prompt: z.string().optional(),
1414
promptFile: z.string().optional(),
15+
publicVerificationRepairInstruction: z.string().optional(),
1516
baseRef: z.string().optional(),
1617
worktreeRoot: z.string().optional(),
1718
runId: z.string().optional(),
@@ -193,6 +194,7 @@ export async function loadTaskSpec(specPath: string): Promise<TaskPolicyInput> {
193194
return {
194195
repositoryRoot,
195196
prompt,
197+
publicVerificationRepairInstruction: parsed.data.publicVerificationRepairInstruction,
196198
baseRef: parsed.data.baseRef,
197199
worktreeRoot:
198200
parsed.data.worktreeRoot === undefined

‎src/agentpatchcheck/types.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const TASK_POLICY_BRAND: unique symbol = Symbol("TaskPolicy");
55
export interface TaskPolicyInput {
66
repositoryRoot: string;
77
prompt: string;
8+
publicVerificationRepairInstruction?: string;
89
baseRef?: string;
910
worktreeRoot?: string;
1011
runId?: string;
@@ -114,6 +115,7 @@ export interface TaskPolicy {
114115
baseCommit: string;
115116
worktreeRoot: string;
116117
prompt: string;
118+
publicVerificationRepairInstruction: string | null;
117119
runId?: string;
118120
codexExecutable?: string;
119121
agentAdapter: AgentAdapterId;
@@ -166,8 +168,12 @@ export interface PublicVerificationFeedback {
166168

167169
/** Harness-owned phase contract for one Agent execution. Hidden Oracle data is never included. */
168170
export type RepairContext =
169-
| { phase: "initial"; publicVerificationFeedback: null }
170-
| { phase: "public-verification-repair"; publicVerificationFeedback: PublicVerificationFeedback };
171+
| { phase: "initial"; publicVerificationFeedback: null; repairInstruction?: null }
172+
| {
173+
phase: "public-verification-repair";
174+
publicVerificationFeedback: PublicVerificationFeedback;
175+
repairInstruction?: string | null;
176+
};
171177

172178
export interface AgentExecutionAttempt {
173179
phase: "initial" | "public-verification-repair";
@@ -340,6 +346,7 @@ export interface TaskPolicyEvidenceSnapshot {
340346
worktreeRoot: string;
341347
promptLength: number;
342348
promptSha256: string;
349+
publicVerificationRepairInstruction?: { length: number; sha256: string } | null;
343350
codexExecutable: string | null;
344351
agentAdapter?: AgentAdapterId;
345352
model: string | null;

0 commit comments

Comments
 (0)