@dailephd/my-dev-kit-lab 0.9.1 → 0.9.3

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/README.md +10 -2
  3. package/dist/src/agents/adapters/codexAdapter.js +4 -1
  4. package/dist/src/cli/help.js +38 -2
  5. package/dist/src/cli/runLabCli.js +9 -1
  6. package/dist/src/commands/buildGalleryCommand.js +16 -3
  7. package/dist/src/commands/runComparePromptsCommand.js +109 -0
  8. package/dist/src/commands/runExperimentRunCommand.js +51 -5
  9. package/dist/src/core/processTree.js +41 -17
  10. package/dist/src/core/runMeasuredCommand.js +14 -8
  11. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +24 -2
  12. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
  13. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
  14. package/dist/src/experiments/plugins/agentSuccessRate/campaignProgress.js +4 -2
  15. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
  16. package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
  17. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
  18. package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
  19. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
  20. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
  21. package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
  22. package/dist/src/gallery/galleryPathSafety.js +111 -0
  23. package/dist/src/gallery/readGallerySources.js +542 -0
  24. package/dist/src/gallery/writeGalleryManifest.js +57 -3
  25. package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
  26. package/dist/src/report/experiments/htmlSafety.js +32 -0
  27. package/dist/src/report/experiments/metricEvidence.js +389 -0
  28. package/dist/src/report/experiments/metricGlossary.js +227 -0
  29. package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
  30. package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
  31. package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
  32. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
  33. package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
  34. package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
  35. package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
  36. package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
  37. package/dist/src/report/experiments/specializedReportSections.js +142 -0
  38. package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +6 -2
  39. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
  40. package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
  41. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
  42. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
  43. package/dist/src/securityValidation/commandRunner.js +13 -6
  44. package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +34 -4
  45. package/dist/src/securityValidation/packageChecks/requiredPackageContents.js +106 -2
  46. package/dist/src/securityValidation/packageChecks/runPackageChecks.js +31 -16
  47. package/dist/src/securityValidation/staticScans/semgrep.js +68 -64
  48. package/dist/src/securityValidation/types.js +0 -4
  49. package/dist/src/securityValidation/validate/runSecurityValidation.js +1 -1
  50. package/docs/METRICS.md +12 -2
  51. package/examples/tutorial-browser/prepare.mjs +13 -13
  52. package/examples/tutorial-browser/server.mjs +13 -13
  53. package/package.json +1 -1
