@dailephd/my-dev-kit-lab 0.9.1 → 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 (53) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/README.md +10 -2
  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 +51 -5
  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 +24 -2
  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/campaignProgress.js +4 -2
  15. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
  16. package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
  17. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
  18. package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
  19. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
  20. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
  21. package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
  22. package/dist/src/gallery/galleryPathSafety.js +111 -0
  23. package/dist/src/gallery/readGallerySources.js +542 -0
  24. package/dist/src/gallery/writeGalleryManifest.js +57 -3
  25. package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
  26. package/dist/src/report/experiments/htmlSafety.js +32 -0
  27. package/dist/src/report/experiments/metricEvidence.js +389 -0
  28. package/dist/src/report/experiments/metricGlossary.js +227 -0
  29. package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
  30. package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
  31. package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
  32. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
  33. package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
  34. package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
  35. package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
  36. package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
  37. package/dist/src/report/experiments/specializedReportSections.js +142 -0
  38. package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +6 -2
  39. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
  40. package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
  41. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
  42. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
  43. package/dist/src/securityValidation/commandRunner.js +13 -6
  44. package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +34 -4
  45. package/dist/src/securityValidation/packageChecks/requiredPackageContents.js +106 -2
  46. package/dist/src/securityValidation/packageChecks/runPackageChecks.js +31 -16
  47. package/dist/src/securityValidation/staticScans/semgrep.js +68 -64
  48. package/dist/src/securityValidation/types.js +0 -4
  49. package/dist/src/securityValidation/validate/runSecurityValidation.js +1 -1
  50. package/docs/METRICS.md +12 -2
  51. package/examples/tutorial-browser/prepare.mjs +13 -13
  52. package/examples/tutorial-browser/server.mjs +13 -13
  53. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to my-dev-kit-lab are documented here.
4
4
 
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.
11
+
12
+ ## [0.9.2] - 2026-10-13
13
+
14
+ ### Security validation and cross-platform corrections
15
+
16
+ - Corrected external npm package validation to derive required entrypoints from validated package metadata while preserving the strict Lab self-package inventory. Missing declared entrypoints, invalid or incomplete metadata, failed or incomplete package evidence, and forbidden contents remain blockers.
17
+ - Eliminated nine inherited Lab-only false positives when validating the external Orchestrator package. This does not change the Orchestrator package or its independent security findings and release status.
18
+ - Corrected Semgrep failed and incomplete scan evidence so neither can produce a false `PASS`; unavailable optional scanners remain explicitly `SKIPPED`.
19
+ - Corrected campaign filesystem identity and path-safety behavior and cross-platform compatibility.
20
+ - Preserved security verdict strictness, severity mappings, mandatory checks, and existing command and report contracts.
21
+
22
+ This section records the intended v0.9.2 release contents. Registry publication status is established through npm evidence, not this changelog.
23
+
5
24
  ## [0.9.1] - 2026-10-12
6
25
 
7
26
  ### Normalized provider telemetry and resumable campaigns
@@ -11,8 +30,6 @@ All notable changes to my-dev-kit-lab are documented here.
11
30
  - Added public campaign CLI options, Codex and Claude timeout presets, campaign progress summaries, and strong/partial/unavailable metric-specific evidence classifications.
12
31
  - Preserved partial evidence without presenting it as complete scientific analysis; uncertain provider invocations are not replayed automatically. Real-provider use remains explicitly opted in and depends on the existing provider adapters.
13
32
 
14
- ## [Unreleased]
15
-
16
33
  ## [0.9.0] - 2026-10-10
17
34
 
18
35
  ### Agent success rate
package/README.md CHANGED
@@ -6,16 +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 latest release is v0.9.1 (normalized provider telemetry and resumable agent-success-rate campaigns), and the repository package version is 0.9.1. The previous release is v0.9.0 (agent-success-rate implementation-patch evaluation). 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 adds normalized provider telemetry and resumable campaign execution. v0.9.2 remains planned.
13
+ v0.9.2 is the previous package release.
14
+
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
+
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.
14
18
 
15
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.
16
20
 
17
21
  ## Current capabilities
18
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
+
19
25
  - **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
20
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.
21
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).
@@ -284,6 +290,8 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
284
290
 
285
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.
286
292
 
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).
294
+
287
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.
288
296
 
