@dailephd/my-dev-kit-lab 0.9.2 → 0.9.3

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 (42) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/README.md +7 -5
  3. package/dist/src/agents/adapters/codexAdapter.js +4 -1
  4. package/dist/src/cli/help.js +38 -2
  5. package/dist/src/cli/runLabCli.js +9 -1
  6. package/dist/src/commands/buildGalleryCommand.js +16 -3
  7. package/dist/src/commands/runComparePromptsCommand.js +109 -0
  8. package/dist/src/commands/runExperimentRunCommand.js +47 -2
  9. package/dist/src/core/processTree.js +41 -17
  10. package/dist/src/core/runMeasuredCommand.js +14 -8
  11. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +23 -1
  12. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
  13. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
  14. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
  15. package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
  16. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
  17. package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
  18. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
  19. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
  20. package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
  21. package/dist/src/gallery/galleryPathSafety.js +111 -0
  22. package/dist/src/gallery/readGallerySources.js +542 -0
  23. package/dist/src/gallery/writeGalleryManifest.js +57 -3
  24. package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
  25. package/dist/src/report/experiments/htmlSafety.js +32 -0
  26. package/dist/src/report/experiments/metricEvidence.js +389 -0
  27. package/dist/src/report/experiments/metricGlossary.js +227 -0
  28. package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
  29. package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
  30. package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
  31. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
  32. package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
  33. package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
  34. package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
  35. package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
  36. package/dist/src/report/experiments/specializedReportSections.js +142 -0
  37. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
  38. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
  39. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
  40. package/dist/src/securityValidation/commandRunner.js +13 -6
  41. package/docs/METRICS.md +11 -1
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.9.3] - 2026-10-16
6
+
7
+ - Improved security-validator runtime evidence and secret-scan accuracy while retaining bounded proof and candidate-versus-confirmed finding distinctions.
8
+ - Added configurable Codex and Claude real-agent prompt contracts, strict structured-diff validation, bounded prompt modes, and persisted campaign prompt identity. Provider enforcement and effectiveness remain unverified without authorized live-provider evidence.
9
+ - Added a generic report-section registry, static HTML metric evidence and caveats, generalized gallery source readers, read-only tutorial-manifest consumption, and `report compare-prompts` artifacts.
10
+ - Added evidence-based glossary descriptors and stable links for registered metrics, with explicit unresolved-metadata behavior for unknown IDs; text and HTML reports share reliability caveats. Prompt comparison validates persisted checkpoint/task/prompt evidence, fails closed for unverified V1 identity, and excludes status-only invalid-output claims. Real-provider invalid-output improvement remains NOT_VERIFIED; no paid smoke was run. The v0.9.3 acceptance clarification requires validated prompt/report evidence contracts, while any observed live-provider effectiveness is a separate authorized study, not a claimed release outcome.
6
11
 
7
12
  ## [0.9.2] - 2026-10-13
8
13
 
package/README.md CHANGED
@@ -6,20 +6,22 @@ my-dev-kit provides local repository indexing and graph-guided retrieval. my-dev
6
6
 
