@dailephd/my-dev-kit-lab 0.9.2 → 0.9.4

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 (44) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +9 -5
  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 +47 -2
  9. package/dist/src/core/processTree.js +41 -17
  10. package/dist/src/core/runMeasuredCommand.js +14 -8
  11. package/dist/src/evaluation/affectedNeighborhood.js +140 -32
  12. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +23 -1
  13. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
  14. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
  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/writePluginExperimentReports.js +4 -2
  39. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
  40. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
  41. package/dist/src/securityValidation/commandRunner.js +13 -6
  42. package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +46 -21
  43. package/docs/METRICS.md +11 -1
  44. package/package.json +3 -3
@@ -4,7 +4,13 @@ import { compareCodeUnits, interpretIndexManifest, readJson } from "./indexSnaps
4
4
  export const AFFECTED_NEIGHBORHOOD_GRAPH_EVIDENCE_SCHEMA_VERSION = "my-dev-kit-lab-affected-neighborhood-graph-evidence-v1";
5
5
  export const AFFECTED_NEIGHBORHOOD_SEED_MAPPING_SCHEMA_VERSION = "my-dev-kit-lab-affected-neighborhood-seed-mapping-v1";
6
6
  const MANIFEST_FILE = "manifest.json";
7
- const SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSION = "2";
7
+ /**
8
+ * Schema 2 (my-dev-kit 1.13.x) identifies a symbol by file path + name. Schema 3 (my-dev-kit 1.14.x)
9
+ * additionally persists each symbol's actual graph `nodeId`, which may be a distinct `symbolq:` identity
10
+ * for same-named members. Both are understood; anything else is rejected.
11
+ */
12
+ const SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSIONS = ["2", "3"];
13
+ const SCHEMA_WITH_PERSISTED_SYMBOL_NODE_IDS = "3";
8
14
  const SUPPORTED_CODE_GRAPH_ARTIFACT_KIND = "code-graph";
9
15
  const SUPPORTED_CODE_GRAPH_VERSION_PATTERN = /^1\./;
10
16
  // Same bounds as the v0.6.0 freshness/snapshot evidence lists.
@@ -57,18 +63,23 @@ function sortWarnings(warnings) {
57
63
  return [...warnings].sort((left, right) => compareCodeUnits(left.code, right.code) || compareCodeUnits(left.subject ?? "", right.subject ?? "") || compareCodeUnits(left.message, right.message));
58
64
  }
59
65
  /**
60
- * Policy: reads indexed-file membership and baseline symbols from a symbol-index artifact. Only
61
- * schema version 2 is understood. A file entry whose symbol list is malformed keeps its
62
- * membership but yields no symbol record; a malformed symbol entry is dropped with a warning.
63
- * Pure.
66
+ * Policy: reads indexed-file membership and baseline symbol identities from a symbol-index artifact.
67
+ * Schema versions 2 and 3 are understood; anything else is unsupported. Under schema 2 a symbol's
68
+ * identity is its upstream file+name node ID. Under schema 3 it is the `nodeId` the index persisted,
69
+ * which is never reconstructed from the simple name: same-named members keep distinct identities,
70
+ * and a record without a usable `nodeId` stays visible as an unmappable identity with a warning
71
+ * instead of receiving a guessed one. A file entry whose symbol list is malformed keeps its
72
+ * membership but yields no symbol record; a malformed symbol entry is dropped with a warning. Pure.
64
73
  */
65
74
  export function interpretSymbolIndexForNeighborhood(symbolIndex) {
66
75
  if (!isRecord(symbolIndex)) {
67
76
  return { ok: false, message: "Symbol index is not a JSON object." };
68
77
  }
69
- if (symbolIndex.schemaVersion !== SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSION) {
70
- return { ok: false, message: `Symbol index schemaVersion is not the supported ${SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSION}.` };
78
+ const schemaVersion = symbolIndex.schemaVersion;
79
+ if (typeof schemaVersion !== "string" || !SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSIONS.includes(schemaVersion)) {
80
+ return { ok: false, message: `Symbol index schemaVersion is not a supported version (${SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSIONS.join(", ")}).` };
71
81
  }
82
+ const persistsNodeIds = schemaVersion === SCHEMA_WITH_PERSISTED_SYMBOL_NODE_IDS;
72
83
  if (!Array.isArray(symbolIndex.files)) {
73
84
  return { ok: false, message: "Symbol index does not list indexed files." };
74
85
  }
@@ -92,29 +103,57 @@ export function interpretSymbolIndexForNeighborhood(symbolIndex) {
92
103
  });
93
104
  continue;
94
105
  }
