@dailephd/my-dev-kit-lab 0.5.1 → 0.6.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 (35) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +23 -8
  3. package/dist/src/agents/adapters/claudeAdapter.js +47 -4
  4. package/dist/src/agents/adapters/codexAdapter.js +64 -5
  5. package/dist/src/cli/help.js +22 -1
  6. package/dist/src/commands/runExperimentDescribeCommand.js +6 -1
  7. package/dist/src/commands/runExperimentRunCommand.js +87 -5
  8. package/dist/src/commands/runWarmIndexCampaignPresentation.js +64 -0
  9. package/dist/src/core/runMeasuredCommand.js +49 -10
  10. package/dist/src/evaluation/indexFreshness.js +199 -0
  11. package/dist/src/evaluation/indexSnapshot.js +393 -0
  12. package/dist/src/evaluation/runMyDevKitRetrieval.js +34 -0
  13. package/dist/src/experiments/plugins/warmIndexReuse/agentEvaluation.js +233 -0
  14. package/dist/src/experiments/plugins/warmIndexReuse/campaignPresets.js +83 -0
  15. package/dist/src/experiments/plugins/warmIndexReuse/config.js +51 -0
  16. package/dist/src/experiments/plugins/warmIndexReuse/execution.js +15 -0
  17. package/dist/src/experiments/plugins/warmIndexReuse/executionArtifact.js +2 -0
  18. package/dist/src/experiments/plugins/warmIndexReuse/fakeAgentEvaluation.js +6 -118
  19. package/dist/src/experiments/plugins/warmIndexReuse/index.js +3 -1
  20. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +35 -11
  21. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +50 -14
  22. package/dist/src/experiments/plugins/warmIndexReuse/realAgentPrompt.js +52 -0
  23. package/dist/src/experiments/plugins/warmIndexReuse/selection.js +35 -0
  24. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +18 -3
  25. package/dist/src/gallery/index.js +1 -0
  26. package/dist/src/gallery/writeGalleryManifest.js +5 -1
  27. package/dist/src/gallery/writeWarmIndexCampaignGallery.js +116 -0
  28. package/dist/src/plots/buildWarmIndexPlotData.js +21 -2
  29. package/dist/src/report/experiments/buildPluginExperimentReport.js +47 -8
  30. package/dist/src/report/experiments/buildWarmIndexReuseReport.js +190 -13
  31. package/dist/src/report/experiments/renderPluginExperimentReportText.js +90 -12
  32. package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +114 -14
  33. package/dist/src/report/experiments/warmIndexReuseReportModel.js +3 -0
  34. package/docs/METRICS.md +39 -2
  35. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import path from "node:path";
2
2
  import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../core/countTokens.js";
3
3
  import { runMeasuredCommand } from "../core/runMeasuredCommand.js";
