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

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 (122) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +19 -3
  3. package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
  4. package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
  5. package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
  6. package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
  7. package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
  8. package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
  9. package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
  10. package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
  11. package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
  12. package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
  13. package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
  14. package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
  15. package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
  16. package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
  17. package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
  18. package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
  19. package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
  20. package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
  21. package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
  22. package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
  23. package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
  24. package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
  25. package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
  26. package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
  27. package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
  28. package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
  29. package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
  30. package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
  31. package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
  32. package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
  33. package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
  34. package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
  35. package/dist/src/agents/adapters/claudeAdapter.js +6 -3
  36. package/dist/src/agents/adapters/codexAdapter.js +51 -10
  37. package/dist/src/agents/index.js +1 -0
  38. package/dist/src/agents/normalizeAgentTelemetry.js +50 -0
  39. package/dist/src/agents/parseAgentTokenUsage.js +71 -49
  40. package/dist/src/agents/providerBuildObservation.js +97 -0
  41. package/dist/src/audits/core/projectInventory.js +1 -0
  42. package/dist/src/cli/help.js +44 -1
  43. package/dist/src/commands/runExperimentDescribeCommand.js +83 -1
  44. package/dist/src/commands/runExperimentRunCommand.js +309 -6
  45. package/dist/src/core/processTree.js +55 -0
  46. package/dist/src/core/runMeasuredCommand.js +108 -22
  47. package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
  48. package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
  49. package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
  50. package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
  51. package/dist/src/evaluation/agentSuccess/index.js +14 -0
  52. package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
  53. package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
  54. package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
  55. package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
  56. package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
  57. package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
  58. package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
  59. package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
  60. package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
  61. package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
  62. package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
  63. package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
  64. package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +62 -0
  65. package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
  66. package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
  67. package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
  68. package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
  69. package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
  70. package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
  71. package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
  72. package/dist/src/evaluation/changeSet/index.js +2 -0
  73. package/dist/src/evaluation/changeSet/types.js +9 -0
  74. package/dist/src/experiments/defaultRegistry.js +2 -0
  75. package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
  76. package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
  77. package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
  78. package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
  79. package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
  80. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpoint.js +216 -0
  81. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +588 -0
  82. package/dist/src/experiments/plugins/agentSuccessRate/campaignEvidenceQuality.js +77 -0
  83. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +314 -0
  84. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +111 -0
  85. package/dist/src/experiments/plugins/agentSuccessRate/campaignProgress.js +173 -0
  86. package/dist/src/experiments/plugins/agentSuccessRate/campaignQueue.js +11 -0
  87. package/dist/src/experiments/plugins/agentSuccessRate/campaignRecovery.js +256 -0
  88. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +207 -0
  89. package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
  90. package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
  91. package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
  92. package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
  93. package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
  94. package/dist/src/experiments/plugins/agentSuccessRate/index.js +26 -0
  95. package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
  96. package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
  97. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +517 -0
  98. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +272 -0
  99. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
  100. package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
  101. package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
  102. package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
  103. package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
  104. package/dist/src/experiments/plugins/agentSuccessRate/timeoutPresets.js +9 -0
  105. package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
  106. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
  107. package/dist/src/report/experiments/agentSuccessCampaignProgressReportModel.js +1 -0
  108. package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
  109. package/dist/src/report/experiments/buildAgentSuccessCampaignProgressReport.js +6 -0
  110. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +266 -0
  111. package/dist/src/report/experiments/buildPluginExperimentReport.js +27 -2
  112. package/dist/src/report/experiments/index.js +4 -0
  113. package/dist/src/report/experiments/renderAgentSuccessCampaignProgressHtml.js +6 -0
  114. package/dist/src/report/experiments/renderAgentSuccessCampaignProgressText.js +25 -0
  115. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +171 -0
  116. package/dist/src/report/experiments/renderAgentSuccessRateText.js +146 -0
  117. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  118. package/dist/src/report/experiments/renderPluginExperimentReportText.js +5 -0
  119. package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +135 -0
  120. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -3
  121. package/docs/METRICS.md +79 -0
  122. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { spawn } from "node:child_process";
4
5
  import { parseCommandString, serializeCommand } from "./commandLine.js";
5
- import { forceTerminateProcess } from "./processTree.js";
6
+ import { forceTerminateProcess, terminateProcessTree } from "./processTree.js";
6
7
  import { buildResolvedCommandInvocation, resolveCommandInvocation } from "./resolveCommand.js";
7
8
  export { parseCommandString } from "./commandLine.js";
