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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +4 -3
  3. package/dist/src/agents/adapters/claudeAdapter.js +6 -3
  4. package/dist/src/agents/adapters/codexAdapter.js +51 -10
  5. package/dist/src/agents/index.js +1 -0
  6. package/dist/src/agents/normalizeAgentTelemetry.js +50 -0
  7. package/dist/src/agents/parseAgentTokenUsage.js +71 -49
  8. package/dist/src/agents/providerBuildObservation.js +97 -0
  9. package/dist/src/audits/core/projectInventory.js +1 -0
  10. package/dist/src/cli/help.js +8 -2
  11. package/dist/src/commands/runExperimentDescribeCommand.js +31 -0
  12. package/dist/src/commands/runExperimentRunCommand.js +192 -5
  13. package/dist/src/core/runMeasuredCommand.js +4 -0
  14. package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +3 -1
  15. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpoint.js +216 -0
  16. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +588 -0
  17. package/dist/src/experiments/plugins/agentSuccessRate/campaignEvidenceQuality.js +77 -0
  18. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +314 -0
  19. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +111 -0
  20. package/dist/src/experiments/plugins/agentSuccessRate/campaignProgress.js +173 -0
  21. package/dist/src/experiments/plugins/agentSuccessRate/campaignQueue.js +11 -0
  22. package/dist/src/experiments/plugins/agentSuccessRate/campaignRecovery.js +256 -0
  23. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +207 -0
  24. package/dist/src/experiments/plugins/agentSuccessRate/index.js +6 -0
  25. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +32 -22
  26. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +29 -7
  27. package/dist/src/experiments/plugins/agentSuccessRate/timeoutPresets.js +9 -0
  28. package/dist/src/report/experiments/agentSuccessCampaignProgressReportModel.js +1 -0
  29. package/dist/src/report/experiments/buildAgentSuccessCampaignProgressReport.js +6 -0
  30. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +11 -0
  31. package/dist/src/report/experiments/buildPluginExperimentReport.js +1 -1
  32. package/dist/src/report/experiments/renderAgentSuccessCampaignProgressHtml.js +6 -0
  33. package/dist/src/report/experiments/renderAgentSuccessCampaignProgressText.js +25 -0
  34. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +3 -1
  35. package/dist/src/report/experiments/renderAgentSuccessRateText.js +7 -0
  36. package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +135 -0
  37. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -3
  38. package/docs/METRICS.md +9 -0
  39. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { mkdtemp, readdir, rm } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { normalizeAgentDurationV091, normalizeAgentTokenUsageV091 } from "../../../agents/index.js";
4
5
  import { resolveWithinRoot } from "../../../core/pathSafety.js";
5
6
  import { isSameOrInside, resolvePhysicalPath } from "../../../evaluation/benchmarkSandbox/pathPolicy.js";
6
7
  import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
@@ -52,18 +53,33 @@ function toProviderStatus(status) {
52
53
  case "agent-unavailable":
53
54
  case "agent-limit-reached":
54
55
  case "invalid-output":
56
+ case "not-invoked":
55
57
  return status;
56
58
  default:
57
59
  return "failed";
58
60
  }
59
61
  }