95
- const seen = new Set();
106
+ const identities = new Map();
96
107
  const duplicates = new Set();
108
+ const missingNodeIdNames = new Set();
97
109
  for (const symbol of entry.symbols) {
98
110
  if (!isRecord(symbol) || !nonEmptyString(symbol.name)) {
99
111
  warnings.push({ code: "invalid-symbol-entry", subject: boundedText(filePath), message: "Symbol index entry lacks a symbol name and was ignored." });
100
112
  continue;
101
113
  }
102
- if (seen.has(symbol.name)) {
103
- duplicates.add(symbol.name);
114
+ const name = symbol.name;
115
+ let identity;
116
+ let identityKey;
117
+ if (!persistsNodeIds) {
118
+ identity = { name, nodeId: symbolNodeId(filePath, name), persisted: false };
119
+ identityKey = identity.nodeId;
120
+ }
121
+ else if (nonEmptyString(symbol.nodeId)) {
122
+ identity = { name, nodeId: symbol.nodeId, persisted: true };
123
+ identityKey = `id:${symbol.nodeId}`;
104
124
  }
105
- seen.add(symbol.name);
125
+ else {
126
+ // No persisted identity: one unmappable identity per name, never a reconstructed one.
127
+ identity = { name, nodeId: null, persisted: true };
128
+ identityKey = `missing:${name}`;
129
+ missingNodeIdNames.add(name);
130
+ }
131
+ if (identities.has(identityKey)) {
132
+ duplicates.add(name);
133
+ continue;
134
+ }
135
+ identities.set(identityKey, identity);
106
136
  }
107
- records.set(filePath, {
108
- symbolNames: [...seen].sort(compareCodeUnits),
109
- duplicateSymbolNames: [...duplicates].sort(compareCodeUnits),
110
- });
137
+ for (const name of [...missingNodeIdNames].sort(compareCodeUnits)) {
138
+ warnings.push({
139
+ code: "symbol-node-id-missing",
140
+ subject: boundedText(`${filePath}#${name}`),
141
+ message: "Schema-3 symbol record has no usable persisted nodeId; its identity is not guessed and it cannot be mapped to a graph node.",
142
+ });
143
+ }
144
+ const sortedRecords = [...identities.values()].sort((left, right) => compareCodeUnits(left.nodeId ?? "", right.nodeId ?? "") || compareCodeUnits(left.name, right.name));
145
+ const names = [...new Set(sortedRecords.map((record) => record.name))].sort(compareCodeUnits);
146
+ const sameName = names.filter((name) => sortedRecords.filter((record) => record.name === name).length > 1);
147
+ // Schema-2 evidence keeps its original shape: each name is its only identity, so no per-record data is added.
148
+ const fileRecord = { symbolNames: names, duplicateSymbolNames: [...duplicates].sort(compareCodeUnits) };
149
+ records.set(filePath, persistsNodeIds ? { ...fileRecord, records: sortedRecords, sameNameSymbolNames: sameName } : fileRecord);
111
150
  }
112
151
  const indexedFilePaths = [...paths].sort(compareCodeUnits);
113
152
  const symbolFiles = new Map();
114
153
  for (const key of [...records.keys()].sort(compareCodeUnits)) {
115
154
  symbolFiles.set(key, records.get(key));
116
155
  }
117
- return { ok: true, value: { indexedFilePaths, symbolFiles, warnings } };
156
+ return { ok: true, value: { schemaVersion, indexedFilePaths, symbolFiles, warnings } };
118
157
  }
119
158
  /**
120
159
  * Policy: validates the structure of a code-graph artifact and preserves its nodes and edges by
@@ -234,7 +273,7 @@ export async function loadAffectedNeighborhoodGraphEvidence(options) {
234
273
  }
235
274
  const symbolIndexSummary = {
236
275
  path: normalizePath(path.relative(path.resolve(indexDir), symbolIndexFile)),
237
- schemaVersion: SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSION,
276
+ schemaVersion: symbols.value.schemaVersion,
238
277
  };
239
278
  const warnings = [...symbols.value.warnings];
240
279
  let codeGraphSummary = null;
@@ -310,10 +349,35 @@ function lookupNode(evidence, expectedNodeId, kind) {
310
349
  export function lookupFileNode(evidence, filePath) {
311
350
  return lookupNode(evidence, fileNodeId(filePath), "file");
312
351
  }
313
- /** Same-file symbol lookup. Never searches for a same-named symbol in another file. */
352
+ /**
353
+ * Same-file symbol lookup by the upstream file+name identity. Never searches for a same-named symbol
354
+ * in another file. This is the schema-2 identity only; it does not find a qualified (`symbolq:`) member.
355
+ */
314
356
  export function lookupSymbolNode(evidence, filePath, symbolName) {
315
357
  return lookupNode(evidence, symbolNodeId(filePath, symbolName), "symbol");
316
358
  }
