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

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 (48) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +10 -3
  3. package/dist/src/agents/adapters/claudeAdapter.js +6 -3
  4. package/dist/src/agents/adapters/codexAdapter.js +51 -10
  5. package/dist/src/agents/index.js +1 -0
  6. package/dist/src/agents/normalizeAgentTelemetry.js +50 -0
  7. package/dist/src/agents/parseAgentTokenUsage.js +71 -49
  8. package/dist/src/agents/providerBuildObservation.js +97 -0
  9. package/dist/src/audits/core/projectInventory.js +1 -0
  10. package/dist/src/cli/help.js +8 -2
  11. package/dist/src/commands/runExperimentDescribeCommand.js +31 -0
  12. package/dist/src/commands/runExperimentRunCommand.js +193 -5
  13. package/dist/src/core/runMeasuredCommand.js +4 -0
  14. package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +3 -1
  15. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpoint.js +216 -0
  16. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +588 -0
  17. package/dist/src/experiments/plugins/agentSuccessRate/campaignEvidenceQuality.js +77 -0
  18. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +314 -0
  19. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +111 -0
  20. package/dist/src/experiments/plugins/agentSuccessRate/campaignProgress.js +175 -0
  21. package/dist/src/experiments/plugins/agentSuccessRate/campaignQueue.js +11 -0
  22. package/dist/src/experiments/plugins/agentSuccessRate/campaignRecovery.js +256 -0
  23. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +207 -0
  24. package/dist/src/experiments/plugins/agentSuccessRate/index.js +6 -0
  25. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +32 -22
  26. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +29 -7
  27. package/dist/src/experiments/plugins/agentSuccessRate/timeoutPresets.js +9 -0
  28. package/dist/src/report/experiments/agentSuccessCampaignProgressReportModel.js +1 -0
  29. package/dist/src/report/experiments/buildAgentSuccessCampaignProgressReport.js +6 -0
  30. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +11 -0
  31. package/dist/src/report/experiments/buildPluginExperimentReport.js +1 -1
  32. package/dist/src/report/experiments/renderAgentSuccessCampaignProgressHtml.js +6 -0
  33. package/dist/src/report/experiments/renderAgentSuccessCampaignProgressText.js +25 -0
  34. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +3 -1
  35. package/dist/src/report/experiments/renderAgentSuccessRateText.js +7 -0
  36. package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +139 -0
  37. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -3
  38. package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
  39. package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +34 -4
  40. package/dist/src/securityValidation/packageChecks/requiredPackageContents.js +106 -2
  41. package/dist/src/securityValidation/packageChecks/runPackageChecks.js +31 -16
  42. package/dist/src/securityValidation/staticScans/semgrep.js +68 -64
  43. package/dist/src/securityValidation/types.js +0 -4
  44. package/dist/src/securityValidation/validate/runSecurityValidation.js +1 -1
  45. package/docs/METRICS.md +9 -0
  46. package/examples/tutorial-browser/prepare.mjs +13 -13
  47. package/examples/tutorial-browser/server.mjs +13 -13
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.2] - 2026-10-13
8
+
9
+ ### Security validation and cross-platform corrections
10
+
11
+ - 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.
12
+ - 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.
13
+ - Corrected Semgrep failed and incomplete scan evidence so neither can produce a false `PASS`; unavailable optional scanners remain explicitly `SKIPPED`.
14
+ - Corrected campaign filesystem identity and path-safety behavior and cross-platform compatibility.
15
+ - Preserved security verdict strictness, severity mappings, mandatory checks, and existing command and report contracts.
16
+
17
+ This section records the intended v0.9.2 release contents. Registry publication status is established through npm evidence, not this changelog.
18
+
19
+ ## [0.9.1] - 2026-10-12
20
+
21
+ ### Normalized provider telemetry and resumable campaigns
22
+
23
+ - Normalized provider token and duration provenance while preserving legacy parser compatibility and separating unavailable telemetry from task outcomes.
24
+ - Added deterministic agent-success-rate campaign queues, immutable checkpoint generations, version 1 compatibility and version 2 recovery epochs, explicit resume and verified completed-unit reuse, and bounded provider-limit continuation.
25
+ - Added public campaign CLI options, Codex and Claude timeout presets, campaign progress summaries, and strong/partial/unavailable metric-specific evidence classifications.
26
+ - 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.
27
+
7
28
  ## [0.9.0] - 2026-10-10
8
29
 
9
30
  ### Agent success rate
package/README.md CHANGED
@@ -6,11 +6,15 @@ 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.0 (agent-success-rate implementation-patch evaluation), and the repository package version is 0.9.0. The previous release is v0.8.2 (context-pack generation experiments), preceded by v0.8.1 (retrieval query strategy comparison). 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
+ 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.
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
- Planned after that: v0.9.1 (provider telemetry and campaign scheduling) and v0.9.2.
13
+ v0.9.1 is the previous published package release.
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.
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.
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
 