8
9
  export async function runMeasuredCommand(options) {
@@ -10,6 +11,11 @@ export async function runMeasuredCommand(options) {
10
11
  const named = readCommandName(options);
11
12
  const commandString = named.commandString;
12
13
  const trailingArgs = [...named.args, ...(options.extraArgs ?? [])];
14
+ const bounded = options.stdoutMaxBytes !== undefined ||
15
+ options.stderrMaxBytes !== undefined ||
16
+ options.inheritParentEnv === false ||
17
+ options.terminateProcessTree === true;
18
+ const childEnv = options.inheritParentEnv === false ? { ...options.env } : { ...process.env, ...options.env };
13
19
  // Windows .cmd/.bat/.ps1 argument assembly is owned by resolveCommand.ts so
14
20
  // that every spawn site (measured commands and managed long-running processes)
15
21
  // shares exactly one shim rule.
@@ -24,7 +30,7 @@ export async function runMeasuredCommand(options) {
24
30
  }, trailingArgs)
25
31
  : resolveCommandInvocation(named.executable, trailingArgs, {
26
32
  cwd: options.cwd,
27
- env: { ...process.env, ...options.env },
33
+ env: childEnv,
28
34
  allowPowerShellShim: options.allowPowerShellShim
29
35
  });
30
36
  const resolution = invocation.resolvedCommand;
@@ -36,8 +42,9 @@ export async function runMeasuredCommand(options) {
36
42
  const startedAt = new Date().toISOString();
37
43
  const started = Date.now();
38
44
  const result = await new Promise((resolve) => {
39
- let stdout = "";
40
- let stderr = "";
45
+ const stdoutCollector = new OutputCollector(options.stdoutMaxBytes);
46
+ const stderrCollector = new OutputCollector(options.stderrMaxBytes);
47
+ let outputLimit;
41
48
  let spawnError;
42
49
  let timedOut = false;
43
50
  let timeout;
@@ -47,9 +54,13 @@ export async function runMeasuredCommand(options) {
47
54
  try {
48
55
  child = spawn(executable, args, {
49
56
  cwd: options.cwd,
50
- env: { ...process.env, ...options.env },
57
+ env: childEnv,
51
58
  shell: false,
52
- stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
59
+ windowsHide: true,
60
+ stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
61
+ // Own process group so a bounded termination can reach descendants. Windows reaches the tree
62
+ // through `taskkill /T`, and `detached` there would open a new console, so it is not set.
63
+ ...(options.terminateProcessTree === true && process.platform !== "win32" ? { detached: true } : {})
53
64
  });
54
65
  }
55
66
  catch (error) {
@@ -66,8 +77,8 @@ export async function runMeasuredCommand(options) {
66
77
  endedAt,
67
78
  durationMs,
68
79
  exitCode: null,
69
- stdout,
70
- stderr,
80
+ stdout: "",
81
+ stderr: "",
71
82
  stdoutPath,
72
83
  stderrPath,
73
84
  telemetryPath,
@@ -76,22 +87,56 @@ export async function runMeasuredCommand(options) {
76
87
  resolvedCommand: resolution
77
88
  };
78
89
  void Promise.all([
79
- writeArtifact(stdoutPath, stdout),
80
- writeArtifact(stderrPath, stderr),
90
+ writeArtifact(stdoutPath, ""),
91
+ writeArtifact(stderrPath, ""),
81
92
  writeArtifact(telemetryPath, JSON.stringify(measured, null, 2))
82
93
  ]).then(() => resolve(measured));
83
94
  return;
84
95
  }
85
96
  // stdout/stderr are always piped in both stdio branches above.
86
- child.stdout.on("data", (chunk) => {
87
- stdout += String(chunk);
88
- });
89
- child.stderr.on("data", (chunk) => {
90
- stderr += String(chunk);
91
- });
97
+ // The process-termination step is shared by the timeout and the opt-in output limit. Bounded callers
98
+ // get an awaited tree termination; everyone else keeps the legacy fire-and-forget single termination.
99
+ const KILL_GRACE_MS = 3000;
100
+ const terminate = () => {
101
+ if (options.terminateProcessTree === true) {
102
+ void terminateProcessTree(child.pid);
103
+ }
104
+ else {
105
+ forceTerminateProcess(child.pid);
106
+ }
107
+ killGraceTimeout ??= setTimeout(() => {
108
+ void settle(null);
109
+ }, KILL_GRACE_MS);
110
+ };
111
+ const collect = (collector, stream, limit) => (chunk) => {
112
+ if (collector.push(chunk) && limit !== undefined && outputLimit === undefined) {
113
+ outputLimit = { stream, limitBytes: limit };
114
+ spawnError = `Command ${stream} exceeded ${limit} bytes.`;
115
+ terminate();
116
+ }
117
+ };
118
+ child.stdout.on("data", collect(stdoutCollector, "stdout", options.stdoutMaxBytes));
119
+ child.stderr.on("data", collect(stderrCollector, "stderr", options.stderrMaxBytes));
92
120
  child.on("error", (error) => {
93
121
  spawnError = error.message;
122
+ // A failed spawn in a working directory that exists is not a missing executable; the OS may be refusing the path (for
123
+ // example a very long cwd on Windows). Report the measured length
124
+ // without asserting a limit of our own.
125
+ if (error.code === "ENOENT" && existsSync(options.cwd)) {
126
+ spawnError += ` (the working directory exists but the process could not be started in it; cwd length ${options.cwd.length} characters)`;
127
+ if (process.platform === "win32") {
128
+ spawnError += "; use a shorter output/sandbox path on this Windows host";
129
+ }
130
+ }
94
131
  });
132
+ // A failed spawn can also make the OS report the never-connected stdout/stderr pipes as errors (Windows:
133
+ // read ENOTCONN). They are stream events of this one failed process: record the first cause once and let
134
+ // the 'close' event settle the result, instead of letting an unhandled 'error' event end the whole process.
135
+ const recordStreamError = (stream) => (error) => {
136
+ spawnError = spawnError ?? `Failed to read command ${stream}: ${error.message}`;
137
+ };
138
+ child.stdout.on("error", recordStreamError("stdout"));
139
+ child.stderr.on("error", recordStreamError("stderr"));
95
140
  if (options.stdinText !== undefined) {
96
141
  // A child that exits or closes stdin before we write must not crash the process (EPIPE).
97
142
  child.stdin?.on("error", (error) => {
@@ -106,7 +151,7 @@ export async function runMeasuredCommand(options) {
106
151
  // intermediate shim (for example a Windows .cmd launcher) can occasionally outlive an
107
152
  // already-killed intermediate process. Without a bounded fallback, a caller whose kill silently
108
153
  // failed would await this promise forever instead of receiving the already-known timeout error.
109
- const KILL_GRACE_MS = 3000;
154
+ // (KILL_GRACE_MS and the shared `terminate` step are declared above, beside the output collectors.)
110
155
  const settle = async (exitCode) => {
111
156
  if (settled)
112
157
  return;
@@ -128,6 +173,8 @@ export async function runMeasuredCommand(options) {
128
173
  child.unref();
129
174
  const endedAt = new Date().toISOString();
130
175
  const durationMs = Date.now() - started;
176
+ const stdout = stdoutCollector.toString();
177
+ const stderr = stderrCollector.toString();
131
178
  await writeArtifact(stdoutPath, stdout);
132
179
  await writeArtifact(stderrPath, stderr);
133
180
  const measured = {
@@ -147,7 +194,8 @@ export async function runMeasuredCommand(options) {
147
194
  telemetryPath,
148
195
  ok: exitCode === 0 && !spawnError && !timedOut,
149
196
  error: spawnError,
150
- resolvedCommand: resolution
197
+ resolvedCommand: resolution,
198
+ ...(bounded ? { timedOut, ...(outputLimit ? { outputLimit } : {}) } : {})
151
199
  };
152
200
  await writeArtifact(telemetryPath, JSON.stringify(measured, null, 2));
153
201
  resolve(measured);
@@ -156,10 +204,7 @@ export async function runMeasuredCommand(options) {
156
204
  timeout = setTimeout(() => {
157
205
  timedOut = true;
158
206
  spawnError = `Command timed out after ${options.timeoutMs}ms.`;
159
- forceTerminateProcess(child.pid);
160
- killGraceTimeout = setTimeout(() => {
161
- void settle(null);
162
- }, KILL_GRACE_MS);
207
+ terminate();
163
208
  }, options.timeoutMs);
164
209
  }
165
210
  child.on("close", (exitCode) => {
@@ -168,6 +213,47 @@ export async function runMeasuredCommand(options) {
168
213
  });
169
214
  return result;
170
215
  }
216
+ /**
217
+ * Accumulates one output stream. Without a limit it reproduces the legacy `text += String(chunk)`
218
+ * behavior exactly. With a limit it keeps raw bytes (so the cut is on a byte boundary), stops at the
219
+ * limit, and reports once when more output than the limit arrived.
220
+ */
221
+ class OutputCollector {
222
+ limit;
223
+ text = "";
224
+ chunks = [];
225
+ bytes = 0;
226
+ exceeded = false;
227
+ constructor(limit) {
228
+ this.limit = limit;
229
+ }
230
+ /** Returns true exactly once: on the chunk that first exceeds the limit. */
231
+ push(chunk) {
232
+ if (this.limit === undefined) {
233
+ this.text += String(chunk);
234
+ return false;
235
+ }
236
+ if (this.exceeded) {
237
+ return false;
238
+ }
239
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
240
+ const remaining = this.limit - this.bytes;
241
+ if (buffer.length > remaining) {
242
+ if (remaining > 0) {
243
+ this.chunks.push(buffer.subarray(0, remaining));
244
+ }
245
+ this.bytes = this.limit;
246
+ this.exceeded = true;
247
+ return true;
248
+ }
249
+ this.chunks.push(buffer);
250
+ this.bytes += buffer.length;
251
+ return false;
252
+ }
253
+ toString() {
254
+ return this.limit === undefined ? this.text : Buffer.concat(this.chunks).toString("utf8");
255
+ }
256
+ }
171
257
  async function writeArtifact(filePath, value) {
172
258
  await mkdir(path.dirname(filePath), { recursive: true });
173
259
  await writeFile(filePath, value, "utf8");
@@ -0,0 +1,37 @@
1
+ import { excerptGitFailure, runSandboxGit } from "../benchmarkSandbox/index.js";
2
+ import { extractPatchCandidate } from "./extractPatchCandidate.js";
3
+ import { parseUnifiedDiff } from "./parseUnifiedDiff.js";
4
+ import { DEFAULT_PATCH_BOUNDS } from "./patchTypes.js";
5
+ import { validatePatchPolicy } from "./validatePatchPolicy.js";
6
+ const APPLY_FLAGS = ["--index", "--whitespace=nowarn", "-"];
7
+ /**
8
+ * Lab-owned patch application. The order is the safety contract:
9
+ * extract exactly one candidate -> parse -> policy -> `git apply --check --index` -> `git apply --index`.
10
+ * Nothing touches the sandbox until the check succeeds, so every earlier failure leaves it unchanged.
11
+ * `--unsafe-paths`, `--reject` and `--3way` are never used, and nothing the agent proposed is executed.
12
+ */
13
+ export async function applyPatchToSandbox(options) {
14
+ const bounds = options.bounds ?? DEFAULT_PATCH_BOUNDS;
15
+ const extracted = extractPatchCandidate(options.rawProposal, bounds);
16
+ if (!extracted.ok)
17
+ return { outcome: "parse-failure", code: extracted.code, message: extracted.message };
18
+ const parsed = parseUnifiedDiff(extracted.patch);
19
+ if (!parsed.ok)
20
+ return { outcome: "parse-failure", code: parsed.code, message: parsed.message };
21
+ const policy = validatePatchPolicy(parsed.files, { protectedFiles: options.protectedFiles, bounds });
22
+ if (!policy.ok)
23
+ return { outcome: "policy-rejection", rejections: policy.rejections };
24
+ const check = await runSandboxGit(options.sandbox, ["apply", "--check", ...APPLY_FLAGS], {
25
+ stdin: extracted.patch,
26
+ label: "apply-check"
27
+ });
28
+ if (!check.ok)
29
+ return { outcome: "git-check-failure", message: excerptGitFailure(check) };
30
+ const apply = await runSandboxGit(options.sandbox, ["apply", ...APPLY_FLAGS], {
31
+ stdin: extracted.patch,
32
+ label: "apply"
33
+ });
34
+ if (!apply.ok)
35
+ return { outcome: "git-apply-failure", message: excerptGitFailure(apply) };
36
+ return { outcome: "success", files: policy.files };
37
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Decides whether the unmodified benchmark can evaluate an implementation.
3
+ *
4
+ * Evaluable exactly when at least one task check fails (there is something to fix) and every regression
5
+ * check passes (there is nothing already broken), and no check was unable to produce a verdict. A check
6
+ * that timed out or errored is "not evaluable" rather than "failed": an indeterminate check cannot prove
7
+ * the task is unsolved. An invalid baseline is a benchmark defect, never an agent failure.
8
+ */
9
+ export function assessBaseline(baseline) {
10
+ const reasons = [];
11
+ const all = [...baseline.taskResults, ...baseline.regressionResults];
12
+ if (baseline.taskResults.length === 0 || baseline.regressionResults.length === 0)
13
+ reasons.push("NO_CHECKS");
14
+ if (all.some((result) => result.status === "timeout" || result.status === "error"))
15
+ reasons.push("CHECK_NOT_EVALUABLE");
16
+ if (baseline.taskResults.length > 0 && !baseline.taskResults.some((result) => result.status === "failed")) {
17
+ if (!reasons.includes("CHECK_NOT_EVALUABLE"))
18
+ reasons.push("ALL_TASK_CHECKS_PASSED");
19
+ }
20
+ if (baseline.regressionResults.some((result) => result.status === "failed"))
21
+ reasons.push("REGRESSION_CHECK_FAILED");
22
+ return { evaluable: reasons.length === 0, reasons };
23
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Contracts for the canonical agent-success implementation benchmark corpus. This is a separate contract family
3
+ * from the retrieval benchmark profiles (`benchmark-project-profiles.json`) and must not be merged into it.
4
+ */
5
+ export const AGENT_SUCCESS_PROJECT_PROFILES_SCHEMA_VERSION = "my-dev-kit-lab-agent-success-project-profiles-v1";
6
+ export const AGENT_SUCCESS_TASK_CATALOG_PATH = "benchmarks/contracts/agent-success-rate-tasks.json";
7
+ export const AGENT_SUCCESS_PROJECT_PROFILES_PATH = "benchmarks/contracts/agent-success-rate-project-profiles.json";
8
+ export const AGENT_SUCCESS_PROJECTS_DIRECTORY = "benchmarks/projects";
9
+ export const AGENT_SUCCESS_CORPUS_PROJECT_IDS = ["agent-success-task-board-node", "agent-success-inventory-node"];
10
+ /** The fixed catalog order. Three tasks per project: one localized, one cross-module, one broad-change. */
11
+ export const AGENT_SUCCESS_CORPUS_TASKS = Object.freeze([
12
+ { id: "asr-board-title-normalization", project: "agent-success-task-board-node", locality: "localized" },
13
+ { id: "asr-board-import-idempotency", project: "agent-success-task-board-node", locality: "cross-module" },
14
+ { id: "asr-board-project-summary", project: "agent-success-task-board-node", locality: "broad-change" },
15
+ { id: "asr-inventory-quantity-boundary", project: "agent-success-inventory-node", locality: "localized" },
16
+ { id: "asr-inventory-reservation-atomicity", project: "agent-success-inventory-node", locality: "cross-module" },
17
+ { id: "asr-inventory-fulfillment-report", project: "agent-success-inventory-node", locality: "broad-change" }
18
+ ]);
19
+ /** Files every project must contain, relative to the project root. */
20
+ export const AGENT_SUCCESS_CORPUS_PROJECT_FILES = Object.freeze({
21
+ "agent-success-task-board-node": Object.freeze([
22
+ "README.md",
23
+ "package.json",
24
+ "src/validation.js",
25
+ "src/taskStore.js",
26
+ "src/taskService.js",
27
+ "src/projectSummary.js",
28
+ "src/index.js",
29
+ "tests/title-primary.check.mjs",
30
+ "tests/title-edge.check.mjs",
31
+ "tests/import-primary.check.mjs",
32
+ "tests/import-edge.check.mjs",
33
+ "tests/summary-primary.check.mjs",
34
+ "tests/summary-edge.check.mjs",
35
+ "tests/regression.check.mjs"
36
+ ]),
37
+ "agent-success-inventory-node": Object.freeze([
38
+ "README.md",
39
+ "package.json",
40
+ "src/quantity.js",
41
+ "src/inventoryStore.js",
42
+ "src/reservationService.js",
43
+ "src/fulfillmentReport.js",
44
+ "src/index.js",
45
+ "tests/quantity-primary.check.mjs",
46
+ "tests/quantity-edge.check.mjs",
47
+ "tests/reservation-primary.check.mjs",
48
+ "tests/reservation-edge.check.mjs",
49
+ "tests/report-primary.check.mjs",
50
+ "tests/report-edge.check.mjs",
51
+ "tests/regression.check.mjs"
52
+ ])
53
+ });
54
+ export const AGENT_SUCCESS_CORPUS_RAW_INCLUDE_GLOBS = Object.freeze(["src/**/*.js"]);
55
+ /** Directory names that must never exist inside a canonical project. */
56
+ export const AGENT_SUCCESS_CORPUS_FORBIDDEN_DIRECTORIES = Object.freeze([
57
+ ".git",
58
+ "node_modules",
59
+ "dist",
60
+ "build",
61
+ "coverage",
62
+ "lab-output"
63
+ ]);
64
+ export class AgentSuccessCorpusError extends Error {
65
+ issues;
66
+ constructor(issues) {
67
+ super(`Invalid agent-success corpus: ${issues
68
+ .slice(0, 5)
69
+ .map((issue) => `${issue.path}: ${issue.code}`)
70
+ .join("; ")}${issues.length > 5 ? `; +${issues.length - 5} more` : ""}`);
71
+ this.name = "AgentSuccessCorpusError";
72
+ this.issues = [...issues];
73
+ }
74
+ }
@@ -0,0 +1,46 @@
1
+ import { DEFAULT_PATCH_BOUNDS } from "./patchTypes.js";
2
+ const FENCED_DIFF_BLOCK = /^```[ \t]*(?:diff|patch)[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim;
3
+ function looksLikeDiffStart(text) {
4
+ return text.startsWith("diff --git ") || text.startsWith("--- ");
5
+ }
6
+ /** Removes trailing line breaks only: a final context line that is a single space must survive. */
7
+ function finalize(body) {
8
+ return `${body.replace(/(?:\r?\n)+$/, "")}\n`;
9
+ }
10
+ /**
11
+ * Extracts exactly one patch candidate from an agent's proposal: either one fenced `diff`/`patch` block, or
12
+ * the whole (trimmed) text when it is itself a unified diff. Ambiguity is rejected rather than guessed.
13
+ * Size and line bounds are enforced before any parsing.
14
+ */
15
+ export function extractPatchCandidate(text, bounds = DEFAULT_PATCH_BOUNDS) {
16
+ if (typeof text !== "string" || text.trim() === "") {
17
+ return { ok: false, code: "EMPTY", message: "the proposal is empty." };
18
+ }
19
+ if (Buffer.byteLength(text, "utf8") > bounds.maxPatchBytes) {
20
+ return { ok: false, code: "PATCH_TOO_LARGE", message: `the proposal exceeds ${bounds.maxPatchBytes} bytes.` };
21
+ }
22
+ if (text.split("\n").length > bounds.maxPatchLines) {
23
+ return { ok: false, code: "PATCH_TOO_MANY_LINES", message: `the proposal exceeds ${bounds.maxPatchLines} lines.` };
24
+ }
25
+ const fenced = [...text.matchAll(FENCED_DIFF_BLOCK)];
26
+ if (fenced.length > 1) {
27
+ return { ok: false, code: "AMBIGUOUS_CANDIDATES", message: "more than one fenced diff/patch block was found." };
28
+ }
29
+ if (fenced.length === 1) {
30
+ const match = fenced[0];
31
+ const outside = text.slice(0, match.index) + text.slice((match.index ?? 0) + match[0].length);
32
+ if (/^diff --git /m.test(outside)) {
33
+ return { ok: false, code: "AMBIGUOUS_CANDIDATES", message: "a fenced block and a raw diff were both present." };
34
+ }
35
+ const body = match[1] ?? "";
36
+ if (body.trim() === "") {
37
+ return { ok: false, code: "EMPTY", message: "the fenced diff block is empty." };
38
+ }
39
+ return { ok: true, patch: finalize(body) };
40
+ }
41
+ const trimmed = text.trimStart();
42
+ if (looksLikeDiffStart(trimmed)) {
43
+ return { ok: true, patch: finalize(trimmed) };
44
+ }
45
+ return { ok: false, code: "NO_CANDIDATE", message: "no fenced diff/patch block and no raw unified diff was found." };
46
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./taskTypes.js";
2
+ export { normalizeProjectRelativePath } from "./taskPaths.js";
3
+ export { assertAgentSuccessTask, validateAgentSuccessTask } from "./validateAgentSuccessTask.js";
4
+ export * from "./patchTypes.js";
5
+ export { extractPatchCandidate } from "./extractPatchCandidate.js";
6
+ export { parseUnifiedDiff } from "./parseUnifiedDiff.js";
7
+ export { validatePatchPolicy } from "./validatePatchPolicy.js";
8
+ export { applyPatchToSandbox } from "./applyPatchToSandbox.js";
9
+ export * from "./verificationTypes.js";
10
+ export { runVerificationCheck, runVerificationChecks } from "./runVerificationCheck.js";
11
+ export { assessBaseline } from "./assessBaseline.js";
12
+ export * from "./corpusTypes.js";
13
+ export { assertAgentSuccessCorpus, validateAgentSuccessCorpus } from "./validateAgentSuccessCorpus.js";
14
+ export { assertReadAgentSuccessCorpus, readAgentSuccessCorpus } from "./readAgentSuccessCorpus.js";
@@ -0,0 +1,210 @@
1
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
2
+ class PatchMalformed extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ }
8
+ }
9
+ function stripCr(line) {
10
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
11
+ }
12
+ /** Mirrors `git apply -p1`: drop exactly one leading path component. */
13
+ function stripOneComponent(rawPath) {
14
+ const slash = rawPath.indexOf("/");
15
+ if (slash < 0) {
16
+ throw new PatchMalformed("PATCH_MALFORMED", "a patch path has no directory component to strip.");
17
+ }
18
+ return rawPath.slice(slash + 1);
19
+ }
20
+ function parseHeaderPath(headerValue) {
21
+ const value = headerValue.split("\t")[0].trimEnd();
22
+ if (value.startsWith('"')) {
23
+ throw new PatchMalformed("UNSUPPORTED_PATH_QUOTING", "quoted patch paths are not supported.");
24
+ }
25
+ if (value === "/dev/null")
26
+ return null;
27
+ return stripOneComponent(value);
28
+ }
29
+ /** `diff --git <a> <b>`: returns the shared path when both sides agree after -p1 stripping, else null. */
30
+ function parseGitHeaderPath(rest) {
31
+ if (rest.startsWith('"')) {
32
+ throw new PatchMalformed("UNSUPPORTED_PATH_QUOTING", "quoted patch paths are not supported.");
33
+ }
34
+ for (let index = rest.indexOf(" "); index >= 0; index = rest.indexOf(" ", index + 1)) {
35
+ const left = rest.slice(0, index);
36
+ const right = rest.slice(index + 1);
37
+ if (!left.includes("/") || !right.includes("/"))
38
+ continue;
39
+ if (stripOneComponent(left) === stripOneComponent(right))
40
+ return stripOneComponent(right);
41
+ }
42
+ return null;
43
+ }
44
+ function newFile(gitHeaderPath) {
45
+ return {
46
+ oldPath: null,
47
+ newPath: null,
48
+ gitHeaderPath,
49
+ isNew: false,
50
+ isDelete: false,
51
+ binary: false,
52
+ rename: false,
53
+ copy: false,
54
+ oldMode: null,
55
+ newMode: null,
56
+ subproject: false,
57
+ hunkCount: 0
58
+ };
59
+ }
60
+ /**
61
+ * Parses a Git/unified diff into per-file records with exact hunk accounting: every hunk must contain
62
+ * precisely the old/new line counts its header declares, so truncated or padded patches are rejected here
63
+ * instead of being half-understood. Only structure is recorded; policy decisions live in validatePatchPolicy.
64
+ */
65
+ export function parseUnifiedDiff(patch) {
66
+ try {
67
+ const lines = patch.split("\n");
68
+ if (lines[lines.length - 1] === "")
69
+ lines.pop();
70
+ const files = [];
71
+ let index = 0;
72
+ const parseFileHeaders = (file) => {
73
+ const oldHeader = stripCr(lines[index]);
74
+ const plusLine = lines[index + 1];
75
+ if (plusLine === undefined || !stripCr(plusLine).startsWith("+++ ")) {
76
+ throw new PatchMalformed("PATCH_MALFORMED", 'a "---" header is not followed by a "+++" header.');
77
+ }
78
+ file.oldPath = parseHeaderPath(oldHeader.slice(4));
79
+ file.newPath = parseHeaderPath(stripCr(plusLine).slice(4));
80
+ if (file.oldPath === null)
81
+ file.isNew = true;
82
+ if (file.newPath === null)
83
+ file.isDelete = true;
84
+ index += 2;
85
+ while (index < lines.length && stripCr(lines[index]).startsWith("@@")) {
86
+ parseHunk(file);
87
+ }
88
+ };
89
+ const parseHunk = (file) => {
90
+ const match = HUNK_HEADER.exec(stripCr(lines[index]));
91
+ if (!match)
92
+ throw new PatchMalformed("PATCH_MALFORMED", "malformed hunk header.");
93
+ let oldRemaining = match[2] === undefined ? 1 : Number(match[2]);
94
+ let newRemaining = match[4] === undefined ? 1 : Number(match[4]);
95
+ index += 1;
96
+ while (oldRemaining > 0 || newRemaining > 0) {
97
+ const line = lines[index];
98
+ if (line === undefined)
99
+ throw new PatchMalformed("PATCH_MALFORMED", "a hunk ends before its declared line counts.");
100
+ const marker = line.length === 0 ? " " : line[0];
101
+ if (marker === " ") {
102
+ oldRemaining -= 1;
103
+ newRemaining -= 1;
104
+ }
105
+ else if (marker === "-") {
106
+ oldRemaining -= 1;
107
+ if (line.startsWith("-Subproject commit "))
108
+ file.subproject = true;
109
+ }
110
+ else if (marker === "+") {
111
+ newRemaining -= 1;
112
+ if (line.startsWith("+Subproject commit "))
113
+ file.subproject = true;
114
+ }
115
+ else if (marker !== "\\") {
116
+ throw new PatchMalformed("PATCH_MALFORMED", "unexpected line inside a hunk.");
117
+ }
118
+ if (oldRemaining < 0 || newRemaining < 0) {
119
+ throw new PatchMalformed("PATCH_MALFORMED", "a hunk has more lines than its header declares.");
120
+ }
121
+ index += 1;
122
+ }
123
+ while (index < lines.length && lines[index].startsWith("\\"))
124
+ index += 1;
125
+ file.hunkCount += 1;
126
+ };
127
+ while (index < lines.length) {
128
+ const line = stripCr(lines[index]);
129
+ if (line.startsWith("diff --git ")) {
130
+ const file = newFile(parseGitHeaderPath(line.slice("diff --git ".length)));
131
+ index += 1;
132
+ while (index < lines.length) {
133
+ const header = stripCr(lines[index]);
134
+ if (header.startsWith("--- ") || header.startsWith("diff --git "))
135
+ break;
136
+ let match;
137
+ if ((match = /^index [0-9a-f]+\.\.[0-9a-f]+(?: (\d+))?$/.exec(header))) {
138
+ if (match[1]) {
139
+ file.oldMode = match[1];
140
+ file.newMode = match[1];
141
+ }
142
+ }
143
+ else if ((match = /^old mode (\d+)$/.exec(header))) {
144
+ file.oldMode = match[1];
145
+ }
146
+ else if ((match = /^new mode (\d+)$/.exec(header))) {
147
+ file.newMode = match[1];
148
+ }
149
+ else if ((match = /^deleted file mode (\d+)$/.exec(header))) {
150
+ file.isDelete = true;
151
+ file.oldMode = match[1];
152
+ }
153
+ else if ((match = /^new file mode (\d+)$/.exec(header))) {
154
+ file.isNew = true;
155
+ file.newMode = match[1];
156
+ }
157
+ else if (/^(?:dis)?similarity index \d+%$/.test(header)) {
158
+ // informational; rename/copy intent is carried by the from/to lines
159
+ }
160
+ else if (/^rename (?:from|to) /.test(header)) {
161
+ file.rename = true;
162
+ }
163
+ else if (/^copy (?:from|to) /.test(header)) {
164
+ file.copy = true;
165
+ }
166
+ else if (/^Binary files .* differ$/.test(header) || header === "GIT binary patch") {
167
+ file.binary = true;
168
+ index += 1;
169
+ while (index < lines.length && !lines[index].startsWith("diff --git "))
170
+ index += 1;
171
+ break;
172
+ }
173
+ else {
174
+ throw new PatchMalformed("PATCH_MALFORMED", "unrecognized extended header line.");
175
+ }
176
+ index += 1;
177
+ }
178
+ if (!file.binary && index < lines.length && stripCr(lines[index]).startsWith("--- ")) {
179
+ parseFileHeaders(file);
180
+ }
181
+ files.push(file);
182
+ }
183
+ else if (line.startsWith("--- ") && lines[index + 1] !== undefined && stripCr(lines[index + 1]).startsWith("+++ ")) {
184
+ const file = newFile(null);
185
+ parseFileHeaders(file);
186
+ files.push(file);
187
+ }
188
+ else if (line.trim() === "") {
189
+ index += 1;
190
+ }
191
+ else {
192
+ throw new PatchMalformed("PATCH_MALFORMED", "unexpected content outside a file section.");
193
+ }
194
+ }
195
+ if (files.length === 0) {
196
+ throw new PatchMalformed("PATCH_MALFORMED", "the patch contains no file sections.");
197
+ }
198
+ for (const file of files) {
199
+ if ((file.oldPath ?? file.newPath ?? file.gitHeaderPath) === null && !file.rename && !file.copy) {
200
+ throw new PatchMalformed("PATCH_MALFORMED", "a file section has no determinable path.");
201
+ }
202
+ }
203
+ return { ok: true, files };
204
+ }
205
+ catch (error) {
206
+ if (error instanceof PatchMalformed)
207
+ return { ok: false, code: error.code, message: error.message };
208
+ throw error;
209
+ }
210
+ }
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_PATCH_BOUNDS = Object.freeze({
2
+ maxPatchBytes: 1_048_576,
3
+ maxChangedPaths: 64,
4
+ maxPatchLines: 20_000
5
+ });