359
+ /**
360
+ * Exact lookup of one baseline symbol identity. The node must exist under the identity the index
361
+ * actually recorded, be a `symbol` node, and (when the graph names a path) belong to the same
362
+ * file; otherwise it is unresolved. A record without a persisted identity is never mapped, and no
363
+ * identity is derived from its name.
364
+ */
365
+ export function lookupSymbolRecordNode(evidence, filePath, record) {
366
+ if (record.nodeId === null)
367
+ return { ok: false, expectedNodeId: "", reason: "symbol-node-id-missing" };
368
+ const lookup = lookupNode(evidence, record.nodeId, "symbol");
369
+ if (!lookup.ok)
370
+ return lookup;
371
+ const node = evidence.nodes.get(lookup.nodeId);
372
+ if (node && node.path !== null && normalizePath(node.path) !== filePath) {
373
+ return { ok: false, expectedNodeId: record.nodeId, reason: "graph-node-path-mismatch" };
374
+ }
375
+ return lookup;
376
+ }
377
+ /** The baseline identities of one indexed file; evidence without per-record data keeps the schema-2 file+name identity. */
378
+ function symbolRecordsOf(filePath, file) {
379
+ return file.records ?? file.symbolNames.map((name) => ({ name, nodeId: symbolNodeId(filePath, name), persisted: false }));
380
+ }
317
381
  /**
318
382
  * Maps confirmed changed indexed files, and the baseline symbols belonging to them, onto baseline
319
383
  * graph nodes. Pure and deterministic: consumes only the v0.6.0 snapshot and freshness assessment
@@ -404,18 +468,21 @@ export function mapAffectedNeighborhoodSeeds(input) {
404
468
  continue;
405
469
  }
406
470
  if (record.duplicateSymbolNames.length > 0) {
407
- warn("duplicate-symbol-records", `Baseline symbol index lists ${record.duplicateSymbolNames.length} symbol name(s) more than once; each counts as one identity.`, filePath);
471
+ warn("duplicate-symbol-records", `Baseline symbol index repeats ${record.duplicateSymbolNames.length} identical symbol record(s); each counts as one identity.`, filePath);
408
472
  }
409
- for (const name of record.symbolNames) {
473
+ // Each distinct baseline identity is a seed in its own right: same-named members that the index
474
+ // persisted with different node IDs are never merged, and none is reconstructed from the name.
475
+ for (const identity of symbolRecordsOf(filePath, record)) {
476
+ const name = identity.name;
410
477
  symbolIdentityCount += 1;
411
- const symbolExpectedId = symbolNodeId(filePath, name);
478
+ const symbolExpectedId = identity.nodeId ?? "";
412
479
  let symbolResolvedId = null;
413
480
  let symbolReason = null;
414
481
  if (!codeGraphLoaded) {
415
482
  symbolReason = "graph-unavailable";
416
483
  }
417
484
  else {
418
- const lookup = lookupSymbolNode(graph, filePath, name);
485
+ const lookup = lookupSymbolRecordNode(graph, filePath, identity);
419
486
  if (lookup.ok)
420
487
  symbolResolvedId = lookup.nodeId;
421
488
  else
@@ -426,7 +493,12 @@ export function mapAffectedNeighborhoodSeeds(input) {
426
493
  seeds.add(symbolResolvedId);
427
494
  }
428
495
  else {
429
- unresolved.push({ subject: "changed-symbol", path: boundedText(filePath), name: boundedText(name), reason: symbolReason });
496
+ unresolved.push({
497
+ subject: "changed-symbol",
498
+ path: boundedText(filePath),
499
+ name: boundedText(name),
500
+ reason: symbolReason,
501
+ });
430
502
  }
431
503
  changedSymbols.push({
432
504
  path: filePath,
@@ -490,11 +562,41 @@ function uniqueSorted(values) {
490
562
  function normalizeExpectedPath(value) {
491
563
  return normalizePath(value).replace(/^(\.\/)+/, "");
492
564
  }
565
+ /**
566
+ * Candidate graph nodes for one expected symbol in one expected file. A request matches a baseline
567
+ * identity by its simple name or by that identity's exact persisted node ID; every matching identity
568
+ * is a distinct candidate (same-named members are never collapsed). A file the symbol index has no
569
+ * record for keeps the schema-2 file+name lookup. Nothing is derived from the name beyond that.
570
+ */
571
+ function resolveExpectedSymbolInFile(graph, filePath, request) {
572
+ const file = graph.symbolFiles.get(filePath);
573
+ if (!file) {
574
+ const lookup = lookupSymbolNode(graph, filePath, request);
575
+ return { nodeIds: lookup.ok ? [lookup.nodeId] : [], identityMissing: false, pathMismatch: !lookup.ok && lookup.reason === "graph-node-path-mismatch" };
576
+ }
577
+ const nodeIds = [];
578
+ let identityMissing = false;
579
+ let pathMismatch = false;
580
+ for (const identity of symbolRecordsOf(filePath, file)) {
581
+ if (identity.name !== request && identity.nodeId !== request)
582
+ continue;
583
+ const lookup = lookupSymbolRecordNode(graph, filePath, identity);
584
+ if (lookup.ok)
585
+ nodeIds.push(lookup.nodeId);
586
+ else if (lookup.reason === "symbol-node-id-missing")
587
+ identityMissing = true;
588
+ else if (lookup.reason === "graph-node-path-mismatch")
589
+ pathMismatch = true;
590
+ }
591
+ return { nodeIds, identityMissing, pathMismatch };
592
+ }
493
593
  /**
494
594
  * Policy: maps a task's expected files and symbols to baseline graph nodes. Files resolve only to
495
595
  * their exact `file:` node. A symbol resolves only within the task's own expected files and only
496
- * when exactly one same-name node exists there; zero matches is unresolved and several is
497
- * ambiguous (no winner is picked, no node is added). Never searches the rest of the graph. Pure.
596
+ * when exactly one matching identity exists there; zero matches is unresolved and several (in one
597
+ * file or across files, including same-named members with distinct persisted node IDs) is
598
+ * ambiguous (no winner is picked, no node is added). A request that is itself a persisted node ID
599
+ * resolves to exactly that identity. Never searches the rest of the graph. Pure.
498
600
  */
