@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.
- package/CHANGELOG.md +20 -1
- package/README.md +20 -4
- package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
- package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
- package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
- package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
- package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
- package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
- package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
- package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
- package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
- package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
- package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
- package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
- package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
- package/dist/src/cli/help.js +54 -2
- package/dist/src/commands/runExperimentDescribeCommand.js +60 -2
- package/dist/src/commands/runExperimentRunCommand.js +208 -5
- package/dist/src/core/processTree.js +55 -0
- package/dist/src/core/runMeasuredCommand.js +104 -22
- package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
- package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
- package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
- package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
- package/dist/src/evaluation/agentSuccess/index.js +14 -0
- package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
- package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
- package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
- package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
- package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
- package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
- package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
- package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
- package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
- package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
- package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
- package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
- package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +60 -0
- package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
- package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
- package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
- package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
- package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
- package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
- package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
- package/dist/src/evaluation/changeSet/index.js +2 -0
- package/dist/src/evaluation/changeSet/types.js +9 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +1 -0
- package/dist/src/experiments/defaultRegistry.js +4 -0
- package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
- package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
- package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
- package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
- package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
- package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
- package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
- package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
- package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
- package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
- package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysis.js +157 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
- package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
- package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
- package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
- package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
- package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
- package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
- package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
- package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
- package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
- package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
- package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
- package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
- package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
- package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -0
- package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
- package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
- package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
- package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +49 -2
- package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
- package/dist/src/report/experiments/index.js +9 -0
- package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
- package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
- package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
- package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
- package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +12 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +15 -15
- package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
- package/docs/METRICS.md +98 -1
- package/package.json +1 -1
|
@@ -3,9 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import { parseAgentCommandTemplate } from "../agents/index.js";
|
|
4
4
|
import { parseAgentId } from "../agents/agentRegistry.js";
|
|
5
5
|
import { readBenchmarkProjectProfiles, readEvaluationCases } from "../evaluation/index.js";
|
|
6
|
+
import { AGENT_SUCCESS_PROJECT_PROFILES_PATH, AGENT_SUCCESS_TASK_CATALOG_PATH, assertReadAgentSuccessCorpus } from "../evaluation/agentSuccess/index.js";
|
|
6
7
|
import { summarizeRetrievalGroundTruthIssuesSafely, validateRetrievalPrecisionRecallCorpus } from "../evaluation/retrievalQuality/index.js";
|
|
7
8
|
import { loadLocalRepositorySubject } from "../evaluation/localRepositorySubject/index.js";
|
|
8
9
|
import { contextStrategyComparisonPlugin, contextWindowScalingPlugin, createDefaultExperimentPluginRegistry, getWarmIndexCampaignPreset, incrementalChangeStalenessPlugin, parseContextBudgetsCliValue, parseWarmIndexCampaignPresetId, resolveExperimentTarget, retrievalPrecisionRecallPlugin, retrievalQueryStrategyComparisonPlugin, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
|
|
10
|
+
import { contextPackGenerationPlugin } from "../experiments/plugins/contextPackGeneration/index.js";
|
|
11
|
+
import { AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS, AGENT_SUCCESS_RATE_REAL_AGENT_IDS, agentSuccessRatePlugin, selectAgentSuccessTasks } from "../experiments/plugins/agentSuccessRate/index.js";
|
|
9
12
|
import { prepareSyntheticContextWindowScalingInputs } from "../experiments/plugins/contextWindowScaling/index.js";
|
|
10
13
|
import { projectExternalLocalTarget, projectRunForExternalLocalPersistence, } from "../experiments/plugins/contextWindowScaling/localSubjectPrivacy.js";
|
|
11
14
|
import { assertWorkRootOutsideTarget } from "../experiments/plugins/contextWindowScaling/localSubjectScratch.js";
|
|
@@ -64,6 +67,33 @@ const RETRIEVAL_PRECISION_RECALL_EXTERNAL_ALLOWED_FLAGS = ["--experiment", "--ou
|
|
|
64
67
|
const RETRIEVAL_QUERY_STRATEGY_COMPARISON_CASES_RESOURCE = "benchmarks/contracts/warm-index-benchmark-cases.json";
|
|
65
68
|
const RETRIEVAL_QUERY_STRATEGY_COMPARISON_ALLOWED_FLAGS = ["--experiment", "--out", "--case", "--benchmark-project", "--kit-command"];
|
|
66
69
|
const RETRIEVAL_QUERY_STRATEGY_COMPARISON_EXTERNAL_ALLOWED_FLAGS = ["--experiment", "--out", "--target", "--local-subject-config", "--kit-command"];
|
|
70
|
+
// context-pack-generation (v0.8.2) reuses the same frozen bundled corpus. Both treatments (raw-full-file and context-pack)
|
|
71
|
+
// always run, so there is no treatment or strategy option. External-local mode (--target with --local-subject-config) is a
|
|
72
|
+
// different subject source: the local-subject config owns the case set, so the bundled corpus filters are not accepted there.
|
|
73
|
+
const CONTEXT_PACK_GENERATION_CASES_RESOURCE = "benchmarks/contracts/warm-index-benchmark-cases.json";
|
|
74
|
+
const CONTEXT_PACK_GENERATION_ALLOWED_FLAGS = ["--experiment", "--out", "--case", "--benchmark-project", "--kit-command"];
|
|
75
|
+
const CONTEXT_PACK_GENERATION_EXTERNAL_ALLOWED_FLAGS = ["--experiment", "--out", "--target", "--local-subject-config", "--kit-command"];
|
|
76
|
+
// agent-success-rate (v0.9.0) owns the bundled six-task implementation corpus (task catalog and project profiles), both
|
|
77
|
+
// resolved from the package root, never the cwd. It runs against the Lab itself only and accepts exactly these options;
|
|
78
|
+
// every other flag is rejected, not ignored. Real providers run only with --agent together with --include-real-agents.
|
|
79
|
+
const AGENT_SUCCESS_RATE_ALLOWED_FLAGS = [
|
|
80
|
+
"--experiment",
|
|
81
|
+
"--out",
|
|
82
|
+
"--case",
|
|
83
|
+
"--benchmark-project",
|
|
84
|
+
"--kit-command",
|
|
85
|
+
"--agent",
|
|
86
|
+
"--include-real-agents",
|
|
87
|
+
"--timeout-ms",
|
|
88
|
+
"--repair-attempts"
|
|
89
|
+
];
|
|
90
|
+
export function assertAgentSuccessRateFlags(argv) {
|
|
91
|
+
const flags = argv.filter((arg) => arg.startsWith("--"));
|
|
92
|
+
const unsupported = [...new Set(flags.filter((flag) => !AGENT_SUCCESS_RATE_ALLOWED_FLAGS.includes(flag)))];
|
|
93
|
+
if (unsupported.length > 0) {
|
|
94
|
+
throw new Error(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} not supported for --experiment ${agentSuccessRatePlugin.metadata.id}; supported options: ${AGENT_SUCCESS_RATE_ALLOWED_FLAGS.join(", ")}.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
67
97
|
export async function runExperimentRunCommandFromArgs(argv, options = {}) {
|
|
68
98
|
try {
|
|
69
99
|
const args = parseRunExperimentArgs(argv);
|
|
@@ -144,7 +174,8 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
|
|
|
144
174
|
`Target root: ${result.target.targetRoot}`,
|
|
145
175
|
`Output: ${String(result.metadata?.outputRoot ?? "")}`,
|
|
146
176
|
`Report JSON: ${reports.outputPaths.jsonPath}`,
|
|
147
|
-
`Report HTML: ${reports.outputPaths.htmlPath}
|
|
177
|
+
`Report HTML: ${reports.outputPaths.htmlPath}`,
|
|
178
|
+
...(args.experimentId === agentSuccessRatePlugin.metadata.id ? agentSuccessRateSummaryLines(result, reports.outputPaths.textPath) : [])
|
|
148
179
|
];
|
|
149
180
|
// Automatic presentation (report already produced above) applies only to warm-index-reuse
|
|
150
181
|
// campaign runs; legacy fake-agent warm-index runs and context-strategy-comparison stay
|
|
@@ -218,8 +249,14 @@ export function parseRunExperimentArgs(argv) {
|
|
|
218
249
|
let contextBudgets;
|
|
219
250
|
let syntheticConfigPath;
|
|
220
251
|
let localSubjectConfigPath;
|
|
252
|
+
const agentFlagValues = [];
|
|
253
|
+
let repairAttempts;
|
|
221
254
|
const seenFlags = [];
|
|
222
255
|
const commandTemplates = {};
|
|
256
|
+
// The flag whitelist runs before any value is interpreted so an unsupported flag is reported as such.
|
|
257
|
+
const preScanIndex = argv.indexOf("--experiment");
|
|
258
|
+
if (preScanIndex >= 0 && argv[preScanIndex + 1] === agentSuccessRatePlugin.metadata.id)
|
|
259
|
+
assertAgentSuccessRateFlags(argv);
|
|
223
260
|
for (let index = 0; index < argv.length; index += 1) {
|
|
224
261
|
const arg = argv[index];
|
|
225
262
|
if (arg !== undefined && arg.startsWith("--"))
|
|
@@ -257,6 +294,12 @@ export function parseRunExperimentArgs(argv) {
|
|
|
257
294
|
else if (arg === "--benchmark-project") {
|
|
258
295
|
benchmarkProjects.push(...splitList(readRequiredValue(argv, ++index, "--benchmark-project")));
|
|
259
296
|
}
|
|
297
|
+
else if (arg === "--agent") {
|
|
298
|
+
agentFlagValues.push(readRequiredValue(argv, ++index, "--agent"));
|
|
299
|
+
}
|
|
300
|
+
else if (arg === "--repair-attempts") {
|
|
301
|
+
repairAttempts = parseRepairAttempts(readRequiredValue(argv, ++index, "--repair-attempts"));
|
|
302
|
+
}
|
|
260
303
|
else if (arg === "--agents") {
|
|
261
304
|
agents = splitList(readRequiredValue(argv, ++index, "--agents")).map((value) => parseAgentId(value));
|
|
262
305
|
}
|
|
@@ -315,7 +358,8 @@ export function parseRunExperimentArgs(argv) {
|
|
|
315
358
|
incrementalChangeStalenessPlugin.metadata.id,
|
|
316
359
|
contextWindowScalingPlugin.metadata.id,
|
|
317
360
|
retrievalPrecisionRecallPlugin.metadata.id,
|
|
318
|
-
retrievalQueryStrategyComparisonPlugin.metadata.id
|
|
361
|
+
retrievalQueryStrategyComparisonPlugin.metadata.id,
|
|
362
|
+
contextPackGenerationPlugin.metadata.id
|
|
319
363
|
];
|
|
320
364
|
if (experimentId === retrievalPrecisionRecallPlugin.metadata.id) {
|
|
321
365
|
// Mode matrix, no precedence rules: neither flag = bundled; both = external-local; exactly one is rejected.
|
|
@@ -367,6 +411,73 @@ export function parseRunExperimentArgs(argv) {
|
|
|
367
411
|
throw new Error("--benchmark-project must list at least one benchmark project id.");
|
|
368
412
|
}
|
|
369
413
|
}
|
|
414
|
+
if (experimentId === contextPackGenerationPlugin.metadata.id) {
|
|
415
|
+
// Mode matrix, no precedence rules: neither flag = bundled; both = external-local; exactly one is rejected.
|
|
416
|
+
if (targetPath !== undefined && localSubjectConfigPath === undefined) {
|
|
417
|
+
throw new Error(`External ${experimentId} targets require --local-subject-config.`);
|
|
418
|
+
}
|
|
419
|
+
if (localSubjectConfigPath !== undefined && targetPath === undefined) {
|
|
420
|
+
throw new Error(`--local-subject-config requires an external --target for ${experimentId}.`);
|
|
421
|
+
}
|
|
422
|
+
const externalMode = targetPath !== undefined;
|
|
423
|
+
if (externalMode && (seenFlags.includes("--case") || seenFlags.includes("--benchmark-project"))) {
|
|
424
|
+
throw new Error("--case and --benchmark-project cannot be combined with --local-subject-config; the local subject config owns the case set.");
|
|
425
|
+
}
|
|
426
|
+
const allowedFlags = externalMode ? CONTEXT_PACK_GENERATION_EXTERNAL_ALLOWED_FLAGS : CONTEXT_PACK_GENERATION_ALLOWED_FLAGS;
|
|
427
|
+
const unsupported = [...new Set(seenFlags.filter((flag) => !allowedFlags.includes(flag)))];
|
|
428
|
+
if (unsupported.length > 0) {
|
|
429
|
+
throw new Error(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} not supported for --experiment ${experimentId}${externalMode ? " in external-local mode" : ""}; supported options: ${allowedFlags.join(", ")}.`);
|
|
430
|
+
}
|
|
431
|
+
if (seenFlags.includes("--case") && caseIds.length === 0) {
|
|
432
|
+
throw new Error("--case must list at least one case id.");
|
|
433
|
+
}
|
|
434
|
+
if (seenFlags.includes("--benchmark-project") && benchmarkProjects.length === 0) {
|
|
435
|
+
throw new Error("--benchmark-project must list at least one benchmark project id.");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (experimentId === agentSuccessRatePlugin.metadata.id) {
|
|
439
|
+
// Mode matrix: no flags = deterministic-fixture (default); --agent together with --include-real-agents = real-agent mode.
|
|
440
|
+
// Everything else is rejected, never coerced: a provider needs both flags, and real-agent options need a provider.
|
|
441
|
+
if (agentFlagValues.length > 1) {
|
|
442
|
+
throw new Error("--agent may be supplied only once; agent-success-rate runs exactly one provider.");
|
|
443
|
+
}
|
|
444
|
+
const agentId = agentFlagValues[0];
|
|
445
|
+
if (agentId !== undefined && !AGENT_SUCCESS_RATE_REAL_AGENT_IDS.includes(agentId)) {
|
|
446
|
+
throw new Error(`--agent must be exactly one of: ${AGENT_SUCCESS_RATE_REAL_AGENT_IDS.join(", ")}.`);
|
|
447
|
+
}
|
|
448
|
+
if (agentId !== undefined && includeRealAgents !== true) {
|
|
449
|
+
throw new Error("--agent requires --include-real-agents; real providers are invoked only with explicit authorization.");
|
|
450
|
+
}
|
|
451
|
+
if (agentId === undefined && includeRealAgents === true) {
|
|
452
|
+
throw new Error("--include-real-agents requires --agent <codex|claude> for agent-success-rate.");
|
|
453
|
+
}
|
|
454
|
+
if (agentId === undefined && timeoutMs !== undefined) {
|
|
455
|
+
throw new Error("--timeout-ms is only supported together with --agent and --include-real-agents (real-agent mode).");
|
|
456
|
+
}
|
|
457
|
+
if (agentId === undefined && kitCommand !== undefined) {
|
|
458
|
+
throw new Error("--kit-command is only supported together with --agent and --include-real-agents (real-agent mode).");
|
|
459
|
+
}
|
|
460
|
+
if (agentId === undefined && repairAttempts !== undefined && repairAttempts > 0) {
|
|
461
|
+
throw new Error("--repair-attempts greater than 0 requires --agent and --include-real-agents (real-agent mode).");
|
|
462
|
+
}
|
|
463
|
+
if (timeoutMs !== undefined && timeoutMs > AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS) {
|
|
464
|
+
throw new Error(`--timeout-ms must be a positive integer no greater than ${AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS}.`);
|
|
465
|
+
}
|
|
466
|
+
if (seenFlags.includes("--case") && caseIds.length === 0) {
|
|
467
|
+
throw new Error("--case must list at least one case id.");
|
|
468
|
+
}
|
|
469
|
+
if (seenFlags.includes("--benchmark-project") && benchmarkProjects.length === 0) {
|
|
470
|
+
throw new Error("--benchmark-project must list at least one benchmark project id.");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
if (agentFlagValues.length > 0) {
|
|
475
|
+
throw new Error(`--agent is only supported for --experiment ${agentSuccessRatePlugin.metadata.id}.`);
|
|
476
|
+
}
|
|
477
|
+
if (repairAttempts !== undefined) {
|
|
478
|
+
throw new Error(`--repair-attempts is only supported for --experiment ${agentSuccessRatePlugin.metadata.id}.`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
370
481
|
if (contextBudgets !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
|
|
371
482
|
throw new Error(`--context-budgets is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
|
|
372
483
|
}
|
|
@@ -376,8 +487,9 @@ export function parseRunExperimentArgs(argv) {
|
|
|
376
487
|
if (localSubjectConfigPath !== undefined &&
|
|
377
488
|
experimentId !== contextWindowScalingPlugin.metadata.id &&
|
|
378
489
|
experimentId !== retrievalPrecisionRecallPlugin.metadata.id &&
|
|
379
|
-
experimentId !== retrievalQueryStrategyComparisonPlugin.metadata.id
|
|
380
|
-
|
|
490
|
+
experimentId !== retrievalQueryStrategyComparisonPlugin.metadata.id &&
|
|
491
|
+
experimentId !== contextPackGenerationPlugin.metadata.id) {
|
|
492
|
+
throw new Error(`--local-subject-config is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}, ${retrievalPrecisionRecallPlugin.metadata.id}, ${retrievalQueryStrategyComparisonPlugin.metadata.id}, or ${contextPackGenerationPlugin.metadata.id}.`);
|
|
381
493
|
}
|
|
382
494
|
if (experimentId === contextWindowScalingPlugin.metadata.id) {
|
|
383
495
|
const unsupported = [...new Set(seenFlags.filter((flag) => !CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS.includes(flag)))];
|
|
@@ -408,7 +520,8 @@ export function parseRunExperimentArgs(argv) {
|
|
|
408
520
|
throw new Error(`External ${experimentId} targets require --local-subject-config.`);
|
|
409
521
|
}
|
|
410
522
|
}
|
|
411
|
-
|
|
523
|
+
// agent-success-rate accepts --kit-command in real-agent mode; its own validation above owns that rule and message.
|
|
524
|
+
if (kitCommand !== undefined && experimentId !== agentSuccessRatePlugin.metadata.id && !KIT_COMMAND_PLUGIN_IDS.includes(experimentId)) {
|
|
412
525
|
throw new Error(`--kit-command is only supported for --experiment ${KIT_COMMAND_PLUGIN_IDS.join(" or ")}.`);
|
|
413
526
|
}
|
|
414
527
|
if (campaignPreset !== undefined) {
|
|
@@ -445,6 +558,8 @@ export function parseRunExperimentArgs(argv) {
|
|
|
445
558
|
requireAgents,
|
|
446
559
|
includeRealAgents,
|
|
447
560
|
commandTemplates: Object.keys(commandTemplates).length > 0 ? commandTemplates : undefined,
|
|
561
|
+
agentId: agentFlagValues[0],
|
|
562
|
+
repairAttempts,
|
|
448
563
|
kitCommand,
|
|
449
564
|
campaignPreset,
|
|
450
565
|
contextBudgets
|
|
@@ -499,13 +614,86 @@ async function loadPluginInputs(args, toolRoot, context, outputRoot) {
|
|
|
499
614
|
? loadRetrievalPrecisionRecallLocalSubjectInputs(args, toolRoot, context, outputRoot)
|
|
500
615
|
: loadRetrievalPrecisionRecallInputs(args, toolRoot, context);
|
|
501
616
|
}
|
|
617
|
+
if (args.experimentId === contextPackGenerationPlugin.metadata.id) {
|
|
618
|
+
return args.localSubjectConfigPath !== undefined
|
|
619
|
+
? loadContextPackGenerationLocalSubjectInputs(args, toolRoot, context, outputRoot)
|
|
620
|
+
: loadContextPackGenerationInputs(args, toolRoot, context);
|
|
621
|
+
}
|
|
502
622
|
if (args.experimentId === retrievalQueryStrategyComparisonPlugin.metadata.id) {
|
|
503
623
|
return args.localSubjectConfigPath !== undefined
|
|
504
624
|
? loadRetrievalQueryStrategyComparisonLocalSubjectInputs(args, toolRoot, context, outputRoot)
|
|
505
625
|
: loadRetrievalQueryStrategyComparisonInputs(args, toolRoot, context);
|
|
506
626
|
}
|
|
627
|
+
if (args.experimentId === agentSuccessRatePlugin.metadata.id) {
|
|
628
|
+
return loadAgentSuccessRateInputs(args, context);
|
|
629
|
+
}
|
|
507
630
|
return undefined;
|
|
508
631
|
}
|
|
632
|
+
// agent-success-rate: the bundled task catalog and project profiles are located through the established package-resource
|
|
633
|
+
// resolver (never the cwd) and validated by the corpus reader, which also checks the canonical projects on disk. The
|
|
634
|
+
// validated tasks reach the plugin through its existing programmatic input; the plugin applies --case/--benchmark-project
|
|
635
|
+
// selection in catalog order and owns execution, repair, science and artifacts. No task file is accepted from the CLI.
|
|
636
|
+
async function loadAgentSuccessRateInputs(args, context) {
|
|
637
|
+
const validation = agentSuccessRatePlugin.validateConfig(args.config);
|
|
638
|
+
if (!validation.valid || !validation.config) {
|
|
639
|
+
throw new Error(`Invalid agent success rate config: ${validation.errors.join("; ")}`);
|
|
640
|
+
}
|
|
641
|
+
resolvePackageResource(context, AGENT_SUCCESS_TASK_CATALOG_PATH);
|
|
642
|
+
resolvePackageResource(context, AGENT_SUCCESS_PROJECT_PROFILES_PATH);
|
|
643
|
+
const corpus = assertReadAgentSuccessCorpus(context.resourceRoot);
|
|
644
|
+
// Fail on unknown case or project ids before any output directory exists.
|
|
645
|
+
selectAgentSuccessTasks(corpus.tasks, validation.config);
|
|
646
|
+
return { agentSuccessTasks: corpus.tasks };
|
|
647
|
+
}
|
|
648
|
+
// context-pack-generation (bundled/self): the frozen warm-index corpus through the established resource resolver, profile
|
|
649
|
+
// and corpus validation. The plugin applies --case/--benchmark-project selection and owns execution, science and artifacts.
|
|
650
|
+
async function loadContextPackGenerationInputs(args, toolRoot, context) {
|
|
651
|
+
const validation = contextPackGenerationPlugin.validateConfig(args.config);
|
|
652
|
+
if (!validation.valid || !validation.config) {
|
|
653
|
+
throw new Error(`Invalid context pack generation config: ${validation.errors.join("; ")}`);
|
|
654
|
+
}
|
|
655
|
+
const projectProfiles = await readBenchmarkProjectProfiles(resolvePackageResource(context, DEFAULT_PROJECT_PROFILES_RESOURCE), toolRoot);
|
|
656
|
+
const cases = await readEvaluationCases(resolvePackageResource(context, CONTEXT_PACK_GENERATION_CASES_RESOURCE), toolRoot, {
|
|
657
|
+
projectProfiles,
|
|
658
|
+
requireProjectProfileRef: true
|
|
659
|
+
});
|
|
660
|
+
const corpusErrors = validateRetrievalPrecisionRecallCorpus(cases);
|
|
661
|
+
if (corpusErrors.length > 0) {
|
|
662
|
+
throw new Error(`Invalid context-pack-generation corpus: ${corpusErrors.join(" ")}`);
|
|
663
|
+
}
|
|
664
|
+
return { cases };
|
|
665
|
+
}
|
|
666
|
+
// External-local context-pack-generation: same loader and safety order as the other external-local retrieval experiments,
|
|
667
|
+
// with the plugin's own config validation. Ground-truth issues carry logical case ids and counts only.
|
|
668
|
+
async function loadContextPackGenerationLocalSubjectInputs(args, toolRoot, context, outputRoot) {
|
|
669
|
+
const validation = contextPackGenerationPlugin.validateConfig(args.config);
|
|
670
|
+
if (!validation.valid || !validation.config) {
|
|
671
|
+
throw new Error(`Invalid context pack generation config: ${validation.errors.join("; ")}`);
|
|
672
|
+
}
|
|
673
|
+
if (outputRoot === undefined || args.localSubjectConfigPath === undefined) {
|
|
674
|
+
throw new Error("Internal error: external-local runs require a resolved experiment output root and config path.");
|
|
675
|
+
}
|
|
676
|
+
const target = resolveExperimentTarget(args.targetPath, toolRoot);
|
|
677
|
+
if (target.isSelf || target.kind !== "external-local") {
|
|
678
|
+
throw new Error("Local-repository subject mode requires an external-local target; --target must not be the Lab repository.");
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
await assertWorkRootOutsideTarget(outputRoot, target.targetRoot);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
throw new Error("Experiment output root must not be inside the external target project.");
|
|
685
|
+
}
|
|
686
|
+
const configPath = path.resolve(context.invocationCwd, args.localSubjectConfigPath);
|
|
687
|
+
const localSubjectConfig = await readJsonConfigFile("--local-subject-config", configPath, args.localSubjectConfigPath);
|
|
688
|
+
const localSubject = await loadLocalRepositorySubject({ config: localSubjectConfig, repositoryPath: target.targetRoot });
|
|
689
|
+
const groundTruthIssues = summarizeRetrievalGroundTruthIssuesSafely(localSubject.evaluationCases);
|
|
690
|
+
if (groundTruthIssues.length > 0) {
|
|
691
|
+
throw new Error(`Invalid context-pack-generation ground truth for the local subject (${groundTruthIssues
|
|
692
|
+
.map((issue) => `case ${issue.caseId}: ${issue.issueCount} issue${issue.issueCount === 1 ? "" : "s"}`)
|
|
693
|
+
.join("; ")}); details withheld.`);
|
|
694
|
+
}
|
|
695
|
+
return { cases: localSubject.evaluationCases, localSubject, env: process.env };
|
|
696
|
+
}
|
|
509
697
|
// External-local retrieval-query-strategy-comparison: same loader and safety order as retrieval-precision-recall, with
|
|
510
698
|
// the plugin's own config validation. Ground-truth issues carry logical case ids and counts only.
|
|
511
699
|
async function loadRetrievalQueryStrategyComparisonLocalSubjectInputs(args, toolRoot, context, outputRoot) {
|
|
@@ -728,6 +916,12 @@ function readRequiredValue(argv, index, label) {
|
|
|
728
916
|
}
|
|
729
917
|
return value;
|
|
730
918
|
}
|
|
919
|
+
function parseRepairAttempts(value) {
|
|
920
|
+
if (!/^[0-2]$/.test(value)) {
|
|
921
|
+
throw new Error("--repair-attempts must be 0, 1 or 2.");
|
|
922
|
+
}
|
|
923
|
+
return Number(value);
|
|
924
|
+
}
|
|
731
925
|
function parsePositiveInteger(label, value) {
|
|
732
926
|
const parsed = Number(value);
|
|
733
927
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -735,6 +929,15 @@ function parsePositiveInteger(label, value) {
|
|
|
735
929
|
}
|
|
736
930
|
return parsed;
|
|
737
931
|
}
|
|
932
|
+
function agentSuccessRateSummaryLines(result, textPath) {
|
|
933
|
+
const mode = readMetadataString(result.metadata?.executionMode) ?? "deterministic-fixture";
|
|
934
|
+
const provider = readMetadataString(result.metadata?.providerId);
|
|
935
|
+
return [
|
|
936
|
+
`Report Text: ${textPath}`,
|
|
937
|
+
`Execution mode: ${mode}`,
|
|
938
|
+
...(provider ? [`Provider: ${provider} (invoked by explicit --include-real-agents authorization)`] : ["Provider: none (deterministic-fixture; no coding agent was invoked)"])
|
|
939
|
+
];
|
|
940
|
+
}
|
|
738
941
|
function readMetadataString(value) {
|
|
739
942
|
return typeof value === "string" && value.trim() ? value : undefined;
|
|
740
943
|
}
|
|
@@ -20,6 +20,61 @@ export function requestProcessTermination(pid) {
|
|
|
20
20
|
// The process may have exited between the liveness check and this signal.
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Opt-in, awaitable descendant-tree termination for bounded execution
|
|
25
|
+
* (see `terminateProcessTree` in `runMeasuredCommand`). The legacy
|
|
26
|
+
* `forceTerminateProcess` above is intentionally unchanged: it stays
|
|
27
|
+
* fire-and-forget and, off Windows, signals one pid only.
|
|
28
|
+
*
|
|
29
|
+
* Windows: `taskkill /pid <pid> /T /F` (shell:false) is awaited, bounded by
|
|
30
|
+
* `waitTimeoutMs`, so the caller knows the tree was asked to die before it
|
|
31
|
+
* proceeds.
|
|
32
|
+
*
|
|
33
|
+
* Other platforms: the child must have been spawned `detached` so it leads its
|
|
34
|
+
* own process group; the whole group is signalled via the negative pid. If no
|
|
35
|
+
* group exists, the single pid is signalled as a fallback.
|
|
36
|
+
*
|
|
37
|
+
* Never throws: the target may already have exited.
|
|
38
|
+
*/
|
|
39
|
+
export async function terminateProcessTree(pid, options = {}) {
|
|
40
|
+
if (!pid) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const platform = options.platform ?? process.platform;
|
|
44
|
+
if (platform === "win32") {
|
|
45
|
+
await new Promise((resolve) => {
|
|
46
|
+
let timer;
|
|
47
|
+
const finish = () => {
|
|
48
|
+
if (timer)
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolve();
|
|
51
|
+
};
|
|
52
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
53
|
+
shell: false,
|
|
54
|
+
stdio: "ignore",
|
|
55
|
+
windowsHide: true
|
|
56
|
+
});
|
|
57
|
+
timer = setTimeout(finish, options.waitTimeoutMs ?? 5000);
|
|
58
|
+
killer.on("error", finish);
|
|
59
|
+
killer.on("close", finish);
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const signal = options.posixSignal ?? "SIGKILL";
|
|
64
|
+
try {
|
|
65
|
+
process.kill(-pid, signal);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// No process group for this pid (or it already exited); fall back to the single pid.
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, signal);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// The process may have exited between the liveness check and this signal.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
23
78
|
/**
|
|
24
79
|
* Forced cleanup for a process that did not stop on request.
|
|
25
80
|
*
|
|
@@ -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:
|
|
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
|
-
|
|
40
|
-
|
|
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,12 @@ export async function runMeasuredCommand(options) {
|
|
|
47
54
|
try {
|
|
48
55
|
child = spawn(executable, args, {
|
|
49
56
|
cwd: options.cwd,
|
|
50
|
-
env:
|
|
57
|
+
env: childEnv,
|
|
51
58
|
shell: false,
|
|
52
|
-
stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
|
|
59
|
+
stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
60
|
+
// Own process group so a bounded termination can reach descendants. Windows reaches the tree
|
|
61
|
+
// through `taskkill /T`, and `detached` there would open a new console, so it is not set.
|
|
62
|
+
...(options.terminateProcessTree === true && process.platform !== "win32" ? { detached: true } : {})
|
|
53
63
|
});
|
|
54
64
|
}
|
|
55
65
|
catch (error) {
|
|
@@ -66,8 +76,8 @@ export async function runMeasuredCommand(options) {
|
|
|
66
76
|
endedAt,
|
|
67
77
|
durationMs,
|
|
68
78
|
exitCode: null,
|
|
69
|
-
stdout,
|
|
70
|
-
stderr,
|
|
79
|
+
stdout: "",
|
|
80
|
+
stderr: "",
|
|
71
81
|
stdoutPath,
|
|
72
82
|
stderrPath,
|
|
73
83
|
telemetryPath,
|
|
@@ -76,22 +86,53 @@ export async function runMeasuredCommand(options) {
|
|
|
76
86
|
resolvedCommand: resolution
|
|
77
87
|
};
|
|
78
88
|
void Promise.all([
|
|
79
|
-
writeArtifact(stdoutPath,
|
|
80
|
-
writeArtifact(stderrPath,
|
|
89
|
+
writeArtifact(stdoutPath, ""),
|
|
90
|
+
writeArtifact(stderrPath, ""),
|
|
81
91
|
writeArtifact(telemetryPath, JSON.stringify(measured, null, 2))
|
|
82
92
|
]).then(() => resolve(measured));
|
|
83
93
|
return;
|
|
84
94
|
}
|
|
85
95
|
// stdout/stderr are always piped in both stdio branches above.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
// The process-termination step is shared by the timeout and the opt-in output limit. Bounded callers
|
|
97
|
+
// get an awaited tree termination; everyone else keeps the legacy fire-and-forget single termination.
|
|
98
|
+
const KILL_GRACE_MS = 3000;
|
|
99
|
+
const terminate = () => {
|
|
100
|
+
if (options.terminateProcessTree === true) {
|
|
101
|
+
void terminateProcessTree(child.pid);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
forceTerminateProcess(child.pid);
|
|
105
|
+
}
|
|
106
|
+
killGraceTimeout ??= setTimeout(() => {
|
|
107
|
+
void settle(null);
|
|
108
|
+
}, KILL_GRACE_MS);
|
|
109
|
+
};
|
|
110
|
+
const collect = (collector, stream, limit) => (chunk) => {
|
|
111
|
+
if (collector.push(chunk) && limit !== undefined && outputLimit === undefined) {
|
|
112
|
+
outputLimit = { stream, limitBytes: limit };
|
|
113
|
+
spawnError = `Command ${stream} exceeded ${limit} bytes.`;
|
|
114
|
+
terminate();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
child.stdout.on("data", collect(stdoutCollector, "stdout", options.stdoutMaxBytes));
|
|
118
|
+
child.stderr.on("data", collect(stderrCollector, "stderr", options.stderrMaxBytes));
|
|
92
119
|
child.on("error", (error) => {
|
|
93
120
|
spawnError = error.message;
|
|
121
|
+
// A failed spawn in a working directory that exists is not a missing executable; the OS may be refusing the path (for
|
|
122
|
+
// example a very long cwd on Windows). Report the measured length
|
|
123
|
+
// without asserting a limit of our own.
|
|
124
|
+
if (error.code === "ENOENT" && existsSync(options.cwd)) {
|
|
125
|
+
spawnError += ` (the working directory exists but the process could not be started in it; cwd length ${options.cwd.length} characters)`;
|
|
126
|
+
}
|
|
94
127
|
});
|
|
128
|
+
// A failed spawn can also make the OS report the never-connected stdout/stderr pipes as errors (Windows:
|
|
129
|
+
// read ENOTCONN). They are stream events of this one failed process: record the first cause once and let
|
|
130
|
+
// the 'close' event settle the result, instead of letting an unhandled 'error' event end the whole process.
|
|
131
|
+
const recordStreamError = (stream) => (error) => {
|
|
132
|
+
spawnError = spawnError ?? `Failed to read command ${stream}: ${error.message}`;
|
|
133
|
+
};
|
|
134
|
+
child.stdout.on("error", recordStreamError("stdout"));
|
|
135
|
+
child.stderr.on("error", recordStreamError("stderr"));
|
|
95
136
|
if (options.stdinText !== undefined) {
|
|
96
137
|
// A child that exits or closes stdin before we write must not crash the process (EPIPE).
|
|
97
138
|
child.stdin?.on("error", (error) => {
|
|
@@ -106,7 +147,7 @@ export async function runMeasuredCommand(options) {
|
|
|
106
147
|
// intermediate shim (for example a Windows .cmd launcher) can occasionally outlive an
|
|
107
148
|
// already-killed intermediate process. Without a bounded fallback, a caller whose kill silently
|
|
108
149
|
// failed would await this promise forever instead of receiving the already-known timeout error.
|
|
109
|
-
|
|
150
|
+
// (KILL_GRACE_MS and the shared `terminate` step are declared above, beside the output collectors.)
|
|
110
151
|
const settle = async (exitCode) => {
|
|
111
152
|
if (settled)
|
|
112
153
|
return;
|
|
@@ -128,6 +169,8 @@ export async function runMeasuredCommand(options) {
|
|
|
128
169
|
child.unref();
|
|
129
170
|
const endedAt = new Date().toISOString();
|
|
130
171
|
const durationMs = Date.now() - started;
|
|
172
|
+
const stdout = stdoutCollector.toString();
|
|
173
|
+
const stderr = stderrCollector.toString();
|
|
131
174
|
await writeArtifact(stdoutPath, stdout);
|
|
132
175
|
await writeArtifact(stderrPath, stderr);
|
|
133
176
|
const measured = {
|
|
@@ -147,7 +190,8 @@ export async function runMeasuredCommand(options) {
|
|
|
147
190
|
telemetryPath,
|
|
148
191
|
ok: exitCode === 0 && !spawnError && !timedOut,
|
|
149
192
|
error: spawnError,
|
|
150
|
-
resolvedCommand: resolution
|
|
193
|
+
resolvedCommand: resolution,
|
|
194
|
+
...(bounded ? { timedOut, ...(outputLimit ? { outputLimit } : {}) } : {})
|
|
151
195
|
};
|
|
152
196
|
await writeArtifact(telemetryPath, JSON.stringify(measured, null, 2));
|
|
153
197
|
resolve(measured);
|
|
@@ -156,10 +200,7 @@ export async function runMeasuredCommand(options) {
|
|
|
156
200
|
timeout = setTimeout(() => {
|
|
157
201
|
timedOut = true;
|
|
158
202
|
spawnError = `Command timed out after ${options.timeoutMs}ms.`;
|
|
159
|
-
|
|
160
|
-
killGraceTimeout = setTimeout(() => {
|
|
161
|
-
void settle(null);
|
|
162
|
-
}, KILL_GRACE_MS);
|
|
203
|
+
terminate();
|
|
163
204
|
}, options.timeoutMs);
|
|
164
205
|
}
|
|
165
206
|
child.on("close", (exitCode) => {
|
|
@@ -168,6 +209,47 @@ export async function runMeasuredCommand(options) {
|
|
|
168
209
|
});
|
|
169
210
|
return result;
|
|
170
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Accumulates one output stream. Without a limit it reproduces the legacy `text += String(chunk)`
|
|
214
|
+
* behavior exactly. With a limit it keeps raw bytes (so the cut is on a byte boundary), stops at the
|
|
215
|
+
* limit, and reports once when more output than the limit arrived.
|
|
216
|
+
*/
|
|
217
|
+
class OutputCollector {
|
|
218
|
+
limit;
|
|
219
|
+
text = "";
|
|
220
|
+
chunks = [];
|
|
221
|
+
bytes = 0;
|
|
222
|
+
exceeded = false;
|
|
223
|
+
constructor(limit) {
|
|
224
|
+
this.limit = limit;
|
|
225
|
+
}
|
|
226
|
+
/** Returns true exactly once: on the chunk that first exceeds the limit. */
|
|
227
|
+
push(chunk) {
|
|
228
|
+
if (this.limit === undefined) {
|
|
229
|
+
this.text += String(chunk);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (this.exceeded) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
236
|
+
const remaining = this.limit - this.bytes;
|
|
237
|
+
if (buffer.length > remaining) {
|
|
238
|
+
if (remaining > 0) {
|
|
239
|
+
this.chunks.push(buffer.subarray(0, remaining));
|
|
240
|
+
}
|
|
241
|
+
this.bytes = this.limit;
|
|
242
|
+
this.exceeded = true;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
this.chunks.push(buffer);
|
|
246
|
+
this.bytes += buffer.length;
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
toString() {
|
|
250
|
+
return this.limit === undefined ? this.text : Buffer.concat(this.chunks).toString("utf8");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
171
253
|
async function writeArtifact(filePath, value) {
|
|
172
254
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
173
255
|
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
|
+
}
|