4
+ import { interpretToolVersionOutput } from "./indexSnapshot.js";
5
+ const VERSION_PROBE_TIMEOUT_MS = 30_000;
4
6
  function parseJsonIfPossible(text) {
5
7
  try {
6
8
  return JSON.parse(text);
@@ -89,6 +91,38 @@ export async function buildMyDevKitIndex(options) {
89
91
  command
90
92
  };
91
93
  }
94
+ /**
95
+ * Runs `<kit-command> --version` once through the shared measured-command runner and returns
96
+ * bounded tool evidence. Kept separate from `buildMyDevKitIndex` so it never counts toward index
97
+ * duration. An unsupported, failing, slow, or empty probe is explicit `unavailable` evidence; it
98
+ * never throws and never fails the caller.
99
+ */
100
+ export async function probeMyDevKitVersion(options) {
101
+ try {
102
+ const command = await runMeasuredCommand({
103
+ commandId: "version",
104
+ commandString: options.kitCommand,
105
+ cwd: process.cwd(),
106
+ outDir: options.commandsDir,
107
+ extraArgs: ["--version"],
108
+ timeoutMs: VERSION_PROBE_TIMEOUT_MS
109
+ });
110
+ return interpretToolVersionOutput({
111
+ ok: command.ok,
112
+ exitCode: command.exitCode,
113
+ stdout: command.stdout,
114
+ error: command.error
115
+ });
116
+ }
117
+ catch (error) {
118
+ return interpretToolVersionOutput({
119
+ ok: false,
120
+ exitCode: null,
121
+ stdout: "",
122
+ error: error instanceof Error ? error.message : String(error)
123
+ });
124
+ }
125
+ }
92
126
  /**
93
127
  * Runs search -> lookup -> slice -> source against an already prepared index. Never invokes
94
128
  * `index`; `durationMs` covers retrieval only.
@@ -0,0 +1,233 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { runAgentPrompt } from "../../../agents/index.js";
5
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
6
+ import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
7
+ import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
8
+ import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
9
+ import { generatePromptVariants } from "../../../prompts/index.js";
10
+ import { buildWarmIndexRealAgentPrompt } from "./realAgentPrompt.js";
11
+ import { taskOutputSegment } from "./selection.js";
12
+ // Classified statuses for which the scorer evaluated an actual agent answer.
13
+ const SCOREABLE_STATUSES = new Set(["completed", "invalid-output"]);
14
+ const SIDE_STRATEGIES = {
15
+ "raw-full-file": "raw-full-file",
16
+ "warm-index-reuse": "my-dev-kit-guided",
17
+ };
18
+ export async function evaluateWarmIndexFakeAgents(options) {
19
+ const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
20
+ const result = [];
21
+ for (const project of options.projects) {
22
+ const tasks = [];
23
+ for (const task of project.tasks) {
24
+ const evaluationCase = casesById.get(task.caseId);
25
+ const run = (variantId) => evaluateFakeSide({
26
+ evaluationCase,
27
+ caseId: task.caseId,
28
+ variantId,
29
+ projectProfiles: options.projectProfiles,
30
+ outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
31
+ cwd: options.cwd,
32
+ env: options.env,
33
+ });
34
+ tasks.push({
35
+ caseId: task.caseId,
36
+ raw: task.rawBaseline ? await run("raw-full-file") : null,
37
+ warm: task.warmRetrieval ? await run("warm-index-reuse") : null,
38
+ });
39
+ }
40
+ result.push({ benchmarkProject: project.benchmarkProject, tasks });
41
+ }
42
+ return result;
43
+ }
44
+ async function evaluateFakeSide(args) {
45
+ const promptStrategy = SIDE_STRATEGIES[args.variantId];
46
+ const base = { variantId: args.variantId, agentId: "fake-agent", promptStrategy };
47
+ try {
48
+ if (!args.evaluationCase) {
49
+ throw new Error(`Evaluation case ${args.caseId} is not available for fake-agent evaluation.`);
50
+ }
51
+ const [promptVariant] = generatePromptVariants({
52
+ cases: [args.evaluationCase],
53
+ projectProfiles: [...args.projectProfiles],
54
+ strategies: [promptStrategy],
55
+ complexityLevels: ["short"],
56
+ });
57
+ if (!promptVariant) {
58
+ throw new Error(`Failed to generate a fake-agent prompt for case ${args.caseId}.`);
59
+ }
60
+ const agentRunResult = await runAgentPrompt({
61
+ runId: `${args.caseId}.fake-agent.${args.variantId}`,
62
+ agentId: "fake-agent",
63
+ promptVariant,
64
+ promptText: promptVariant.promptText,
65
+ cwd: args.cwd,
66
+ outDir: args.outDir,
67
+ env: args.env,
68
+ });
69
+ return buildSideEvidenceFromRun({ base, caseId: args.caseId, outDir: args.outDir, agentRunResult, expectedAnswerKey: promptVariant.expectedAnswerKey });
70
+ }
71
+ catch (error) {
72
+ return buildFailedSideEvidence(base, error);
73
+ }
74
+ }
75
+ /**
76
+ * Real-agent campaign evaluator (v0.5.2 Batch 3): consumes the exact context text the warm-index
77
+ * execution stage already produced for one side. Never calls my-dev-kit, never independently
78
+ * inspects the repository, and never runs a side with no usable context.
79
+ */
80
+ export async function evaluateWarmIndexRealAgentCampaign(options) {
81
+ const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
82
+ const result = [];
83
+ for (const project of options.projects) {
84
+ const tasks = [];
85
+ for (const task of project.tasks) {
86
+ const evaluationCase = casesById.get(task.caseId);
87
+ const rawContext = task.rawBaseline?.contextText;
88
+ const rawEligible = typeof rawContext === "string" && rawContext.trim().length > 0;
89
+ const warmContext = task.warmRetrieval?.contextText;
90
+ const warmEligible = task.warmRetrieval !== undefined &&
91
+ task.warmRetrieval.skipped === false &&
92
+ typeof warmContext === "string" &&
93
+ warmContext.trim().length > 0;
94
+ const runSide = (variantId, contextText) => evaluateRealSide({
95
+ evaluationCase,
96
+ caseId: task.caseId,
97
+ variantId,
98
+ contextText,
99
+ projectProfiles: options.projectProfiles,
100
+ outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
101
+ agentId: options.agentId,
102
+ timeoutMs: options.timeoutMs,
103
+ env: options.env,
104
+ });
105
+ tasks.push({
106
+ caseId: task.caseId,
107
+ raw: rawEligible ? await runSide("raw-full-file", rawContext) : null,
108
+ warm: warmEligible ? await runSide("warm-index-reuse", warmContext) : null,
109
+ });
110
+ }
111
+ result.push({ benchmarkProject: project.benchmarkProject, tasks });
112
+ }
113
+ return result;
114
+ }
115
+ async function evaluateRealSide(args) {
116
+ const promptStrategy = SIDE_STRATEGIES[args.variantId];
117
+ const base = { variantId: args.variantId, agentId: args.agentId, promptStrategy };
118
+ let neutralCwd;
119
+ try {
120
+ if (!args.evaluationCase) {
121
+ throw new Error(`Evaluation case ${args.caseId} is not available for real-agent evaluation.`);
122
+ }
123
+ // Generated only for internal scoring/metadata (expectedAnswerKey, promptComplexityLevel); its
124
+ // promptText is never sent to the provider.
125
+ const [promptVariant] = generatePromptVariants({
126
+ cases: [args.evaluationCase],
127
+ projectProfiles: [...args.projectProfiles],
128
+ strategies: [promptStrategy],
129
+ complexityLevels: ["short"],
130
+ });
131
+ if (!promptVariant) {
132
+ throw new Error(`Failed to generate an internal scoring prompt variant for case ${args.caseId}.`);
133
+ }
134
+ const realPromptText = buildWarmIndexRealAgentPrompt({
135
+ evaluationCase: args.evaluationCase,
136
+ variantId: args.variantId,
137
+ contextText: args.contextText,
138
+ });
139
+ // Neutral working directory: the provider must not run from the repository, the benchmark
140
+ // target, the installed package, or the agent artifact directory.
141
+ neutralCwd = await mkdtemp(path.join(os.tmpdir(), "my-dev-kit-lab-warm-agent-"));
142
+ const agentRunResult = await runAgentPrompt({
143
+ runId: `${args.caseId}.${args.agentId}.${args.variantId}`,
144
+ agentId: args.agentId,
145
+ promptVariant,
146
+ promptText: realPromptText,
147
+ promptTransport: "stdin",
148
+ timeoutMs: args.timeoutMs,
149
+ requireAvailable: false,
150
+ env: args.env,
151
+ cwd: neutralCwd,
152
+ outDir: args.outDir,
153
+ });
154
+ return buildSideEvidenceFromRun({
155
+ base,
156
+ caseId: args.caseId,
157
+ outDir: args.outDir,
158
+ agentRunResult,
159
+ expectedAnswerKey: promptVariant.expectedAnswerKey,
160
+ });
161
+ }
162
+ catch (error) {
163
+ return buildFailedSideEvidence(base, error);
164
+ }
165
+ finally {
166
+ if (neutralCwd) {
167
+ try {
168
+ await rm(neutralCwd, { recursive: true, force: true });
169
+ }
170
+ catch {
171
+ // Best-effort cleanup only: { force: true } suppresses a missing path, but not an
172
+ // EPERM/EBUSY from an OS still holding this directory open -- for example a timed-out
173
+ // provider process whose kill did not (yet) fully release it as its own cwd. A cleanup
174
+ // failure here must never replace or hide the side's already-classified evidence.
175
+ }
176
+ }
177
+ }
178
+ }
179
+ // Shared scoring/classification/evidence assembly for both the fake-agent and real-agent paths.
180
+ function buildSideEvidenceFromRun(args) {
181
+ const { base, caseId, outDir, agentRunResult, expectedAnswerKey } = args;
182
+ const parsedAnswer = parseAgentAnswer({
183
+ text: agentRunResult.finalAnswerText,
184
+ answerKey: expectedAnswerKey,
185
+ tokenUsage: agentRunResult.tokenUsage,
186
+ });
187
+ const classification = classifyAgentRunOutcome({ agentRunResult, parsedAnswer });
188
+ const correctness = scoreCorrectness({
189
+ caseId,
190
+ answerKey: expectedAnswerKey,
191
+ parsedAnswer,
192
+ status: classification.status,
193
+ });
194
+ const scoreable = SCOREABLE_STATUSES.has(classification.status) && Number.isFinite(correctness.correctnessScore);
195
+ const totalTokens = agentRunResult.tokenUsage.totalTokens;
196
+ return {
197
+ ...base,
198
+ status: classification.status,
199
+ correctness: {
200
+ available: scoreable,
201
+ score: scoreable ? correctness.correctnessScore : null,
202
+ passed: scoreable ? correctness.passed : null,
203
+ failureReasons: [...correctness.failureReasons],
204
+ },
205
+ tokenUsage: {
206
+ totalTokens: typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null,
207
+ source: agentRunResult.tokenUsageSource,
208
+ reliability: agentRunResult.tokenUsageReliability,
209
+ },
210
+ durationMs: agentRunResult.durationMs,
211
+ warnings: [...classification.warnings],
212
+ errors: [...classification.errors],
213
+ artifactPaths: {
214
+ promptPath: path.join(outDir, "prompt.txt"),
215
+ agentRunResultPath: path.join(outDir, "agent-run-result.json"),
216
+ stdoutPath: agentRunResult.stdoutPath,
217
+ stderrPath: agentRunResult.stderrPath,
218
+ telemetryPath: agentRunResult.telemetryPath,
219
+ },
220
+ };
221
+ }
222
+ function buildFailedSideEvidence(base, error) {
223
+ return {
224
+ ...base,
225
+ status: "failed",
226
+ correctness: { available: false, score: null, passed: null, failureReasons: [] },
227
+ tokenUsage: { totalTokens: null, source: "unavailable", reliability: "unavailable" },
228
+ durationMs: null,
229
+ warnings: [],
230
+ errors: [error instanceof Error ? error.message : String(error)],
231
+ artifactPaths: {},
232
+ };
233
+ }
@@ -0,0 +1,83 @@
1
+ // v0.5.2 Batch 1 -- frozen warm-index real-agent campaign preset contract.
2
+ //
3
+ // This module owns preset identity, provider assignment, bundled resource paths, the frozen case
4
+ // list per preset, and the default per-agent timeout. It contains no CLI parsing, no report
5
+ // rendering, and no provider execution logic; those remain owned elsewhere.
6
+ export const WARM_INDEX_CAMPAIGN_PRESET_IDS = ["codex-full", "claude-full", "codex-timeout-isolation"];
7
+ const CASES_RESOURCE_PATH = "benchmarks/contracts/warm-index-benchmark-cases.json";
8
+ const PROJECT_PROFILES_RESOURCE_PATH = "benchmarks/contracts/benchmark-project-profiles.json";
9
+ const DEFAULT_TIMEOUT_MS = 240_000;
10
+ // Exact v0.5.1 12-case corpus, in corpus order. Frozen; do not reorder or extend in Batch 1.
11
+ const FULL_CAMPAIGN_CASE_IDS = Object.freeze([
12
+ "warm-medium-import-dedupe",
13
+ "warm-medium-create-project-task",
14
+ "warm-medium-complete-idempotent",
15
+ "warm-medium-composite-filter",
16
+ "warm-medium-project-summary",
17
+ "warm-medium-broad-workflow-map",
18
+ "warm-large-health-label",
19
+ "warm-large-ts-analytics-snapshot",
20
+ "warm-large-ts-leaderboard",
21
+ "warm-large-python-parser-metrics",
22
+ "warm-large-python-pipeline",
23
+ "warm-large-broad-analytics-comparison",
24
+ ]);
25
+ // Exactly the three large/mixed cases covering cross-module, localized, and broad-change.
26
+ const TIMEOUT_ISOLATION_CASE_IDS = Object.freeze([
27
+ "warm-large-health-label",
28
+ "warm-large-ts-leaderboard",
29
+ "warm-large-broad-analytics-comparison",
30
+ ]);
31
+ const WARM_INDEX_CAMPAIGN_PRESETS = Object.freeze({
32
+ "codex-full": Object.freeze({
33
+ id: "codex-full",
34
+ agentId: "codex",
35
+ casesResourcePath: CASES_RESOURCE_PATH,
36
+ projectProfilesResourcePath: PROJECT_PROFILES_RESOURCE_PATH,
37
+ caseIds: FULL_CAMPAIGN_CASE_IDS,
38
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
39
+ }),
40
+ "claude-full": Object.freeze({
41
+ id: "claude-full",
42
+ agentId: "claude",
43
+ casesResourcePath: CASES_RESOURCE_PATH,
44
+ projectProfilesResourcePath: PROJECT_PROFILES_RESOURCE_PATH,
45
+ caseIds: FULL_CAMPAIGN_CASE_IDS,
46
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
47
+ }),
48
+ "codex-timeout-isolation": Object.freeze({
49
+ id: "codex-timeout-isolation",
50
+ agentId: "codex",
51
+ casesResourcePath: CASES_RESOURCE_PATH,
52
+ projectProfilesResourcePath: PROJECT_PROFILES_RESOURCE_PATH,
53
+ caseIds: TIMEOUT_ISOLATION_CASE_IDS,
54
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
55
+ }),
56
+ });
57
+ /** Fails explicitly for any value outside the exact three frozen preset IDs. */
58
+ export function parseWarmIndexCampaignPresetId(value) {
59
+ if (WARM_INDEX_CAMPAIGN_PRESET_IDS.includes(value)) {
60
+ return value;
61
+ }
62
+ throw new Error(`Unknown warm-index campaign preset: ${value}. Supported presets: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}.`);
63
+ }
64
+ export function getWarmIndexCampaignPreset(id) {
65
+ const preset = WARM_INDEX_CAMPAIGN_PRESETS[id];
66
+ if (!preset) {
67
+ throw new Error(`Unknown warm-index campaign preset: ${id}. Supported presets: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}.`);
68
+ }
69
+ return preset;
70
+ }
71
+ /**
72
+ * Resolves the effective per-agent timeout for a campaign: the preset default when no override was
73
+ * requested, or an explicit finite positive integer override.
74
+ */
75
+ export function resolveWarmIndexCampaignTimeoutMs(preset, requestedTimeoutMs) {
76
+ if (requestedTimeoutMs === undefined) {
77
+ return preset.defaultTimeoutMs;
78
+ }
79
+ if (!Number.isFinite(requestedTimeoutMs) || !Number.isInteger(requestedTimeoutMs) || requestedTimeoutMs <= 0) {
80
+ throw new Error(`Campaign timeoutMs must be a finite positive integer; received ${requestedTimeoutMs}.`);
81
+ }
82
+ return requestedTimeoutMs;
83
+ }
@@ -1,4 +1,5 @@
1
1
  import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
2
+ import { WARM_INDEX_CAMPAIGN_PRESET_IDS } from "./campaignPresets.js";
2
3
  export const defaultWarmIndexReuseConfig = {
3
4
  casesPath: "examples/token-savings-cases.json",
4
5
  projectProfilesPath: "benchmarks/contracts/benchmark-project-profiles.json",
@@ -12,6 +13,9 @@ const SUPPORTED_FIELDS = [
12
13
  "kitCommand",
13
14
  "caseIds",
14
15
  "benchmarkProjects",
16
+ "campaignPreset",
17
+ "includeRealAgents",
18
+ "timeoutMs",
15
19
  ];
16
20
  export const warmIndexReuseConfigDefinition = {
17
21
  fields: [
@@ -21,6 +25,21 @@ export const warmIndexReuseConfigDefinition = {
21
25
  { name: "kitCommand", type: "string", description: "my-dev-kit command used for index and retrieval." },
22
26
  { name: "caseIds", type: "array", description: "Evaluation case IDs to include." },
23
27
  { name: "benchmarkProjects", type: "array", description: "Benchmark project IDs to include." },
28
+ {
29
+ name: "campaignPreset",
30
+ type: "string",
31
+ description: `Real-agent campaign preset id: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}. Absent selects the legacy deterministic fake-agent path.`,
32
+ },
33
+ {
34
+ name: "includeRealAgents",
35
+ type: "boolean",
36
+ description: "Required (must be true) together with campaignPreset; rejected without it.",
37
+ },
38
+ {
39
+ name: "timeoutMs",
40
+ type: "number",
41
+ description: "Optional per-agent timeout override (ms) for a campaign; defaults to the preset's 240000 ms. Rejected without campaignPreset.",
42
+ },
24
43
  ],
25
44
  };
26
45
  /**
@@ -51,6 +70,38 @@ export function validateWarmIndexReuseConfig(config) {
51
70
  errors.push(`${field} must be a non-empty array of non-empty strings when provided.`);
52
71
  }
53
72
  }
73
+ // Campaign fields are validated against the RAW supplied object, not the merged/normalized one,
74
+ // so legacy defaults (casesPath/projectProfilesPath) never look like an explicit override.
75
+ const raw = isPlainObject(config) ? config : {};
76
+ const rawCampaignPreset = raw.campaignPreset;
77
+ if (rawCampaignPreset !== undefined) {
78
+ if (typeof rawCampaignPreset !== "string" || !WARM_INDEX_CAMPAIGN_PRESET_IDS.includes(rawCampaignPreset)) {
79
+ errors.push(`campaignPreset must be one of: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}.`);
80
+ }
81
+ if (raw.casesPath !== undefined) {
82
+ errors.push("campaignPreset cannot be combined with an explicit casesPath; the preset owns the production corpus.");
83
+ }
84
+ if (raw.projectProfilesPath !== undefined) {
85
+ errors.push("campaignPreset cannot be combined with an explicit projectProfilesPath; the preset owns the project profiles.");
86
+ }
87
+ if (raw.includeRealAgents !== true) {
88
+ errors.push("campaignPreset requires includeRealAgents to be exactly true.");
89
+ }
90
+ if (raw.timeoutMs !== undefined) {
91
+ const timeoutMs = raw.timeoutMs;
92
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || !Number.isInteger(timeoutMs) || timeoutMs <= 0) {
93
+ errors.push("timeoutMs must be a finite positive integer when campaignPreset is set.");
94
+ }
95
+ }
96
+ }
97
+ else {
98
+ if (raw.includeRealAgents !== undefined) {
99
+ errors.push("includeRealAgents is only supported together with campaignPreset.");
100
+ }
101
+ if (raw.timeoutMs !== undefined) {
102
+ errors.push("timeoutMs is only supported together with campaignPreset.");
103
+ }
104
+ }
54
105
  if (errors.length > 0) {
55
106
  return invalidExperimentConfig(errors);
56
107
  }
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { resolveWithinRoot } from "../../../core/pathSafety.js";
3
+ import { assessIndexFreshness } from "../../../evaluation/indexFreshness.js";
3
4
  import { runMyDevKitRetrievalFromIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
4
5
  import { runRawFullFileBaseline } from "../../../evaluation/runRawFullFileBaseline.js";
5
6
  import { groupWarmIndexCases, taskOutputSegment } from "./selection.js";
@@ -90,8 +91,15 @@ async function executeTask(args) {
90
91
  }
91
92
  let warmRetrieval;
92
93
  let warmStatus = "failed";
94
+ let indexFreshness;
93
95
  if (!args.session) {
94
96
  errors.push(args.setupFailure ?? { side: "warm", code: "warm-index-setup-failed", message: "No warm index session." });
97
+ // No snapshot exists, so freshness is conservatively unknown rather than assumed.
98
+ indexFreshness = await assessIndexFreshness({
99
+ snapshot: null,
100
+ targetRoot: evaluationCase.absoluteTargetRoot,
101
+ sourceRoots: evaluationCase.sourceRoots,
102
+ });
95
103
  }
96
104
  else {
97
105
  try {
@@ -101,6 +109,12 @@ async function executeTask(args) {
101
109
  errors.push({ side: "warm", code: "warm-session-target-mismatch", message: errorMessage(error) });
102
110
  }
103
111
  if (!errors.some((error) => error.side === "warm")) {
112
+ // Observational only: measured immediately before reuse, and retrieval below runs regardless.
113
+ indexFreshness = await assessIndexFreshness({
114
+ snapshot: args.session.indexSnapshot,
115
+ targetRoot: args.session.targetRoot,
116
+ sourceRoots: args.session.sourceRoots,
117
+ });
104
118
  try {
105
119
  // requireKit: false retains subordinate command evidence; a skipped result caused by a
106
120
  // failed command is classified as failed below rather than as an honest skip.
@@ -134,6 +148,7 @@ async function executeTask(args) {
134
148
  warmStatus,
135
149
  rawBaseline,
136
150
  warmRetrieval,
151
+ indexFreshness,
137
152
  warnings,
138
153
  errors,
139
154
  };
@@ -46,6 +46,7 @@ function summarizeTask(task) {
46
46
  commands: warm.commands.map(summarizeCommand),
47
47
  }
48
48
  : null,
49
+ indexFreshness: task.indexFreshness ? structuredClone(task.indexFreshness) : null,
49
50
  warnings: [...task.warnings],
50
51
  errors: task.errors.map((error) => ({ ...error })),
51
52
  };
@@ -62,6 +63,7 @@ export function summarizeProjectExecution(project) {
62
63
  sessionPrepared: project.session !== undefined,
63
64
  buildDurationMs: project.build ? project.build.durationMs : null,
64
65
  indexCommand: project.build ? summarizeCommand(project.build.command) : null,
66
+ indexSnapshot: project.session ? structuredClone(project.session.indexSnapshot) : null,
65
67
  tasks: project.tasks.map(summarizeTask),
66
68
  warnings: [...project.warnings],
67
69
  errors: [...project.errors],
@@ -1,118 +1,6 @@
1
- import path from "node:path";
2
- import { runAgentPrompt } from "../../../agents/index.js";
3
- import { resolveWithinRoot } from "../../../core/pathSafety.js";
4
- import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
5
- import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
6
- import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
7
- import { generatePromptVariants } from "../../../prompts/index.js";
8
- import { taskOutputSegment } from "./selection.js";
9
- // Classified statuses for which the scorer evaluated an actual agent answer.
10
- const SCOREABLE_STATUSES = new Set(["completed", "invalid-output"]);
11
- const SIDE_STRATEGIES = {
12
- "raw-full-file": "raw-full-file",
13
- "warm-index-reuse": "my-dev-kit-guided",
14
- };
15
- export async function evaluateWarmIndexFakeAgents(options) {
16
- const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
17
- const result = [];
18
- for (const project of options.projects) {
19
- const tasks = [];
20
- for (const task of project.tasks) {
21
- const evaluationCase = casesById.get(task.caseId);
22
- const run = (variantId) => evaluateSide({
23
- evaluationCase,
24
- caseId: task.caseId,
25
- variantId,
26
- projectProfiles: options.projectProfiles,
27
- outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
28
- cwd: options.cwd,
29
- env: options.env,
30
- });
31
- tasks.push({
32
- caseId: task.caseId,
33
- raw: task.rawBaseline ? await run("raw-full-file") : null,
34
- warm: task.warmRetrieval ? await run("warm-index-reuse") : null,
35
- });
36
- }
37
- result.push({ benchmarkProject: project.benchmarkProject, tasks });
38
- }
39
- return result;
40
- }
41
- async function evaluateSide(args) {
42
- const promptStrategy = SIDE_STRATEGIES[args.variantId];
43
- const base = { variantId: args.variantId, agentId: "fake-agent", promptStrategy };
44
- try {
45
- if (!args.evaluationCase) {
46
- throw new Error(`Evaluation case ${args.caseId} is not available for fake-agent evaluation.`);
47
- }
48
- const [promptVariant] = generatePromptVariants({
49
- cases: [args.evaluationCase],
50
- projectProfiles: [...args.projectProfiles],
51
- strategies: [promptStrategy],
52
- complexityLevels: ["short"],
53
- });
54
- if (!promptVariant) {
55
- throw new Error(`Failed to generate a fake-agent prompt for case ${args.caseId}.`);
56
- }
57
- const agentRunResult = await runAgentPrompt({
58
- runId: `${args.caseId}.fake-agent.${args.variantId}`,
59
- agentId: "fake-agent",
60
- promptVariant,
61
- promptText: promptVariant.promptText,
62
- cwd: args.cwd,
63
- outDir: args.outDir,
64
- env: args.env,
65
- });
66
- const parsedAnswer = parseAgentAnswer({
67
- text: agentRunResult.finalAnswerText,
68
- answerKey: promptVariant.expectedAnswerKey,
69
- tokenUsage: agentRunResult.tokenUsage,
70
- });
71
- const classification = classifyAgentRunOutcome({ agentRunResult, parsedAnswer });
72
- const correctness = scoreCorrectness({
73
- caseId: args.caseId,
74
- answerKey: promptVariant.expectedAnswerKey,
75
- parsedAnswer,
76
- status: classification.status,
77
- });
78
- const scoreable = SCOREABLE_STATUSES.has(classification.status) && Number.isFinite(correctness.correctnessScore);
79
- const totalTokens = agentRunResult.tokenUsage.totalTokens;
80
- return {
81
- ...base,
82
- status: classification.status,
83
- correctness: {
84
- available: scoreable,
85
- score: scoreable ? correctness.correctnessScore : null,
86
- passed: scoreable ? correctness.passed : null,
87
- failureReasons: [...correctness.failureReasons],
88
- },
89
- tokenUsage: {
90
- totalTokens: typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null,
91
- source: agentRunResult.tokenUsageSource,
92
- reliability: agentRunResult.tokenUsageReliability,
93
- },
94
- durationMs: agentRunResult.durationMs,
95
- warnings: [...classification.warnings],
96
- errors: [...classification.errors],
97
- artifactPaths: {
98
- promptPath: path.join(args.outDir, "prompt.txt"),
99
- agentRunResultPath: path.join(args.outDir, "agent-run-result.json"),
100
- stdoutPath: agentRunResult.stdoutPath,
101
- stderrPath: agentRunResult.stderrPath,
102
- telemetryPath: agentRunResult.telemetryPath,
103
- },
104
- };
105
- }
106
- catch (error) {
107
- return {
108
- ...base,
109
- status: "failed",
110
- correctness: { available: false, score: null, passed: null, failureReasons: [] },
111
- tokenUsage: { totalTokens: null, source: "unavailable", reliability: "unavailable" },
112
- durationMs: null,
113
- warnings: [],
114
- errors: [error instanceof Error ? error.message : String(error)],
115
- artifactPaths: {},
116
- };
117
- }
118
- }
1
+ // v0.5.2 Batch 3 -- thin compatibility re-export. The shared bounded evidence types,
2
+ // scoring/classification pipeline, and both the fake-agent and real-agent campaign evaluators now
3
+ // live in one owner: ./agentEvaluation.ts. This file exists only so existing direct imports (for
4
+ // example src/experiments/plugins/warmIndexReuse/metrics.ts and
5
+ // src/report/experiments/buildWarmIndexReuseReport.ts) and existing tests keep working unchanged.
6
+ export { evaluateWarmIndexFakeAgents, } from "./agentEvaluation.js";
@@ -1,8 +1,10 @@
1
+ export * from "./agentEvaluation.js";
2
+ export * from "./campaignPresets.js";
1
3
  export * from "./config.js";
2
4
  export * from "./execution.js";
3
5
  export * from "./executionArtifact.js";
4
- export * from "./fakeAgentEvaluation.js";
5
6
  export * from "./metrics.js";
6
7
  export * from "./plugin.js";
8
+ export * from "./realAgentPrompt.js";
7
9
  export * from "./selection.js";
8
10
  export * from "./warmIndexSession.js";