499
601
  export function mapAffectedNeighborhoodTask(input) {
500
602
  const { graph } = input;
@@ -561,15 +663,21 @@ export function mapAffectedNeighborhoodTask(input) {
561
663
  const resolvedSymbols = [];
562
664
  for (const name of expectedSymbols) {
563
665
  // Scope is the task's expected files, whether or not their own file node resolved.
564
- const candidates = uniqueSorted(expectedFiles.flatMap((filePath) => {
565
- const lookup = lookupSymbolNode(graph, filePath, name);
566
- return lookup.ok ? [lookup.nodeId] : [];
567
- }));
568
- if (candidates.length === 1) {
666
+ const found = expectedFiles.map((filePath) => resolveExpectedSymbolInFile(graph, filePath, name));
667
+ const candidates = uniqueSorted(found.flatMap((entry) => entry.nodeIds));
668
+ // A same-name identity that persisted no node ID cannot be excluded, so a lone mappable candidate
669
+ // is not trusted as "the" owner when such a sibling exists.
670
+ const identityMissing = found.some((entry) => entry.identityMissing);
671
+ if (candidates.length === 1 && !identityMissing) {
569
672
  resolvedSymbols.push({ name, nodeId: candidates[0] });
570
673
  }
571
- else if (candidates.length === 0) {
572
- unresolved.push({ subject: "expected-symbol", name: boundedText(name), reason: "graph-node-missing" });
674
+ else if (candidates.length <= 1) {
675
+ const reason = identityMissing
676
+ ? "symbol-node-id-missing"
677
+ : found.some((entry) => entry.pathMismatch)
678
+ ? "graph-node-path-mismatch"
679
+ : "graph-node-missing";
680
+ unresolved.push({ subject: "expected-symbol", name: boundedText(name), reason });
573
681
  }
574
682
  else {
575
683
  ambiguous.push({ name: boundedText(name), candidateNodeIds: candidates });
@@ -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";
@@ -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;
@@ -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. */