@dailephd/my-dev-kit-lab 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +20 -4
  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/cli/help.js +54 -2
  36. package/dist/src/commands/runExperimentDescribeCommand.js +60 -2
  37. package/dist/src/commands/runExperimentRunCommand.js +208 -5
  38. package/dist/src/core/processTree.js +55 -0
  39. package/dist/src/core/runMeasuredCommand.js +104 -22
  40. package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
  41. package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
  42. package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
  43. package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
  44. package/dist/src/evaluation/agentSuccess/index.js +14 -0
  45. package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
  46. package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
  47. package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
  48. package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
  49. package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
  50. package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
  51. package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
  52. package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
  53. package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
  54. package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
  55. package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
  56. package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
  57. package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +60 -0
  58. package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
  59. package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
  60. package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
  61. package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
  62. package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
  63. package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
  64. package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
  65. package/dist/src/evaluation/changeSet/index.js +2 -0
  66. package/dist/src/evaluation/changeSet/types.js +9 -0
  67. package/dist/src/evaluation/runMyDevKitRetrieval.js +1 -0
  68. package/dist/src/experiments/defaultRegistry.js +4 -0
  69. package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
  70. package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
  71. package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
  72. package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
  73. package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
  74. package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
  75. package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
  76. package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
  77. package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
  78. package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
  79. package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
  80. package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
  81. package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
  82. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
  83. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
  84. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
  85. package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
  86. package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
  87. package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
  88. package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
  89. package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
  90. package/dist/src/experiments/plugins/contextPackGeneration/analysis.js +157 -0
  91. package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
  92. package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
  93. package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
  94. package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
  95. package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
  96. package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
  97. package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
  98. package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
  99. package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
  100. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
  101. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
  102. package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
  103. package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
  104. package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
  105. package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
  106. package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
  107. package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
  108. package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
  109. package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -0
  110. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
  111. package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
  112. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
  113. package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
  114. package/dist/src/report/experiments/buildPluginExperimentReport.js +49 -2
  115. package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
  116. package/dist/src/report/experiments/index.js +9 -0
  117. package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
  118. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
  119. package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
  120. package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
  121. package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
  122. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
  123. package/dist/src/report/experiments/renderPluginExperimentReportText.js +12 -0
  124. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +15 -15
  125. package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
  126. package/docs/METRICS.md +98 -1
  127. package/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ import { normalizeProjectRelativePath } from "./taskPaths.js";