7
7
  Whole-ecosystem workflows are centralized in [my-dev-kit/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md). Its [command-surface compatibility map](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md#915-command-surface-compatibility-map) documents which Lab outputs can safely seed my-dev-kit investigation, Observer reference work, or Orchestrator evidence, and which look-alike artifacts are not direct inputs.
8
8
 
9
- The current release-document and package-metadata version is v0.9.2. It corrects external npm package entrypoint validation, preserves strict Lab self-package validation, eliminates nine inherited false positives, corrects Semgrep failed/incomplete-scan evidence, and includes campaign filesystem identity, path-safety, and cross-platform corrections; security verdict strictness is unchanged. v0.9.1 (normalized provider telemetry and resumable agent-success-rate campaigns) is the previous published release. Publication is established separately through registry evidence. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for current state.
9
+ Version v0.9.3 adds hardened prompt contracts and output validation, extensible reports with metric evidence, broader gallery inputs, and prompt-template comparison. Security validation and verdict strictness remain unchanged. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for current state.
10
10
 
11
11
  v0.9.0 adds the `agent-success-rate` plugin, the eighth registered experiment plugin, which evaluates implementation patches under the `raw-full-file` and `context-pack` treatments.
12
12
 
13
- v0.9.1 is the previous published package release.
13
+ v0.9.2 is the previous package release.
14
14
 
15
- The v0.9.2 release corrects external npm package entrypoint validation, preserves strict Lab self-package validation, eliminates nine inherited false positives, corrects Semgrep failed/incomplete-scan evidence, and includes campaign filesystem identity, path-safety, and cross-platform corrections. Security verdict strictness is unchanged. Readiness was accepted for the exact implementation SHA, and the release candidate passed its release-PR validation. The current release-document state is final; actual npm publication is established separately through registry evidence.
15
+ The v0.9.2 release corrected external npm package entrypoint validation, preserved strict Lab self-package validation, eliminated nine inherited false positives, corrected Semgrep failed/incomplete-scan evidence, and included campaign filesystem identity, path-safety, and cross-platform corrections. Security verdict strictness is unchanged. The v0.9.2 release-document state is final; actual npm publication is established separately through registry evidence.
16
16
 
17
- The original prompt/report/gallery feature milestone remains planned for v0.9.3. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for implementation-stage evidence and lifecycle state.
17
+ The v0.9.3 release adds configurable Codex/Claude prompt contracts, strict structured-diff output handling, generic report sections, HTML metric evidence and caveats, source-based gallery items including tutorial manifests, and prompt-template comparison. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for release and validation details.
18
18
 
19
19
  v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--version`, `security validate`, `audit`, the `experiment` family, `report render`, `plots generate`, `gallery build`, `demo final`, and the historical direct final-demo invocation form), a writable lab workspace model kept separate from the installed package and the inspected target, and a permanent packed-tarball installation/execution acceptance gate (`npm run verify:packed-package`). See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for status detail and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime path model. The "Installed CLI" section below documents the shipped command surface; the source-checkout `npm run` workflow in Quickstart remains available for contributors.
20
20
 
21
21
  ## Current capabilities
22
22
 
23
+ Package metadata is `0.9.3`. The v0.9.3 release adds configurable Codex/Claude prompt contracts, strict structured-diff output handling, generic report sections, HTML metric evidence and caveats, source-based gallery items including tutorial manifests, and prompt-template comparison. Registered metrics have evidence-based glossary definitions and links, and prompt comparison validates persisted identity and checkpoint evidence. Real-provider improvement remains NOT_VERIFIED because no paid smoke was authorized; its measurement is a separate opt-in empirical objective, not a v0.9.3 release gate. Bounded tool-use remains prompt guidance, not provider enforcement. See [Current State](docs/CURRENT_STATE.md) and [the roadmap](docs/ROADMAP.md).
24
+
23
25
  - **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
24
26
  - **Measure warm-index reuse (v0.5.0–v0.5.1, released):** the `warm-index-reuse` experiment plugin builds one my-dev-kit index per benchmark project, reuses it across that project's tasks, and compares every task with a matched `raw-full-file` baseline. Reports separate one-time index-build cost from per-task retrieval cost, show amortized index cost and cumulative measurements, and include deterministic fake-agent correctness and token evidence; `plots generate` renders four warm-index SVG charts. The v0.5.1 release adds a dedicated 12-task benchmark corpus (`benchmarks/contracts/warm-index-benchmark-cases.json`: six medium and six large/mixed tasks tagged `localized`, `cross-module`, or `broad-change`) for comparing warm-index behavior as task count grows. Estimated context tokens (a character-based context-size estimate) and fake-agent total tokens (simulated harness telemetry) are reported separately, and neither is provider billing telemetry.
25
27
  - **Run selectable real-agent warm-index campaigns (v0.5.2, released):** `--campaign codex-full`, `claude-full`, or `codex-timeout-isolation` (with `--include-real-agents`) runs exactly one provider — Codex or Claude — against the exact raw and warm contexts the experiment already measured, over the frozen production corpus each preset owns. Provider outcomes (`completed`, `failed`, `timeout`, `invalid-output`, `agent-unavailable`, `agent-limit-reached`, `skipped`) are classified and reported separately from warm-index infrastructure status, so a campaign can complete its indexing/retrieval work even when provider evidence is partial or unavailable. A campaign run automatically produces the report, the same four warm-index SVG charts, and a three-item gallery (report, plots, bounded execution evidence). Its report screenshot is best-effort presentation evidence: unavailable browser runtime or failed capture is reported explicitly, no PNG is fabricated, and neither outcome fails an otherwise successful campaign. Legacy runs without `--campaign` keep the existing deterministic fake-agent, report-only behavior unchanged. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment) and [docs/METRICS.md](docs/METRICS.md#warm-index-reuse-metrics).
@@ -288,7 +290,7 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
288
290
 
289
291
  `npm run security:validate` (contributor/source-checkout) and `my-dev-kit-lab security validate` (installed CLI) both call the same standalone security-validation command owner. It checks local CLI/package boundaries and can inspect another local project with `--target <path>`. The generic audit command can reuse those results through `--types security`, but it does not replace the standalone validator or its reports.
290
292
 
291
- **External npm package validation (v0.9.2 release documentation):** the external-target package-content correction keeps Lab self-validation's strict required inventory and checks external packages against declared entrypoints. Missing entrypoints, invalid or incomplete metadata, failed package evidence, and forbidden contents remain blockers. The nine inherited Lab-only Orchestrator false positives are eliminated. The prompt/report/gallery feature scope is preserved as planned v0.9.3 work. Earlier failed readiness attempts remain historical and are superseded by the accepted readiness result for the exact validated SHA; see [CURRENT_STATE.md](docs/CURRENT_STATE.md).
293
+ **External npm package validation (v0.9.2 release documentation):** the external-target package-content correction keeps Lab self-validation's strict required inventory and checks external packages against declared entrypoints. Missing entrypoints, invalid or incomplete metadata, failed package evidence, and forbidden contents remain blockers. The nine inherited Lab-only Orchestrator false positives are eliminated. The prompt/report/gallery feature scope is included in v0.9.3. Earlier failed readiness attempts remain historical and are superseded by the accepted readiness result for the exact validated SHA; see [CURRENT_STATE.md](docs/CURRENT_STATE.md).
292
294
 
293
295
  Android validation uses `--profile android`. Its default path is static and non-destructive: it starts zero Gradle processes, zero external tools, and zero network operations. Only confirmed `SecurityFinding` records can become audit issues; Android `CandidateEvidence` remains review-only evidence.
294
296
 
@@ -148,7 +148,10 @@ export async function runCliAgent(request, adapter) {
148
148
  env: request.env,
149
149
  timeoutMs: request.timeoutMs,
150
150
  stdinText: command.stdinText,
151
- ...(launch ? { stdoutMaxBytes: 8 * 1024 * 1024, stderrMaxBytes: 1024 * 1024, terminateProcessTree: true } : {})
151
+ ...(launch ? { stdoutMaxBytes: 8 * 1024 * 1024, stderrMaxBytes: 1024 * 1024 } : {}),
152
+ // PATH-resolved providers can be Windows .cmd shims too. Always request tree termination
153
+ // for provider timeouts; trustedExecutable only controls additional output bounds.
154
+ terminateProcessTree: true
152
155
  });
153
156
  const ended = Date.now();
154
157
  const combinedOutput = `${measured.stdout}\n${measured.stderr}`;
@@ -12,6 +12,7 @@ export function renderTopLevelHelp() {
12
12
  " my-dev-kit-lab [--workspace <path>] experiment run --experiment <id> [options]",
13
13
  " my-dev-kit-lab [--workspace <path>] experiment controlled --cases <path> --out <dir> [options]",
14
14
  " my-dev-kit-lab [--workspace <path>] report render --experiment <dir> --out <dir> [options]",
15
+ " my-dev-kit-lab [--workspace <path>] report compare-prompts --baseline <run-root> --candidate <run-root> --out <output-root>",
15
16
  " my-dev-kit-lab [--workspace <path>] plots generate --experiment <dir> --out <dir>",
16
17
  " my-dev-kit-lab [--workspace <path>] gallery build --out <dir> [options]",
17
18
  " my-dev-kit-lab tutorial validate --scenario <path> [--target-contract <path>]",
@@ -32,6 +33,7 @@ export function renderTopLevelHelp() {
32
33
  " experiment run Run a registered experiment plugin",
33
34
  " experiment controlled Run a controlled context-strategy-comparison experiment",
34
35
  " report render Render an experiment report from controlled-experiment artifacts",
36
+ " report compare-prompts Compare two completed real-agent runs by prompt template (read-only)",
35
37
  " plots generate Generate experiment plot artifacts",
36
38
  " gallery build Build a gallery manifest/index",
37
39
  " tutorial validate Validate a tutorial scenario and optional target contract",
@@ -312,6 +314,20 @@ export function renderExperimentRunHelp() {
312
314
  " are reported separately. Results are descriptive for the finite",
313
315
  " corpus: no winner, ranking, composite score or significance claim.",
314
316
  "",
317
+ "agent-success-rate prompt contract options (real-agent mode only):",
318
+ " --prompt-template <legacy-diff-v1|strict-diff-v1> Prompt/response contract (real-agent mode only; default",
319
+ " legacy-diff-v1, the unchanged historical prompt). strict-diff-v1",
320
+ " requires one JSON object {schemaVersion, patch}.",
321
+ " --prompt-length <standard|short> Instruction length (real-agent mode only; default standard).",
322
+ " short trims instruction wording, never context or restrictions.",
323
+ " --prompt-explanation <legacy|none> Explanation policy (real-agent mode only; default legacy).",
324
+ " none rejects any prose outside the required response.",
325
+ " --prompt-tool-use <prohibited|bounded> Tool-use statement (real-agent mode only; default prohibited).",
326
+ " bounded states an explicit zero command budget.",
327
+ " --max-command-count <0> Command-count guidance (real-agent mode only; only 0 is",
328
+ " accepted). Prompt options never enable a provider and are not",
329
+ " proof that a provider could not use tools.",
330
+ "",
315
331
  "agent-success-rate resumable campaigns and timeouts only:",
316
332
  ...AGENT_CAMPAIGN_OPTION_HELP.map(line => ` ${line}`),
317
333
  "Examples (replace bracketed values; explicit source-root is required when package source proof is unavailable):",
@@ -352,11 +368,30 @@ export function renderReportHelp() {
352
368
  "",
353
369
  "Usage:",
354
370
  " my-dev-kit-lab report render [options]",
371
+ " my-dev-kit-lab report compare-prompts --baseline <run-root> --candidate <run-root> --out <output-root>",
355
372
  " my-dev-kit-lab report --help",
356
373
  "",
357
374
  "Commands:",
358
375
  " render Render an HTML/JSON experiment report from controlled-experiment artifacts.",
359
- " Run \"my-dev-kit-lab report render --help\" for details."
376
+ " Run \"my-dev-kit-lab report render --help\" for details.",
377
+ " compare-prompts Compare two completed real-agent agent-success-rate runs by prompt template (read-only).",
378
+ " Run \"my-dev-kit-lab report compare-prompts --help\" for details."
379
+ ].join("\n");
380
+ }
381
+ export function renderReportComparePromptsHelp() {
382
+ return [
383
+ "my-dev-kit-lab report compare-prompts - compare persisted evidence from two prompt-template runs",
384
+ "",
385
+ "Usage:",
386
+ " my-dev-kit-lab report compare-prompts --baseline <run-root> --candidate <run-root> --out <output-root>",
387
+ "",
388
+ "Required (each exactly once):",
389
+ " --baseline <run-root> Completed agent-success-rate run output root (baseline template)",
390
+ " --candidate <run-root> Completed agent-success-rate run output root (candidate template)",
391
+ " --out <output-root> Output root; must not overlap either run root",
392
+ "",
393
+ "Writes prompt-template-comparison.json, .txt and .html. The command only reads persisted evidence:",
394
+ "it never invokes a provider, creates a campaign or modifies a checkpoint."
360
395
  ].join("\n");
361
396
  }
362
397
  export function renderReportRenderHelp() {
@@ -439,7 +474,8 @@ export function renderGalleryBuildHelp() {
439
474
  " --report <dir> Path to a rendered report directory",
440
475
  " --plots <dir> Path to a plots output directory",
441
476
  " --visualizations <dir> Path to a visualization-demos output directory",
442
- " --experiment <dir> Path to a controlled-experiment output directory"
477
+ " --experiment <dir> Path to a controlled-experiment output directory",
478
+ " --source <path> Artifact root or supported manifest to include; repeatable"
443
479
  ].join("\n");
444
480
  }
445
481
  export function renderSecurityHelp() {
@@ -10,10 +10,11 @@ import { runExperimentRunCommandFromArgs } from "../commands/runExperimentRunCom
10
10
  import { runRenderExperimentReportCommand } from "../commands/renderExperimentReportCommand.js";
11
11
  import { runGenerateExperimentPlotsCommand } from "../commands/generateExperimentPlotsCommand.js";
12
12
  import { runBuildGalleryCommand } from "../commands/buildGalleryCommand.js";
13
+ import { runComparePromptsCommand } from "../commands/runComparePromptsCommand.js";
13
14
  import { runTutorialValidateCommandFromArgs } from "../commands/runTutorialValidateCommand.js";
14
15
  import { runTutorialRunCommandFromArgs } from "../commands/runTutorialRunCommand.js";
15
16
  import { createLabExecutionContext, discoverPackageRoot } from "../runtime/index.js";
16
- import { renderDemoHelp, renderExperimentControlledHelp, renderExperimentDescribeHelp, renderExperimentHelp, renderExperimentListHelp, renderExperimentRunHelp, renderFinalDemoHelp, renderGalleryBuildHelp, renderGalleryHelp, renderPlotsGenerateHelp, renderPlotsHelp, renderReportHelp, renderReportRenderHelp, renderSecurityHelp, renderTopLevelHelp, renderTutorialHelp, renderTutorialRunHelp, renderTutorialValidateHelp, renderUnknownCommandError } from "./help.js";
17
+ import { renderDemoHelp, renderExperimentControlledHelp, renderExperimentDescribeHelp, renderExperimentHelp, renderExperimentListHelp, renderExperimentRunHelp, renderFinalDemoHelp, renderGalleryBuildHelp, renderGalleryHelp, renderPlotsGenerateHelp, renderPlotsHelp, renderReportHelp, renderReportRenderHelp, renderReportComparePromptsHelp, renderSecurityHelp, renderTopLevelHelp, renderTutorialHelp, renderTutorialRunHelp, renderTutorialValidateHelp, renderUnknownCommandError } from "./help.js";
17
18
  export const CLI_USAGE_EXIT_CODE = 2;
18
19
  /**
19
20
  * Recognized legacy final-demo flag tokens (mirrors parseRunFinalDemoArgs in
@@ -188,6 +189,13 @@ async function runReportFamily(argv, writers) {
188
189
  }
189
190
  return runRenderExperimentReportCommand(rest);
190
191
  }
192
+ if (subcommand === "compare-prompts") {
193
+ if (rest[0] === "--help" || rest[0] === "-h") {
194
+ writers.stdout(renderReportComparePromptsHelp());
195
+ return 0;
196
+ }
197
+ return runComparePromptsCommand(rest);
198
+ }
191
199
  writers.stderr(renderUnknownCommandError(`report ${subcommand}`));
192
200
  return CLI_USAGE_EXIT_CODE;
193
201
  }
@@ -1,11 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { writeExperimentGalleryManifest } from "../gallery/index.js";
3
+ import { looksLikeUrl } from "../gallery/galleryPathSafety.js";
3
4
  export function parseBuildGalleryArgs(argv) {
4
5
  let outDir = "";
5
6
  let reportDir;
6
7
  let plotsDir;
7
8
  let visualizationsDir;
8
9
  let experimentDir;
10
+ const sources = [];
9
11
  for (let index = 0; index < argv.length; index += 1) {
10
12
  const arg = argv[index];
11
13
  if (arg === "--report") {
@@ -24,14 +26,24 @@ export function parseBuildGalleryArgs(argv) {
24
26
  experimentDir = argv[index + 1] ?? "";
25
27
  index += 1;
26
28
  }
29
+ else if (arg === "--source") {
30
+ const value = argv[index + 1];
31
+ if (value === undefined || value.trim() === "" || value.startsWith("--")) {
32
+ throw new Error("--source requires a non-empty artifact root or supported manifest path.");
33
+ }
34
+ if (looksLikeUrl(value))
35
+ throw new Error(`Gallery source ${value}: URLs are not supported; supply a local path.`);
36
+ sources.push(value);
37
+ index += 1;
38
+ }
27
39
  else if (arg === "--out") {
28
40
  outDir = argv[index + 1] ?? "";
29
41
  index += 1;
30
42
  }
31
43
  }
32
44
  if (!outDir)
33
- throw new Error("Usage: --out <dir> [--report <dir>] [--plots <dir>] [--visualizations <dir>] [--experiment <dir>]");
34
- return { outDir, reportDir, plotsDir, visualizationsDir, experimentDir };
45
+ throw new Error("Usage: --out <dir> [--report <dir>] [--plots <dir>] [--visualizations <dir>] [--experiment <dir>] [--source <artifact-root-or-manifest>]...");
46
+ return { outDir, reportDir, plotsDir, visualizationsDir, experimentDir, ...(sources.length > 0 ? { sources } : {}) };
35
47
  }
36
48
  export async function runBuildGalleryFromArgs(args, repoRoot = process.cwd()) {
37
49
  return writeExperimentGalleryManifest({
@@ -39,7 +51,8 @@ export async function runBuildGalleryFromArgs(args, repoRoot = process.cwd()) {
39
51
  reportDir: args.reportDir ? path.resolve(repoRoot, args.reportDir) : undefined,
40
52
  plotsDir: args.plotsDir ? path.resolve(repoRoot, args.plotsDir) : undefined,
41
53
  visualizationsDir: args.visualizationsDir ? path.resolve(repoRoot, args.visualizationsDir) : undefined,
42
- experimentDir: args.experimentDir ? path.resolve(repoRoot, args.experimentDir) : undefined
54
+ experimentDir: args.experimentDir ? path.resolve(repoRoot, args.experimentDir) : undefined,
55
+ sources: args.sources?.map((source) => path.resolve(repoRoot, source))
43
56
  });
44
57
  }
45
58
  export async function runBuildGalleryCommand(argv) {
@@ -0,0 +1,109 @@
1
+ import { mkdir, realpath, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadPromptComparisonSide } from "../report/experiments/promptTemplateComparisonEvidence.js";
4
+ import { buildPromptTemplateComparison, PROMPT_TEMPLATE_COMPARISON_HTML_FILE, PROMPT_TEMPLATE_COMPARISON_JSON_FILE, PROMPT_TEMPLATE_COMPARISON_TEXT_FILE } from "../report/experiments/promptTemplateComparisonModel.js";
5
+ import { renderPromptTemplateComparisonHtml, renderPromptTemplateComparisonJson, renderPromptTemplateComparisonText } from "../report/experiments/renderPromptTemplateComparison.js";
6
+ const FLAGS = ["--baseline", "--candidate", "--out"];
7
+ /** Exactly --baseline, --candidate and --out, each once. Unknown flags, positional arguments and empty values are rejected. */
8
+ export function parseComparePromptsArgs(argv) {
9
+ const values = new Map();
10
+ for (let index = 0; index < argv.length; index += 1) {
11
+ const arg = argv[index];
12
+ if (!FLAGS.includes(arg)) {
13
+ throw new Error(arg.startsWith("--") ? `Unknown option for report compare-prompts: ${arg}` : `Unexpected argument for report compare-prompts: ${arg}`);
14
+ }
15
+ if (values.has(arg))
16
+ throw new Error(`Duplicate option: ${arg}`);
17
+ const value = argv[index + 1];
18
+ if (value === undefined || value.trim() === "" || value.startsWith("--"))
19
+ throw new Error(`${arg} requires a non-empty path.`);
20
+ values.set(arg, value);
21
+ index += 1;
22
+ }
23
+ for (const flag of FLAGS) {
24
+ if (!values.has(flag))
25
+ throw new Error(`Missing required option: ${flag}. Usage: report compare-prompts --baseline <run-root> --candidate <run-root> --out <output-root>`);
26
+ }
27
+ return { baseline: values.get("--baseline"), candidate: values.get("--candidate"), out: values.get("--out") };
28
+ }
29
+ /** Physical path of the nearest existing ancestor plus the not-yet-existing remainder. */
30
+ async function physicalPath(candidate) {
31
+ let existing = path.resolve(candidate);
32
+ const remainder = [];
33
+ for (;;) {
34
+ try {
35
+ const real = await realpath(existing);
36
+ return path.join(real, ...remainder);
37
+ }
38
+ catch {
39
+ const parent = path.dirname(existing);
40
+ if (parent === existing)
41
+ return path.resolve(candidate);
42
+ remainder.unshift(path.basename(existing));
43
+ existing = parent;
44
+ }
45
+ }
46
+ }
47
+ function overlaps(a, b) {
48
+ const key = (value) => (process.platform === "win32" ? value.toLowerCase() : value);
49
+ const ra = path.relative(key(a), key(b));
50
+ const rb = path.relative(key(b), key(a));
51
+ const inside = (relative) => relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
52
+ return inside(ra) || inside(rb);
53
+ }
54
+ export async function runComparePrompts(args, cwd = process.cwd(), options = {}) {
55
+ const baselineRoot = path.resolve(cwd, args.baseline);
56
+ const candidateRoot = path.resolve(cwd, args.candidate);
57
+ const outRoot = path.resolve(cwd, args.out);
58
+ const [physicalBaseline, physicalCandidate, physicalOut] = await Promise.all([baselineRoot, candidateRoot, outRoot].map(physicalPath));
59
+ if (path.relative(process.platform === "win32" ? physicalBaseline.toLowerCase() : physicalBaseline, process.platform === "win32" ? physicalCandidate.toLowerCase() : physicalCandidate) === "") {
60
+ throw new Error("The baseline and candidate run roots are identical; a comparison needs two distinct runs.");
61
+ }
62
+ if (overlaps(physicalOut, physicalBaseline) || overlaps(physicalOut, physicalCandidate)) {
63
+ throw new Error("The output root must not overlap the baseline or candidate run root.");
64
+ }
65
+ try {
66
+ if (!(await stat(physicalOut)).isDirectory())
67
+ throw new Error("The output root exists and is not a directory.");
68
+ }
69
+ catch (error) {
70
+ if (error.code !== "ENOENT")
71
+ throw error;
72
+ }
73
+ const [baseline, candidate] = await Promise.all([loadPromptComparisonSide(baselineRoot, "baseline", options), loadPromptComparisonSide(candidateRoot, "candidate", options)]);
74
+ for (const side of [baseline, candidate]) {
75
+ if (side.execution === null) {
76
+ throw new Error(`The ${side.role} run evidence is unreadable or invalid: ${side.unverifiable.join("; ")}`);
77
+ }
78
+ }
79
+ const report = buildPromptTemplateComparison(baseline, candidate);
80
+ await mkdir(physicalOut, { recursive: true });
81
+ const paths = {
82
+ json: path.join(physicalOut, PROMPT_TEMPLATE_COMPARISON_JSON_FILE),
83
+ text: path.join(physicalOut, PROMPT_TEMPLATE_COMPARISON_TEXT_FILE),
84
+ html: path.join(physicalOut, PROMPT_TEMPLATE_COMPARISON_HTML_FILE)
85
+ };
86
+ await writeFile(paths.json, renderPromptTemplateComparisonJson(report), "utf8");
87
+ await writeFile(paths.text, renderPromptTemplateComparisonText(report), "utf8");
88
+ await writeFile(paths.html, renderPromptTemplateComparisonHtml(report), "utf8");
89
+ return { report, paths };
90
+ }
91
+ export async function runComparePromptsCommand(argv, options = {}) {
92
+ try {
93
+ const result = await runComparePrompts(parseComparePromptsArgs(argv), process.cwd(), options);
94
+ const report = result.report;
95
+ console.log([
96
+ `Comparison: ${report.comparison.status}`,
97
+ `Matched units: ${report.cohort.matchedUnitCount}`,
98
+ `Behavioral improvement: ${report.behavioralImprovement.status}`,
99
+ `JSON: ${result.paths.json}`,
100
+ `Text: ${result.paths.text}`,
101
+ `HTML: ${result.paths.html}`
102
+ ].join("\n"));
103
+ return 0;
104
+ }
105
+ catch (error) {
106
+ console.error(error instanceof Error ? error.message : String(error));
107
+ return 1;
108
+ }
109
+ }
@@ -19,7 +19,8 @@ import { createLabExecutionContext, resolvePackageResource } from "../runtime/in
19
19
  import { runWarmIndexCampaignPresentation } from "./runWarmIndexCampaignPresentation.js";
20
20
  import { observeProviderBuild } from "../agents/providerBuildObservation.js";
21
21
  import { resolveAgentTimeoutPreset } from "../experiments/plugins/agentSuccessRate/timeoutPresets.js";
22
- import { validateAgentSuccessRateConfig } from "../experiments/plugins/agentSuccessRate/config.js";
22
+ import { resolveAgentSuccessPromptContract, validateAgentSuccessRateConfig } from "../experiments/plugins/agentSuccessRate/config.js";
23
+ import { AGENT_SUCCESS_PROMPT_EXPLANATION_MODES, AGENT_SUCCESS_PROMPT_LENGTH_MODES, AGENT_SUCCESS_PROMPT_TEMPLATE_IDS, AGENT_SUCCESS_PROMPT_TOOL_USE_MODES } from "../experiments/plugins/agentSuccessRate/realAgentPrompt.js";
23
24
  import { AGENT_SUCCESS_RATE_DEFAULT_AGENT_TIMEOUT_MS } from "../experiments/plugins/agentSuccessRate/realAgentExecution.js";
24
25
  import { AGENT_SUCCESS_RATE_TREATMENT_IDS } from "../experiments/plugins/agentSuccessRate/metadata.js";
25
26
  import { executeAgentSuccessCampaignV2, prepareAgentSuccessCampaign } from "../experiments/plugins/agentSuccessRate/campaignExecution.js";
@@ -98,6 +99,7 @@ const AGENT_SUCCESS_RATE_ALLOWED_FLAGS = [
98
99
  "--include-real-agents",
99
100
  "--timeout-ms",
100
101
  "--repair-attempts",
102
+ "--prompt-template", "--prompt-length", "--prompt-explanation", "--prompt-tool-use", "--max-command-count",
101
103
  "--agent-campaign", "--one-case", "--campaign-source-root", "--timeout-preset",
102
104
  "--expected-checkpoint-revision", "--limit-case", "--limit-treatment", "--limit-epoch", "--authorize-additional-invocation"
103
105
  ];
@@ -295,7 +297,7 @@ async function runAgentCampaignCommand(args, context, outputRoot, inputs, runId,
295
297
  const config = validation.config;
296
298
  const tasks = selectAgentSuccessTasks(inputs.agentSuccessTasks, config);
297
299
  const runtime = options.campaignRuntime;
298
- const provider = config.agentId ? { kind: "real-agent", providerId: config.agentId, includeRealAgents: true, timeoutMs: config.timeoutMs ?? AGENT_SUCCESS_RATE_DEFAULT_AGENT_TIMEOUT_MS, repairAttempts: (config.repairAttempts ?? 0), kitCommand: config.kitCommand, contextDependencies: runtime?.contextDependencies, env: runtime?.env } : { kind: "deterministic-fixture" };
300
+ const provider = config.agentId ? { kind: "real-agent", providerId: config.agentId, includeRealAgents: true, timeoutMs: config.timeoutMs ?? AGENT_SUCCESS_RATE_DEFAULT_AGENT_TIMEOUT_MS, repairAttempts: (config.repairAttempts ?? 0), kitCommand: config.kitCommand, contextDependencies: runtime?.contextDependencies, env: runtime?.env, ...(resolveAgentSuccessPromptContract(config) ? { promptContract: resolveAgentSuccessPromptContract(config) } : {}) } : { kind: "deterministic-fixture" };
299
301
  const preparation = { boundary, sourceRoot, corpusFile: resolvePackageResource(context, AGENT_SUCCESS_TASK_CATALOG_PATH), corpusRoot: context.packageRoot, tasks, treatments: AGENT_SUCCESS_RATE_TREATMENT_IDS, provider, dependencies: runtime?.dependencies };
300
302
  // Source, corpus and canonical project byte checks precede even a task-free version probe.
301
303
  let prepared = await prepareAgentSuccessCampaign(preparation);
@@ -381,6 +383,11 @@ export function parseRunExperimentArgs(argv) {
381
383
  let localSubjectConfigPath;
382
384
  const agentFlagValues = [];
383
385
  let repairAttempts;
386
+ let promptTemplate;
387
+ let promptLength;
388
+ let promptExplanation;
389
+ let promptToolUse;
390
+ let maxCommandCount;
384
391
  const seenFlags = [];
385
392
  const commandTemplates = {};
386
393
  // The flag whitelist runs before any value is interpreted so an unsupported flag is reported as such.
@@ -459,6 +466,24 @@ export function parseRunExperimentArgs(argv) {
459
466
  else if (arg === "--agent") {
460
467
  agentFlagValues.push(readRequiredValue(argv, ++index, "--agent"));
461
468
  }
469
+ else if (arg === "--prompt-template") {
470
+ promptTemplate = parseChoice("--prompt-template", readRequiredValue(argv, ++index, arg), AGENT_SUCCESS_PROMPT_TEMPLATE_IDS);
471
+ }
472
+ else if (arg === "--prompt-length") {
473
+ promptLength = parseChoice("--prompt-length", readRequiredValue(argv, ++index, arg), AGENT_SUCCESS_PROMPT_LENGTH_MODES);
474
+ }
475
+ else if (arg === "--prompt-explanation") {
476
+ promptExplanation = parseChoice("--prompt-explanation", readRequiredValue(argv, ++index, arg), AGENT_SUCCESS_PROMPT_EXPLANATION_MODES);
477
+ }
478
+ else if (arg === "--prompt-tool-use") {
479
+ promptToolUse = parseChoice("--prompt-tool-use", readRequiredValue(argv, ++index, arg), AGENT_SUCCESS_PROMPT_TOOL_USE_MODES);
480
+ }
481
+ else if (arg === "--max-command-count") {
482
+ const value = readRequiredValue(argv, ++index, arg);
483
+ if (!/^\d+$/.test(value) || Number(value) !== 0)
484
+ throw new Error("--max-command-count must be the integer 0; the benchmark permits no commands.");
485
+ maxCommandCount = 0;
486
+ }
462
487
  else if (arg === "--repair-attempts") {
463
488
  repairAttempts = parseRepairAttempts(readRequiredValue(argv, ++index, "--repair-attempts"));
464
489
  }
@@ -650,6 +675,10 @@ export function parseRunExperimentArgs(argv) {
650
675
  if (timeoutMs !== undefined && timeoutMs > AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS) {
651
676
  throw new Error(`--timeout-ms must be a positive integer no greater than ${AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS}.`);
652
677
  }
678
+ const promptFlagsSupplied = PROMPT_CONTRACT_FLAGS.filter((flag) => seenFlags.includes(flag));
679
+ if (agentId === undefined && promptFlagsSupplied.length > 0) {
680
+ throw new Error(`${promptFlagsSupplied.join(", ")} ${promptFlagsSupplied.length === 1 ? "is" : "are"} only supported together with --agent and --include-real-agents (real-agent mode); prompt options never enable a provider.`);
681
+ }
653
682
  if (seenFlags.includes("--case") && caseIds.length === 0) {
654
683
  throw new Error("--case must list at least one case id.");
655
684
  }
@@ -658,6 +687,10 @@ export function parseRunExperimentArgs(argv) {
658
687
  }
659
688
  }
660
689
  else {
690
+ const unrelatedPromptFlags = PROMPT_CONTRACT_FLAGS.filter((flag) => seenFlags.includes(flag));
691
+ if (unrelatedPromptFlags.length > 0) {
692
+ throw new Error(`${unrelatedPromptFlags.join(", ")} ${unrelatedPromptFlags.length === 1 ? "is" : "are"} only supported for --experiment ${agentSuccessRatePlugin.metadata.id}.`);
693
+ }
661
694
  if (agentFlagValues.length > 0) {
662
695
  throw new Error(`--agent is only supported for --experiment ${agentSuccessRatePlugin.metadata.id}.`);
663
696
  }
@@ -748,6 +781,11 @@ export function parseRunExperimentArgs(argv) {
748
781
  commandTemplates: Object.keys(commandTemplates).length > 0 ? commandTemplates : undefined,
749
782
  agentId: agentFlagValues[0],
750
783
  repairAttempts,
784
+ promptTemplate,
785
+ promptLength,
786
+ promptExplanation,
787
+ promptToolUse,
788
+ maxCommandCount,
751
789
  kitCommand,
752
790
  campaignPreset,
753
791
  contextBudgets
@@ -1104,6 +1142,13 @@ function readRequiredValue(argv, index, label) {
1104
1142
  }
1105
1143
  return value;
1106
1144
  }
1145
+ const PROMPT_CONTRACT_FLAGS = ["--prompt-template", "--prompt-length", "--prompt-explanation", "--prompt-tool-use", "--max-command-count"];
1146
+ function parseChoice(label, value, allowed) {
1147
+ if (!allowed.includes(value)) {
1148
+ throw new Error(`${label} must be one of: ${allowed.join(", ")}.`);
1149
+ }
1150
+ return value;
1151
+ }
1107
1152
  function parseRepairAttempts(value) {
1108
1153
  if (!/^[0-2]$/.test(value)) {
1109
1154
  throw new Error("--repair-attempts must be 0, 1 or 2.");
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import path from "node:path";
2
3
  /**
3
4
  * Asks a process to terminate normally.
4
5
  *
@@ -27,52 +28,75 @@ export function requestProcessTermination(pid) {
27
28
  * fire-and-forget and, off Windows, signals one pid only.
28
29
  *
29
30
  * Windows: `taskkill /pid <pid> /T /F` (shell:false) is awaited, bounded by
30
- * `waitTimeoutMs`, so the caller knows the tree was asked to die before it
31
- * proceeds.
31
+ * `waitTimeoutMs`. Returns false if the command fails or times out.
32
32
  *
33
33
  * Other platforms: the child must have been spawned `detached` so it leads its
34
34
  * own process group; the whole group is signalled via the negative pid. If no
35
35
  * group exists, the single pid is signalled as a fallback.
36
36
  *
37
- * Never throws: the target may already have exited.
37
+ * Never throws. Returns true only when termination was confirmed (or no PID was supplied).
38
38
  */
39
39
  export async function terminateProcessTree(pid, options = {}) {
40
40
  if (!pid) {
41
- return;
41
+ return true;
42
42
  }
43
43
  const platform = options.platform ?? process.platform;
44
44
  if (platform === "win32") {
45
- await new Promise((resolve) => {
45
+ return await new Promise((resolve) => {
46
46
  let timer;
47
- const finish = () => {
47
+ let settled = false;
48
+ const finish = (terminated) => {
49
+ if (settled)
50
+ return;
51
+ settled = true;
48
52
  if (timer)
49
53
  clearTimeout(timer);
50
- resolve();
54
+ resolve(terminated);
51
55
  };
52
- const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
53
- shell: false,
54
- stdio: "ignore",
55
- windowsHide: true
56
- });
57
- timer = setTimeout(finish, options.waitTimeoutMs ?? 5000);
58
- killer.on("error", finish);
59
- killer.on("close", finish);
56
+ let killer;
57
+ try {
58
+ const systemRoot = Object.entries(process.env).find(([name]) => name.toLowerCase() === "systemroot")?.[1];
59
+ const taskkill = systemRoot ? path.join(systemRoot, "System32", "taskkill.exe") : "taskkill";
60
+ killer = spawn(taskkill, ["/pid", String(pid), "/T", "/F"], {
61
+ shell: false,
62
+ stdio: "ignore",
63
+ windowsHide: true
64
+ });
65
+ }
66
+ catch {
67
+ finish(false);
68
+ return;
69
+ }
70
+ timer = setTimeout(() => {
71
+ if (killer.pid) {
72
+ try {
73
+ killer.kill();
74
+ }
75
+ catch {
76
+ // The bounded helper may already have exited.
77
+ }
78
+ }
79
+ finish(false);
80
+ }, options.waitTimeoutMs ?? 5000);
81
+ killer.on("error", () => finish(false));
82
+ killer.on("close", (code) => finish(code === 0));
60
83
  });
61
- return;
62
84
  }
63
85
  const signal = options.posixSignal ?? "SIGKILL";
64
86
  try {
65
87
  process.kill(-pid, signal);
66
- return;
88
+ return true;
67
89
  }
68
90
  catch {
69
91
  // No process group for this pid (or it already exited); fall back to the single pid.
70
92
  }
71
93
  try {
72
94
  process.kill(pid, signal);
95
+ return true;
73
96
  }
74
97
  catch {
75
98
  // The process may have exited between the liveness check and this signal.
99
+ return false;
76
100
  }
77
101
  }
78
102
  /**
@@ -49,6 +49,7 @@ export async function runMeasuredCommand(options) {
49
49
  let timedOut = false;
50
50
  let timeout;
51
51
  let killGraceTimeout;
52
+ let terminationPromise;
52
53
  let settled = false;
53
54
  let child;
54
55
  try {
@@ -99,7 +100,7 @@ export async function runMeasuredCommand(options) {
99
100
  const KILL_GRACE_MS = 3000;
100
101
  const terminate = () => {
101
102
  if (options.terminateProcessTree === true) {
102
- void terminateProcessTree(child.pid);
103
+ terminationPromise ??= terminateProcessTree(child.pid);
103
104
  }
104
105
  else {
105
106
  forceTerminateProcess(child.pid);
@@ -145,14 +146,19 @@ export async function runMeasuredCommand(options) {
145
146
  child.stdin?.write(options.stdinText, "utf8");
146
147
  child.stdin?.end();
147
148
  }
148
- // forceTerminateProcess is fire-and-forget (it never confirms the target process tree actually
149
- // exited: on Windows it spawns a detached `taskkill /T /F` and does not await or check its
150
- // result). That is normally sufficient, but a grandchild process spawned through an
151
- // intermediate shim (for example a Windows .cmd launcher) can occasionally outlive an
152
- // already-killed intermediate process. Without a bounded fallback, a caller whose kill silently
153
- // failed would await this promise forever instead of receiving the already-known timeout error.
154
- // (KILL_GRACE_MS and the shared `terminate` step are declared above, beside the output collectors.)
149
+ // Tree termination is awaited before a bounded result is returned. The helper has its own
150
+ // platform-specific time bound; a failed or timed-out tree kill remains visible in `error`.
151
+ // Non-tree callers retain the legacy kill-grace fallback below.
155
152
  const settle = async (exitCode) => {
153
+ if (settled)
154
+ return;
155
+ if (terminationPromise) {
156
+ const terminated = await terminationPromise;
157
+ if (!terminated) {
158
+ const cleanupError = "Could not confirm termination of the command process tree.";
159
+ spawnError = spawnError ? `${spawnError} ${cleanupError}` : cleanupError;
160
+ }
161
+ }
156
162
  if (settled)
157
163
  return;
158
164
  settled = true;