@@ -49,6 +49,7 @@ export async function runMeasuredCommand(options) {
49
49
  let timedOut = false;
50
50
  let timeout;
51
51
  let killGraceTimeout;
52
+ let terminationPromise;
52
53
  let settled = false;
53
54
  let child;
54
55
  try {
@@ -99,7 +100,7 @@ export async function runMeasuredCommand(options) {
99
100
  const KILL_GRACE_MS = 3000;
100
101
  const terminate = () => {
101
102
  if (options.terminateProcessTree === true) {
102
- void terminateProcessTree(child.pid);
103
+ terminationPromise ??= terminateProcessTree(child.pid);
103
104
  }
104
105
  else {
105
106
  forceTerminateProcess(child.pid);
@@ -145,14 +146,19 @@ export async function runMeasuredCommand(options) {
145
146
  child.stdin?.write(options.stdinText, "utf8");
146
147
  child.stdin?.end();
147
148
  }
148
- // forceTerminateProcess is fire-and-forget (it never confirms the target process tree actually
149
- // exited: on Windows it spawns a detached `taskkill /T /F` and does not await or check its
150
- // result). That is normally sufficient, but a grandchild process spawned through an
151
- // intermediate shim (for example a Windows .cmd launcher) can occasionally outlive an
152
- // already-killed intermediate process. Without a bounded fallback, a caller whose kill silently
153
- // failed would await this promise forever instead of receiving the already-known timeout error.
154
- // (KILL_GRACE_MS and the shared `terminate` step are declared above, beside the output collectors.)
149
+ // Tree termination is awaited before a bounded result is returned. The helper has its own
150
+ // platform-specific time bound; a failed or timed-out tree kill remains visible in `error`.
151
+ // Non-tree callers retain the legacy kill-grace fallback below.
155
152
  const settle = async (exitCode) => {
153
+ if (settled)
154
+ return;
155
+ if (terminationPromise) {
156
+ const terminated = await terminationPromise;
157
+ if (!terminated) {
158
+ const cleanupError = "Could not confirm termination of the command process tree.";
159
+ spawnError = spawnError ? `${spawnError} ${cleanupError}` : cleanupError;
160
+ }
161
+ }
156
162
  if (settled)
157
163
  return;
158
164
  settled = true;
@@ -4,7 +4,8 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { resolveWithinRoot } from "../../../core/pathSafety.js";
6
6
  import { isSameOrInside, resolvePhysicalPath } from "../../../evaluation/benchmarkSandbox/pathPolicy.js";
7
- import { campaignAssert, campaignHash, canonicalCampaignJson, compareCampaignIdentity, validateCampaignIdentity } from "./campaignIdentity.js";
7
+ import { CAMPAIGN_IDENTITY_VERSION_V2, campaignAssert, campaignHash, canonicalCampaignJson, compareCampaignIdentity, validateCampaignIdentity } from "./campaignIdentity.js";
8
+ import { PROMPT_CONTRACT_ARTIFACT_FILE, renderPromptContractArtifact } from "./promptContract.js";
8
9
  import { CAMPAIGN_CHECKPOINT_MAX_BYTES, requiredCampaignArtifacts, validateCampaignCheckpoint, validateCampaignCompletion, validateCampaignHistory, validateCampaignRelativePath } from "./campaignCheckpoint.js";
9
10
  import { projectCampaignTreatmentEvidence } from "./campaignCheckpoint.js";
10
11
  import { analyzeAgentSuccessTreatment } from "./analysis.js";
@@ -46,7 +47,7 @@ async function plainAncestors(candidate) {
46
47
  }
47
48
  }
48
49
  /** Resolve filesystem aliases before inspecting ancestors (macOS exposes /var as /private/var). */
49
- async function resolveCampaignRoot(candidate) {
50
+ export async function resolveCampaignRoot(candidate) {
50
51
  const resolved = path.resolve(candidate);
51
52
  let existing = resolved;
52
53
  const remainder = [];
@@ -160,8 +161,29 @@ export async function prepareCampaignCompletion(boundary, identity, task, source
160
161
  validateCampaignCompletion(result, identity, task, source.treatmentId);
161
162
  return result;
162
163
  }
164
+ /**
165
+ * A V2 identity binds the effective prompt contract. Every attempt directory that holds provider artifacts must also hold
166
+ * prompt-contract.json whose bytes equal the deterministic rendering of the identity's contract. The expectation comes from
167
+ * the checksummed identity, so the sidecar cannot vouch for itself and the closed V1 checkpoint schema is not extended.
168
+ */
169
+ async function verifyPromptContractArtifacts(root, identity, references) {
170
+ if (identity.schemaVersion !== CAMPAIGN_IDENTITY_VERSION_V2)
171
+ return;
172
+ const expected = Buffer.from(renderPromptContractArtifact(identity.promptContract), "utf8");
173
+ const directories = new Set(references.filter((r) => r.kind === "provider").map((r) => path.posix.dirname(r.path)));
174
+ for (const directoryPath of [...directories].sort()) {
175
+ const bytes = await boundedRead(await safeFile(root, `${directoryPath}/${PROMPT_CONTRACT_ARTIFACT_FILE}`), 1024 * 1024);
176
+ campaignAssert(bytes.equals(expected), "PROMPT_CONTRACT_ARTIFACT");
177
+ }
178
+ }
163
179
  async function verifyArtifacts(boundary, checkpoint) {
164
180
  const root = await validateRoot(boundary);
181
+ for (const unit of checkpoint.units)
182
+ await verifyPromptContractArtifacts(root, checkpoint.identity, unit.completion?.artifacts ?? []);
183
+ if (checkpoint.schemaVersion === CAMPAIGN_CHECKPOINT_SCHEMA_V2)
184
+ for (const unit of checkpoint.units)
185
+ for (const epoch of unit.epochs)
186
+ await verifyPromptContractArtifacts(root, checkpoint.identity, epoch.terminal?.artifacts ?? []);
165
187
  for (const unit of checkpoint.units)
166
188
  for (const reference of unit.completion?.artifacts ?? []) {
167
189
  const bytes = await boundedRead(await safeFile(root, reference.path), 32 * 1024 * 1024);
@@ -20,6 +20,7 @@ import { buildAgentSuccessRateExecutionArtifact, validateAgentSuccessRateExecuti
20
20
  import { buildAgentSuccessRateAnalysisArtifact, validateAgentSuccessRateAnalysisArtifact, validateAgentSuccessRateArtifactFamily, AGENT_SUCCESS_RATE_ANALYSIS_ARTIFACT_FILE } from "./analysisArtifact.js";
21
21
  import { defaultContextDependencies, sealTask, writeAgentSuccessRateArtifactTargets } from "./plugin.js";
22
22
  import { DEFAULT_AGENT_SUCCESS_KIT_COMMAND } from "./contextGeneration.js";
23
+ import { promptContractMatchesCurrentRenderer } from "./promptContract.js";
23
24
  import { executeResumableCampaign } from "./campaignResumableExecution.js";
24
25
  const preparations = new WeakMap();
25
26
  async function establishPhysicalRoot(root) {
@@ -81,7 +82,7 @@ async function produceIdentity(input) {
81
82
  projects.push({ projectId, files });
82
83
  }
83
84
  const provider = input.provider;
84
- const allowed = provider.kind === "deterministic-fixture" ? ["kind"] : provider.kind === "trusted-simulated-provider" ? ["kind", "simulationId", "providerId", "timeoutMs", "repairAttempts", "runAgent", "contextDependencies"] : ["kind", "providerId", "timeoutMs", "repairAttempts", "includeRealAgents", "trustedExecutable", "kitCommand", "contextDependencies", "env"];
85
+ const allowed = provider.kind === "deterministic-fixture" ? ["kind"] : provider.kind === "trusted-simulated-provider" ? ["kind", "simulationId", "providerId", "timeoutMs", "repairAttempts", "runAgent", "contextDependencies", "promptContract"] : ["kind", "providerId", "timeoutMs", "repairAttempts", "includeRealAgents", "trustedExecutable", "kitCommand", "contextDependencies", "env", "promptContract"];
85
86
  campaignAssert(Object.keys(provider).every(key => allowed.includes(key)), "INCONSISTENT_EXECUTION_MODE");
86
87
  const simulated = provider.kind === "trusted-simulated-provider";
87
88
  campaignAssert(["deterministic-fixture", "trusted-simulated-provider", "real-agent"].includes(provider.kind), "INVALID_EXECUTION_MODE");
@@ -102,6 +103,9 @@ async function produceIdentity(input) {
102
103
  await rm(commandsDir, { recursive: true, force: true });
103
104
  }
104
105
  }
106
+ const promptContract = provider.kind === "deterministic-fixture" ? null : provider.promptContract ?? null;
107
+ if (promptContract !== null && provider.kind !== "deterministic-fixture")
108
+ campaignAssert(promptContract.providerId === provider.providerId && promptContractMatchesCurrentRenderer(promptContract), "PROMPT_CONTRACT_UNVERIFIABLE");
105
109
  const functions = (object) => Object.entries(object).map(([key, value]) => ({ key, source: String(value) })).sort((a, b) => a.key < b.key ? -1 : 1);
106
110
  return createCampaignIdentity({
107
111
  tasks: input.tasks,
@@ -109,6 +113,7 @@ async function produceIdentity(input) {
109
113
  config: provider.kind === "deterministic-fixture" ? { providerId: provider.kind, timeoutMs: 1000, repairAttempts: 0 } : { providerId: provider.providerId, timeoutMs: provider.timeoutMs, repairAttempts: provider.repairAttempts },
110
114
  contextConfiguration: { contract: "existing-agent-success-context-v1", treatments: [...input.treatments] },
111
115
  retrievalConfiguration: { contract: simulated ? "trusted-simulated-retrieval-v1" : real ? "real-provider-retrieval-v1" : "no-retrieval-fixture-v1", dependencies: simulated ? functions(provider.contextDependencies) : real ? functions(provider.contextDependencies ?? defaultContextDependencies()) : [], ...(real ? { kitCommand: provider.kitCommand ?? DEFAULT_AGENT_SUCCESS_KIT_COMMAND, version: retrievalVersion } : {}), executionDependencies: functions(input.dependencies ?? {}) },
116
+ promptContract,
112
117
  verifiedProviderObservation: simulated ? Buffer.from(canonicalCampaignJson({ contract: "trusted-simulated-provider-v1", simulationId: provider.simulationId, implementation: String(provider.runAgent) })) : observed
113
118
  });
114
119
  }
@@ -206,7 +211,7 @@ export async function executeAgentSuccessCampaign(prepared, args) {
206
211
  const result = provider.kind === "trusted-simulated-provider" && contexts ? await executeAgentSuccessRepairTreatment({ ...common, repair: {
207
212
  providerId: provider.providerId, repairAttempts: provider.repairAttempts,
208
213
  createProposalSource: ({ attemptNumber, feedback }) => createRealAgentProposalSource({
209
- settings: { providerId: provider.providerId, timeoutMs: provider.timeoutMs, outDir: boundary.outputRoot, privateRoots: common.privateRoots, protectedRoots: [boundary.toolRoot, boundary.outputRoot, runtimeRoot, canonicalProjectRoot],
214
+ settings: { providerId: provider.providerId, timeoutMs: provider.timeoutMs, ...(provider.promptContract ? { promptContract: provider.promptContract } : {}), outDir: boundary.outputRoot, privateRoots: common.privateRoots, protectedRoots: [boundary.toolRoot, boundary.outputRoot, runtimeRoot, canonicalProjectRoot],
210
215
  runAgent: async (request) => {
211
216
  await assertCampaignDispatch(boundary, identity, ownership);
212
217
  await prepareCampaignProviderOutput(boundary, identity, ownership, agentSuccessAgentArtifactDirectory(task.benchmarkProject, task.id, unit.treatmentId, attemptNumber));
@@ -1,11 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { validateAgentSuccessTask } from "../../../evaluation/agentSuccess/index.js";
3
3
  import { AGENT_SUCCESS_RATE_TREATMENT_IDS } from "./metadata.js";
4
+ import { assertPromptContractShape, promptContractDigest } from "./promptContract.js";
4
5
  export const CAMPAIGN_IDENTITY_VERSION = "my-dev-kit-lab-agent-success-rate-campaign-identity-v1";
6
+ /** V2 extends V1 with the normalized prompt contract and its digest; V1 evidence stays readable and unchanged. */
7
+ export const CAMPAIGN_IDENTITY_VERSION_V2 = "my-dev-kit-lab-agent-success-rate-campaign-identity-v2";
5
8
  export const CAMPAIGN_COMPONENTS = ["corpus", "tasks", "projects", "checks", "behavior", "context", "retrieval", "execution", "providerBuild"];
6
9
  export const CAMPAIGN_SOURCE_FILES = ["src/agents/types.ts", "src/agents/normalizeAgentTelemetry.ts", "src/agents/parseAgentTokenUsage.ts", "src/agents/runAgentPrompt.ts", "src/agents/adapters/codexAdapter.ts", "src/agents/adapters/claudeAdapter.ts", "src/evaluation/classifyAgentRunOutcome.ts", "src/experiments/plugins/agentSuccessRate/execution.ts", "src/experiments/plugins/agentSuccessRate/realAgentExecution.ts", "src/experiments/plugins/agentSuccessRate/contextGeneration.ts", "src/experiments/plugins/agentSuccessRate/repairExecution.ts", "src/experiments/plugins/agentSuccessRate/repairPolicy.ts", "src/experiments/plugins/agentSuccessRate/analysis.ts", "src/experiments/plugins/agentSuccessRate/plugin.ts"];
7
10
  /** Additional transitive execution/safety owners; a missing source contract cannot certify reuse. */
8
- export const CAMPAIGN_DEPENDENCY_FILES = ["src/experiments/plugins/agentSuccessRate/executionTypes.ts", "src/experiments/plugins/agentSuccessRate/analysisTypes.ts", "src/experiments/plugins/agentSuccessRate/attemptEvidence.ts", "src/experiments/plugins/agentSuccessRate/campaignIdentity.ts", "src/experiments/plugins/agentSuccessRate/campaignCheckpoint.ts", "src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.ts", "src/evaluation/agentSuccess/patchTypes.ts", "src/evaluation/agentSuccess/taskTypes.ts", "src/evaluation/agentSuccess/verificationTypes.ts", "src/evaluation/agentSuccess/corpusTypes.ts", "src/evaluation/benchmarkSandbox/types.ts", "src/evaluation/benchmarkSandbox/errors.ts", "src/evaluation/changeSet/types.ts", "src/agents/agentRegistry.ts", "src/core/pathSafety.ts", "src/core/runMeasuredCommand.ts", "src/core/resolveCommand.ts", "src/core/processTree.ts", "src/core/fileGlobs.ts", "src/core/countTokens.ts", "src/core/commandLine.ts", "src/runtime/managedProcess.ts", "src/evaluation/runMyDevKitRetrieval.ts", "src/evaluation/agentSuccess/assessBaseline.ts", "src/evaluation/agentSuccess/applyPatchToSandbox.ts", "src/evaluation/agentSuccess/extractPatchCandidate.ts", "src/evaluation/agentSuccess/parseUnifiedDiff.ts", "src/evaluation/agentSuccess/runVerificationCheck.ts", "src/evaluation/agentSuccess/taskPaths.ts", "src/evaluation/agentSuccess/validatePatchPolicy.ts", "src/evaluation/agentSuccess/validateAgentSuccessTask.ts", "src/evaluation/agentSuccess/readAgentSuccessCorpus.ts", "src/evaluation/agentSuccess/validateAgentSuccessCorpus.ts", "src/evaluation/benchmarkSandbox/createBenchmarkSandbox.ts", "src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.ts", "src/evaluation/benchmarkSandbox/pathPolicy.ts", "src/evaluation/benchmarkSandbox/treeSnapshot.ts", "src/evaluation/benchmarkSandbox/gitExecutor.ts", "src/evaluation/benchmarkSandbox/minimalHostEnv.ts", "src/evaluation/changeSet/captureChangeSet.ts", "src/experiments/plugins/agentSuccessRate/agentTaskProjection.ts", "src/experiments/plugins/agentSuccessRate/realAgentPrompt.ts", "src/experiments/plugins/agentSuccessRate/repairFeedback.ts", "src/experiments/plugins/agentSuccessRate/repairAnalysis.ts", "src/experiments/plugins/agentSuccessRate/metadata.ts", "src/experiments/plugins/agentSuccessRate/config.ts", "src/experiments/plugins/agentSuccessRate/executionArtifact.ts", "src/experiments/plugins/agentSuccessRate/analysisArtifact.ts", "src/experiments/outputPaths.ts"];
11
+ export const CAMPAIGN_DEPENDENCY_FILES = ["src/experiments/plugins/agentSuccessRate/executionTypes.ts", "src/experiments/plugins/agentSuccessRate/analysisTypes.ts", "src/experiments/plugins/agentSuccessRate/attemptEvidence.ts", "src/experiments/plugins/agentSuccessRate/campaignIdentity.ts", "src/experiments/plugins/agentSuccessRate/campaignCheckpoint.ts", "src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.ts", "src/evaluation/agentSuccess/patchTypes.ts", "src/evaluation/agentSuccess/taskTypes.ts", "src/evaluation/agentSuccess/verificationTypes.ts", "src/evaluation/agentSuccess/corpusTypes.ts", "src/evaluation/benchmarkSandbox/types.ts", "src/evaluation/benchmarkSandbox/errors.ts", "src/evaluation/changeSet/types.ts", "src/agents/agentRegistry.ts", "src/core/pathSafety.ts", "src/core/runMeasuredCommand.ts", "src/core/resolveCommand.ts", "src/core/processTree.ts", "src/core/fileGlobs.ts", "src/core/countTokens.ts", "src/core/commandLine.ts", "src/runtime/managedProcess.ts", "src/evaluation/runMyDevKitRetrieval.ts", "src/evaluation/agentSuccess/assessBaseline.ts", "src/evaluation/agentSuccess/applyPatchToSandbox.ts", "src/evaluation/agentSuccess/extractPatchCandidate.ts", "src/evaluation/agentSuccess/parseUnifiedDiff.ts", "src/evaluation/agentSuccess/runVerificationCheck.ts", "src/evaluation/agentSuccess/taskPaths.ts", "src/evaluation/agentSuccess/validatePatchPolicy.ts", "src/evaluation/agentSuccess/validateAgentSuccessTask.ts", "src/evaluation/agentSuccess/readAgentSuccessCorpus.ts", "src/evaluation/agentSuccess/validateAgentSuccessCorpus.ts", "src/evaluation/benchmarkSandbox/createBenchmarkSandbox.ts", "src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.ts", "src/evaluation/benchmarkSandbox/pathPolicy.ts", "src/evaluation/benchmarkSandbox/treeSnapshot.ts", "src/evaluation/benchmarkSandbox/gitExecutor.ts", "src/evaluation/benchmarkSandbox/minimalHostEnv.ts", "src/evaluation/changeSet/captureChangeSet.ts", "src/experiments/plugins/agentSuccessRate/agentTaskProjection.ts", "src/experiments/plugins/agentSuccessRate/realAgentPrompt.ts", "src/experiments/plugins/agentSuccessRate/promptContract.ts", "src/experiments/plugins/agentSuccessRate/strictDiffResponse.ts", "src/experiments/plugins/agentSuccessRate/repairFeedback.ts", "src/experiments/plugins/agentSuccessRate/repairAnalysis.ts", "src/experiments/plugins/agentSuccessRate/metadata.ts", "src/experiments/plugins/agentSuccessRate/config.ts", "src/experiments/plugins/agentSuccessRate/executionArtifact.ts", "src/experiments/plugins/agentSuccessRate/analysisArtifact.ts", "src/experiments/outputPaths.ts"];
9
12
  export function campaignAssert(condition, code = "INVALID_CHECKPOINT") {
10
13
  if (!condition)
11
14
  throw new Error(code);
@@ -42,8 +45,10 @@ export function campaignExact(value, keys) {
42
45
  campaignAssert(Object.keys(value).sort().join("\0") === [...keys].sort().join("\0"));
43
46
  }
44
47
  export function validateCampaignIdentity(value) {
45
- campaignExact(value, ["schemaVersion", "algorithm", "pluginContract", "digest", "selected", "treatmentOrder", "config", "components"]);
46
- campaignAssert(value.schemaVersion === CAMPAIGN_IDENTITY_VERSION && value.algorithm === "sha256" && value.pluginContract === "agent-success-rate/1.0.0", "INCOMPATIBLE_IDENTITY");
48
+ const v2 = typeof value === "object" && value !== null && value.schemaVersion === CAMPAIGN_IDENTITY_VERSION_V2;
49
+ const baseKeys = ["schemaVersion", "algorithm", "pluginContract", "digest", "selected", "treatmentOrder", "config", "components"];
50
+ campaignExact(value, v2 ? [...baseKeys, "promptContract", "promptContractDigest"] : baseKeys);
51
+ campaignAssert(value.schemaVersion === (v2 ? CAMPAIGN_IDENTITY_VERSION_V2 : CAMPAIGN_IDENTITY_VERSION) && value.algorithm === "sha256" && value.pluginContract === "agent-success-rate/1.0.0", "INCOMPATIBLE_IDENTITY");
47
52
  campaignAssert(Array.isArray(value.selected) && value.selected.length > 0 && value.selected.length <= 1000);
48
53
  const ids = new Set();
49
54
  for (const row of value.selected) {
@@ -60,6 +65,17 @@ export function validateCampaignIdentity(value) {
60
65
  campaignExact(component, ["version", "digest"]);
61
66
  campaignAssert(component.version === "1" && (component.digest === null || isCampaignDigest(component.digest)));
62
67
  }
68
+ if (v2) {
69
+ try {
70
+ assertPromptContractShape(value.promptContract);
71
+ }
72
+ catch {
73
+ campaignAssert(false, "PROMPT_CONTRACT_INVALID");
74
+ }
75
+ const contract = value.promptContract;
76
+ campaignAssert(contract.providerId === value.config.providerId, "PROMPT_CONTRACT_PROVIDER_MISMATCH");
77
+ campaignAssert(isCampaignDigest(value.promptContractDigest) && value.promptContractDigest === promptContractDigest(contract), "PROMPT_CONTRACT_DIGEST");
78
+ }
63
79
  const { digest, ...body } = value;
64
80
  campaignAssert(isCampaignDigest(digest) && digest === campaignHash(canonicalCampaignJson(body)), "IDENTITY_INTEGRITY");
65
81
  }
@@ -83,7 +99,12 @@ export function createCampaignIdentity(input) {
83
99
  projects = digest(input.projects.map((p) => ({ projectId: p.projectId, files: manifest(p.files) })).sort((a, b) => a.projectId < b.projectId ? -1 : a.projectId > b.projectId ? 1 : 0));
84
100
  }
85
101
  const values = { corpus: input.corpusBytes === null ? null : campaignHash(input.corpusBytes), tasks: digest(input.tasks), projects, checks: digest(input.tasks.map((t) => ({ id: t.id, task: t.taskChecks, regression: t.regressionChecks, protected: t.protectedFiles }))), behavior: digest(input.tasks.map((t) => ({ id: t.id, facts: t.behaviorFacts, expected: t.expectedEditFiles, allowed: t.allowedEditFiles }))), context: input.contextConfiguration === null ? null : digest(input.contextConfiguration), retrieval: input.retrievalConfiguration === null ? null : digest(input.retrievalConfiguration), execution: source, providerBuild: input.config.providerId === "deterministic-fixture" ? digest("no-provider-invocation-v1") : input.verifiedProviderObservation === null ? null : campaignHash(input.verifiedProviderObservation) };
86
- const body = { schemaVersion: CAMPAIGN_IDENTITY_VERSION, algorithm: "sha256", pluginContract: "agent-success-rate/1.0.0", selected: input.tasks.map((t) => ({ caseId: t.id, projectId: t.benchmarkProject, taskDigest: digest(t) })), treatmentOrder: [...AGENT_SUCCESS_RATE_TREATMENT_IDS], config: structuredClone(input.config), components: Object.fromEntries(CAMPAIGN_COMPONENTS.map((key) => [key, { version: "1", digest: values[key] }])) };
102
+ const contract = input.promptContract ?? null;
103
+ if (contract !== null) {
104
+ assertPromptContractShape(contract);
105
+ campaignAssert(input.config.providerId !== "deterministic-fixture" && contract.providerId === input.config.providerId, "PROMPT_CONTRACT_PROVIDER_MISMATCH");
106
+ }
107
+ const body = { schemaVersion: contract === null ? CAMPAIGN_IDENTITY_VERSION : CAMPAIGN_IDENTITY_VERSION_V2, algorithm: "sha256", pluginContract: "agent-success-rate/1.0.0", selected: input.tasks.map((t) => ({ caseId: t.id, projectId: t.benchmarkProject, taskDigest: digest(t) })), treatmentOrder: [...AGENT_SUCCESS_RATE_TREATMENT_IDS], config: structuredClone(input.config), components: Object.fromEntries(CAMPAIGN_COMPONENTS.map((key) => [key, { version: "1", digest: values[key] }])), ...(contract === null ? {} : { promptContract: structuredClone(contract), promptContractDigest: promptContractDigest(contract) }) };
87
108
  const result = { ...body, digest: digest(body) };
88
109
  validateCampaignIdentity(result);
89
110
  return result;
@@ -96,6 +117,10 @@ export function compareCampaignIdentity(left, right) {
96
117
  catch {
97
118
  return "UNVERIFIABLE";
98
119
  }
120
+ if (left.schemaVersion !== right.schemaVersion)
121
+ return "MISMATCH";
122
+ if (left.schemaVersion === CAMPAIGN_IDENTITY_VERSION_V2 && right.schemaVersion === CAMPAIGN_IDENTITY_VERSION_V2 && (canonicalCampaignJson(left.promptContract) !== canonicalCampaignJson(right.promptContract) || left.promptContractDigest !== right.promptContractDigest))
123
+ return "MISMATCH";
99
124
  if (canonicalCampaignJson(left.selected) !== canonicalCampaignJson(right.selected) || canonicalCampaignJson(left.config) !== canonicalCampaignJson(right.config))
100
125
  return "MISMATCH";
101
126
  let missing = false;
@@ -1,5 +1,6 @@
1
- import { lstat, readFile, realpath } from "node:fs/promises";
1
+ import { lstat, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { isSameOrInside, resolvePhysicalPath } from "../../../evaluation/benchmarkSandbox/pathPolicy.js";
3
4
  import { normalizeAgentDurationV091 as normalizedDuration, normalizeAgentTokenUsageV091 as normalizedTokens } from "../../../agents/normalizeAgentTelemetry.js";
4
5
  import { validateAgentSuccessRateExecutionArtifact } from "./executionArtifact.js";
5
6
  import { validateAgentSuccessRateAnalysisArtifact, validateAgentSuccessRateArtifactFamily } from "./analysisArtifact.js";
@@ -88,7 +89,8 @@ export async function readCampaignProgressScientificFamily(snapshot, boundary) {
88
89
  const load = async (name) => {
89
90
  const ref = cp.finalization.artifacts.find(a => a.path === name);
90
91
  const file = path.join(boundary.outputRoot, name), stat = await lstat(file);
91
- campaignAssert(stat.isFile() && !stat.isSymbolicLink() && stat.size === ref.bytes && path.resolve(await realpath(file)).toLowerCase() === path.resolve(file).toLowerCase(), "SCIENTIFIC_ARTIFACT_UNSAFE");
92
+ const [physicalRoot, physicalFile] = await Promise.all([resolvePhysicalPath(boundary.outputRoot), resolvePhysicalPath(file)]);
93
+ campaignAssert(stat.isFile() && !stat.isSymbolicLink() && stat.size === ref.bytes && isSameOrInside(physicalRoot, physicalFile) && path.dirname(physicalFile) === physicalRoot, "SCIENTIFIC_ARTIFACT_UNSAFE");
92
94
  const bytes = await readFile(file);
93
95
  campaignAssert(bytes.length === ref.bytes && campaignHash(bytes) === ref.sha256, "SCIENTIFIC_ARTIFACT_CHANGED");
94
96
  return JSON.parse(bytes.toString("utf8"));
@@ -119,7 +119,7 @@ export async function executeResumableCampaign(prepared, args) {
119
119
  campaignAssert(provider.kind === "deterministic-fixture" || contexts !== null, "PROVIDER_CONTEXT_REQUIRED");
120
120
  const result = provider.kind !== "deterministic-fixture" && contexts ? await executeAgentSuccessRepairTreatment({ ...common, repair: {
121
121
  providerId: provider.providerId, repairAttempts: provider.repairAttempts,
122
- createProposalSource: ({ attemptNumber, feedback }) => createRealAgentProposalSource({ settings: { providerId: provider.providerId, timeoutMs: provider.timeoutMs, ...(provider.kind === "real-agent" ? { trustedExecutable: provider.trustedExecutable, env: provider.env } : {}), outDir: epochRoot, privateRoots: common.privateRoots, protectedRoots: [boundary.toolRoot, boundary.outputRoot, activeRuntime, canonicalProjectRoot], runAgent: async (request) => {
122
+ createProposalSource: ({ attemptNumber, feedback }) => createRealAgentProposalSource({ settings: { providerId: provider.providerId, timeoutMs: provider.timeoutMs, ...(provider.promptContract ? { promptContract: provider.promptContract } : {}), ...(provider.kind === "real-agent" ? { trustedExecutable: provider.trustedExecutable, env: provider.env } : {}), outDir: epochRoot, privateRoots: common.privateRoots, protectedRoots: [boundary.toolRoot, boundary.outputRoot, activeRuntime, canonicalProjectRoot], runAgent: async (request) => {
123
123
  await fresh();
124
124
  campaignAssert((await read()).checkpoint?.integrity === claimed.integrity, "STALE_INVOCATION_CLAIM");
125
125
  await prepareCampaignProviderOutput(boundary, identity, owner, `${epoch.namespace}/${agentSuccessAgentArtifactDirectory(unit.task.benchmarkProject, unit.task.id, unit.treatmentId, attemptNumber)}`);
@@ -2,12 +2,37 @@ import { parseCommandString } from "../../../core/commandLine.js";
2
2
  import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
3
3
  import { DEFAULT_AGENT_SUCCESS_KIT_COMMAND } from "./contextGeneration.js";
4
4
  import { AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS } from "./realAgentExecution.js";
5
+ import { createPromptContract } from "./promptContract.js";
6
+ import { AGENT_SUCCESS_PROMPT_EXPLANATION_MODES, AGENT_SUCCESS_PROMPT_LENGTH_MODES, AGENT_SUCCESS_PROMPT_TEMPLATE_IDS, AGENT_SUCCESS_PROMPT_TOOL_USE_MODES } from "./realAgentPrompt.js";
5
7
  export const AGENT_SUCCESS_RATE_REAL_AGENT_IDS = ["codex", "claude"];
8
+ const PROMPT_FIELD_ALLOWED_VALUES = {
9
+ promptTemplate: AGENT_SUCCESS_PROMPT_TEMPLATE_IDS,
10
+ promptLength: AGENT_SUCCESS_PROMPT_LENGTH_MODES,
11
+ promptExplanation: AGENT_SUCCESS_PROMPT_EXPLANATION_MODES,
12
+ promptToolUse: AGENT_SUCCESS_PROMPT_TOOL_USE_MODES
13
+ };
14
+ const PROMPT_FIELDS = [...Object.keys(PROMPT_FIELD_ALLOWED_VALUES), "maxCommandCount"];
15
+ /** True when any prompt-contract option was explicitly supplied (even with its default value). */
16
+ export function hasExplicitPromptContractSelection(config) {
17
+ return PROMPT_FIELDS.some((field) => config[field] !== undefined);
18
+ }
19
+ /** The explicit prompt contract for a validated real-agent config, or null when no prompt option was supplied. */
20
+ export function resolveAgentSuccessPromptContract(config) {
21
+ if (config.agentId === undefined || !hasExplicitPromptContractSelection(config))
22
+ return null;
23
+ return createPromptContract(config.agentId, {
24
+ ...(config.promptTemplate !== undefined ? { templateId: config.promptTemplate } : {}),
25
+ ...(config.promptLength !== undefined ? { lengthMode: config.promptLength } : {}),
26
+ ...(config.promptExplanation !== undefined ? { explanationMode: config.promptExplanation } : {}),
27
+ ...(config.promptToolUse !== undefined ? { toolUseMode: config.promptToolUse } : {}),
28
+ ...(config.maxCommandCount !== undefined ? { maxCommandCount: config.maxCommandCount } : {})
29
+ });
30
+ }
6
31
  export const AGENT_SUCCESS_RATE_REPAIR_ATTEMPT_VALUES = [0, 1, 2];
7
32
  export const defaultAgentSuccessRateConfig = {
8
33
  outDir: "lab-output/agent-success-rate"
9
34
  };
10
- const SUPPORTED_FIELDS = ["outDir", "caseIds", "benchmarkProjects", "agentId", "includeRealAgents", "timeoutMs", "kitCommand", "repairAttempts"];
35
+ const SUPPORTED_FIELDS = ["outDir", "caseIds", "benchmarkProjects", "agentId", "includeRealAgents", "timeoutMs", "kitCommand", "repairAttempts", ...PROMPT_FIELDS];
11
36
  /** Names that are recognized only to give a precise rejection. */
12
37
  const REJECTED_FIELD_MESSAGES = {
13
38
  commandTemplate: "commandTemplate is not supported; real providers use the fixed adapter invocation.",
@@ -31,7 +56,12 @@ export const agentSuccessRateConfigDefinition = {
31
56
  { name: "includeRealAgents", type: "boolean", description: "Must be exactly true whenever agentId is set; rejected otherwise." },
32
57
  { name: "timeoutMs", type: "number", description: "Per-attempt provider timeout in ms (real-agent mode only)." },
33
58
  { name: "kitCommand", type: "string", description: "my-dev-kit command used for context-pack retrieval (real-agent mode only)." },
34
- { name: "repairAttempts", type: "number", description: "Repairs allowed after the initial attempt: 0 (default), 1 or 2. Real-agent mode only; at most 3 attempts per treatment." }
59
+ { name: "repairAttempts", type: "number", description: "Repairs allowed after the initial attempt: 0 (default), 1 or 2. Real-agent mode only; at most 3 attempts per treatment." },
60
+ { name: "promptTemplate", type: "string", description: "Prompt template: legacy-diff-v1 (default) or strict-diff-v1. Real-agent mode only." },
61
+ { name: "promptLength", type: "string", description: "Prompt length: standard (default) or short. Real-agent mode only." },
62
+ { name: "promptExplanation", type: "string", description: "Explanation policy: legacy (default) or none. Real-agent mode only." },
63
+ { name: "promptToolUse", type: "string", description: "Tool-use statement: prohibited (default) or bounded (explicit zero command budget). Real-agent mode only." },
64
+ { name: "maxCommandCount", type: "number", description: "Maximum command count guidance; only 0 is accepted. Real-agent mode only." }
35
65
  ]
36
66
  };
37
67
  export function validateAgentSuccessRateConfig(config) {
@@ -116,6 +146,21 @@ export function validateAgentSuccessRateConfig(config) {
116
146
  normalized.kitCommand = DEFAULT_AGENT_SUCCESS_KIT_COMMAND;
117
147
  }
118
148
  }
149
+ const suppliedPromptFields = PROMPT_FIELDS.filter((field) => raw[field] !== undefined);
150
+ if (suppliedPromptFields.length > 0) {
151
+ if (agentId === undefined || raw.includeRealAgents !== true) {
152
+ errors.push(`${suppliedPromptFields.join(", ")} require real-agent mode (agentId together with includeRealAgents=true); prompt options never enable a provider.`);
153
+ }
154
+ for (const [field, allowed] of Object.entries(PROMPT_FIELD_ALLOWED_VALUES)) {
155
+ const value = raw[field];
156
+ if (value !== undefined && !(typeof value === "string" && allowed.includes(value))) {
157
+ errors.push(`${field} must be one of: ${allowed.join(", ")}.`);
158
+ }
159
+ }
160
+ if (raw.maxCommandCount !== undefined && !(typeof raw.maxCommandCount === "number" && Number.isInteger(raw.maxCommandCount) && raw.maxCommandCount === 0)) {
161
+ errors.push("maxCommandCount must be the integer 0; the benchmark permits no commands.");
162
+ }
163
+ }
119
164
  return errors.length > 0 ? invalidExperimentConfig(errors) : validExperimentConfig(normalized);
120
165
  }
121
166
  function isRepairAttemptsValue(value) {
@@ -9,7 +9,7 @@ import { summarizeExperimentRun } from "../../results.js";
9
9
  import { analyzeAgentSuccessRate } from "./analysis.js";
10
10
  import { projectAgentFacingTask } from "./agentTaskProjection.js";
11
11
  import { AGENT_SUCCESS_RATE_ANALYSIS_ARTIFACT_FILE, buildAgentSuccessRateAnalysisArtifact, validateAgentSuccessRateAnalysisArtifact, validateAgentSuccessRateArtifactFamily } from "./analysisArtifact.js";
12
- import { agentSuccessRateConfigDefinition, defaultAgentSuccessRateConfig, validateAgentSuccessRateConfig } from "./config.js";
12
+ import { agentSuccessRateConfigDefinition, defaultAgentSuccessRateConfig, resolveAgentSuccessPromptContract, validateAgentSuccessRateConfig } from "./config.js";
13
13
  import { AgentSuccessPackContextBuilder, DEFAULT_AGENT_SUCCESS_KIT_COMMAND, buildContextTaskInput, buildRawFullFileContext, collectForbiddenAgentValues } from "./contextGeneration.js";
14
14
  import { AGENT_SUCCESS_RATE_SELF_ONLY_MESSAGE, AgentSuccessRateInputError, boundMessage, executeAgentSuccessCase, readAgentSuccessTasksInput, resolveControlledBenchmarkProject, resolveSandboxRuntimeRoot, selectAgentSuccessTasks } from "./execution.js";
15
15
  import { AGENT_SUCCESS_RATE_EXECUTION_ARTIFACT_FILE, buildAgentSuccessRateExecutionArtifact, validateAgentSuccessRateExecutionArtifact } from "./executionArtifact.js";
@@ -83,6 +83,7 @@ export const agentSuccessRatePlugin = {
83
83
  timeoutMs: real.timeoutMs,
84
84
  env: readAgentEnvInput(inputs),
85
85
  runAgent: readRunAgentInput(inputs),
86
+ ...(real.promptContract ? { promptContract: real.promptContract } : {}),
86
87
  outDir,
87
88
  protectedRoots: [context.toolRoot, outDir, runtimeRoot, packageRoot(), ...projectRoots.values()],
88
89
  privateRoots: [outDir, context.toolRoot],
@@ -399,7 +400,8 @@ function readRealAgentConfig(config) {
399
400
  providerId: config.agentId,
400
401
  timeoutMs: config.timeoutMs ?? AGENT_SUCCESS_RATE_DEFAULT_AGENT_TIMEOUT_MS,
401
402
  kitCommand: config.kitCommand ?? DEFAULT_AGENT_SUCCESS_KIT_COMMAND,
402
- repairAttempts: config.repairAttempts ?? 0
403
+ repairAttempts: config.repairAttempts ?? 0,
404
+ promptContract: resolveAgentSuccessPromptContract(config)
403
405
  };
404
406
  }
405
407
  /** Captures hidden values for the leak guard, then returns a copy of the task with the reference fixture removed. */
@@ -0,0 +1,121 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AGENT_SUCCESS_PROMPT_EXPLANATION_MODES, AGENT_SUCCESS_PROMPT_LENGTH_MODES, AGENT_SUCCESS_PROMPT_TEMPLATE_IDS, AGENT_SUCCESS_PROMPT_TOOL_USE_MODES, REAL_AGENT_CONTEXT_BEGIN, REAL_AGENT_CONTEXT_END, describeAgentSuccessPromptInstructions, isLegacyDefaultPromptSettings } from "./realAgentPrompt.js";
3
+ /**
4
+ * PromptContractV1 is the typed, persisted identity of the effective patch-generation prompt for one provider. It is
5
+ * deliberately separate from the context-treatment identity (raw-full-file / context-pack): that identity names the
6
+ * context transport, this one names the instruction template. It never carries source/context bodies, trusted checks
7
+ * or provider output.
8
+ */
9
+ export const PROMPT_CONTRACT_SCHEMA_VERSION = "1.0.0";
10
+ export const PROMPT_CONTRACT_ARTIFACT_FILE = "prompt-contract.json";
11
+ const CONTRACT_FIELDS = ["schemaVersion", "templateId", "providerId", "lengthMode", "explanationMode", "toolUseMode", "maxCommandCount", "rendererDigest"];
12
+ function sha256(value) {
13
+ return createHash("sha256").update(value, "utf8").digest("hex");
14
+ }
15
+ /** Sorted-key canonical JSON for flat, finite, string/number/array/object data (the only shapes this module hashes). */
16
+ function canonicalJson(value) {
17
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string")
18
+ return JSON.stringify(value);
19
+ if (Array.isArray(value))
20
+ return `[${value.map(canonicalJson).join(",")}]`;
21
+ const record = value;
22
+ return `{${Object.keys(record)
23
+ .sort()
24
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
25
+ .join(",")}}`;
26
+ }
27
+ /**
28
+ * Digest of the normalized, versioned template definition: the effective settings plus the exact instruction lines and
29
+ * context delimiters the renderer emits for them. It never reads task data, the answer key or hidden checks.
30
+ */
31
+ export function computePromptRendererDigest(settings) {
32
+ return sha256(canonicalJson({
33
+ definition: "my-dev-kit-lab-agent-success-rate-prompt-template-v1",
34
+ settings: {
35
+ templateId: settings.templateId,
36
+ providerId: settings.providerId,
37
+ lengthMode: settings.lengthMode,
38
+ explanationMode: settings.explanationMode,
39
+ toolUseMode: settings.toolUseMode,
40
+ maxCommandCount: settings.maxCommandCount
41
+ },
42
+ delimiters: [REAL_AGENT_CONTEXT_BEGIN, REAL_AGENT_CONTEXT_END],
43
+ instructions: describeAgentSuccessPromptInstructions(settings)
44
+ }));
45
+ }
46
+ /** Normalizes a selection (omitted fields take the inherited defaults) into a complete contract. */
47
+ export function createPromptContract(providerId, selection = {}) {
48
+ const settings = {
49
+ templateId: selection.templateId ?? "legacy-diff-v1",
50
+ providerId,
51
+ lengthMode: selection.lengthMode ?? "standard",
52
+ explanationMode: selection.explanationMode ?? "legacy",
53
+ toolUseMode: selection.toolUseMode ?? "prohibited",
54
+ maxCommandCount: selection.maxCommandCount ?? 0
55
+ };
56
+ const contract = {
57
+ schemaVersion: PROMPT_CONTRACT_SCHEMA_VERSION,
58
+ templateId: settings.templateId,
59
+ providerId,
60
+ lengthMode: settings.lengthMode,
61
+ explanationMode: settings.explanationMode,
62
+ toolUseMode: settings.toolUseMode,
63
+ maxCommandCount: 0,
64
+ rendererDigest: computePromptRendererDigest(settings)
65
+ };
66
+ assertPromptContractShape(contract);
67
+ return contract;
68
+ }
69
+ /** Throws on unknown fields, missing fields or out-of-vocabulary values. Does not recompute the renderer digest. */
70
+ export function assertPromptContractShape(value) {
71
+ const fail = (reason) => {
72
+ throw new Error(`INVALID_PROMPT_CONTRACT: ${reason}`);
73
+ };
74
+ if (typeof value !== "object" || value === null || Array.isArray(value))
75
+ fail("not an object");
76
+ const record = value;
77
+ const keys = Object.keys(record).sort();
78
+ if (keys.join("\0") !== [...CONTRACT_FIELDS].sort().join("\0"))
79
+ fail("fields are not exactly the PromptContractV1 fields");
80
+ if (record.schemaVersion !== PROMPT_CONTRACT_SCHEMA_VERSION)
81
+ fail("unsupported schemaVersion");
82
+ if (!AGENT_SUCCESS_PROMPT_TEMPLATE_IDS.includes(record.templateId))
83
+ fail("unknown templateId");
84
+ if (record.providerId !== "codex" && record.providerId !== "claude")
85
+ fail("unknown providerId");
86
+ if (!AGENT_SUCCESS_PROMPT_LENGTH_MODES.includes(record.lengthMode))
87
+ fail("unknown lengthMode");
88
+ if (!AGENT_SUCCESS_PROMPT_EXPLANATION_MODES.includes(record.explanationMode))
89
+ fail("unknown explanationMode");
90
+ if (!AGENT_SUCCESS_PROMPT_TOOL_USE_MODES.includes(record.toolUseMode))
91
+ fail("unknown toolUseMode");
92
+ if (record.maxCommandCount !== 0)
93
+ fail("maxCommandCount must be 0");
94
+ if (typeof record.rendererDigest !== "string" || !/^[a-f0-9]{64}$/.test(record.rendererDigest))
95
+ fail("invalid rendererDigest");
96
+ }
97
+ /** True when the contract's renderer digest equals the digest the current renderer produces for its settings. */
98
+ export function promptContractMatchesCurrentRenderer(contract) {
99
+ return contract.rendererDigest === computePromptRendererDigest(promptSettingsOf(contract));
100
+ }
101
+ export function promptSettingsOf(contract) {
102
+ return {
103
+ templateId: contract.templateId,
104
+ providerId: contract.providerId,
105
+ lengthMode: contract.lengthMode,
106
+ explanationMode: contract.explanationMode,
107
+ toolUseMode: contract.toolUseMode,
108
+ maxCommandCount: contract.maxCommandCount
109
+ };
110
+ }
111
+ export function promptContractDigest(contract) {
112
+ assertPromptContractShape(contract);
113
+ return sha256(canonicalJson(contract));
114
+ }
115
+ export function isLegacyDefaultPromptContract(contract) {
116
+ return isLegacyDefaultPromptSettings(contract);
117
+ }
118
+ /** Exact bytes of the per-attempt prompt-contract.json artifact; deterministic, so recovery can recompute and compare. */
119
+ export function renderPromptContractArtifact(contract) {
120
+ return `${canonicalJson({ promptContract: contract, promptContractDigest: promptContractDigest(contract) })}\n`;
121
+ }
@@ -1,4 +1,4 @@
1
- import { mkdtemp, readdir, rm } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { normalizeAgentDurationV091, normalizeAgentTokenUsageV091 } from "../../../agents/index.js";
@@ -9,6 +9,8 @@ import { sanitizePathSegment } from "../../outputPaths.js";
9
9
  import { findPromptLeaks } from "./contextGeneration.js";
10
10
  import { boundMessage } from "./execution.js";
11
11
  import { buildAgentSuccessRealAgentPrompt } from "./realAgentPrompt.js";
12
+ import { PROMPT_CONTRACT_ARTIFACT_FILE, createPromptContract, promptSettingsOf, renderPromptContractArtifact } from "./promptContract.js";
13
+ import { validateAgentSuccessFinalAnswer } from "./strictDiffResponse.js";
12
14
  import { summarizeAgentSuccessRepairFeedback } from "./repairFeedback.js";
13
15
  export const AGENT_SUCCESS_RATE_DEFAULT_AGENT_TIMEOUT_MS = 240_000;
14
16
  export const AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS = 1_800_000;
@@ -109,6 +111,9 @@ async function assertNeutralCwd(cwd, protectedRoots) {
109
111
  */
110
112
  export function createRealAgentProposalSource(args) {
111
113
  const { settings } = args;
114
+ const promptContract = settings.promptContract ?? createPromptContract(settings.providerId);
115
+ if (promptContract.providerId !== settings.providerId)
116
+ throw new Error("PROMPT_CONTRACT_PROVIDER_MISMATCH");
112
117
  const attemptNumber = args.attemptNumber ?? 1;
113
118
  return async ({ treatmentId, invoke, notInvokedReason }) => {
114
119
  let runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(null, "not-invoked", null);
@@ -165,6 +170,7 @@ export function createRealAgentProposalSource(args) {
165
170
  task,
166
171
  treatmentId,
167
172
  contextText: context.text,
173
+ prompt: promptSettingsOf(promptContract),
168
174
  ...(args.repair ? { repair: { feedback: args.repair.feedback, attemptNumber, maxAttempts: args.repair.maxAttempts } } : {})
169
175
  });
170
176
  const leaks = findPromptLeaks(promptText, args.forbidden);
@@ -256,6 +262,14 @@ export function createRealAgentProposalSource(args) {
256
262
  const agentDurationMs = Number.isFinite(result.durationMs) && result.durationMs >= 0 ? result.durationMs : Date.now() - wallStarted;
257
263
  const agentTokenUsage = tokenEvidence(result);
258
264
  runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(result, providerStatus, agentDurationMs);
265
+ // The effective contract is persisted next to the provider artifacts so recovery can prove which prompt produced them.
266
+ try {
267
+ await mkdir(attemptAbsolute, { recursive: true });
268
+ await writeFile(path.join(attemptAbsolute, PROMPT_CONTRACT_ARTIFACT_FILE), renderPromptContractArtifact(promptContract), { encoding: "utf8", flag: "wx" });
269
+ }
270
+ catch {
271
+ return noProposal("prompt-contract-not-persisted", "PROMPT_CONTRACT_NOT_PERSISTED", "The effective prompt contract could not be persisted; the provider answer was not evaluated.");
272
+ }
259
273
  if (providerStatus !== "completed") {
260
274
  return noProposal(classification.statusReason, PROVIDER_STATUS_CODES[providerStatus] ?? "PROVIDER_FAILED", `Provider ${settings.providerId} ended with status ${providerStatus}: ${classification.statusReason}`, {
261
275
  agentDurationMs,
@@ -267,6 +281,22 @@ export function createRealAgentProposalSource(args) {
267
281
  }
268
282
  realAgent.finalAnswerAvailable = true;
269
283
  realAgent.providerStatusReason = null;
270
- return { kind: "proposal", ...runtimeTelemetry, text: result.finalAnswerText, realAgent, agentDurationMs, agentTokenUsage, errors: [], contextFile };
284
+ // Application-level output contract, applied only to a completed provider's extracted final answer. An invalid
285
+ // response is handed to the patch pipeline as an empty proposal: it is recorded as malformed, never re-interpreted
286
+ // through legacy extraction, and the diagnostic reason code is retained as a bounded error.
287
+ const output = validateAgentSuccessFinalAnswer(promptContract, result.finalAnswerText);
288
+ if (!output.ok) {
289
+ return {
290
+ kind: "proposal",
291
+ ...runtimeTelemetry,
292
+ text: "",
293
+ realAgent,
294
+ agentDurationMs,
295
+ agentTokenUsage,
296
+ errors: [{ code: `OUTPUT_CONTRACT_${output.code}`, message: `The provider final answer violated the ${promptContract.templateId} output contract (${output.code}).` }],
297
+ contextFile
298
+ };
299
+ }
300
+ return { kind: "proposal", ...runtimeTelemetry, text: output.patchText, realAgent, agentDurationMs, agentTokenUsage, errors: [], contextFile };
271
301
  };
272
302
  }