2
+ import { DEFAULT_PATCH_BOUNDS } from "./patchTypes.js";
3
+ const MAX_REPORTED_REJECTIONS = 20;
4
+ const SYMLINK_MODE = "120000";
5
+ const GITLINK_MODE = "160000";
6
+ function describeStatus(file) {
7
+ if (file.isNew || (file.oldPath === null && file.newPath !== null))
8
+ return "added";
9
+ if (file.isDelete || (file.newPath === null && file.oldPath !== null))
10
+ return "deleted";
11
+ return "modified";
12
+ }
13
+ /**
14
+ * Applies the Lab's patch policy to a parsed diff. Everything a patch could use to escape the benchmark
15
+ * copy or to alter protected files is rejected here, before Git sees the patch. A file outside the task's
16
+ * allowed edit scope is NOT rejected: such edits are legitimate evidence for later measurement.
17
+ */
18
+ export function validatePatchPolicy(files, options) {
19
+ const bounds = options.bounds ?? DEFAULT_PATCH_BOUNDS;
20
+ const rejections = [];
21
+ const reject = (rejection) => {
22
+ if (rejections.length < MAX_REPORTED_REJECTIONS)
23
+ rejections.push(rejection);
24
+ };
25
+ const protectedSet = new Set();
26
+ for (const entry of options.protectedFiles) {
27
+ const normalized = normalizeProjectRelativePath(entry);
28
+ if (normalized.ok)
29
+ protectedSet.add(normalized.path);
30
+ }
31
+ const accepted = new Map();
32
+ for (const file of files) {
33
+ const label = file.newPath ?? file.oldPath ?? file.gitHeaderPath ?? undefined;
34
+ if (file.binary)
35
+ reject({ code: "BINARY_PATCH", path: label, message: "binary patches are not accepted." });
36
+ if (file.rename || file.copy)
37
+ reject({ code: "RENAME_OR_COPY", path: label, message: "rename and copy patches are not accepted." });
38
+ if (file.oldPath !== null && file.newPath !== null && file.oldPath !== file.newPath) {
39
+ reject({ code: "PATH_MISMATCH", path: label, message: "the old and new paths differ." });
40
+ }
41
+ if (file.oldMode === SYMLINK_MODE || file.newMode === SYMLINK_MODE) {
42
+ reject({ code: "SYMLINK_MODE", path: label, message: "symbolic-link entries are not accepted." });
43
+ }
44
+ if (file.oldMode === GITLINK_MODE || file.newMode === GITLINK_MODE || file.subproject) {
45
+ reject({ code: "SUBMODULE", path: label, message: "submodule/gitlink changes are not accepted." });
46
+ }
47
+ const candidates = new Set([file.oldPath, file.newPath, file.oldPath === null && file.newPath === null ? file.gitHeaderPath : null].filter((value) => value !== null));
48
+ for (const candidate of candidates) {
49
+ const normalized = normalizeProjectRelativePath(candidate);
50
+ if (!normalized.ok) {
51
+ reject({ code: "UNSAFE_PATH", message: `unsafe patch path (${normalized.code}).` });
52
+ continue;
53
+ }
54
+ if (protectedSet.has(normalized.path)) {
55
+ reject({ code: "PROTECTED_PATH", path: normalized.path, message: "the patch touches a protected file." });
56
+ }
57
+ accepted.set(normalized.path, describeStatus(file));
58
+ }
59
+ }
60
+ if (accepted.size > bounds.maxChangedPaths) {
61
+ rejections.unshift({ code: "TOO_MANY_PATHS", message: `the patch changes more than ${bounds.maxChangedPaths} paths.` });
62
+ }
63
+ if (rejections.length > 0)
64
+ return { ok: false, rejections: rejections.slice(0, MAX_REPORTED_REJECTIONS) };
65
+ return { ok: true, files: [...accepted].map(([path, status]) => ({ path, status })).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) };
66
+ }
@@ -0,0 +1 @@
1
+ export const VERIFICATION_OUTPUT_LIMIT_BYTES = 1_048_576;
@@ -0,0 +1,127 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { copyFile, lstat, mkdir, realpath, rm, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { resolveWithinRoot } from "../../core/pathSafety.js";
5
+ import { BenchmarkSandboxError } from "./errors.js";
6
+ import { excerptGitFailure, runSandboxGit } from "./gitExecutor.js";
7
+ import { BENCHMARK_SANDBOX_EXCLUDED_NAMES, isSameOrInside, resolvePhysicalPath, SANDBOX_ID_PATTERN } from "./pathPolicy.js";
8
+ import { digestTreeSnapshot, scanProjectTree, snapshotProjectTree } from "./treeSnapshot.js";
9
+ import { BENCHMARK_SANDBOX_BASELINE_BRANCH, BENCHMARK_SANDBOX_OWNER_MARKER_FILE, BENCHMARK_SANDBOX_SCHEMA_VERSION } from "./types.js";
10
+ const BASELINE_COMMIT_MESSAGE = "benchmark sandbox baseline";
11
+ /** Fixed dates make the baseline commit id a pure function of the copied content, so repeated runs agree. */
12
+ const BASELINE_COMMIT_DATE_ENV = Object.freeze({ GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z", GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z" });
13
+ async function pathExists(target) {
14
+ try {
15
+ await lstat(target);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ async function resolveCanonicalProjectRoot(canonicalProjectRoot) {
23
+ let stats;
24
+ try {
25
+ stats = await lstat(path.resolve(canonicalProjectRoot));
26
+ }
27
+ catch {
28
+ throw new BenchmarkSandboxError("CANONICAL_ROOT_INVALID", "the canonical project root does not exist.");
29
+ }
30
+ if (stats.isSymbolicLink() || !(await stat(path.resolve(canonicalProjectRoot))).isDirectory()) {
31
+ throw new BenchmarkSandboxError("CANONICAL_ROOT_INVALID", "the canonical project root must be a real directory.");
32
+ }
33
+ return realpath(path.resolve(canonicalProjectRoot));
34
+ }
35
+ async function mustGit(target, args, label, env) {
36
+ const result = await runSandboxGit(target, args, { label, env });
37
+ if (result.unavailable) {
38
+ throw new BenchmarkSandboxError("GIT_UNAVAILABLE", "the git executable is not available.");
39
+ }
40
+ if (!result.ok) {
41
+ throw new BenchmarkSandboxError("GIT_FAILED", `git ${args[0] ?? ""} failed: ${excerptGitFailure(result)}`);
42
+ }
43
+ return result;
44
+ }
45
+ /**
46
+ * Creates one independent, Git-backed disposable copy of a controlled benchmark project inside the
47
+ * caller-supplied runtime root. Nothing is ever written to the canonical project, and the runtime root is
48
+ * never defaulted. Order: validate -> scan source (reject links) -> copy -> manifest -> fresh Git baseline.
49
+ * Any failure after the sandbox directory exists removes that (owned) directory before rethrowing.
50
+ */
51
+ export async function createBenchmarkSandbox(options) {
52
+ if (!SANDBOX_ID_PATTERN.test(options.sandboxId)) {
53
+ throw new BenchmarkSandboxError("INVALID_SANDBOX_ID", `sandbox id must match ${SANDBOX_ID_PATTERN.source}.`);
54
+ }
55
+ const canonicalProjectRoot = await resolveCanonicalProjectRoot(options.canonicalProjectRoot);
56
+ const runtimeRoot = path.resolve(options.runtimeRoot);
57
+ const physicalRuntimeRoot = await resolvePhysicalPath(runtimeRoot);
58
+ if (isSameOrInside(canonicalProjectRoot, physicalRuntimeRoot) || isSameOrInside(physicalRuntimeRoot, canonicalProjectRoot)) {
59
+ throw new BenchmarkSandboxError("RUNTIME_OVERLAP", "the runtime root and the canonical project must not overlap.");
60
+ }
61
+ const sandboxRoot = resolveWithinRoot(runtimeRoot, options.sandboxId);
62
+ if (await pathExists(sandboxRoot)) {
63
+ throw new BenchmarkSandboxError("SANDBOX_EXISTS", "refusing to overwrite an existing sandbox directory.");
64
+ }
65
+ // Reject links and unsupported entries before anything is created under the runtime root.
66
+ const sourceFiles = await scanProjectTree(canonicalProjectRoot, BENCHMARK_SANDBOX_EXCLUDED_NAMES);
67
+ if (sourceFiles.length === 0) {
68
+ throw new BenchmarkSandboxError("EMPTY_PROJECT", "the canonical project has no copyable files.");
69
+ }
70
+ await mkdir(runtimeRoot, { recursive: true });
71
+ await mkdir(sandboxRoot);
72
+ try {
73
+ const marker = {
74
+ schemaVersion: BENCHMARK_SANDBOX_SCHEMA_VERSION,
75
+ sandboxId: options.sandboxId,
76
+ runtimeRoot,
77
+ canonicalProjectRoot
78
+ };
79
+ await writeFile(path.join(sandboxRoot, BENCHMARK_SANDBOX_OWNER_MARKER_FILE), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
80
+ const projectRoot = path.join(sandboxRoot, "project");
81
+ const evidenceRoot = path.join(sandboxRoot, "evidence");
82
+ await mkdir(projectRoot);
83
+ await mkdir(evidenceRoot);
84
+ for (const relativePath of sourceFiles) {
85
+ const segments = relativePath.split("/");
86
+ const destination = path.join(projectRoot, ...segments);
87
+ await mkdir(path.dirname(destination), { recursive: true });
88
+ await copyFile(path.join(canonicalProjectRoot, ...segments), destination, fsConstants.COPYFILE_EXCL);
89
+ }
90
+ const manifest = await snapshotProjectTree(projectRoot, { excludedNames: [".git"] });
91
+ if (manifest.length !== sourceFiles.length || manifest.some((entry, index) => entry.path !== sourceFiles[index])) {
92
+ throw new BenchmarkSandboxError("COPY_MISMATCH", "the copied tree does not match the scanned source file list.");
93
+ }
94
+ const git = { projectRoot, evidenceRoot };
95
+ await mustGit(git, ["init", "-q", `--initial-branch=${BENCHMARK_SANDBOX_BASELINE_BRANCH}`], "init");
96
+ await mustGit(git, ["config", "user.name", "my-dev-kit-lab sandbox"], "config-name");
97
+ await mustGit(git, ["config", "user.email", "sandbox@my-dev-kit-lab.invalid"], "config-email");
98
+ await mustGit(git, ["config", "commit.gpgsign", "false"], "config-gpg");
99
+ await mustGit(git, ["config", "core.autocrlf", "false"], "config-autocrlf");
100
+ await mustGit(git, ["config", "core.longpaths", "true"], "config-longpaths");
101
+ await mustGit(git, ["config", "core.symlinks", "false"], "config-symlinks");
102
+ await mustGit(git, ["add", "-A", "--force"], "add");
103
+ await mustGit(git, ["commit", "-q", "--no-verify", "-m", BASELINE_COMMIT_MESSAGE], "commit", BASELINE_COMMIT_DATE_ENV);
104
+ const commit = (await mustGit(git, ["rev-parse", "HEAD"], "rev-parse")).stdout.trim();
105
+ const status = await mustGit(git, ["status", "--porcelain"], "status");
106
+ if (status.stdout.trim() !== "") {
107
+ throw new BenchmarkSandboxError("BASELINE_NOT_CLEAN", "git status is not clean after the baseline commit.");
108
+ }
109
+ return {
110
+ schemaVersion: BENCHMARK_SANDBOX_SCHEMA_VERSION,
111
+ sandboxId: options.sandboxId,
112
+ sandboxRoot,
113
+ projectRoot,
114
+ evidenceRoot,
115
+ runtimeRoot,
116
+ canonicalProjectRoot,
117
+ baseline: { commit, fileCount: manifest.length, digest: digestTreeSnapshot(manifest), manifest }
118
+ };
119
+ }
120
+ catch (error) {
121
+ // Only this call's own directory exists at this point; removal cannot touch anything else.
122
+ await rm(sandboxRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined);
123
+ if (error instanceof BenchmarkSandboxError)
124
+ throw error;
125
+ throw new BenchmarkSandboxError("COPY_FAILED", error instanceof Error ? error.message : String(error));
126
+ }
127
+ }
@@ -0,0 +1,11 @@
1
+ const MAX_MESSAGE_LENGTH = 500;
2
+ /** Bounded, typed failure of sandbox creation, baseline, or tree inspection. */
3
+ export class BenchmarkSandboxError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ const bounded = message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, MAX_MESSAGE_LENGTH)}...` : message;
7
+ super(`Benchmark sandbox error (${code}): ${bounded}`);
8
+ this.name = "BenchmarkSandboxError";
9
+ this.code = code;
10
+ }
11
+ }
@@ -0,0 +1,60 @@
1
+ import path from "node:path";
2
+ import { runMeasuredCommand } from "../../core/runMeasuredCommand.js";
3
+ import { buildMinimalHostEnv } from "./minimalHostEnv.js";
4
+ // Git (including Git for Windows) opens this as an empty config file; os.devNull (\\.\nul) is rejected by Git on Windows.
5
+ const NULL_CONFIG_PATH = process.platform === "win32" ? "NUL" : "/dev/null";
6
+ const DEFAULT_GIT_TIMEOUT_MS = 60_000;
7
+ const GIT_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;
8
+ const sequenceByEvidenceRoot = new Map();
9
+ function nextSequence(evidenceRoot) {
10
+ const next = (sequenceByEvidenceRoot.get(evidenceRoot) ?? 0) + 1;
11
+ sequenceByEvidenceRoot.set(evidenceRoot, next);
12
+ return next;
13
+ }
14
+ /**
15
+ * Runs Git for a sandbox through the shared measured-command owner (structured arguments, shell:false,
16
+ * bounded output, awaited tree termination, evidence files). The environment is isolated from the user's
17
+ * and the system's Git configuration so hashes and diffs do not depend on the developer machine.
18
+ */
19
+ export async function runSandboxGit(target, args, options = {}) {
20
+ const label = (options.label ?? args[0] ?? "git").replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 32);
21
+ const commandId = `git-${String(nextSequence(target.evidenceRoot)).padStart(4, "0")}-${label}`;
22
+ const env = buildMinimalHostEnv(process.env, {
23
+ GIT_CONFIG_NOSYSTEM: "1",
24
+ GIT_CONFIG_GLOBAL: NULL_CONFIG_PATH,
25
+ GIT_TERMINAL_PROMPT: "0",
26
+ GIT_OPTIONAL_LOCKS: "0",
27
+ HOME: target.evidenceRoot,
28
+ USERPROFILE: target.evidenceRoot,
29
+ ...options.env
30
+ });
31
+ const result = await runMeasuredCommand({
32
+ commandId,
33
+ executable: "git",
34
+ args: ["-c", "core.quotepath=false", "-c", "core.autocrlf=false", "-c", "core.fsmonitor=false", ...args],
35
+ cwd: target.projectRoot,
36
+ outDir: path.join(target.evidenceRoot, "git"),
37
+ env,
38
+ inheritParentEnv: false,
39
+ stdinText: options.stdin,
40
+ timeoutMs: options.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS,
41
+ stdoutMaxBytes: GIT_OUTPUT_LIMIT_BYTES,
42
+ stderrMaxBytes: GIT_OUTPUT_LIMIT_BYTES,
43
+ terminateProcessTree: true
44
+ });
45
+ const unavailable = result.resolvedCommand?.resolutionKind === "unavailable";
46
+ return {
47
+ ok: result.ok,
48
+ exitCode: result.exitCode,
49
+ stdout: result.stdout,
50
+ stderr: result.stderr,
51
+ timedOut: result.timedOut === true,
52
+ unavailable,
53
+ error: result.error
54
+ };
55
+ }
56
+ /** Bounded single-line excerpt of Git diagnostics for error messages. */
57
+ export function excerptGitFailure(result) {
58
+ const text = (result.stderr || result.stdout || result.error || "no output").trim().replace(/\s+/g, " ");
59
+ return text.length > 300 ? `${text.slice(0, 300)}...` : text;
60
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./errors.js";
2
+ export * from "./types.js";
3
+ export { createBenchmarkSandbox } from "./createBenchmarkSandbox.js";
4
+ export { removeBenchmarkSandbox } from "./removeBenchmarkSandbox.js";
5
+ export { digestTreeSnapshot, scanProjectTree, sha256OfBuffer, snapshotProjectTree } from "./treeSnapshot.js";
6
+ export { buildMinimalHostEnv } from "./minimalHostEnv.js";
7
+ export { excerptGitFailure, runSandboxGit } from "./gitExecutor.js";
8
+ export { BENCHMARK_SANDBOX_EXCLUDED_NAMES, SANDBOX_ID_PATTERN } from "./pathPolicy.js";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Variables a child process needs to locate and start `node` or `git` on the supported platforms. Nothing
3
+ * else is forwarded from the parent: provider keys, tokens, cookies and project secrets must never reach
4
+ * Lab-run verification or Git processes. Names are matched case-insensitively (Windows environments are).
5
+ */
6
+ const FORWARDED_NAMES = [
7
+ "path",
8
+ "systemroot",
9
+ "systemdrive",
10
+ "windir",
11
+ "comspec",
12
+ "pathext",
13
+ "temp",
14
+ "tmp",
15
+ "tmpdir",
16
+ "lang",
17
+ "lc_all"
18
+ ];
19
+ /** Defense in depth: even an allowed name is dropped if it looks credential-like. */
20
+ const CREDENTIAL_LIKE = /(key|token|secret|passw|credential|auth|cookie|session)/i;
21
+ export function buildMinimalHostEnv(source = process.env, extras = {}) {
22
+ const env = {};
23
+ const actualNames = Object.keys(source);
24
+ for (const wanted of FORWARDED_NAMES) {
25
+ const actual = actualNames.find((name) => name.toLowerCase() === wanted);
26
+ if (actual === undefined)
27
+ continue;
28
+ const value = source[actual];
29
+ if (value === undefined || CREDENTIAL_LIKE.test(actual))
30
+ continue;
31
+ env[actual] = value;
32
+ }
33
+ for (const [name, value] of Object.entries(extras)) {
34
+ env[name] = value;
35
+ }
36
+ return env;
37
+ }
@@ -0,0 +1,49 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /** Sandbox identities become one directory name beneath the caller's runtime root. */
4
+ export const SANDBOX_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
5
+ /**
6
+ * Directory/file names never copied into a sandbox and never part of its baseline. `.git` would import the
7
+ * canonical history; the rest are generated or Lab-internal outputs that are not benchmark source.
8
+ */
9
+ export const BENCHMARK_SANDBOX_EXCLUDED_NAMES = Object.freeze([
10
+ ".git",
11
+ "node_modules",
12
+ "dist",
13
+ "build",
14
+ "coverage",
15
+ "lab-output",
16
+ ".my-dev-kit-context",
17
+ ".my-dev-kit-orchestrator"
18
+ ]);
19
+ /**
20
+ * Physical path of a path that may not exist yet: the realpath of the deepest existing ancestor plus the
21
+ * not-yet-created remainder. Resolving physically (not lexically) is what makes overlap checks hold across
22
+ * links, junctions, and Windows drive aliases.
23
+ */
24
+ export async function resolvePhysicalPath(candidate) {
25
+ const resolved = path.resolve(candidate);
26
+ const remainder = [];
27
+ let current = resolved;
28
+ for (;;) {
29
+ try {
30
+ const physical = await realpath(current);
31
+ return remainder.length === 0 ? physical : path.join(physical, ...remainder.reverse());
32
+ }
33
+ catch (error) {
34
+ const code = error.code;
35
+ const parent = path.dirname(current);
36
+ if ((code !== "ENOENT" && code !== "ENOTDIR") || parent === current)
37
+ return resolved;
38
+ remainder.push(path.basename(current));
39
+ current = parent;
40
+ }
41
+ }
42
+ }
43
+ /** True when `candidate` equals `root` or lies beneath it. Both arguments must already be physical paths. */
44
+ export function isSameOrInside(root, candidate) {
45
+ const relative = path.relative(root, candidate);
46
+ if (relative === "")
47
+ return true;
48
+ return !(relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative));
49
+ }
@@ -0,0 +1,74 @@
1
+ import { lstat, readFile, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveWithinRoot } from "../../core/pathSafety.js";
4
+ import { isSameOrInside, resolvePhysicalPath, SANDBOX_ID_PATTERN } from "./pathPolicy.js";
5
+ import { BENCHMARK_SANDBOX_OWNER_MARKER_FILE, BENCHMARK_SANDBOX_SCHEMA_VERSION } from "./types.js";
6
+ /**
7
+ * Removes one sandbox, and only a sandbox this subsystem created: the directory must be a real directory
8
+ * directly inside the runtime root and carry an ownership marker whose schema, sandbox id and runtime root
9
+ * match. Any mismatch is a refusal that leaves everything on disk untouched. The runtime root itself is
10
+ * never removed.
11
+ */
12
+ export async function removeBenchmarkSandbox(options) {
13
+ if (!SANDBOX_ID_PATTERN.test(options.sandboxId)) {
14
+ return { removed: false, reason: "invalid sandbox id." };
15
+ }
16
+ const runtimeRoot = path.resolve(options.runtimeRoot);
17
+ let sandboxRoot;
18
+ try {
19
+ sandboxRoot = resolveWithinRoot(runtimeRoot, options.sandboxId);
20
+ }
21
+ catch {
22
+ return { removed: false, reason: "sandbox path escapes the runtime root." };
23
+ }
24
+ let stats;
25
+ try {
26
+ stats = await lstat(sandboxRoot);
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT") {
30
+ try {
31
+ await lstat(runtimeRoot);
32
+ return { removed: true, alreadyAbsent: true };
33
+ }
34
+ catch {
35
+ return { removed: false, reason: "the runtime root does not exist." };
36
+ }
37
+ }
38
+ return { removed: false, reason: "the sandbox path could not be inspected." };
39
+ }
40
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
41
+ return { removed: false, reason: "the sandbox path is not a plain directory." };
42
+ }
43
+ if (!isSameOrInside(await resolvePhysicalPath(runtimeRoot), await resolvePhysicalPath(sandboxRoot))) {
44
+ return { removed: false, reason: "the sandbox resolves outside the runtime root." };
45
+ }
46
+ let marker;
47
+ try {
48
+ marker = JSON.parse(await readFile(path.join(sandboxRoot, BENCHMARK_SANDBOX_OWNER_MARKER_FILE), "utf8"));
49
+ }
50
+ catch {
51
+ return { removed: false, reason: "no readable ownership marker; the directory is not a recognized sandbox." };
52
+ }
53
+ if (marker.schemaVersion !== BENCHMARK_SANDBOX_SCHEMA_VERSION ||
54
+ marker.sandboxId !== options.sandboxId ||
55
+ typeof marker.runtimeRoot !== "string" ||
56
+ path.resolve(marker.runtimeRoot) !== runtimeRoot) {
57
+ return { removed: false, reason: "the ownership marker does not match this sandbox." };
58
+ }
59
+ try {
60
+ await rm(sandboxRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
61
+ }
62
+ catch (error) {
63
+ return { removed: false, reason: `removal failed: ${error instanceof Error ? error.message : String(error)}` };
64
+ }
65
+ try {
66
+ await lstat(sandboxRoot);
67
+ return { removed: false, reason: "the sandbox directory still exists after removal." };
68
+ }
69
+ catch (error) {
70
+ return error.code === "ENOENT"
71
+ ? { removed: true, alreadyAbsent: false }
72
+ : { removed: false, reason: "the sandbox state could not be verified after removal." };
73
+ }
74
+ }
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { BenchmarkSandboxError } from "./errors.js";
5
+ export function sha256OfBuffer(data) {
6
+ return createHash("sha256").update(data).digest("hex");
7
+ }
8
+ function compareCodeUnits(left, right) {
9
+ return left < right ? -1 : left > right ? 1 : 0;
10
+ }
11
+ /**
12
+ * Lists every regular file below `root` as sorted project-relative POSIX paths. Entries whose name is in
13
+ * `excludedNames` are skipped at any depth. A symbolic link or junction anywhere else is rejected rather
14
+ * than followed (a link could point outside the project), as is any entry that is neither a file nor a
15
+ * directory.
16
+ */
17
+ export async function scanProjectTree(root, excludedNames) {
18
+ const excluded = new Set(excludedNames);
19
+ const files = [];
20
+ async function walk(absoluteDir, relativeDir) {
21
+ const entries = await readdir(absoluteDir, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ if (excluded.has(entry.name))
24
+ continue;
25
+ const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
26
+ if (entry.isSymbolicLink()) {
27
+ throw new BenchmarkSandboxError("SYMLINK_REJECTED", `symbolic link or junction at ${relativePath}`);
28
+ }
29
+ if (entry.isDirectory()) {
30
+ await walk(path.join(absoluteDir, entry.name), relativePath);
31
+ }
32
+ else if (entry.isFile()) {
33
+ files.push(relativePath);
34
+ }
35
+ else {
36
+ throw new BenchmarkSandboxError("UNSUPPORTED_ENTRY", `unsupported entry type at ${relativePath}`);
37
+ }
38
+ }
39
+ }
40
+ await walk(root, "");
41
+ return files.sort(compareCodeUnits);
42
+ }
43
+ /** Deterministic content identity of a project tree: sorted paths with sha256 and size of every file. */
44
+ export async function snapshotProjectTree(root, options) {
45
+ const files = await scanProjectTree(root, options.excludedNames);
46
+ const entries = [];
47
+ for (const relativePath of files) {
48
+ const data = await readFile(path.join(root, ...relativePath.split("/")));
49
+ entries.push({ path: relativePath, sha256: sha256OfBuffer(data), size: data.length });
50
+ }
51
+ return entries;
52
+ }
53
+ /** Aggregate digest over sorted "path<TAB>sha256<LF>" lines (same convention as the staleness source state). */
54
+ export function digestTreeSnapshot(entries) {
55
+ const serialized = entries.map((entry) => `${entry.path}\t${entry.sha256}\n`).join("");
56
+ return createHash("sha256").update(Buffer.from(serialized, "utf8")).digest("hex");
57
+ }
@@ -0,0 +1,3 @@
1
+ export const BENCHMARK_SANDBOX_SCHEMA_VERSION = "my-dev-kit-lab-benchmark-sandbox-v1";
2
+ export const BENCHMARK_SANDBOX_OWNER_MARKER_FILE = "sandbox-owner.json";
3
+ export const BENCHMARK_SANDBOX_BASELINE_BRANCH = "sandbox-baseline";
@@ -0,0 +1,98 @@
1
+ import { BenchmarkSandboxError, excerptGitFailure, runSandboxGit, snapshotProjectTree } from "../benchmarkSandbox/index.js";
2
+ import { CHANGE_SET_SCHEMA_VERSION, ChangeSetError } from "./types.js";
3
+ function compareCodeUnits(left, right) {
4
+ return left < right ? -1 : left > right ? 1 : 0;
5
+ }
6
+ async function git(sandbox, args, label) {
7
+ const result = await runSandboxGit(sandbox, args, { label });
8
+ if (!result.ok) {
9
+ throw new ChangeSetError("GIT_FAILED", `git ${args[0] ?? ""} failed: ${excerptGitFailure(result)}`);
10
+ }
11
+ return result;
12
+ }
13
+ /** Parses `git diff --numstat -z --no-renames`: records of "added<TAB>deleted<TAB>path" separated by NUL. */
14
+ function parseNumstat(output) {
15
+ const stats = new Map();
16
+ for (const record of output.split("\0")) {
17
+ if (record === "")
18
+ continue;
19
+ const firstTab = record.indexOf("\t");
20
+ const secondTab = firstTab < 0 ? -1 : record.indexOf("\t", firstTab + 1);
21
+ if (firstTab < 0 || secondTab < 0) {
22
+ throw new ChangeSetError("INCONSISTENT_EVIDENCE", "unrecognized numstat record.");
23
+ }
24
+ const added = record.slice(0, firstTab);
25
+ const deleted = record.slice(firstTab + 1, secondTab);
26
+ stats.set(record.slice(secondTab + 1), {
27
+ additions: added === "-" ? null : Number(added),
28
+ deletions: deleted === "-" ? null : Number(deleted)
29
+ });
30
+ }
31
+ return stats;
32
+ }
33
+ /**
34
+ * Captures what changed in a sandbox relative to its own baseline commit. File status and hashes come from
35
+ * comparing the baseline manifest with the working tree (independent of Git ignore rules and of anything the
36
+ * agent claimed); line counts and the cumulative diff come from Git against the baseline commit with rename
37
+ * detection disabled. The two views must agree or capture fails rather than reporting doubtful evidence.
38
+ *
39
+ * Call it right after patch application: it stages the working tree (`git add -A --force`) and reflects
40
+ * whatever exists on disk at that moment, including files produced by later verification runs.
41
+ */
42
+ export async function captureChangeSet(options) {
43
+ const { sandbox } = options;
44
+ let working;
45
+ try {
46
+ working = await snapshotProjectTree(sandbox.projectRoot, { excludedNames: [".git"] });
47
+ }
48
+ catch (error) {
49
+ if (error instanceof BenchmarkSandboxError && (error.code === "SYMLINK_REJECTED" || error.code === "UNSUPPORTED_ENTRY")) {
50
+ throw new ChangeSetError("SYMLINK_IN_SANDBOX", error.message);
51
+ }
52
+ throw error;
53
+ }
54
+ const baselineByPath = new Map(sandbox.baseline.manifest.map((entry) => [entry.path, entry.sha256]));
55
+ const workingByPath = new Map(working.map((entry) => [entry.path, entry.sha256]));
56
+ const records = [];
57
+ for (const [relativePath, before] of baselineByPath) {
58
+ const after = workingByPath.get(relativePath);
59
+ if (after === undefined) {
60
+ records.push({ relativePath, status: "deleted", beforeSha256: before, afterSha256: null });
61
+ }
62
+ else if (after !== before) {
63
+ records.push({ relativePath, status: "modified", beforeSha256: before, afterSha256: after });
64
+ }
65
+ }
66
+ for (const [relativePath, after] of workingByPath) {
67
+ if (!baselineByPath.has(relativePath)) {
68
+ records.push({ relativePath, status: "added", beforeSha256: null, afterSha256: after });
69
+ }
70
+ }
71
+ records.sort((left, right) => compareCodeUnits(left.relativePath, right.relativePath));
72
+ await git(sandbox, ["add", "-A", "--force"], "stage");
73
+ const numstat = parseNumstat((await git(sandbox, ["diff", "--cached", "--numstat", "--no-renames", "-z", sandbox.baseline.commit], "numstat")).stdout);
74
+ const diffResult = await git(sandbox, ["diff", "--cached", "--no-renames", "--no-ext-diff", "--no-color", "--src-prefix=a/", "--dst-prefix=b/", sandbox.baseline.commit], "diff");
75
+ const changedFiles = records.map((record) => {
76
+ const stats = numstat.get(record.relativePath);
77
+ if (stats === undefined) {
78
+ throw new ChangeSetError("INCONSISTENT_EVIDENCE", `Git reports no change for ${record.relativePath}, which differs from the baseline manifest.`);
79
+ }
80
+ return { ...record, additions: stats.additions, deletions: stats.deletions };
81
+ });
82
+ const count = (status) => changedFiles.filter((file) => file.status === status).length;
83
+ const sum = (pick) => changedFiles.reduce((total, file) => total + (pick(file) ?? 0), 0);
84
+ const diff = diffResult.stdout === "" || diffResult.stdout.endsWith("\n") ? diffResult.stdout : `${diffResult.stdout}\n`;
85
+ return {
86
+ schemaVersion: CHANGE_SET_SCHEMA_VERSION,
87
+ sandboxId: sandbox.sandboxId,
88
+ baselineCommit: sandbox.baseline.commit,
89
+ changedFiles,
90
+ addedCount: count("added"),
91
+ modifiedCount: count("modified"),
92
+ deletedCount: count("deleted"),
93
+ changedCount: changedFiles.length,
94
+ totalAdditions: sum((file) => file.additions),
95
+ totalDeletions: sum((file) => file.deletions),
96
+ diff
97
+ };
98
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export { captureChangeSet } from "./captureChangeSet.js";
@@ -0,0 +1,9 @@
1
+ export const CHANGE_SET_SCHEMA_VERSION = "my-dev-kit-lab-change-set-v1";
2
+ export class ChangeSetError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(`Change set error (${code}): ${message.length > 500 ? `${message.slice(0, 500)}...` : message}`);
6
+ this.name = "ChangeSetError";
7
+ this.code = code;
8
+ }
9
+ }
@@ -183,6 +183,7 @@ export async function buildMyDevKitIndex(options) {
183
183
  ...(options.excludePaths ?? []).flatMap((excludePath) => ["--exclude", excludePath]),
184
184
  "--out",
185
185
  options.indexDir,
186
+ ...(options.callGraph === true ? ["--call-graph"] : []),
186
187
  ...(mode.kind === "incremental" ? ["--incremental", "--refresh-scope", mode.refreshScope] : []),
187
188
  "--json"
188
189
  ]
@@ -1,5 +1,7 @@
1
1
  import { ExperimentPluginRegistry } from "./registry.js";
2
+ import { agentSuccessRatePlugin } from "./plugins/agentSuccessRate/index.js";
2
3
  import { contextStrategyComparisonPlugin } from "./plugins/contextStrategyComparison/index.js";
4
+ import { contextPackGenerationPlugin } from "./plugins/contextPackGeneration/index.js";
3
5
  import { contextWindowScalingPlugin } from "./plugins/contextWindowScaling/index.js";
4
6
  import { incrementalChangeStalenessPlugin } from "./plugins/incrementalChangeStaleness/index.js";
5
7
  import { retrievalPrecisionRecallPlugin } from "./plugins/retrievalPrecisionRecall/index.js";
@@ -13,5 +15,7 @@ export function createDefaultExperimentPluginRegistry() {
13
15
  registry.register(contextWindowScalingPlugin);
14
16
  registry.register(retrievalPrecisionRecallPlugin);
15
17
  registry.register(retrievalQueryStrategyComparisonPlugin);
18
+ registry.register(contextPackGenerationPlugin);
19
+ registry.register(agentSuccessRatePlugin);
16
20
  return registry;
17
21
  }
@@ -0,0 +1,12 @@
1
+ export const AGENT_FACING_TASK_FIELDS = ["caseId", "benchmarkProject", "title", "instruction", "query", "taskLocality"];
2
+ /** Builds a fresh object from named fields. It never spreads, serializes or iterates the source task. */
3
+ export function projectAgentFacingTask(task) {
4
+ return Object.freeze({
5
+ caseId: task.id,
6
+ benchmarkProject: task.benchmarkProject,
7
+ title: task.title,
8
+ instruction: task.instruction,
9
+ query: task.query,
10
+ taskLocality: task.taskLocality
11
+ });
12
+ }