289
297
  Optional local scanners are reported as `skipped` when unavailable, never as passed. This local availability rule does not make CodeQL coverage optional for release readiness: the exact candidate must pass the repository-controlled GitHub CodeQL workflow and have its Code Scanning alerts reviewed. The framework does not provide runtime isolation proof, device or APK/AAB analysis, signing verification, Play Console validation, automatic fixes, or manual pentesting. Manual pentest remains deferred until post-v1/version TBD.
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { lstat, mkdir, readFile, readdir, realpath } from "node:fs/promises";
1
+ import { lstat, mkdir, readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { parseAgentCommandTemplate } from "../agents/index.js";
4
4
  import { parseAgentId } from "../agents/agentRegistry.js";
@@ -19,11 +19,12 @@ 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";
26
- import { readCampaignCheckpointV2, validateCampaignOutputPath } from "../experiments/plugins/agentSuccessRate/campaignCheckpointStore.js";
27
+ import { readCampaignCheckpointV2, resolveCampaignRoot, validateCampaignOutputPath } from "../experiments/plugins/agentSuccessRate/campaignCheckpointStore.js";
27
28
  import { validateCampaignLimitAuthorization } from "../experiments/plugins/agentSuccessRate/campaignRecovery.js";
28
29
  import { campaignIdentifier } from "../experiments/plugins/agentSuccessRate/campaignIdentity.js";
29
30
  import { readCampaignProgress } from "../experiments/plugins/agentSuccessRate/campaignProgress.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
  ];
@@ -263,7 +265,8 @@ async function runAgentCampaignCommand(args, context, outputRoot, inputs, runId,
263
265
  const executingMetadata = JSON.parse(await readFile(path.join(context.packageRoot, "package.json"), "utf8"));
264
266
  if (executingMetadata.name !== packageName || typeof executingMetadata.version !== "string" || !validPackageVersion.test(executingMetadata.version))
265
267
  throw new Error();
266
- if (!(await lstat(path.join(sourceRoot, "src"))).isDirectory() || path.resolve(await realpath(sourceRoot)).toLowerCase() !== sourceRoot.toLowerCase())
268
+ await resolveCampaignRoot(sourceRoot);
269
+ if (!(await lstat(path.join(sourceRoot, "src"))).isDirectory())
267
270
  throw new Error();
268
271
  const metadata = JSON.parse(await readFile(path.join(sourceRoot, "package.json"), "utf8"));
269
272
  if (metadata.name !== packageName || metadata.version !== executingMetadata.version)
@@ -294,7 +297,7 @@ async function runAgentCampaignCommand(args, context, outputRoot, inputs, runId,
294
297
  const config = validation.config;
295
298
  const tasks = selectAgentSuccessTasks(inputs.agentSuccessTasks, config);
296
299
  const runtime = options.campaignRuntime;
297
- 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" };
298
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 };
299
302
  // Source, corpus and canonical project byte checks precede even a task-free version probe.
300
303
  let prepared = await prepareAgentSuccessCampaign(preparation);
@@ -380,6 +383,11 @@ export function parseRunExperimentArgs(argv) {
380
383
  let localSubjectConfigPath;
381
384
  const agentFlagValues = [];
382
385
  let repairAttempts;
386
+ let promptTemplate;
387
+ let promptLength;
388
+ let promptExplanation;
389
+ let promptToolUse;
390
+ let maxCommandCount;
383
391
  const seenFlags = [];
384
392
  const commandTemplates = {};
385
393
  // The flag whitelist runs before any value is interpreted so an unsupported flag is reported as such.
@@ -458,6 +466,24 @@ export function parseRunExperimentArgs(argv) {
458
466
  else if (arg === "--agent") {
459
467
  agentFlagValues.push(readRequiredValue(argv, ++index, "--agent"));
460
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
+ }
461
487
  else if (arg === "--repair-attempts") {
462
488
  repairAttempts = parseRepairAttempts(readRequiredValue(argv, ++index, "--repair-attempts"));
463
489
  }
@@ -649,6 +675,10 @@ export function parseRunExperimentArgs(argv) {
649
675
  if (timeoutMs !== undefined && timeoutMs > AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS) {
650
676
  throw new Error(`--timeout-ms must be a positive integer no greater than ${AGENT_SUCCESS_RATE_MAX_AGENT_TIMEOUT_MS}.`);
651
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
+ }
652
682
  if (seenFlags.includes("--case") && caseIds.length === 0) {
653
683
  throw new Error("--case must list at least one case id.");
654
684
  }
@@ -657,6 +687,10 @@ export function parseRunExperimentArgs(argv) {
657
687
  }
658
688
  }
659
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
+ }
660
694
  if (agentFlagValues.length > 0) {
661
695
  throw new Error(`--agent is only supported for --experiment ${agentSuccessRatePlugin.metadata.id}.`);
662
696
  }
@@ -747,6 +781,11 @@ export function parseRunExperimentArgs(argv) {
747
781
  commandTemplates: Object.keys(commandTemplates).length > 0 ? commandTemplates : undefined,
748
782
  agentId: agentFlagValues[0],
749
783
  repairAttempts,
784
+ promptTemplate,
785
+ promptLength,
786
+ promptExplanation,
787
+ promptToolUse,
788
+ maxCommandCount,
750
789
  kitCommand,
751
790
  campaignPreset,
752
791
  contextBudgets
@@ -1103,6 +1142,13 @@ function readRequiredValue(argv, index, label) {
1103
1142
  }
1104
1143
  return value;
1105
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
+ }
1106
1152
  function parseRepairAttempts(value) {
1107
1153
  if (!/^[0-2]$/.test(value)) {
1108
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
  /**