@@ -27,6 +31,7 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
27
31
  - **Measure retrieval precision and recall (v0.8.0, released):** the `retrieval-precision-recall` plugin runs the existing my-dev-kit search, lookup, slice, and source lifecycle for each case and compares the retrieved files and symbols with a deterministic answer key, without invoking any agent. It reports file precision and recall, symbol precision and recall, fact coverage (from explicit fact-to-context mappings), irrelevant context ratio, the estimated retrieved token count, and the missed and irrelevant identities, as `retrieval-precision-recall-execution.json` plus `report.json`, `report.html`, and `report.txt`. By default it runs a bundled 12-case corpus over two benchmark projects against the Lab itself. It can also run over an explicitly selected local Git repository with `--target <repository> --local-subject-config <path>`; in that mode the repository is treated as read-only and durable outputs redact the repository path and file, symbol, and fact identities while keeping the numeric evidence. These are set-based measures with no ranking, winner, or composite score. See [docs/COMMANDS.md](docs/COMMANDS.md), [docs/WORKFLOWS.md](docs/WORKFLOWS.md), and [docs/METRICS.md](docs/METRICS.md).
28
32
  - **Compare retrieval query strategies (v0.8.1, released):** the `retrieval-query-strategy-comparison` plugin compares seven fixed my-dev-kit retrieval strategies without running any coding agent: `keyword-search`, `symbol-lookup`, `graph-neighborhood`, `source-slice`, `data-model-graph`, `model-view-lineage`, and `combined-graph-guided`. In bundled mode it uses the same frozen 12-case warm-index benchmark corpus as `retrieval-precision-recall`, and every selected case receives all seven strategies (filters narrow cases or projects, never strategies). File and symbol quality keeps the inherited v0.8.0 set-based semantics and adds balanced file and symbol F1 (`2TP / (2TP + FP + FN)`); fact coverage and the estimated retrieved token count complete the four comparison objectives. Strategies are compared over matched complete cases only, as macro means for four scopes (`overall`, `localized`, `cross-module`, `broad-change`, reusing each case's existing task-locality label), using Pareto dominance on mean file F1, mean symbol F1, mean fact coverage (all maximized) and mean retrieved tokens (minimized). A best strategy is named only when the Pareto front has a single member; when several strategies remain nondominated the report states a tradeoff. There is no scalar or composite score and no ordinal ranking. The plugin can also run over an explicitly selected local Git repository through the existing `LocalRepositorySubject` boundary with `--target <repository> --local-subject-config <path>`; the repository stays read-only and durable output redacts private file, symbol, fact, semantic-node, warning, and case-title identities while preserving the numeric results. It writes `retrieval-query-strategy-comparison-execution.json`, `retrieval-query-strategy-comparison-analysis.json`, and `report.json`, `report.html`, and `report.txt`. No context-pack behavior is included. See [docs/COMMANDS.md](docs/COMMANDS.md), [docs/WORKFLOWS.md](docs/WORKFLOWS.md), and [docs/METRICS.md](docs/METRICS.md#retrieval-query-strategy-comparison-metrics-v081).
29
33
  - **Generate auditable task context packs (v0.8.2):** `context-pack-generation` runs both fixed treatments, `raw-full-file` and `context-pack`, for each selected case. A pack contains a task summary, relevant files and symbols, bounded source slices, call relationships, tests, and evidence notes. Analysis reuses retrieval fact coverage and file/symbol precision, recall, and F1, reports estimated context size and paired raw-versus-pack tokens saved, percent saved, and objective deltas, and names no composite winner or ranking. Bundled runs persist pack artifacts and reports include a bounded preview. External-local runs calculate science on real identities, redact before persistence, omit durable pack bodies, and show redacted report previews. See [docs/COMMANDS.md](docs/COMMANDS.md), [docs/WORKFLOWS.md](docs/WORKFLOWS.md), and [docs/METRICS.md](docs/METRICS.md).
34
+ - **Run resumable agent-success-rate campaigns (v0.9.1):** provider telemetry is normalized with provenance, and explicit campaign runs support deterministic case ordering, checkpoints, one-case execution, validated resume, safe completed-unit reuse, provider-limit pauses, authorized bounded continuation, and plugin-specific progress evidence. Campaigns use the existing Codex and Claude adapters only after explicit real-provider opt-in. Checkpoint/progress evidence is operational; complete scientific reports are available only after finalization.
30
35
  - **Evaluate implementation patches (v0.9.0):** the `agent-success-rate` plugin runs both `raw-full-file` and `context-pack` treatments over a bundled six-task implementation corpus in disposable benchmark copies and decides success from trusted checks on the changed code. The default deterministic mode applies reference patches and runs no coding agent; real Codex or Claude runs (with optional bounded repair) are explicit opt-in. It runs from the installed CLI or a built source checkout (see [docs/COMMANDS.md](docs/COMMANDS.md#agent-success-rate-v090)).
31
36
  - **Assess affected-neighborhood evidence (v0.6.1, released; retained in the current package):** using the baseline my-dev-kit graph of the same prepared warm index (loaded once per session), each task's confirmed modified or missing indexed files, and the baseline symbols those files contain, are mapped to graph nodes. The affected neighborhood is those seed nodes plus their direct one-hop neighbors (edges of any kind, either direction). The task's `expectedFiles` and file-scoped `expectedSymbols` are mapped to graph nodes and compared with that neighborhood, which yields six numeric warm-side metrics (`changedFileCount`, `changedSymbolCount`, `affectedNodeCount`, `affectedEdgeCount`, `taskOverlapCount`, `taskOverlapPercent`) plus categorical evidence: a `related`, `unrelated`, or `unknown` relationship and a `recommended`, `not-indicated`, or `unknown` `reindexRecommendation`. Incomplete evidence is reported as `unknown`, never as unrelated. The assessment is recorded additively in `warm-index-execution.json` and shown in the existing `report.json`/`report.txt`/`report.html` warm-index reports. It is observational only: retrieval, execution status, and provider status are unchanged, nothing is reindexed automatically, and no CLI flag, experiment plugin, plot, or gallery item was added. See [docs/METRICS.md](docs/METRICS.md#affected-neighborhood-metrics-v061) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment).
32
37
  - **Audit repository health:** run conservative code-rot detectors for TypeScript/JavaScript, Python, Java, and Kotlin, or adapt security findings into the common audit report.
@@ -259,7 +264,7 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
259
264
  - Claude and Codex token totals are used only when the CLI/run exposes them; missing provider token evidence stays explicitly unavailable (never zero, and never substituted with an estimated context-token count)
260
265
  - Codex and Claude campaign runs can produce timeouts, invalid-output, agent-unavailable, or agent-limit-reached outcomes; these are structured provider states, not warm-index infrastructure failures
261
266
  - Small projects may make raw-full-file cheaper than my-dev-kit-guided; larger localized tasks are where my-dev-kit is expected to become more useful
262
- - The generic experiment-plugin framework has eight registered plugins: `context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, `context-window-scaling`, `retrieval-precision-recall`, `retrieval-query-strategy-comparison`, `context-pack-generation`, and `agent-success-rate` (v0.9.0). The v0.7.0 context-window-scaling plugin is released; v0.7.1 added caller-supplied synthetic repositories; v0.7.2 added an explicitly selected local Git repository. The context-pack-generation plugin shipped in v0.8.2; the agent-success-rate plugin shipped in v0.9.0.
267
+ - The generic experiment-plugin framework has eight registered plugins: `context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, `context-window-scaling`, `retrieval-precision-recall`, `retrieval-query-strategy-comparison`, `context-pack-generation`, and `agent-success-rate` (v0.9.0). The v0.7.0 context-window-scaling plugin is released; v0.7.1 added caller-supplied synthetic repositories; v0.7.2 added an explicitly selected local Git repository. The context-pack-generation plugin shipped in v0.8.2; agent-success-rate shipped in v0.9.0 and gained resumable campaigns in v0.9.1.
263
268
  - `retrieval-precision-recall` (v0.8.0) measures set-based retrieval quality, not ranked retrieval: it has no ranking metrics, no retrieval-strategy comparison of its own (the separate `retrieval-query-strategy-comparison` plugin added in v0.8.1 compares strategies), no winner or composite score, and no real agents. Its lifecycle expands only the top search candidate. An external-local run fails as a whole if any case fails a safety or execution check, and then writes no normal artifact or report family. Durable external-local identities are redacted, although the console summary of a successful run can still show the physical output directory. Platform path-length limits can make a run with very long paths fail cleanly instead of executing.
264
269
  - `retrieval-query-strategy-comparison` (v0.8.1) runs no coding agent and does not evaluate answer quality. Its retrieved token count is a context-size estimate, not provider billing telemetry. Semantic strategies (`data-model-graph`, `model-view-lineage`) can truthfully be partial or unavailable for a case, and a case is then excluded from the matched cross-strategy comparison for every strategy while its per-strategy evidence is kept. The Pareto front is a set of nondominated strategies in canonical strategy order, not an ordinal ranking. The plugin reports no significance test or confidence interval and selects no strategy automatically. External-local mode rejects `--case` and `--benchmark-project`, builds one private base index per configured case, and fails as a whole, writing no normal artifact or report family, if any safety or privacy check fails.
265
270
  - `warm-index-reuse` without `--campaign` still uses the deterministic fake agent only. With `--campaign`, exactly one real provider (Codex or Claude) runs per campaign preset — there is no multi-agent matrix, retry, provider switching, or scheduler. The v0.5.1 expanded corpus is deterministic fake-agent benchmark evidence when run without `--campaign`. No warm-index mode calculates a token-savings percentage, break-even task, winner, or ranking, and component-duration sums are not wall-clock latency
@@ -283,6 +288,8 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
283
288
 
284
289
  `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.
285
290
 
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).
292
+
286
293
  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.
287
294
 
288
295
  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.
@@ -54,8 +54,8 @@ export const claudeAdapter = {
54
54
  };
55
55
  /**
56
56
  * Parses a single JSON result envelope (`{ "result": "..." }`) when stdout is valid JSON, never
57
- * the raw envelope itself. Falls back to the legacy plain-text behavior when stdout is not valid
58
- * JSON, so existing argv-mode text output keeps working unchanged.
57
+ * the raw envelope itself. Malformed JSON-shaped output stays empty; legacy plain text output
58
+ * retains its fallback.
59
59
  */
60
60
  export function parseClaudeFinalAnswer(text) {
61
61
  const trimmedInput = text.trim();
@@ -71,9 +71,12 @@ export function parseClaudeFinalAnswer(text) {
71
71
  // Valid JSON object without a string result: empty, never the raw envelope.
72
72
  return { finalAnswerText: "", finalAnswerParseStatus: "empty" };
73
73
  }
74
+ return { finalAnswerText: "", finalAnswerParseStatus: "empty" };
74
75
  }
75
76
  catch {
76
- // Not valid JSON; fall through to the legacy plain-text behavior below.
77
+ // A malformed envelope is not a legacy text answer.
78
+ if (/^[\[{]/.test(trimmedInput))
79
+ return { finalAnswerText: "", finalAnswerParseStatus: "empty" };
77
80
  }
78
81
  }
79
82
  return { finalAnswerText: trimmedInput, finalAnswerParseStatus: trimmedInput ? "parsed" : "empty" };
@@ -1,6 +1,7 @@
1
1
  import { runMeasuredCommand } from "../../core/runMeasuredCommand.js";
2
2
  import { applyPromptToCommandTemplate } from "../runAgentPrompt.js";
3
3
  import { parseAgentTokenUsage } from "../parseAgentTokenUsage.js";
4
+ import { trustedProviderLaunch } from "../providerBuildObservation.js";
4
5
  // v0.5.2 Batch 2 -- frozen stdin invocation for the real-agent evaluation transport. Every flag
5
6
  // here is planner-owned; do not add sandbox/model/approval-policy flags in this batch.
6
7
  const CODEX_STDIN_ARGS = [
@@ -51,13 +52,21 @@ export const codexAdapter = {
51
52
  /**
52
53
  * Recognizes a Codex JSONL event stream (at least one line parses to an object with a string
53
54
  * `type`) and, when recognized, uses the LAST `item.completed` `agent_message` text as the final
54
- * answer -- never the raw JSONL. Falls back to the legacy plain-text behavior when no event-shaped
55
- * line is found, so non-JSONL Codex output (and existing tests) keep working unchanged.
55
+ * answer -- never the raw JSONL. JSON-shaped output without an answer stays empty. Plain text
56
+ * retains the legacy fallback. A single formatted JSON event is supported as well.
56
57
  */
57
58
  export function parseCodexFinalAnswer(text) {
58
- let sawEventLine = false;
59
+ let sawEventLine = /^[\s]*[\[{]/.test(text);
59
60
  let lastAgentMessageText = null;
60
- for (const line of text.split(/\r?\n/)) {
61
+ const candidates = function* () {
62
+ const trimmed = text.trim();
63
+ if (trimmed.startsWith("{") || trimmed.startsWith("["))
64
+ yield trimmed;
65
+ for (const match of text.matchAll(/[^\r\n]+/g))
66
+ if (match[0].trim() !== trimmed)
67
+ yield match[0];
68
+ };
69
+ for (const line of candidates()) {
61
70
  const trimmedLine = line.trim();
62
71
  if (!trimmedLine)
63
72
  continue;
@@ -69,7 +78,10 @@ export function parseCodexFinalAnswer(text) {
69
78
  // Agent output can mix JSONL with incidental non-JSON noise; ignore malformed lines.
70
79
  continue;
71
80
  }
72
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
81
+ if (!parsed || typeof parsed !== "object")
82
+ continue;
83
+ sawEventLine = true;
84
+ if (Array.isArray(parsed))
73
85
  continue;
74
86
  const event = parsed;
75
87
  if (typeof event.type !== "string")
@@ -95,7 +107,10 @@ export function parseCodexFinalAnswer(text) {
95
107
  export async function runCliAgent(request, adapter) {
96
108
  const started = Date.now();
97
109
  const command = adapter.buildCommand(request);
98
- const available = await adapter.isAvailable(request);
110
+ if (request.trustedExecutable && (request.commandTemplate || request.promptTransport !== "stdin" || adapter.id === "fake-agent"))
111
+ throw new Error("INVALID_TRUSTED_PROVIDER_REQUEST");
112
+ const launch = request.trustedExecutable ? await trustedProviderLaunch(request.trustedExecutable, adapter.id) : null;
113
+ const available = launch !== null || await adapter.isAvailable(request);
99
114
  if (!available) {
100
115
  const ended = Date.now();
101
116
  const status = request.requireAvailable ? "failed" : "skipped";
@@ -127,18 +142,20 @@ export async function runCliAgent(request, adapter) {
127
142
  }
128
143
  const measured = await runMeasuredCommand({
129
144
  commandId: `${adapter.id}-agent-run`,
130
- commandString: command.command,
131
- extraArgs: command.args,
145
+ ...(launch ? { executable: launch.executable, args: [...launch.prefix, ...command.args], resolveCommand: false } : { commandString: command.command, extraArgs: command.args }),
132
146
  cwd: request.commandTemplate?.cwd ?? request.cwd,
133
147
  outDir: request.outDir,
134
148
  env: request.env,
135
149
  timeoutMs: request.timeoutMs,
136
- stdinText: command.stdinText
150
+ stdinText: command.stdinText,
151
+ ...(launch ? { stdoutMaxBytes: 8 * 1024 * 1024, stderrMaxBytes: 1024 * 1024, terminateProcessTree: true } : {})
137
152
  });
138
153
  const ended = Date.now();
139
154
  const combinedOutput = `${measured.stdout}\n${measured.stderr}`;
140
155
  const parsedAnswer = adapter.parseFinalAnswer(measured.stdout || measured.stderr);
141
156
  const parsedUsage = adapter.parseTokenUsage(combinedOutput);
157
+ // Bound campaigns require process-channel diagnostics. Never search final agent message/patch bodies.
158
+ const condition = launch ? trustedProcessCondition(measured.stdout, measured.stderr, measured.timedOut === true) : null;
142
159
  return {
143
160
  runId: request.runId,
144
161
  agentId: adapter.id,
@@ -164,6 +181,30 @@ export async function runCliAgent(request, adapter) {
164
181
  tokenUsageSource: parsedUsage.tokenUsageSource,
165
182
  tokenUsageReliability: parsedUsage.tokenUsageReliability,
166
183
  warnings: parsedUsage.warnings,
167
- errors: measured.ok ? [] : [measured.error ?? "Agent command failed."]
184
+ errors: [...(measured.ok ? [] : [measured.error ?? "Agent command failed."]), ...(condition === "limit" ? ["Provider rate limit reached."] : condition === "timeout" ? ["Provider command timed out."] : [])]
168
185
  };
169
186
  }
187
+ function trustedProcessCondition(stdout, stderr, timedOut) {
188
+ if (timedOut)
189
+ return "timeout";
190
+ const diagnostics = [];
191
+ const isPatch = (text) => /^(?:diff --git |@@ |\+\+\+ |--- )/m.test(text);
192
+ // Non-interactive CLI diagnostic lines, not arbitrary substrings or diff additions.
193
+ if (!isPatch(stderr))
194
+ diagnostics.push(...stderr.split(/\r?\n/).filter(line => /^(?:error:\s*)?(?:rate limit (?:exceeded|reached)|usage limit (?:exceeded|reached)|quota (?:exceeded|exhausted)|too many requests|you(?:'ve| have) (?:hit|reached) (?:your |the )?(?:usage |session |rate )?limit)/i.test(line.trim())));
195
+ for (const line of stdout.split(/\r?\n/).slice(0, 1000)) {
196
+ try {
197
+ const event = JSON.parse(line);
198
+ if (!event || typeof event !== "object")
199
+ continue;
200
+ if (event.type !== "error" && event.type !== "turn.failed" && event.is_error !== true)
201
+ continue;
202
+ const error = event.error;
203
+ const message = typeof error === "string" ? error : error && typeof error === "object" ? error.message : event.message;
204
+ if (typeof message === "string" && message.length <= 2048 && !isPatch(message))
205
+ diagnostics.push(message);
206
+ }
207
+ catch { /* Not a structured error event; agent output is deliberately not diagnostic evidence. */ }
208
+ }
209
+ return diagnostics.some(text => /\b(?:rate\s+limit|usage\s+limit|quota|too many requests|session\s+limit)\b/i.test(text)) ? "limit" : null;
210
+ }
@@ -2,6 +2,7 @@ export * from "./types.js";
2
2
  export * from "./agentRegistry.js";
3
3
  export * from "./runAgentPrompt.js";
4
4
  export * from "./parseAgentTokenUsage.js";
5
+ export { normalizeAgentTokenUsageV091, normalizeAgentDurationV091 } from "./normalizeAgentTelemetry.js";
5
6
  export * from "./adapters/fakeAgentAdapter.js";
6
7
  export * from "./adapters/codexAdapter.js";
7
8
  export * from "./adapters/claudeAdapter.js";
@@ -0,0 +1,50 @@
1
+ const EVIDENCE_LEVEL = {
2
+ "provider-reported": "provider-reported",
3
+ "cli-json": "cli-reported",
4
+ "agent-reported": "parsed-from-output",
5
+ "estimated-from-text": "estimated",
6
+ otel: "unavailable",
7
+ unavailable: "unavailable"
8
+ };
9
+ function normalizeCount(value) {
10
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 ? value : null;
11
+ }
12
+ /**
13
+ * Classifies the legacy evidence channel, not the origin of each individual field.
14
+ * Parser-derived totals are preserved; missing totals are never calculated here.
15
+ * The existing contract has no verified OTel attribution, so its channel remains unavailable.
16
+ */
17
+ export function normalizeAgentTokenUsageV091(usage, reliability) {
18
+ const counts = {
19
+ totalTokens: normalizeCount(usage.totalTokens),
20
+ inputTokens: normalizeCount(usage.inputTokens),
21
+ outputTokens: normalizeCount(usage.outputTokens),
22
+ cachedInputTokens: normalizeCount(usage.cachedInputTokens),
23
+ reasoningTokens: normalizeCount(usage.reasoningTokens)
24
+ };
25
+ const numericReasons = {};
26
+ for (const key of Object.keys(counts)) {
27
+ numericReasons[key] = counts[key] !== null ? null : usage[key] == null ? "missing-token-count" : "invalid-token-count";
28
+ }
29
+ const hasCount = Object.values(counts).some((count) => count !== null);
30
+ return {
31
+ source: usage.source,
32
+ reliability,
33
+ evidenceLevel: EVIDENCE_LEVEL[usage.source],
34
+ classificationReason: usage.source === "otel" ? "unattributed-source" : usage.source === "unavailable" ? "source-unavailable" : null,
35
+ numericAvailability: hasCount ? "available" : "unavailable",
36
+ condition: hasCount ? "token-available" : "token-unavailable",
37
+ ...counts,
38
+ numericReasons
39
+ };
40
+ }
41
+ /** Accepts the current Lab wall-clock measurement, never provider-reported timing. */
42
+ export function normalizeAgentDurationV091(durationMs) {
43
+ if (durationMs == null) {
44
+ return { durationMs: null, source: "unavailable", reason: "missing-duration" };
45
+ }
46
+ if (!Number.isFinite(durationMs) || durationMs < 0) {
47
+ return { durationMs: null, source: "unavailable", reason: "invalid-duration" };
48
+ }
49
+ return { durationMs, source: "lab-wall-clock", reason: null };
50
+ }
@@ -13,11 +13,11 @@ const numberFields = {
13
13
  };
14
14
  export function parseAgentTokenUsage(text) {
15
15
  const warnings = [];
16
- const jsonUsage = parseJsonUsage(text);
16
+ const jsonUsage = parseJsonUsage(text, warnings);
17
17
  if (jsonUsage.found) {
18
18
  return buildResult(jsonUsage.usage, "cli-json", text, warnings);
19
19
  }
20
- const plainTextUsage = parsePlainTextUsage(text);
20
+ const plainTextUsage = jsonUsage.structured ? { found: false, usage: {} } : parsePlainTextUsage(text);
21
21
  if (plainTextUsage.found) {
22
22
  return buildResult(plainTextUsage.usage, "agent-reported", text, warnings);
23
23
  }
@@ -29,57 +29,84 @@ export function parseAgentTokenUsage(text) {
29
29
  warnings
30
30
  };
31
31
  }
32
- function parseJsonUsage(text) {
33
- const usage = {};
32
+ function parseJsonUsage(text, warnings) {
33
+ let usage;
34
+ let structured = false;
35
+ let visited = 0;
36
+ const warn = (message) => { if (!warnings.includes(message))
37
+ warnings.push(message); };
38
+ const visit = (value, depth) => {
39
+ if (!value || typeof value !== "object")
40
+ return;
41
+ if (++visited > 2048 || depth > 16) {
42
+ warn("Token usage traversal limit reached; additional envelopes were ignored.");
43
+ return;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ for (const child of value) {
47
+ if (visited > 2048)
48
+ break;
49
+ visit(child, depth + 1);
50
+ }
51
+ return;
52
+ }
53
+ const record = value;
54
+ const local = {};
55
+ for (const [key, aliases] of Object.entries(numberFields)) {
56
+ for (const alias of aliases) {
57
+ const count = record[alias];
58
+ if (count === undefined)
59
+ continue;
60
+ if (typeof count !== "number" || !Number.isFinite(count) || !Number.isInteger(count) || count < 0) {
61
+ warn("Invalid token count ignored in agent output.");
62
+ continue;
63
+ }
64
+ if (local[key] !== undefined && local[key] !== count)
65
+ warn("Conflicting token aliases; legacy maximum precedence retained within one usage record.");
66
+ local[key] = Math.max(local[key] ?? 0, count);
67
+ }
68
+ }
69
+ if (hasAnyUsage(local)) {
70
+ if (usage === undefined)
71
+ usage = local;
72
+ else if (JSON.stringify(usage) !== JSON.stringify(local))
73
+ warn("Multiple token usage records; first valid record retained without combining attempts.");
74
+ }
75
+ // Supported envelope traversal only: patch/tool payload objects are not usage metadata.
76
+ for (const key of ["usage", "token_usage", "tokenUsage", "response", "message", "data", "event", "result"]) {
77
+ if (visited > 2048)
78
+ break;
79
+ visit(record[key], depth + 1);
80
+ }
81
+ };
82
+ let candidates = 0;
34
83
  for (const candidate of collectJsonCandidates(text)) {
84
+ if (++candidates > 4096) {
85
+ warn("Token usage candidate limit reached; additional lines were ignored.");
86
+ break;
87
+ }
35
88
  try {
36
- mergeUsage(usage, extractUsage(JSON.parse(candidate)));
89
+ const value = JSON.parse(candidate);
90
+ structured = true;
91
+ visit(value, 0);
37
92
  }
38
93
  catch {
39
94
  // Agent output is often mixed JSONL plus text; malformed lines are ignored.
40
95
  }
41
96
  }
42
- return { found: hasAnyUsage(usage), usage };
97
+ return { found: usage !== undefined, usage: usage ?? {}, structured };
43
98
  }
44
- function collectJsonCandidates(text) {
99
+ function* collectJsonCandidates(text) {
45
100
  const trimmed = text.trim();
46
- const candidates = [];
47
101
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
48
- candidates.push(trimmed);
102
+ yield trimmed;
49
103
  }
50
- for (const line of text.split(/\r?\n/)) {
51
- const lineTrimmed = line.trim();
52
- if (lineTrimmed.startsWith("{") || lineTrimmed.startsWith("[")) {
53
- candidates.push(lineTrimmed);
104
+ for (const match of text.matchAll(/[^\r\n]+/g)) {
105
+ const lineTrimmed = match[0].trim();
106
+ if (lineTrimmed !== trimmed && (lineTrimmed.startsWith("{") || lineTrimmed.startsWith("["))) {
107
+ yield lineTrimmed;
54
108
  }
55
109
  }
56
- return [...new Set(candidates)];
57
- }
58
- function extractUsage(value) {
59
- const usage = {};
60
- visit(value, usage);
61
- return usage;
62
- }
63
- function visit(value, usage) {
64
- if (!value || typeof value !== "object") {
65
- return;
66
- }
67
- if (Array.isArray(value)) {
68
- value.forEach((item) => visit(item, usage));
69
- return;
70
- }
71
- const record = value;
72
- for (const [usageKey, fieldNames] of Object.entries(numberFields)) {
73
- for (const fieldName of fieldNames) {
74
- const fieldValue = record[fieldName];
75
- if (typeof fieldValue === "number" && Number.isFinite(fieldValue)) {
76
- usage[usageKey] = Math.max(usage[usageKey] ?? 0, fieldValue);
77
- }
78
- }
79
- }
80
- for (const child of Object.values(record)) {
81
- visit(child, usage);
82
- }
83
110
  }
84
111
  function parsePlainTextUsage(text) {
85
112
  const usage = {};
@@ -91,20 +118,15 @@ function parsePlainTextUsage(text) {
91
118
  ["totalTokens", /total\s+tokens?\s*[:=]\s*(\d+)/i]
92
119
  ];
93
120
  for (const [key, pattern] of patterns) {
94
- const match = text.match(pattern);
121
+ const match = text.match(new RegExp(pattern.source.replace("(\\d+)", "(\\d+)(?![\\d.eE])"), pattern.flags));
95
122
  if (match?.[1]) {
96
- usage[key] = Number(match[1]);
123
+ const count = Number(match[1]);
124
+ if (Number.isFinite(count) && Number.isInteger(count))
125
+ usage[key] = count;
97
126
  }
98
127
  }
99
128
  return { found: hasAnyUsage(usage), usage };
100
129
  }
101
- function mergeUsage(target, source) {
102
- for (const key of Object.keys(numberFields)) {
103
- if (source[key] !== undefined) {
104
- target[key] = Math.max(target[key] ?? 0, source[key]);
105
- }
106
- }
107
- }
108
130
  function hasAnyUsage(usage) {
109
131
  return Object.values(usage).some((value) => typeof value === "number");
110
132
  }
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { resolveCommand } from "../core/resolveCommand.js";
6
+ import { runMeasuredCommand } from "../core/runMeasuredCommand.js";
7
+ const launches = new WeakMap();
8
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
9
+ const fail = () => { throw new Error("PROVIDER_BUILD_UNVERIFIABLE: use a native executable or an attributable node entrypoint; unsupported wrappers cannot authorize a campaign."); };
10
+ async function inspect(alias) {
11
+ const actual = await realpath(alias);
12
+ const stat = await lstat(actual);
13
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024 * 1024 || stat.size === 0)
14
+ return fail();
15
+ const bytes = await readFile(actual);
16
+ if (bytes.length !== stat.size || await realpath(alias) !== actual)
17
+ return fail();
18
+ return { proof: { alias: path.resolve(alias), actual, digest: hash(bytes) }, bytes };
19
+ }
20
+ function native(bytes) {
21
+ return bytes.subarray(0, 2).toString() === "MZ" || bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) || ["feedface", "feedfacf", "cefaedfe", "cffaedfe", "cafebabe"].includes(bytes.subarray(0, 4).toString("hex"));
22
+ }
23
+ /** Read-only task-free observation. All temporary version diagnostics are removed. No shell templates. */
24
+ export async function observeProviderBuild(providerId, options = {}) {
25
+ if (providerId !== "codex" && providerId !== "claude")
26
+ return fail();
27
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "lab-provider-build-"));
28
+ try {
29
+ const env = { ...process.env, ...options.env };
30
+ const resolved = resolveCommand(providerId, { cwd, env, allowPowerShellShim: false });
31
+ if (!resolved.resolvedPath || resolved.resolutionKind === "unavailable")
32
+ return fail();
33
+ const selected = await inspect(resolved.resolvedPath);
34
+ let executable = selected.proof.actual;
35
+ const prefix = [];
36
+ const files = [selected.proof];
37
+ let limitation = "Observed launch bytes only; transitive dependencies are not attested.";
38
+ if (!native(selected.bytes)) {
39
+ const text = selected.bytes.toString("utf8");
40
+ // A deliberately closed, attributable wrapper subset. Never execute arbitrary wrapper text.
41
+ const cmd = /^@echo off\r?\n(?:node|"([^"\r\n]+)") "([^"\r\n]+)" %\*\r?\n?$/i.exec(text);
42
+ const shebang = /^#!\/usr\/bin\/env node\r?\n/.test(text);
43
+ if (!cmd && !shebang)
44
+ return fail();
45
+ const node = resolveCommand(cmd?.[1] ?? "node", { cwd, env, allowPowerShellShim: false });
46
+ if (!node.resolvedPath || node.resolutionKind === "unavailable")
47
+ return fail();
48
+ const interpreter = await inspect(node.resolvedPath);
49
+ if (!native(interpreter.bytes))
50
+ return fail();
51
+ files.push(interpreter.proof);
52
+ executable = interpreter.proof.actual;
53
+ if (cmd) {
54
+ if (!path.isAbsolute(cmd[2]))
55
+ return fail();
56
+ const entry = await inspect(cmd[2]);
57
+ files.push(entry.proof);
58
+ prefix.push(entry.proof.actual);
59
+ }
60
+ else
61
+ prefix.push(selected.proof.actual);
62
+ limitation = "Restricted node wrapper, interpreter and entrypoint bytes observed; transitive dependencies are not attested.";
63
+ }
64
+ const version = await runMeasuredCommand({ commandId: `${providerId}-build-version`, executable, args: [...prefix, "--version"], resolveCommand: false, cwd, outDir: cwd, env, timeoutMs: 5000, stdoutMaxBytes: 4096, stderrMaxBytes: 4096, terminateProcessTree: true });
65
+ const observedVersion = version.stdout.trim();
66
+ if (!version.ok || version.timedOut || version.outputLimit || !/^[\x20-\x7e]{1,512}$/.test(observedVersion) || !/\d+\.\d+/.test(observedVersion))
67
+ return fail();
68
+ const capability = Object.freeze({ providerId });
69
+ const bytes = Buffer.from(JSON.stringify({ contract: "local-provider-launch-v1", providerId, origin: "inspected-local-executable", executable, prefix, files, version: observedVersion, adapterContract: `${providerId}-fixed-stdin-v1`, limitation }));
70
+ launches.set(capability, { executable, prefix, files, bytes });
71
+ await trustedProviderObservation(capability, providerId);
72
+ return capability;
73
+ }
74
+ catch {
75
+ return fail();
76
+ }
77
+ finally {
78
+ await rm(cwd, { recursive: true, force: true });
79
+ }
80
+ }
81
+ export async function trustedProviderObservation(capability, providerId) {
82
+ const launch = launches.get(capability);
83
+ if (!launch || capability.providerId !== providerId)
84
+ return fail();
85
+ for (const file of launch.files) {
86
+ const current = await inspect(file.alias);
87
+ if (current.proof.actual !== file.actual || current.proof.digest !== file.digest)
88
+ throw new Error("PROVIDER_BUILD_CHANGED");
89
+ }
90
+ return Buffer.from(launch.bytes);
91
+ }
92
+ /** Fixed adapter args are appended by the adapter owner; no PATH lookup occurs on this launch. */
93
+ export async function trustedProviderLaunch(capability, providerId) {
94
+ await trustedProviderObservation(capability, providerId);
95
+ const launch = launches.get(capability);
96
+ return { executable: launch.executable, prefix: [...launch.prefix] };
97
+ }
@@ -145,6 +145,7 @@ const EXCLUDED_DIR_NAMES = new Set([
145
145
  ".my-dev-kit",
146
146
  ".my-dev-kit-v1",
147
147
  ".my-dev-kit-lab",
148
+ ".my-dev-kit-context",
148
149
  "__pycache__",
149
150
  ".idea",
150
151
  ".vscode",
@@ -306,11 +306,16 @@ export function renderExperimentRunHelp() {
306
306
  " with fixed-category feedback and the untrusted previous diff; it",
307
307
  " never follows a provider, sandbox or verification infrastructure",
308
308
  " problem.",
309
- " Writes report.json, report.html and report.txt plus",
309
+ " Ordinary noncampaign execution writes report.json, report.html and report.txt plus",
310
310
  " agent-success-rate-execution.json and",
311
311
  " agent-success-rate-analysis.json. Initial-attempt and final success",
312
312
  " are reported separately. Results are descriptive for the finite",
313
- " corpus: no winner, ranking, composite score or significance claim."
313
+ " corpus: no winner, ranking, composite score or significance claim.",
314
+ "",
315
+ "agent-success-rate resumable campaigns and timeouts only:",
316
+ ...AGENT_CAMPAIGN_OPTION_HELP.map(line => ` ${line}`),
317
+ "Examples (replace bracketed values; explicit source-root is required when package source proof is unavailable):",
318
+ ...AGENT_CAMPAIGN_EXAMPLES.map(line => ` ${line}`)
314
319
  ].join("\n");
315
320
  }
316
321
  export function renderExperimentControlledHelp() {
@@ -527,3 +532,4 @@ export function renderTutorialRunHelp() {
527
532
  " 2 Invalid command usage"
528
533
  ].join("\n");
529
534
  }
535
+ import { AGENT_CAMPAIGN_EXAMPLES, AGENT_CAMPAIGN_OPTION_HELP } from "../commands/runExperimentDescribeCommand.js";