@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.
- package/CHANGELOG.md +6 -1
- package/README.md +7 -5
- package/dist/src/agents/adapters/codexAdapter.js +4 -1
- package/dist/src/cli/help.js +38 -2
- package/dist/src/cli/runLabCli.js +9 -1
- package/dist/src/commands/buildGalleryCommand.js +16 -3
- package/dist/src/commands/runComparePromptsCommand.js +109 -0
- package/dist/src/commands/runExperimentRunCommand.js +47 -2
- package/dist/src/core/processTree.js +41 -17
- package/dist/src/core/runMeasuredCommand.js +14 -8
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +23 -1
- package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
- package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
- package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
- package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
- package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
- package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
- package/dist/src/gallery/galleryPathSafety.js +111 -0
- package/dist/src/gallery/readGallerySources.js +542 -0
- package/dist/src/gallery/writeGalleryManifest.js +57 -3
- package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
- package/dist/src/report/experiments/htmlSafety.js +32 -0
- package/dist/src/report/experiments/metricEvidence.js +389 -0
- package/dist/src/report/experiments/metricGlossary.js +227 -0
- package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
- package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
- package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
- package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
- package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
- package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
- package/dist/src/report/experiments/specializedReportSections.js +142 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
- package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
- package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
- package/dist/src/securityValidation/commandRunner.js +13 -6
- package/docs/METRICS.md +11 -1
- 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
|
-
## [
|
|
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
|
-
|
|
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.
|
|
13
|
+
v0.9.2 is the previous package release.
|
|
14
14
|
|
|
15
|
-
The v0.9.2 release
|
|
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
|
|
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
|
|
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
|
|
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}`;
|
package/dist/src/cli/help.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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;
|