62
+ /** Runtime-only projection. The evaluation pipeline copies named legacy fields, never this envelope. */
63
+ export function projectAgentSuccessProviderTelemetryV091(result, providerStatus, durationMs) {
64
+ const tokenUsage = normalizeAgentTokenUsageV091(result?.tokenUsage ?? { source: "unavailable" }, result?.tokenUsageReliability ?? "unavailable");
65
+ const conflict = result !== null && result.tokenUsage.source !== result.tokenUsageSource;
66
+ if (conflict) {
67
+ // Retain both identities and reliability; disagreement cannot certify either channel.
68
+ tokenUsage.evidenceLevel = "unavailable";
69
+ tokenUsage.classificationReason = "unattributed-source";
70
+ }
71
+ return {
72
+ normalizedTelemetry: { providerStatus, tokenUsage, duration: normalizeAgentDurationV091(durationMs) },
73
+ tokenUsageSourceConflict: conflict ? { usageSource: result.tokenUsage.source, resultSource: result.tokenUsageSource } : null
74
+ };
75
+ }
60
76
  function tokenEvidence(result) {
61
- const finite = (value) => (typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null);
77
+ const normalized = normalizeAgentTokenUsageV091(result.tokenUsage, result.tokenUsageReliability);
62
78
  return {
63
79
  // Only provider-, agent-, CLI- or telemetry-reported totals count; a text-length estimate is not provider usage.
64
- totalTokens: result.tokenUsageSource === "unavailable" || result.tokenUsageSource === "estimated-from-text" ? null : finite(result.tokenUsage.totalTokens),
65
- inputTokens: finite(result.tokenUsage.inputTokens),
66
- outputTokens: finite(result.tokenUsage.outputTokens),
80
+ totalTokens: [result.tokenUsageSource, result.tokenUsage.source].some((source) => source === "unavailable" || source === "estimated-from-text") ? null : normalized.totalTokens,
81
+ inputTokens: normalized.inputTokens,
82
+ outputTokens: normalized.outputTokens,
67
83
  source: result.tokenUsageSource,
68
84
  reliability: result.tokenUsageReliability
69
85
  };
@@ -95,6 +111,7 @@ export function createRealAgentProposalSource(args) {
95
111
  const { settings } = args;
96
112
  const attemptNumber = args.attemptNumber ?? 1;
97
113
  return async ({ treatmentId, invoke, notInvokedReason }) => {
114
+ let runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(null, "not-invoked", null);
98
115
  const context = args.contexts[treatmentId];
99
116
  const task = args.task;
100
117
  const contextArtifactPath = context.text !== null ? agentSuccessContextArtifactPath(task.benchmarkProject, task.caseId, treatmentId) : null;
@@ -128,6 +145,7 @@ export function createRealAgentProposalSource(args) {
128
145
  realAgent.providerStatusReason = boundMessage(reason, settings.privateRoots);
129
146
  return {
130
147
  kind: "no-proposal",
148
+ ...runtimeTelemetry,
131
149
  realAgent,
132
150
  agentDurationMs: measured.agentDurationMs ?? null,
133
151
  agentTokenUsage: measured.agentTokenUsage ?? null,
@@ -138,7 +156,7 @@ export function createRealAgentProposalSource(args) {
138
156
  if (!invoke) {
139
157
  realAgent.context.contextArtifactPath = null;
140
158
  realAgent.providerStatusReason = boundMessage(notInvokedReason ?? "not-invoked", settings.privateRoots);
141
- return { kind: "no-proposal", realAgent, agentDurationMs: null, agentTokenUsage: null, errors: [], contextFile: null };
159
+ return { kind: "no-proposal", ...runtimeTelemetry, realAgent, agentDurationMs: null, agentTokenUsage: null, errors: [], contextFile: null };
142
160
  }
143
161
  if (context.availability === "unavailable" || context.text === null) {
144
162
  return noProposal(`context-unavailable: ${context.reason ?? "unknown"}`, "CONTEXT_UNAVAILABLE", `The ${treatmentId} context could not be built (${context.reason ?? "unknown"}); the provider was not invoked.`);
@@ -186,6 +204,7 @@ export function createRealAgentProposalSource(args) {
186
204
  promptVariant: buildTransportVariant(task, treatmentId),
187
205
  promptText,
188
206
  promptTransport: "stdin",
207
+ trustedExecutable: settings.trustedExecutable,
189
208
  timeoutMs: settings.timeoutMs,
190
209
  requireAvailable: false,
191
210
  env: settings.env,
@@ -218,7 +237,9 @@ export function createRealAgentProposalSource(args) {
218
237
  realAgent.providerStatus = "failed";
219
238
  if (notNeutral || !realAgent.providerInvoked)
220
239
  realAgent.agentArtifactDirectory = null;
221
- return noProposal(notNeutral ? "provider-cwd-not-neutral" : "provider-invocation-error", notNeutral ? "AGENT_CWD_NOT_NEUTRAL" : "PROVIDER_FAILED", notNeutral ? "The provider working directory overlapped a protected root; the provider was not invoked." : `The provider invocation raised an error: ${invocationError ?? "unknown"}`, { agentDurationMs: notNeutral ? null : Date.now() - wallStarted });
240
+ const agentDurationMs = notNeutral ? null : Date.now() - wallStarted;
241
+ runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(null, realAgent.providerStatus, agentDurationMs);
242
+ return noProposal(notNeutral ? "provider-cwd-not-neutral" : "provider-invocation-error", notNeutral ? "AGENT_CWD_NOT_NEUTRAL" : "PROVIDER_FAILED", notNeutral ? "The provider working directory overlapped a protected root; the provider was not invoked." : `The provider invocation raised an error: ${invocationError ?? "unknown"}`, { agentDurationMs });
222
243
  }
223
244
  // Provider health is classified with the answer text withheld: patch content must never be read as a limit/timeout.
224
245
  const classification = classifyAgentRunOutcome({ agentRunResult: { ...result, finalAnswerText: "" } });
@@ -234,6 +255,7 @@ export function createRealAgentProposalSource(args) {
234
255
  };
235
256
  const agentDurationMs = Number.isFinite(result.durationMs) && result.durationMs >= 0 ? result.durationMs : Date.now() - wallStarted;
236
257
  const agentTokenUsage = tokenEvidence(result);
258
+ runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(result, providerStatus, agentDurationMs);
237
259
  if (providerStatus !== "completed") {
238
260
  return noProposal(classification.statusReason, PROVIDER_STATUS_CODES[providerStatus] ?? "PROVIDER_FAILED", `Provider ${settings.providerId} ended with status ${providerStatus}: ${classification.statusReason}`, {
239
261
  agentDurationMs,
@@ -245,6 +267,6 @@ export function createRealAgentProposalSource(args) {
245
267
  }
246
268
  realAgent.finalAnswerAvailable = true;
247
269
  realAgent.providerStatusReason = null;
248
- return { kind: "proposal", text: result.finalAnswerText, realAgent, agentDurationMs, agentTokenUsage, errors: [], contextFile };
270
+ return { kind: "proposal", ...runtimeTelemetry, text: result.finalAnswerText, realAgent, agentDurationMs, agentTokenUsage, errors: [], contextFile };
249
271
  };
250
272
  }
@@ -0,0 +1,9 @@
1
+ export const AGENT_TIMEOUT_PRESETS = Object.freeze({
2
+ codex: Object.freeze({ standard: 240000, extended: 900000, maximum: 1800000 }),
3
+ claude: Object.freeze({ standard: 240000, extended: 900000, maximum: 1800000 })
4
+ });
5
+ export function resolveAgentTimeoutPreset(provider, preset) {
6
+ if (!Object.hasOwn(AGENT_TIMEOUT_PRESETS, provider) || !Object.hasOwn(AGENT_TIMEOUT_PRESETS[provider], preset))
7
+ throw new Error("--timeout-preset must be standard, extended or maximum for codex or claude.");
8
+ return AGENT_TIMEOUT_PRESETS[provider][preset];
9
+ }
@@ -0,0 +1 @@
1
+ export { CAMPAIGN_PROGRESS_SCHEMA, validateCampaignProgress } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
@@ -0,0 +1,6 @@
1
+ import { assertValidatedCampaignProgress } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
2
+ /** All representations copy the same validated revision; no renderer evaluates tasks. */
3
+ export function buildAgentSuccessCampaignProgressReport(snapshot, boundary) {
4
+ assertValidatedCampaignProgress(snapshot, boundary);
5
+ return structuredClone(snapshot.model);
6
+ }
@@ -1,3 +1,4 @@
1
+ import { validateCampaignProgress } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
1
2
  import { attemptEvidenceOf } from "../../experiments/plugins/agentSuccessRate/attemptEvidence.js";
2
3
  import { AGENT_SUCCESS_RATE_PLUGIN_ID, AGENT_SUCCESS_RATE_REAL_AGENT_EXECUTION_MODE, AGENT_SUCCESS_RATE_TREATMENT_IDS } from "../../experiments/plugins/agentSuccessRate/metadata.js";
3
4
  import { AGENT_SUCCESS_RATE_REPORT_SCHEMA_VERSION } from "./agentSuccessRateReportModel.js";
@@ -211,7 +212,17 @@ export function buildAgentSuccessRateReport(run) {
211
212
  })()
212
213
  : null;
213
214
  const overall = total === 0 || evaluable === 0 ? "unavailable" : unavailable === 0 ? "complete" : "partial";
215
+ const progress = run.agentSuccessCampaignProgress;
216
+ let campaignEvidence;
217
+ if (progress !== undefined) {
218
+ validateCampaignProgress(progress);
219
+ if (progress.state !== "complete" || !progress.finalScientificArtifactsAvailable)
220
+ throw new Error("Campaign final report requires complete validated evidence.");
221
+ const { schemaVersion, campaignIdentity, checkpointGeneration, checkpointRevision, state, selectedCaseCount, totalTreatmentUnits, unitCounts, caseCounts, comparisons, finalScientificArtifactsAvailable } = progress;
222
+ campaignEvidence = structuredClone({ schemaVersion, campaignIdentity, checkpointGeneration, checkpointRevision, state, selectedCaseCount, totalTreatmentUnits, unitCounts, caseCounts, comparisons, finalScientificArtifactsAvailable });
223
+ }
214
224
  return {
225
+ ...(campaignEvidence ? { campaignEvidence } : {}),
215
226
  schemaVersion: AGENT_SUCCESS_RATE_REPORT_SCHEMA_VERSION,
216
227
  identity: {
217
228
  runId: run.runId,
@@ -17,7 +17,7 @@ const RETRIEVAL_QUERY_STRATEGY_COMPARISON_BULK_KEYS = ["caseExecutionEvidence",
17
17
  // Same for context-pack-generation: the typed section presents execution evidence and analysis.
18
18
  const CONTEXT_PACK_GENERATION_BULK_KEYS = ["caseExecutionEvidence", "analysis"];
19
19
  // Same for agent-success-rate: the typed section presents execution evidence and analysis.
20
- const AGENT_SUCCESS_RATE_BULK_KEYS = ["caseExecutionEvidence", "analysis"];
20
+ const AGENT_SUCCESS_RATE_BULK_KEYS = ["caseExecutionEvidence", "analysis", "agentSuccessCampaignProgress"];
21
21
  const V043_BULK_ARRAY_KEYS = [
22
22
  "v043StageContextExecutions",
23
23
  "v043StageContextEvaluations",
@@ -0,0 +1,6 @@
1
+ import { renderAgentSuccessCampaignProgressText } from "./renderAgentSuccessCampaignProgressText.js";
2
+ /** A static accessible projection of the same text; every dynamic character is escaped. */
3
+ export function renderAgentSuccessCampaignProgressHtml(model) {
4
+ const text = renderAgentSuccessCampaignProgressText(model).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
5
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Campaign progress</title></head><body><main><h1>Campaign progress and evidence</h1><pre style="white-space:pre-wrap">${text}</pre></main></body></html>\n`;
6
+ }
@@ -0,0 +1,25 @@
1
+ export function renderAgentSuccessCampaignProgressText(m) {
2
+ return [
3
+ "Agent Success Rate Campaign Progress", `Schema: ${m.schemaVersion}`, `Campaign: ${m.campaignIdentity}`, `Execution mode: ${m.executionMode}`,
4
+ `Checkpoint revision: ${m.checkpointGeneration} (${m.checkpointRevision})`, `State: ${m.state}`,
5
+ `Completed evaluations: ${m.unitCounts.completed}/${m.totalTreatmentUnits} treatment units`,
6
+ `Completed cases: ${m.caseCounts.completed}/${m.selectedCaseCount}`,
7
+ ...Object.entries(m.unitCounts).map(([s, n]) => `Treatment state ${s}: ${n}`),
8
+ ...Object.entries(m.caseCounts).map(([s, n]) => `Case state ${s}: ${n}`),
9
+ `Provider limit: ${m.knownProviderLimit}; uncertainty: ${m.knownUncertainty}`,
10
+ `Diagnostics: ${m.diagnostics.join(", ") || "none"}`,
11
+ `Scientific results: ${m.scientificCompleteness}; validated final artifacts: ${m.finalScientificArtifactsAvailable}`,
12
+ `Completed cases: ${m.completedCaseIds.join(", ") || "none"}`, `Unfinished cases: ${m.unfinishedCaseIds.join(", ") || "none"}`,
13
+ ...m.cases.flatMap(c => c.treatments.flatMap(t => [
14
+ `${c.caseId} / ${t.treatmentId}: ${t.state}; evaluated task success=${t.evaluatedTaskSuccess ?? "unavailable"}; epochs=${t.epochCount}; historical limits=${t.historicalProviderLimitCount}; diagnostics=${t.diagnostics.join(", ") || "none"}`,
15
+ ...t.attempts.map(a => ` Attempt ${a.attemptNumber}: provider invoked=${a.providerInvoked}; Lab duration=${a.duration.durationMs ?? "unavailable"} ms (${a.duration.source}); tokens=${a.tokens.totalTokens ?? "unavailable"}; input=${a.tokens.inputTokens ?? "unavailable"}; output=${a.tokens.outputTokens ?? "unavailable"}; source=${a.tokens.source}; reliability=${a.tokens.reliability}; category=${a.tokens.evidenceLevel}; numeric availability=${a.tokens.numericAvailability}; interpretation=${a.tokenInterpretation}`)
16
+ ])),
17
+ "Comparisons (context-pack minus raw-full-file; descriptive matched subset means):",
18
+ ...m.comparisons.flatMap(q => [
19
+ `${q.quantity}: ${q.quality}; basis=${q.basis}; measurement=${q.measurementBasis}; matched=${q.matchedCaseIds.length}/${q.selectedCaseCount}; excluded=${q.excludedCaseIds.length}`,
20
+ ` Matched: ${q.matchedCaseIds.join(", ") || "none"}; excluded: ${q.excludedCaseIds.join(", ") || "none"}`,
21
+ ` Raw mean=${q.rawFullFileMean ?? "unavailable"}; context mean=${q.contextPackMean ?? "unavailable"}; delta=${q.meanDifference ?? "unavailable"}; unit=${q.unit}`,
22
+ ...q.reasons.map((r, i) => ` ${r}: ${q.reasonExplanations[i]}`)
23
+ ]), "Scientific limitations:", ...m.limitations.map(l => `- ${l}`), ""
24
+ ].join("\n");
25
+ }
@@ -1,4 +1,4 @@
1
- import { AGENT_SUCCESS_BASIS_LABEL, formatAgentSuccessMetric, formatAgentSuccessPairs, formatAgentSuccessTotal } from "./renderAgentSuccessRateText.js";
1
+ import { AGENT_SUCCESS_BASIS_LABEL, formatAgentSuccessMetric, formatAgentSuccessPairs, formatAgentSuccessTotal, renderCampaignEvidenceSummary } from "./renderAgentSuccessRateText.js";
2
2
  function escapeHtml(value) {
3
3
  return String(value ?? "")
4
4
  .replaceAll("&", "&amp;")
@@ -22,6 +22,8 @@ export function renderAgentSuccessRateHtml(report) {
22
22
  const realMode = report.identity.executionMode === "real-agent";
23
23
  const m = formatAgentSuccessMetric;
24
24
  const parts = [];
25
+ if (report.campaignEvidence)
26
+ parts.push(`<section><h2>Campaign evidence</h2><pre>${escapeHtml(renderCampaignEvidenceSummary(report))}</pre></section>`);
25
27
  parts.push(`<section id="agent-success-rate">
26
28
  <h2>Agent Success Rate</h2>
27
29
  <h3>Experiment identity</h3>
@@ -1,3 +1,8 @@
1
+ export function renderCampaignEvidenceSummary(report) {
2
+ const m = report.campaignEvidence;
3
+ return m ? [`Campaign evidence: ${m.state}; checkpoint revision ${m.checkpointGeneration} (${m.checkpointRevision}); completed evaluations ${m.unitCounts.completed}/${m.totalTreatmentUnits}; completed cases ${m.caseCounts.completed}/${m.selectedCaseCount}`,
4
+ ...m.comparisons.map(q => `${q.quantity}: ${q.quality}; ${q.basis}; ${q.measurementBasis}; matched ${q.matchedCaseIds.length}/${q.selectedCaseCount}; excluded ${q.excludedCaseIds.length}; raw=${q.rawFullFileMean ?? "unavailable"}; context=${q.contextPackMean ?? "unavailable"}; delta=${q.meanDifference ?? "unavailable"}; unit=${q.unit}; reasons=${q.reasons.join(", ") || "none"}; ${q.reasonExplanations.join(" ")}`)].join("\n") : "";
5
+ }
1
6
  /** One display form for every metric: an absent value is labelled, never rendered as zero. */
2
7
  export function formatAgentSuccessMetric(metric) {
3
8
  if (!metric)
@@ -135,5 +140,7 @@ export function renderAgentSuccessRateTextLines(report) {
135
140
  lines.push(`error ${error.caseId} / ${error.treatmentId} attempt ${error.attemptNumber} ${sanitize(error.code)}: ${sanitize(error.message)}`);
136
141
  for (const artifact of report.artifacts)
137
142
  lines.push(`artifact ${sanitize(artifact.id)}: ${artifact.path ?? "[reference withheld]"}`);
143
+ if (report.campaignEvidence)
144
+ lines.push(renderCampaignEvidenceSummary(report));
138
145
  return lines;
139
146
  }
@@ -0,0 +1,135 @@
1
+ import { lstat, mkdir, open, readdir, realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { campaignAssert, campaignHash, canonicalCampaignJson } from "../../experiments/plugins/agentSuccessRate/campaignIdentity.js";
4
+ import { validateCampaignOutputPath } from "../../experiments/plugins/agentSuccessRate/campaignCheckpointStore.js";
5
+ import { readCampaignProgressScientificFamily } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
6
+ import { agentSuccessRatePlugin, mapAgentSuccessRateToRun } from "../../experiments/plugins/agentSuccessRate/plugin.js";
7
+ import { writePluginExperimentReports } from "./writePluginExperimentReports.js";
8
+ import { buildPluginExperimentReport } from "./buildPluginExperimentReport.js";
9
+ import { renderPluginExperimentReportText } from "./renderPluginExperimentReportText.js";
10
+ import { renderPluginExperimentReportHtml } from "./renderPluginExperimentReportHtml.js";
11
+ import { buildAgentSuccessCampaignProgressReport } from "./buildAgentSuccessCampaignProgressReport.js";
12
+ import { renderAgentSuccessCampaignProgressText } from "./renderAgentSuccessCampaignProgressText.js";
13
+ import { renderAgentSuccessCampaignProgressHtml } from "./renderAgentSuccessCampaignProgressHtml.js";
14
+ async function plain(p, kind) {
15
+ const s = await lstat(p);
16
+ campaignAssert(!s.isSymbolicLink() && (kind === "file" ? s.isFile() : s.isDirectory()) && path.resolve(await realpath(p)).toLowerCase() === path.resolve(p).toLowerCase(), "PROGRESS_UNSAFE_PATH");
17
+ }
18
+ async function writeExclusive(p, content) {
19
+ const h = await open(p, "wx");
20
+ try {
21
+ await h.writeFile(content, "utf8");
22
+ await h.sync();
23
+ }
24
+ finally {
25
+ await h.close();
26
+ }
27
+ }
28
+ async function readExpected(p, expected, code) {
29
+ await plain(p, "file");
30
+ const length = Buffer.byteLength(expected);
31
+ campaignAssert(length <= 64 * 1024 * 1024, "PROGRESS_DATA_BOUND");
32
+ const h = await open(p, "r");
33
+ try {
34
+ const before = await h.stat();
35
+ campaignAssert(before.isFile() && before.size === length, code);
36
+ const buffer = Buffer.alloc(length + 1);
37
+ let size = 0;
38
+ for (;;) {
39
+ const n = await h.read(buffer, size, buffer.length - size, size);
40
+ size += n.bytesRead;
41
+ if (!n.bytesRead || size === buffer.length)
42
+ break;
43
+ }
44
+ campaignAssert(size === length && (await h.stat()).size === length && buffer.subarray(0, size).toString("utf8") === expected, code);
45
+ }
46
+ finally {
47
+ await h.close();
48
+ }
49
+ }
50
+ async function owned(parent, name, owner) {
51
+ const p = path.join(parent, name);
52
+ let created = false;
53
+ try {
54
+ await mkdir(p);
55
+ created = true;
56
+ }
57
+ catch (e) {
58
+ if (e.code !== "EEXIST")
59
+ throw e;
60
+ }
61
+ await plain(p, "directory");
62
+ if (created)
63
+ await writeExclusive(path.join(p, "owner.json"), owner);
64
+ await readExpected(path.join(p, "owner.json"), owner, "PROGRESS_OWNER_MISMATCH");
65
+ return p;
66
+ }
67
+ /** Revision-specific files are exclusive. The manifest is published last and verified before returning success. */
68
+ export async function writeAgentSuccessCampaignProgressReport(args) {
69
+ const model = buildAgentSuccessCampaignProgressReport(args.snapshot, args.boundary);
70
+ await validateCampaignOutputPath(args.boundary);
71
+ const root = path.resolve(args.boundary.outputRoot);
72
+ await plain(root, "directory");
73
+ const family = await owned(root, "campaign-progress-v1", "agent-success-rate-campaign-progress-v1");
74
+ const identity = await owned(family, model.campaignIdentity, `agent-success-rate-campaign-progress-v1:${model.campaignIdentity}`);
75
+ // Ownership blockers can change without a checkpoint transition. Keep both projections immutable at that revision.
76
+ const modelDigest = campaignHash(canonicalCampaignJson(model));
77
+ const revision = `revision-${String(model.checkpointGeneration).padStart(10, "0")}-${model.checkpointRevision}-${modelDigest.slice(0, 16)}`;
78
+ const outDir = path.join(identity, revision);
79
+ const scientific = model.state === "complete" ? await readCampaignProgressScientificFamily(args.snapshot, args.boundary) : null;
80
+ const run = scientific ? mapAgentSuccessRateToRun({ runId: scientific.execution.runId, startedAt: scientific.execution.startedAt, completedAt: scientific.execution.completedAt,
81
+ target: { kind: "self", targetRoot: "[redacted]", toolRoot: "[redacted]", packageName: "@dailephd/my-dev-kit-lab", packageVersion: "0.9.0", hasPackageJson: true, hasLockfile: true, branch: null, commit: null, hasGit: false, isSelf: true },
82
+ caseEvidence: scientific.execution.cases.map(c => ({ ...c, caseName: c.caseId })), analysis: scientific.analysis.analysis, patchArtifactPaths: [],
83
+ providerId: scientific.execution.realAgent?.providerId, ...(scientific.execution.realAgent ? { realSettings: { timeoutMs: scientific.execution.realAgent.timeoutMs, repairAttempts: scientific.execution.realAgent.repairAttempts ?? 0 } } : {}) }) : null;
84
+ if (run)
85
+ Object.assign(run, { agentSuccessCampaignProgress: model });
86
+ const files = [
87
+ { name: "agent-success-rate-campaign-progress.json", content: `${JSON.stringify(model, null, 2)}\n`, phase: "json-written" },
88
+ { name: "agent-success-rate-campaign-progress.txt", content: renderAgentSuccessCampaignProgressText(model), phase: "text-written" },
89
+ { name: "agent-success-rate-campaign-progress.html", content: renderAgentSuccessCampaignProgressHtml(model), phase: "html-written" }
90
+ ];
91
+ if (run) {
92
+ const built = buildPluginExperimentReport({ run, plugin: agentSuccessRatePlugin.metadata, outputRoot: outDir, generatedAt: run.completedAt });
93
+ const report = { ...built, metadata: { ...built.metadata, outputRoot: "[redacted]" } };
94
+ files.push({ name: "report.json", content: `${JSON.stringify({ report, outputPaths: { outDir: "[redacted]", jsonPath: "report.json", htmlPath: "report.html", textPath: "report.txt" } }, null, 2)}\n`, phase: "json-written" }, { name: "report.txt", content: renderPluginExperimentReportText(report), phase: "text-written" }, { name: "report.html", content: renderPluginExperimentReportHtml(report), phase: "html-written" });
95
+ }
96
+ const marker = canonicalCampaignJson({ schemaVersion: "agent-success-rate-campaign-progress-publication-v1", campaignIdentity: model.campaignIdentity, checkpointGeneration: model.checkpointGeneration, checkpointRevision: model.checkpointRevision, modelDigest, files: files.map(f => ({ name: f.name, sha256: campaignHash(f.content), bytes: Buffer.byteLength(f.content) })) });
97
+ let created = false;
98
+ try {
99
+ await mkdir(outDir);
100
+ created = true;
101
+ }
102
+ catch (e) {
103
+ if (e.code !== "EEXIST")
104
+ throw e;
105
+ }
106
+ await plain(outDir, "directory");
107
+ if (created) {
108
+ for (const file of files.slice(0, 3)) {
109
+ await writeExclusive(path.join(outDir, file.name), file.content);
110
+ await args.failureHook?.(file.phase);
111
+ }
112
+ // Exclusive directory reservation owns these ordinary report names; the existing writer remains their owner.
113
+ if (run)
114
+ await writePluginExperimentReports({ run, plugin: agentSuccessRatePlugin.metadata, outputRoot: outDir, generatedAt: run.completedAt, redactOutputRoot: true, exclusive: true });
115
+ for (const f of files)
116
+ await readExpected(path.join(outDir, f.name), f.content, "PROGRESS_SNAPSHOT_INTEGRITY");
117
+ // The legacy writer owns report rendering. Flush its campaign-only output before committing the group marker.
118
+ for (const f of files.slice(3)) {
119
+ const h = await open(path.join(outDir, f.name), "r+");
120
+ try {
121
+ await h.sync();
122
+ }
123
+ finally {
124
+ await h.close();
125
+ }
126
+ }
127
+ await args.failureHook?.("before-manifest");
128
+ await writeExclusive(path.join(outDir, "manifest.json"), marker);
129
+ }
130
+ campaignAssert((await readdir(outDir)).sort().join(",") === [...files.map(f => f.name), "manifest.json"].sort().join(","), "PROGRESS_INCOMPLETE_OR_UNKNOWN_SNAPSHOT");
131
+ await readExpected(path.join(outDir, "manifest.json"), marker, "PROGRESS_SNAPSHOT_COLLISION");
132
+ for (const f of files)
133
+ await readExpected(path.join(outDir, f.name), f.content, "PROGRESS_SNAPSHOT_INTEGRITY");
134
+ return { model, outputPaths: { outDir, jsonPath: path.join(outDir, files[0].name), textPath: path.join(outDir, files[1].name), htmlPath: path.join(outDir, files[2].name), manifestPath: path.join(outDir, "manifest.json") } };
135
+ }
@@ -31,9 +31,10 @@ export async function writePluginExperimentReports(args) {
31
31
  const serializedPaths = args.redactOutputRoot
32
32
  ? { outDir: "[redacted]", jsonPath: "report.json", htmlPath: "report.html", textPath: "report.txt" }
33
33
  : outputPaths;
34
- await writeFile(outputPaths.jsonPath, `${JSON.stringify({ report, outputPaths: serializedPaths }, null, 2)}\n`, "utf8");
35
- await writeFile(outputPaths.htmlPath, renderPluginExperimentReportHtml(report), "utf8");
36
- await writeFile(outputPaths.textPath, renderPluginExperimentReportText(report), "utf8");
34
+ const writeOptions = args.exclusive ? { encoding: "utf8", flag: "wx" } : "utf8";
35
+ await writeFile(outputPaths.jsonPath, `${JSON.stringify({ report, outputPaths: serializedPaths }, null, 2)}\n`, writeOptions);
36
+ await writeFile(outputPaths.htmlPath, renderPluginExperimentReportHtml(report), writeOptions);
37
+ await writeFile(outputPaths.textPath, renderPluginExperimentReportText(report), writeOptions);
37
38
  return { report, outputPaths };
38
39
  }
39
40
  function readString(value) {
package/docs/METRICS.md CHANGED
@@ -938,3 +938,12 @@ Cumulative versus final: durations and tokens accumulate over all provider attem
938
938
  - Paired differences report the mean of the `raw-full-file` values, the mean of the `context-pack` values, and the mean of `context-pack` minus `raw-full-file` over matched cases.
939
939
  - These are **descriptive**. No weighted composite score, ranking, automatic winner, causal claim, or statistical-significance test is produced (`statisticalEffect` is `not-assessed`). The corpus is finite and controlled, so results do not generalize beyond it, and a single run carries no variance estimate.
940
940
  - Provider telemetry can be unavailable, and in deterministic-fixture mode both treatments receive the same reference patch, so those results measure the pipeline only.
941
+
942
+
943
+ ## Campaign progress evidence (v0.9.1 implemented; unreleased)
944
+
945
+ Token evidence levels are `provider-reported`, `cli-reported`, `parsed-from-output`, `unavailable`, and `estimated`. The original telemetry source is retained. Numeric availability is recorded per token field, so partial measurements remain partial and normalization never fills missing fields or invents totals; unattributed OpenTelemetry evidence is unavailable. Duration provenance is `lab-wall-clock` for valid observed elapsed time and `unavailable` for missing or invalid values; campaign comparison time is Lab-measured wall-clock duration, not provider-native time.
946
+
947
+ The plugin-specific progress projection is `my-dev-kit-lab-agent-success-rate-campaign-progress-v1`; the writer publishes immutable revision-specific JSON, HTML, and text snapshots. It accounts for selected cases and treatment units, while execution epochs remain recovery history and are excluded from selected-unit counts. Paused provider-limit and uncertain states remain distinct. Partial progress is not a complete scientific analysis artifact.
948
+
949
+ Evidence quality is metric-specific: task outcomes, token measurements, and duration can have different strong, partial, or unavailable classifications. Strong describes the completeness and provenance of the available evidence; it is not statistical significance. Partial evidence does not mean task failure. Missing token evidence is unavailable, never zero; estimated tokens are not provider usage. Duration is Lab-measured wall-clock time, not provider-native time. Comparisons use matched completed cases and state their denominator and exclusions; the existing paired direction is preserved. These descriptive comparisons establish no causal effect, winner, p-value, or general provider performance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {