@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,5 +1,35 @@
1
- import { WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION, } from "./warmIndexReuseReportModel.js";
1
+ import { MAX_REPORT_FRESHNESS_CHANGES, MAX_REPORT_FRESHNESS_UNRESOLVED, WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION, } from "./warmIndexReuseReportModel.js";
2
2
  const WARM_INDEX_REUSE_PLUGIN_ID = "warm-index-reuse";
3
+ const COMMON_LIMITATIONS = [
4
+ "Context estimated-token counts use the character-based estimate and are not provider billing telemetry.",
5
+ "Correctness is deterministic answer-key scoring, not semantic LLM judging.",
6
+ "Component-duration sums are not equivalent to provider end-to-end latency.",
7
+ "Failed or missing prefix measurements make cumulative metrics unavailable; missing values are never treated as zero.",
8
+ "No composite score, winner, ranking, or break-even task is calculated.",
9
+ ];
10
+ const FAKE_AGENT_LIMITATIONS = [
11
+ "Agent evidence is deterministic simulated fake-agent evidence.",
12
+ "Fake-agent total tokens are simulated harness telemetry, not provider billing telemetry.",
13
+ ];
14
+ const CODEX_CAMPAIGN_LIMITATIONS = [
15
+ "Agent evidence comes from Codex CLI campaign execution over the exact runner-supplied benchmark contexts.",
16
+ "Token totals are CLI-exposed adapter telemetry when available.",
17
+ "Token evidence must be interpreted using tokenUsageSource/tokenUsageReliability.",
18
+ "Context estimates are never substituted for missing agent token totals.",
19
+ ];
20
+ const CLAUDE_CAMPAIGN_LIMITATIONS = [
21
+ "Agent evidence comes from Claude CLI campaign execution over the exact runner-supplied benchmark contexts.",
22
+ "Token totals are used when exposed by the CLI output.",
23
+ "Token totals may be unavailable for some CLI/version/run outcomes.",
24
+ "Unavailable token totals remain unavailable and are never replaced by zero or a context estimate.",
25
+ ];
26
+ const FRESHNESS_LIMITATIONS = [
27
+ "Freshness compares only files proven by the index snapshot to have been indexed. It does not establish that the entire repository is unchanged.",
28
+ "A new file that was not represented in the snapshot is outside the current freshness comparison and is not automatically evidence that the index is stale.",
29
+ "File content SHA-256 is the comparison identity. Modified timestamps are diagnostic metadata only.",
30
+ "Freshness is observational evidence. It does not trigger reindexing, suppress warm retrieval, alter execution status, or alter provider/agent outcome status.",
31
+ "Freshness statuses: fresh means all represented indexed files were completely compared and their content identities still match; stale means a complete comparison confirmed at least one represented indexed file is modified or missing; partially-stale means at least one represented indexed file is confirmed changed but comparison evidence is incomplete; unknown means no confirmed change established staleness but evidence is insufficient to prove freshness. A task without an assessment is reported as not assessed, which is not the same as unknown.",
32
+ ];
3
33
  const COST_MODEL = [
4
34
  "Index construction is a one-time cost per benchmark project; task retrieval is the repeated warm cost.",
5
35
  "Cold start: for the first task in a project, the cumulative warm component duration is the one-time index build plus the first retrieval.",
@@ -8,15 +38,15 @@ const COST_MODEL = [
8
38
  "Raw cumulative duration sums one raw-context construction per task; warm cumulative component duration includes one index build plus each warm retrieval once.",
9
39
  "Cumulative durations are sums of measured components, not wall-clock or end-to-end latency, and are not presented as a speedup.",
10
40
  ];
11
- const LIMITATIONS = [
12
- "Context estimated-token counts use the existing character-based estimate and are not provider billing telemetry.",
13
- "Agent evidence comes from the deterministic fake agent only; real agents such as Codex or Claude are not run by warm-index-reuse in v0.5.0.",
14
- "Fake-agent correctness is deterministic benchmark-harness evidence from the existing answer-key scorer, not a measure of real-model correctness.",
15
- "Fake-agent token totals are simulated harness telemetry, not provider billing telemetry; estimated context tokens are a separate character-based context-size estimate.",
16
- "Component-duration sums are not equivalent to provider end-to-end latency.",
17
- "Failed or missing prefix measurements make cumulative metrics unavailable; missing values are never treated as zero.",
18
- "No composite score, winner, ranking, or break-even task is calculated.",
19
- ];
41
+ const OUTCOME_STATUS_TO_COUNT_KEY = {
42
+ completed: "completed",
43
+ failed: "failed",
44
+ timeout: "timeout",
45
+ "invalid-output": "invalidOutput",
46
+ "agent-unavailable": "agentUnavailable",
47
+ "agent-limit-reached": "agentLimitReached",
48
+ skipped: "skipped",
49
+ };
20
50
  /**
21
51
  * Presents the warm-index metrics already calculated on the run. It performs no amortization,
22
52
  * cumulative, or availability calculation of its own.
@@ -31,6 +61,14 @@ export function buildWarmIndexReuseReport(run) {
31
61
  if (executions.length !== metricProjects.length) {
32
62
  throw new Error("Invalid warm-index report source: project executions and metrics are inconsistent.");
33
63
  }
64
+ const campaignPreset = readString(run.metadata?.campaignPreset);
65
+ const campaignAgentId = readString(run.metadata?.campaignAgentId);
66
+ const campaignTimeoutMs = readNumber(run.metadata?.campaignTimeoutMs);
67
+ if (campaignAgentId !== undefined && campaignAgentId !== "codex" && campaignAgentId !== "claude") {
68
+ throw new Error(`Invalid warm-index report source: unsupported campaign agent id ${campaignAgentId}.`);
69
+ }
70
+ const isCampaign = campaignPreset !== undefined;
71
+ const allSides = [];
34
72
  const projects = executions.map((execution, projectIndex) => {
35
73
  const metrics = metricProjects[projectIndex];
36
74
  if (metrics.benchmarkProject !== execution.benchmarkProject ||
@@ -47,6 +85,9 @@ export function buildWarmIndexReuseReport(run) {
47
85
  taskCount: metrics.tasks.length,
48
86
  tasks: metrics.tasks.map((task, taskIndex) => {
49
87
  const agents = agentProjects?.[projectIndex]?.tasks[taskIndex];
88
+ const rawEvidence = agents?.raw ?? null;
89
+ const warmEvidence = agents?.warm ?? null;
90
+ allSides.push(rawEvidence, warmEvidence);
50
91
  return {
51
92
  taskOrdinal: task.taskOrdinal,
52
93
  caseId: task.caseId,
@@ -54,12 +95,23 @@ export function buildWarmIndexReuseReport(run) {
54
95
  warmStatus: execution.tasks[taskIndex].warmStatus,
55
96
  raw: task.raw,
56
97
  warm: task.warm,
57
- rawAgent: toReportAgent(agents?.raw ?? null),
58
- warmAgent: toReportAgent(agents?.warm ?? null),
98
+ rawAgent: toReportAgent(rawEvidence),
99
+ warmAgent: toReportAgent(warmEvidence),
100
+ indexFreshness: toReportFreshness(execution.tasks[taskIndex].indexFreshness),
59
101
  };
60
102
  }),
61
103
  };
62
104
  });
105
+ const agentIdentity = deriveAgentIdentity({ isCampaign, campaignAgentId, sides: allSides });
106
+ const agentCampaign = isCampaign
107
+ ? buildCampaignSummary({
108
+ presetId: campaignPreset,
109
+ agentId: agentIdentity.id,
110
+ timeoutMs: campaignTimeoutMs ?? 0,
111
+ selectedCaseCount: projects.reduce((sum, project) => sum + project.taskCount, 0),
112
+ sides: allSides,
113
+ })
114
+ : null;
63
115
  const sides = projects.flatMap((project) => project.tasks.flatMap((task) => [task.raw, task.warm]));
64
116
  return {
65
117
  schemaVersion: WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION,
@@ -73,14 +125,133 @@ export function buildWarmIndexReuseReport(run) {
73
125
  agentTotalTokensAvailableCount: sides.filter((side) => side.agentTotalTokens.availability === "available").length,
74
126
  },
75
127
  costModel: [...COST_MODEL],
76
- limitations: [...LIMITATIONS],
128
+ limitations: buildLimitations(agentIdentity),
77
129
  projects,
130
+ agent: agentIdentity,
131
+ agentCampaign,
132
+ indexFreshnessSummary: summarizeFreshness(projects.flatMap((project) => project.tasks)),
133
+ };
134
+ }
135
+ function boundedList(source, limit, map) {
136
+ const items = source.slice(0, limit).map(map);
137
+ return { totalCount: source.length, displayedCount: items.length, omittedCount: source.length - items.length, items };
138
+ }
139
+ /**
140
+ * Presents persisted freshness evidence only: it never recomputes a status, count, or hash and
141
+ * never touches the filesystem. Absent evidence stays null rather than becoming `unknown`.
142
+ */
143
+ function toReportFreshness(freshness) {
144
+ if (!freshness)
145
+ return null;
146
+ return {
147
+ status: freshness.status,
148
+ assessedAt: freshness.assessedAt,
149
+ baselineSnapshotStatus: freshness.baselineSnapshotStatus,
150
+ indexedFileCount: freshness.indexedFileCount,
151
+ comparableFileCount: freshness.comparableFileCount,
152
+ unchangedFileCount: freshness.unchangedFileCount,
153
+ changedFileCount: freshness.changedFileCount,
154
+ missingFileCount: freshness.missingFileCount,
155
+ unresolvedFileCount: freshness.unresolvedFileCount,
156
+ changes: boundedList(freshness.changes, MAX_REPORT_FRESHNESS_CHANGES, (change) => ({ path: change.path, changeType: change.changeType })),
157
+ unresolved: boundedList(freshness.unresolved, MAX_REPORT_FRESHNESS_UNRESOLVED, (entry) => ({
158
+ path: entry.path,
159
+ reasonCode: entry.reasonCode,
160
+ message: entry.message,
161
+ })),
162
+ warnings: [...freshness.warnings],
78
163
  };
79
164
  }
165
+ function summarizeFreshness(tasks) {
166
+ const assessed = tasks.flatMap((task) => (task.indexFreshness ? [task.indexFreshness] : []));
167
+ const count = (status) => assessed.filter((freshness) => freshness.status === status).length;
168
+ return {
169
+ assessedTaskCount: assessed.length,
170
+ unassessedTaskCount: tasks.length - assessed.length,
171
+ freshTaskCount: count("fresh"),
172
+ staleTaskCount: count("stale"),
173
+ partiallyStaleTaskCount: count("partially-stale"),
174
+ unknownTaskCount: count("unknown"),
175
+ };
176
+ }
177
+ function deriveAgentIdentity(args) {
178
+ const nonNullAgentIds = new Set(args.sides.filter((side) => side !== null).map((side) => side.agentId));
179
+ if (!args.isCampaign) {
180
+ if ([...nonNullAgentIds].some((agentId) => agentId !== "fake-agent")) {
181
+ throw new Error(`Invalid warm-index report source: a legacy non-campaign run must not contain non-fake agent evidence, found ${[...nonNullAgentIds].join(", ")}.`);
182
+ }
183
+ return { id: "fake-agent", mode: "deterministic-fake" };
184
+ }
185
+ const campaignAgentId = args.campaignAgentId;
186
+ if (!campaignAgentId) {
187
+ throw new Error("Invalid warm-index report source: a campaign run must carry a campaignAgentId.");
188
+ }
189
+ for (const agentId of nonNullAgentIds) {
190
+ if (agentId !== campaignAgentId) {
191
+ throw new Error(`Invalid warm-index report source: campaign metadata says ${campaignAgentId} but agent evidence contains ${agentId}.`);
192
+ }
193
+ }
194
+ return { id: campaignAgentId, mode: "real-provider" };
195
+ }
196
+ function buildCampaignSummary(args) {
197
+ const scheduledSideCount = args.selectedCaseCount * 2;
198
+ const executedSides = args.sides.filter((side) => side !== null);
199
+ const executedSideCount = executedSides.length;
200
+ const notRunForMissingContextCount = scheduledSideCount - executedSideCount;
201
+ const outcomeCounts = {
202
+ completed: 0,
203
+ failed: 0,
204
+ timeout: 0,
205
+ invalidOutput: 0,
206
+ agentUnavailable: 0,
207
+ agentLimitReached: 0,
208
+ skipped: 0,
209
+ };
210
+ for (const side of executedSides) {
211
+ const key = OUTCOME_STATUS_TO_COUNT_KEY[side.status];
212
+ if (!key) {
213
+ throw new Error(`Invalid warm-index report source: unsupported campaign agent status "${side.status}".`);
214
+ }
215
+ outcomeCounts[key] += 1;
216
+ }
217
+ const tokenAvailableSideCount = executedSides.filter((side) => typeof side.tokenUsage.totalTokens === "number" && Number.isFinite(side.tokenUsage.totalTokens)).length;
218
+ const agentEvidenceStatus = executedSideCount === 0
219
+ ? "unavailable"
220
+ : executedSideCount === scheduledSideCount && outcomeCounts.completed === executedSideCount
221
+ ? "complete"
222
+ : "partial";
223
+ const tokenEvidenceStatus = executedSideCount === 0
224
+ ? "unavailable"
225
+ : tokenAvailableSideCount === 0
226
+ ? "unavailable"
227
+ : tokenAvailableSideCount === executedSideCount
228
+ ? "complete"
229
+ : "partial";
230
+ return {
231
+ presetId: args.presetId,
232
+ agentId: args.agentId,
233
+ timeoutMs: args.timeoutMs,
234
+ selectedCaseCount: args.selectedCaseCount,
235
+ scheduledSideCount,
236
+ executedSideCount,
237
+ notRunForMissingContextCount,
238
+ outcomeCounts,
239
+ agentEvidenceStatus,
240
+ tokenEvidenceStatus,
241
+ };
242
+ }
243
+ function buildLimitations(agent) {
244
+ if (agent.mode === "deterministic-fake") {
245
+ return [...COMMON_LIMITATIONS, ...FAKE_AGENT_LIMITATIONS, ...FRESHNESS_LIMITATIONS];
246
+ }
247
+ const providerLimitations = agent.id === "codex" ? CODEX_CAMPAIGN_LIMITATIONS : CLAUDE_CAMPAIGN_LIMITATIONS;
248
+ return [...COMMON_LIMITATIONS, ...providerLimitations, ...FRESHNESS_LIMITATIONS];
249
+ }
80
250
  function toReportAgent(agent) {
81
251
  if (!agent)
82
252
  return null;
83
253
  return {
254
+ agentId: agent.agentId,
84
255
  status: agent.status,
85
256
  passed: agent.correctness.passed,
86
257
  tokenUsageSource: agent.tokenUsage.source,
@@ -89,3 +260,9 @@ function toReportAgent(agent) {
89
260
  errors: [...agent.errors],
90
261
  };
91
262
  }
263
+ function readString(value) {
264
+ return typeof value === "string" && value ? value : undefined;
265
+ }
266
+ function readNumber(value) {
267
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
268
+ }
@@ -265,10 +265,28 @@ function formatWarmMetric(metric) {
265
265
  }
266
266
  function formatWarmAgent(agent) {
267
267
  if (agent === null)
268
- return "not run (no context evidence for this side)";
268
+ return "not run (no usable context evidence for this side)";
269
269
  const passed = agent.passed === null ? "unavailable" : String(agent.passed);
270
270
  const issues = [...agent.errors, ...agent.warnings].slice(0, 3).join("; ");
271
- return `status ${agent.status}, passed ${passed}, token source ${agent.tokenUsageSource}, reliability ${agent.tokenUsageReliability}${issues ? `, notes: ${issues}` : ""}`;
271
+ return `agent ${agent.agentId}, status ${agent.status}, passed ${passed}, token source ${agent.tokenUsageSource}, reliability ${agent.tokenUsageReliability}${issues ? `, notes: ${issues}` : ""}`;
272
+ }
273
+ function renderCampaignSection(lines, campaign) {
274
+ pushSection(lines, "Real-Agent Campaign");
275
+ lines.push(fieldLine("Campaign Preset", campaign.presetId));
276
+ lines.push(fieldLine("Agent ID", campaign.agentId));
277
+ lines.push(fieldLine("Campaign Timeout", `${campaign.timeoutMs} ms`));
278
+ lines.push(fieldLine("Scheduled Agent Sides", campaign.scheduledSideCount));
279
+ lines.push(fieldLine("Executed Agent Sides", campaign.executedSideCount));
280
+ lines.push(fieldLine("Not Run For Missing Context", campaign.notRunForMissingContextCount));
281
+ lines.push(fieldLine("Agent Evidence Status", campaign.agentEvidenceStatus));
282
+ lines.push(fieldLine("Token Evidence Status", campaign.tokenEvidenceStatus));
283
+ lines.push(fieldLine("Completed Agent Sides", campaign.outcomeCounts.completed));
284
+ lines.push(fieldLine("Failed Agent Sides", campaign.outcomeCounts.failed));
285
+ lines.push(fieldLine("Timeout Agent Sides", campaign.outcomeCounts.timeout));
286
+ lines.push(fieldLine("Invalid-Output Agent Sides", campaign.outcomeCounts.invalidOutput));
287
+ lines.push(fieldLine("Agent-Unavailable Sides", campaign.outcomeCounts.agentUnavailable));
288
+ lines.push(fieldLine("Agent-Limit-Reached Sides", campaign.outcomeCounts.agentLimitReached));
289
+ lines.push(fieldLine("Skipped Agent Sides", campaign.outcomeCounts.skipped));
272
290
  }
273
291
  function renderWarmIndexReuseSection(lines, section) {
274
292
  if (section === null) {
@@ -279,8 +297,14 @@ function renderWarmIndexReuseSection(lines, section) {
279
297
  lines.push(fieldLine("Task Count", section.summary.taskCount));
280
298
  lines.push(fieldLine("Prepared Session Project Count", section.summary.preparedSessionProjectCount));
281
299
  lines.push(fieldLine("Incomplete Or Failed Project Count", section.summary.incompleteProjectCount));
282
- lines.push(fieldLine("Fake-Agent Correctness Available (task sides)", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`));
283
- lines.push(fieldLine("Fake-Agent Token Totals Available (task sides)", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`));
300
+ lines.push(fieldLine("Agent ID", section.agent.id));
301
+ lines.push(fieldLine("Agent Mode", section.agent.mode));
302
+ lines.push(fieldLine("Agent Correctness Available (task sides)", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`));
303
+ lines.push(fieldLine("Agent Token Totals Available (task sides)", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`));
304
+ renderFreshnessSummary(lines, section);
305
+ if (section.agentCampaign) {
306
+ renderCampaignSection(lines, section.agentCampaign);
307
+ }
284
308
  lines.push("Cold Start And Warm Reuse:");
285
309
  pushDashList(lines, section.costModel);
286
310
  lines.push("Limitations:");
@@ -301,6 +325,7 @@ function renderWarmIndexReuseSection(lines, section) {
301
325
  const rows = [
302
326
  ["Raw Status", task.rawStatus],
303
327
  ["Warm Status", task.warmStatus],
328
+ ["Index Freshness Status", task.indexFreshness ? task.indexFreshness.status : "not assessed"],
304
329
  ["Raw Context Characters", formatWarmMetric(task.raw.contextCharacters)],
305
330
  ["Warm Context Characters", formatWarmMetric(task.warm.contextCharacters)],
306
331
  ["Raw Estimated Context Tokens (estimate)", formatWarmMetric(task.raw.contextEstimatedTokens)],
@@ -312,21 +337,74 @@ function renderWarmIndexReuseSection(lines, section) {
312
337
  ["Cumulative Warm Component Duration", formatWarmMetric(task.warm.cumulativeComponentDurationMs)],
313
338
  ["Cumulative Raw Estimated Context Tokens (estimate)", formatWarmMetric(task.raw.cumulativeEstimatedContextTokens)],
314
339
  ["Cumulative Warm Estimated Context Tokens (estimate)", formatWarmMetric(task.warm.cumulativeEstimatedContextTokens)],
315
- ["Raw Fake-Agent Evaluation", formatWarmAgent(task.rawAgent)],
316
- ["Warm Fake-Agent Evaluation", formatWarmAgent(task.warmAgent)],
317
- ["Raw Agent Correctness (fake agent)", formatWarmMetric(task.raw.agentCorrectness)],
318
- ["Warm Agent Correctness (fake agent)", formatWarmMetric(task.warm.agentCorrectness)],
319
- ["Raw Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.raw.agentTotalTokens)],
320
- ["Warm Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.warm.agentTotalTokens)],
321
- ["Cumulative Raw Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.raw.cumulativeAgentTotalTokens)],
322
- ["Cumulative Warm Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.warm.cumulativeAgentTotalTokens)],
340
+ ["Raw Agent Evaluation", formatWarmAgent(task.rawAgent)],
341
+ ["Warm Agent Evaluation", formatWarmAgent(task.warmAgent)],
342
+ ["Raw Agent Correctness", formatWarmMetric(task.raw.agentCorrectness)],
343
+ ["Warm Agent Correctness", formatWarmMetric(task.warm.agentCorrectness)],
344
+ ["Raw Agent Total Tokens", formatWarmMetric(task.raw.agentTotalTokens)],
345
+ ["Warm Agent Total Tokens", formatWarmMetric(task.warm.agentTotalTokens)],
346
+ ["Cumulative Raw Agent Total Tokens", formatWarmMetric(task.raw.cumulativeAgentTotalTokens)],
347
+ ["Cumulative Warm Agent Total Tokens", formatWarmMetric(task.warm.cumulativeAgentTotalTokens)],
323
348
  ];
324
349
  for (const [label, value] of rows) {
325
350
  lines.push(fieldLine(label, value));
326
351
  }
352
+ renderTaskFreshnessDetail(lines, task.indexFreshness);
327
353
  }
328
354
  });
329
355
  }
356
+ function renderFreshnessSummary(lines, section) {
357
+ // Reports serialized before v0.6.0 Batch 3 lack the summary; render them as having no assessment.
358
+ const summary = section.indexFreshnessSummary ?? {
359
+ assessedTaskCount: 0,
360
+ unassessedTaskCount: section.summary.taskCount,
361
+ freshTaskCount: 0,
362
+ staleTaskCount: 0,
363
+ partiallyStaleTaskCount: 0,
364
+ unknownTaskCount: 0,
365
+ };
366
+ pushSection(lines, "Index Freshness Summary");
367
+ lines.push(fieldLine("Assessed Tasks", summary.assessedTaskCount));
368
+ lines.push(fieldLine("Unassessed Tasks", summary.unassessedTaskCount));
369
+ lines.push(fieldLine("Fresh Tasks", summary.freshTaskCount));
370
+ lines.push(fieldLine("Stale Tasks", summary.staleTaskCount));
371
+ lines.push(fieldLine("Partially-Stale Tasks", summary.partiallyStaleTaskCount));
372
+ lines.push(fieldLine("Unknown Tasks", summary.unknownTaskCount));
373
+ }
374
+ function pushFreshnessBoundedNote(lines, list) {
375
+ if (list.omittedCount > 0) {
376
+ lines.push(`Displayed: ${list.displayedCount} of ${list.totalCount}`);
377
+ lines.push(`Omitted: ${list.omittedCount}`);
378
+ }
379
+ }
380
+ /** Renders the persisted freshness counts and bounded evidence; hashes are never printed. */
381
+ function renderTaskFreshnessDetail(lines, freshness) {
382
+ if (!freshness)
383
+ return;
384
+ lines.push(fieldLine("Baseline Snapshot Status", freshness.baselineSnapshotStatus));
385
+ lines.push(fieldLine("Indexed Files", freshness.indexedFileCount));
386
+ lines.push(fieldLine("Comparable Files", freshness.comparableFileCount));
387
+ lines.push(fieldLine("Unchanged Files", freshness.unchangedFileCount));
388
+ lines.push(fieldLine("Modified Files", freshness.changedFileCount));
389
+ lines.push(fieldLine("Missing Files", freshness.missingFileCount));
390
+ lines.push(fieldLine("Unresolved Comparisons", freshness.unresolvedFileCount));
391
+ lines.push("Freshness Changes:");
392
+ pushFreshnessBoundedNote(lines, freshness.changes);
393
+ if (freshness.changes.items.length === 0) {
394
+ lines.push("- none");
395
+ }
396
+ for (const change of freshness.changes.items) {
397
+ lines.push(`- ${sanitizeScalar(change.changeType)} ${sanitizeScalar(change.path)}`);
398
+ }
399
+ lines.push("Freshness Unresolved Evidence:");
400
+ pushFreshnessBoundedNote(lines, freshness.unresolved);
401
+ if (freshness.unresolved.items.length === 0) {
402
+ lines.push("- none");
403
+ }
404
+ for (const entry of freshness.unresolved.items) {
405
+ lines.push(`- ${entry.path === null ? "run-level" : sanitizeScalar(entry.path)} [${sanitizeScalar(entry.reasonCode)}] ${sanitizeScalar(entry.message)}`);
406
+ }
407
+ }
330
408
  export function renderPluginExperimentReportText(report) {
331
409
  const lines = [];
332
410
  pushSection(lines, "Plugin Experiment Report");
@@ -1,3 +1,4 @@
1
+ import { agentLabel } from "../../experiments/plugins/warmIndexReuse/metrics.js";
1
2
  const UNIT_LABELS = {
2
3
  ms: "ms",
3
4
  characters: "chars",
@@ -19,17 +20,108 @@ export function renderWarmIndexReuseHtml(section) {
19
20
  ["Tasks", String(section.summary.taskCount)],
20
21
  ["Projects with a prepared index session", String(section.summary.preparedSessionProjectCount)],
21
22
  ["Incomplete or failed projects", String(section.summary.incompleteProjectCount)],
22
- ["Fake-agent correctness available (task sides)", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`],
23
- ["Fake-agent token totals available (task sides)", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`],
23
+ ["Agent ID", section.agent.id],
24
+ ["Agent mode", section.agent.mode],
25
+ ["Agent correctness available", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`],
26
+ ["Agent token totals available", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`],
24
27
  ])}
28
+ ${renderFreshnessSummary(section.indexFreshnessSummary ?? legacyFreshnessSummary(section.summary.taskCount))}
29
+ ${section.agentCampaign ? renderCampaignSection(section.agentCampaign) : ""}
25
30
  <h3>Cold Start And Warm Reuse</h3>
26
31
  ${list(section.costModel)}
27
32
  <h3>Limitations</h3>
28
33
  ${list(section.limitations)}
29
- ${section.projects.length === 0 ? "<p>No warm-index project evidence was recorded.</p>" : section.projects.map(renderProject).join("\n")}
34
+ ${section.projects.length === 0 ? "<p>No warm-index project evidence was recorded.</p>" : section.projects.map((project, index) => renderProject(project, index, section.agent.id)).join("\n")}
30
35
  </section>`;
31
36
  }
32
- function renderProject(project, index) {
37
+ // Reports serialized before v0.6.0 Batch 3 lack the summary; they are rendered as having no assessment.
38
+ function legacyFreshnessSummary(taskCount) {
39
+ return {
40
+ assessedTaskCount: 0,
41
+ unassessedTaskCount: taskCount,
42
+ freshTaskCount: 0,
43
+ staleTaskCount: 0,
44
+ partiallyStaleTaskCount: 0,
45
+ unknownTaskCount: 0,
46
+ };
47
+ }
48
+ function renderFreshnessSummary(summary) {
49
+ return `<h3>Index Freshness</h3>
50
+ ${table(["Field", "Value"], [
51
+ ["Assessed tasks", String(summary.assessedTaskCount)],
52
+ ["Unassessed tasks", String(summary.unassessedTaskCount)],
53
+ ["Fresh tasks", String(summary.freshTaskCount)],
54
+ ["Stale tasks", String(summary.staleTaskCount)],
55
+ ["Partially-stale tasks", String(summary.partiallyStaleTaskCount)],
56
+ ["Unknown tasks", String(summary.unknownTaskCount)],
57
+ ])}`;
58
+ }
59
+ /** Persisted freshness evidence per task; hashes are deliberately absent from this presentation. */
60
+ function renderProjectFreshness(tasks) {
61
+ const overview = table(["Task", "Freshness", "Baseline", "Indexed", "Comparable", "Unchanged", "Modified", "Missing", "Unresolved"], tasks.map((task) => {
62
+ const freshness = task.indexFreshness;
63
+ const label = `${task.taskOrdinal}. ${task.caseId}`;
64
+ return freshness
65
+ ? [
66
+ label,
67
+ freshness.status,
68
+ freshness.baselineSnapshotStatus,
69
+ String(freshness.indexedFileCount),
70
+ String(freshness.comparableFileCount),
71
+ String(freshness.unchangedFileCount),
72
+ String(freshness.changedFileCount),
73
+ String(freshness.missingFileCount),
74
+ String(freshness.unresolvedFileCount),
75
+ ]
76
+ : [label, "not assessed", "unavailable", "", "", "", "", "", ""];
77
+ }));
78
+ const detailRows = tasks.flatMap((task) => {
79
+ const freshness = task.indexFreshness;
80
+ if (!freshness)
81
+ return [];
82
+ const label = `${task.taskOrdinal}. ${task.caseId}`;
83
+ return [
84
+ ...freshness.changes.items.map((change) => [label, change.changeType, change.path, ""]),
85
+ ...freshness.unresolved.items.map((entry) => [label, "unresolved", entry.path ?? "run-level", `${entry.reasonCode}: ${entry.message}`]),
86
+ ];
87
+ });
88
+ const omitted = tasks.flatMap((task) => {
89
+ const freshness = task.indexFreshness;
90
+ if (!freshness)
91
+ return [];
92
+ return [
93
+ ...(freshness.changes.omittedCount > 0 ? [`Task ${task.taskOrdinal}: ${freshness.changes.omittedCount} of ${freshness.changes.totalCount} changes omitted.`] : []),
94
+ ...(freshness.unresolved.omittedCount > 0
95
+ ? [`Task ${task.taskOrdinal}: ${freshness.unresolved.omittedCount} of ${freshness.unresolved.totalCount} unresolved entries omitted.`]
96
+ : []),
97
+ ];
98
+ });
99
+ return `<h4>Index Freshness</h4>
100
+ ${overview}
101
+ ${detailRows.length === 0 ? "" : table(["Task", "Evidence type", "Path", "Detail"], detailRows)}
102
+ ${omitted.length === 0 ? "" : list(omitted)}`;
103
+ }
104
+ function renderCampaignSection(campaign) {
105
+ return `<h3>Real-Agent Campaign</h3>
106
+ ${table(["Field", "Value"], [
107
+ ["Preset", campaign.presetId],
108
+ ["Agent", campaign.agentId],
109
+ ["Timeout (ms)", String(campaign.timeoutMs)],
110
+ ["Scheduled agent sides", String(campaign.scheduledSideCount)],
111
+ ["Executed agent sides", String(campaign.executedSideCount)],
112
+ ["Not run for missing context", String(campaign.notRunForMissingContextCount)],
113
+ ["Agent evidence status", campaign.agentEvidenceStatus],
114
+ ["Token evidence status", campaign.tokenEvidenceStatus],
115
+ ["Completed agent sides", String(campaign.outcomeCounts.completed)],
116
+ ["Failed agent sides", String(campaign.outcomeCounts.failed)],
117
+ ["Timeout agent sides", String(campaign.outcomeCounts.timeout)],
118
+ ["Invalid-output agent sides", String(campaign.outcomeCounts.invalidOutput)],
119
+ ["Agent-unavailable sides", String(campaign.outcomeCounts.agentUnavailable)],
120
+ ["Agent-limit-reached sides", String(campaign.outcomeCounts.agentLimitReached)],
121
+ ["Skipped agent sides", String(campaign.outcomeCounts.skipped)],
122
+ ])}`;
123
+ }
124
+ function renderProject(project, index, agentId) {
33
125
  const unavailableRows = project.tasks.flatMap((task) => [
34
126
  ["Raw context characters", task.raw.contextCharacters],
35
127
  ["Warm context characters", task.warm.contextCharacters],
@@ -42,12 +134,12 @@ function renderProject(project, index) {
42
134
  ["Cumulative warm component duration", task.warm.cumulativeComponentDurationMs],
43
135
  ["Cumulative raw estimated context tokens", task.raw.cumulativeEstimatedContextTokens],
44
136
  ["Cumulative warm estimated context tokens", task.warm.cumulativeEstimatedContextTokens],
45
- ["Raw fake-agent correctness", task.raw.agentCorrectness],
46
- ["Warm fake-agent correctness", task.warm.agentCorrectness],
47
- ["Raw fake-agent total tokens", task.raw.agentTotalTokens],
48
- ["Warm fake-agent total tokens", task.warm.agentTotalTokens],
49
- ["Cumulative raw fake-agent total tokens", task.raw.cumulativeAgentTotalTokens],
50
- ["Cumulative warm fake-agent total tokens", task.warm.cumulativeAgentTotalTokens],
137
+ ["Raw agent correctness", task.raw.agentCorrectness],
138
+ ["Warm agent correctness", task.warm.agentCorrectness],
139
+ ["Raw agent total tokens", task.raw.agentTotalTokens],
140
+ ["Warm agent total tokens", task.warm.agentTotalTokens],
141
+ ["Cumulative raw agent total tokens", task.raw.cumulativeAgentTotalTokens],
142
+ ["Cumulative warm agent total tokens", task.warm.cumulativeAgentTotalTokens],
51
143
  ]
52
144
  .filter(([, metric]) => metric.availability !== "available")
53
145
  .map(([label, metric]) => [String(task.taskOrdinal), task.caseId, label, metric.availability, metric.reason ?? ""]));
@@ -91,12 +183,13 @@ function renderProject(project, index) {
91
183
  formatMetric(task.raw.cumulativeEstimatedContextTokens),
92
184
  formatMetric(task.warm.cumulativeEstimatedContextTokens),
93
185
  ]))}
186
+ ${renderProjectFreshness(project.tasks)}
94
187
  <p class="muted">Estimated tokens are character-based context-size estimates, not provider token usage.</p>
95
- <p class="muted">Agent columns are deterministic fake-agent evidence; token totals are simulated harness telemetry, not provider billing telemetry.</p>
188
+ <p class="muted">${agentColumnsNote(agentId)}</p>
96
189
  ${table([
97
190
  "Task",
98
- "Raw fake-agent status",
99
- "Warm fake-agent status",
191
+ "Raw agent status",
192
+ "Warm agent status",
100
193
  "Raw correctness",
101
194
  "Warm correctness",
102
195
  "Raw agent tokens",
@@ -120,11 +213,18 @@ function renderProject(project, index) {
120
213
  ]))}
121
214
  ${unavailableRows.length === 0 ? "" : table(["Task", "Case", "Metric", "Availability", "Reason"], unavailableRows)}`;
122
215
  }
216
+ function agentColumnsNote(agentId) {
217
+ if (agentId === "fake-agent") {
218
+ return "Agent columns are deterministic fake-agent evidence; token totals are simulated harness telemetry, not provider billing telemetry.";
219
+ }
220
+ const label = agentLabel(agentId);
221
+ return `Agent columns are ${label} campaign evidence; token totals are provider-reported adapter telemetry when available and are never replaced by a context-size estimate.`;
222
+ }
123
223
  function formatAgent(agent) {
124
224
  if (agent === null)
125
225
  return "not run";
126
226
  const passed = agent.passed === null ? "" : agent.passed ? ", passed" : ", not passed";
127
- return `${agent.status}${passed}${agent.errors[0] ? ` (${agent.errors[0]})` : ""}`;
227
+ return `${agentLabel(agent.agentId)}: ${agent.status}${passed}${agent.errors[0] ? ` (${agent.errors[0]})` : ""}`;
128
228
  }
129
229
  function formatMetric(metric) {
130
230
  return metric.availability === "available" ? `${metric.value} ${UNIT_LABELS[metric.unit]}` : metric.availability;
@@ -1 +1,4 @@
1
1
  export const WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION = "my-dev-kit-lab-warm-index-report-v1";
2
+ /** Report presentation limits; the full persisted lists stay in warm-index-execution.json. */
3
+ export const MAX_REPORT_FRESHNESS_CHANGES = 20;
4
+ export const MAX_REPORT_FRESHNESS_UNRESOLVED = 10;
package/docs/METRICS.md CHANGED
@@ -26,7 +26,7 @@ Each entry below uses the same fields: **Meaning**, **Appears in** (the artifact
26
26
  **Token savings notes:**
27
27
  - A positive token savings value means the my-dev-kit-guided strategy used fewer tokens than raw-full-file for that run pair.
28
28
  - A negative token savings value means my-dev-kit-guided used more tokens. This can happen on small projects where raw-full-file is cheaper.
29
- - Token savings are only computed when both paired runs expose token totals. Claude does not expose token totals. Codex may expose token totals but can produce timeouts or invalid-output runs.
29
+ - Token savings are only computed when both paired runs expose token totals. Claude's token totals depend on that run's actual CLI output and may be unavailable; Codex may expose token totals but can produce timeouts or invalid-output runs. See "Warm-index real-agent campaign metrics" below for the explicit per-run `tokenEvidenceStatus` signal used by the `warm-index-reuse` campaign path.
30
30
  - Token counts in fake-agent runs are estimated using `Math.ceil(characterCount / 4)`. These are context-size estimates, not provider billing totals.
31
31
 
32
32
  **Correctness scoring notes:**
@@ -515,4 +515,41 @@ The plugin evaluates each task side that has context evidence once with the dete
515
515
 
516
516
  **Generic outcome metrics.** Available values also appear as `ExperimentMetric` entries on each outcome, with `variantId`, `caseId`, `unit`, and `description`. Both the `raw-full-file` and `warm-index-reuse` outcomes can carry `context-character-count`, `context-estimated-token-count`, `operation-duration-ms` (raw context construction or warm retrieval), `cumulative-component-duration-ms`, `cumulative-context-estimated-token-count`, `agent-correctness-score`, `agent-total-tokens`, and `cumulative-agent-total-tokens`. The warm outcome also carries `amortized-index-build-duration-ms`. Unavailable metrics are omitted rather than emitted as zero. Run-level metrics are limited to `warm-index-project-count`, `warm-index-task-count`, and `warm-index-session-prepared-project-count`.
517
517
 
518
- **Interpretation.** The report separates the one-time index cost from per-task retrieval cost and shows the fixed cost falling per task as more tasks reuse the index. It calculates no token-savings percentage, duration-reduction percentage, break-even task, composite score, winner, or ranking. Warm-index plots map the same precomputed values: amortized index build duration, raw versus retrieved estimated context tokens, fake-agent correctness, and cumulative fake-agent total tokens. Unavailable values become skipped plot points with their reason.
518
+ **Interpretation.** The report separates the one-time index cost from per-task retrieval cost and shows the fixed cost falling per task as more tasks reuse the index. It calculates no token-savings percentage, duration-reduction percentage, break-even task, composite score, winner, or ranking. Warm-index plots map the same precomputed values: amortized index build duration, raw versus retrieved estimated context tokens, fake-agent correctness (or campaign agent correctness for a `--campaign` run), and cumulative fake-agent total tokens. Unavailable values become skipped plot points with their reason.
519
+
520
+ ### Warm-index real-agent campaign metrics (v0.5.2, released)
521
+
522
+ Available in the installed v0.5.2 CLI. A `--campaign` run replaces the deterministic fake-agent evaluation described above with one real Codex or Claude provider evaluation per task side with context evidence; every direct measurement, derived measurement, and the strict-prefix rule above are unchanged and reused as-is. Only the per-side agent evidence and its rollup are additive.
523
+
524
+ - `presetId`, `agentId`, `timeoutMs`, `selectedCaseCount`
525
+ Meaning: which campaign preset ran, which single provider (`codex` or `claude`) it used, its resolved timeout, and how many cases were selected.
526
+ - `scheduledSideCount`, `executedSideCount`, `notRunForMissingContextCount`
527
+ Meaning: how many task sides were scheduled for real-agent evaluation, how many actually ran, and how many were not run because that side had no context evidence (for example, a warm side with no prepared index).
528
+ - `outcomeCounts` (`completed`, `failed`, `timeout`, `invalidOutput`, `agentUnavailable`, `agentLimitReached`, `skipped`)
529
+ Meaning: exact counts of each real-agent outcome across executed sides, from `classifyAgentRunOutcome`'s status mapping. None of these are folded into a single pass/fail count.
530
+ - `agentEvidenceStatus` (`complete`, `partial`, `unavailable`)
531
+ Meaning: `complete` when every scheduled side executed and every executed side reported `completed`; `unavailable` when no side executed; `partial` otherwise.
532
+ Interpretation: this is infrastructure/provider evidence completeness, not correctness. A campaign can report `agentEvidenceStatus: "partial"` while the underlying index/execution infrastructure completed normally — infrastructure status and provider status are reported separately, never conflated.
533
+ - `tokenEvidenceStatus` (`complete`, `partial`, `unavailable`)
534
+ Meaning: `complete` when every executed side reports a finite total-token count; `unavailable` when no executed side does; `partial` otherwise.
535
+ Caveat: Claude and Codex token exposure depends on that provider's actual CLI output for the run; token evidence is reported as `unavailable`/`partial` explicitly rather than assumed. No fixed claim that either provider always or never exposes a token total is made — `tokenEvidenceStatus` is the authoritative, per-run signal.
536
+
537
+ No token-savings percentage, break-even task, winner, ranking, or provider billing metric is calculated for a campaign run, matching the fake-agent path above.
538
+
539
+ ### Index freshness evidence (v0.6.0)
540
+
541
+ Status: released in v0.6.0 and present in the current package. The freshness values below are report evidence, not metric formulas: no `ExperimentMetric` ID, plot, or gallery item is added for them.
542
+
543
+ Before each task's warm retrieval, the warm-index run compares the files the index snapshot proves were indexed against their current state and persists one assessment per task (`indexFreshness` in `warm-index-execution.json`). The report only presents that persisted assessment; it never re-reads the filesystem, re-hashes files, or invokes my-dev-kit.
544
+
545
+ - **Snapshot-scoped comparison.** Only files listed by the index snapshot are compared. Freshness does not establish that the whole repository is unchanged.
546
+ - **SHA-256 is the authority.** A file is modified when its current SHA-256 differs from the snapshot's, or missing when it no longer exists. A changed modified time with identical content is not a change; timestamps are diagnostic metadata only.
547
+ - **New and non-indexed files.** A file that was not represented in the snapshot is outside the comparison and is not, by itself, evidence that the index is stale. Lab does not guess which files my-dev-kit would index.
548
+ - **Four statuses.**
549
+ - `fresh`: the snapshot was complete, every represented file was compared, and every content identity still matches.
550
+ - `stale`: a complete comparison confirmed at least one represented file is modified or missing.
551
+ - `partially-stale`: at least one represented file is confirmed changed, but comparison evidence is incomplete (a partial snapshot or unresolved comparisons).
552
+ - `unknown`: no confirmed change established staleness, but evidence is insufficient to prove freshness (an unavailable or partial snapshot, or an unresolved comparison). It is never treated as `fresh`.
553
+ - **`unknown` versus not assessed.** `unknown` means an assessment ran and could not prove freshness. A task with no assessment (for example a report from before v0.6.0) is shown as `not assessed` and counted as unassessed, never as `unknown`.
554
+ - **Report-level counts.** `report.warmIndexReuse.indexFreshnessSummary` holds direct presentation counts of assessed, unassessed, `fresh`, `stale`, `partially-stale`, and `unknown` tasks (assessed equals the sum of the four statuses). They are counts, not `ExperimentMetric` formulas: no percentage, score, or ranking is calculated. Each task's `indexFreshness` lists at most 20 changed files and 10 unresolved comparisons, with the persisted totals and the omitted count; hashes stay in `warm-index-execution.json`.
555
+ - **Observational only.** Freshness does not trigger reindexing, suppress or alter warm retrieval, or change execution status, correctness, provider or agent outcome status, or token evidence. v0.6.0 makes no reindex recommendation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {