@dailephd/my-dev-kit-lab 0.7.1 → 0.7.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 (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +6 -5
  3. package/dist/src/cli/help.js +15 -4
  4. package/dist/src/commands/runExperimentDescribeCommand.js +5 -2
  5. package/dist/src/commands/runExperimentRunCommand.js +117 -18
  6. package/dist/src/core/fileGlobs.js +44 -0
  7. package/dist/src/evaluation/localRepositorySubject/config.js +296 -0
  8. package/dist/src/evaluation/localRepositorySubject/evaluationCase.js +36 -0
  9. package/dist/src/evaluation/localRepositorySubject/expectedFiles.js +51 -0
  10. package/dist/src/evaluation/localRepositorySubject/gitRepository.js +128 -0
  11. package/dist/src/evaluation/localRepositorySubject/index.js +8 -0
  12. package/dist/src/evaluation/localRepositorySubject/inventory.js +127 -0
  13. package/dist/src/evaluation/localRepositorySubject/loadLocalRepositorySubject.js +48 -0
  14. package/dist/src/evaluation/localRepositorySubject/manifest.js +52 -0
  15. package/dist/src/evaluation/localRepositorySubject/types.js +55 -0
  16. package/dist/src/evaluation/projectFileTree.js +45 -1
  17. package/dist/src/evaluation/runMyDevKitRetrieval.js +3 -1
  18. package/dist/src/evaluation/runRawFullFileBaseline.js +18 -4
  19. package/dist/src/evaluation/targetImmutability/captureTargetSnapshot.js +29 -7
  20. package/dist/src/evaluation/targetImmutability/compareTargetSnapshots.js +4 -0
  21. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectErrors.js +35 -0
  22. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExclusions.js +75 -0
  23. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExecution.js +136 -0
  24. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +128 -0
  25. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectScratch.js +85 -0
  26. package/dist/src/experiments/plugins/contextWindowScaling/metadata.js +2 -2
  27. package/dist/src/experiments/plugins/contextWindowScaling/plugin.js +93 -0
  28. package/dist/src/experiments/plugins/contextWindowScaling/projectProfile.js +4 -9
  29. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +4 -3
  30. package/dist/src/report/experiments/writePluginExperimentReports.js +8 -2
  31. package/docs/METRICS.md +2 -0
  32. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.2] - 2026-10-03
8
+
9
+ ### Local-repository experiments
10
+
11
+ - Added explicitly selected local Git repositories as `context-window-scaling` subjects: `experiment run --experiment context-window-scaling --target <repository> --local-subject-config <path>`, with a strict `LocalRepositorySubjectConfigV1`. The experiment, its treatments, budgets, token estimator, correctness, success, plots, and report schemas are unchanged, and bundled and synthetic modes behave as before.
12
+ - Contexts for both treatments are built only from regular files inside the declared source roots that Git does not ignore and that are at most 1 MiB; ignored, oversized, symbolic-link, and other unsafe entries are excluded, and the guided treatment fails if retrieval returns an excluded file.
13
+ - The repository is treated as read-only: private scratch state is created outside it and removed, repository state is compared before and after the run, and an output directory equal to or inside the repository is rejected. A detected change fails the run and is not reverted.
14
+ - Durable external-local output omits the repository path, source text, and file names: exact file identities appear as numbered `<redacted file N>` placeholders marked by `fileIdentityRedaction: "redacted"`, and the report target carries `privacyProjection: "external-local-redacted"`. A new `local-repository-subject-manifest.json` records the logical subject ID, full Git commit, branch, dirty state, safety policy, source roots, case IDs, and aggregate size counts.
15
+ - A failed external-local run exits nonzero with a bounded error and writes no report, execution artifact, or manifest.
16
+ - Fixed a crash in which a local-repository run pointed at a directory past the Windows path-length limit ended with an unhandled stream error instead of a classified error, and made known-file redaction in error text cover backslash-separated paths.
17
+ - Extended help, `experiment describe` text, and the packed-package acceptance gate (`npm run verify:packed-package`) to cover the installed external-local run with the real published my-dev-kit, rejection cases, privacy scanning that includes JSON-escaped Windows paths, and target immutability.
18
+ - Replaced a trailing-slash regular expression in glob base normalization with a linear scan after a CodeQL polynomial-regular-expression finding; glob selection behavior is unchanged.
19
+ - No retrieval precision, recall, ranking, or winner metric was added; those remain planned for v0.8.0.
20
+
7
21
  ## [0.7.1] - 2026-10-02
8
22
 
9
23
  ### Synthetic large-repository generator
package/README.md CHANGED
@@ -6,7 +6,7 @@ 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.7.1 (synthetic large-repository generator), and the published package and installed CLI are version 0.7.1. The previous release is v0.7.0 (context-window scaling), and v0.7.2 (real/local external repository experiments) is planned after the current release. 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 latest release is v0.7.2 (local-repository experiments), and the published package and installed CLI are version 0.7.2. The previous release is v0.7.1 (synthetic large-repository generator). 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.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.
12
12
 
@@ -16,9 +16,10 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
16
16
  - **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.
17
17
  - **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).
18
18
  - **Record index freshness evidence (v0.6.0):** a warm-index run captures an index snapshot after its one index build — the exact indexed file set from the my-dev-kit index contract, each with SHA-256 content identity, size, and modified-time metadata, plus the my-dev-kit version and index-command evidence — and assesses freshness immediately before each task's warm retrieval as `fresh`, `stale`, `partially-stale`, or `unknown`. The evidence appears in `warm-index-execution.json` and in the existing `report.json`/`report.txt`/`report.html` warm-index reports. It is observational only: it covers only files the snapshot lists (not the whole repository), never changes retrieval, execution or provider status, and adds no CLI flag, metric, plot, gallery item, or reindex recommendation in v0.6.0. See [docs/METRICS.md](docs/METRICS.md#index-freshness-evidence-v060) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment).
19
- - **Compare no-refresh, partial-refresh, and full-refresh retrieval after a controlled change (v0.6.3, previous release):** the `incremental-change-staleness` experiment plugin runs six frozen controlled-change scenario families (unrelated-file, local-implementation, exported-symbol, public-API, import-graph, and test-only changes) against four disposable target copies — `stale-index` (no refresh), `changed-files-refresh` and `affected-neighborhood-refresh` (real partial refreshes performed by my-dev-kit 1.12.5 through `index --incremental --refresh-scope`, selected internally with no Lab-level flag), and `full-refresh` (a complete new post-change index, used as a comparison reference rather than a preferred treatment). Each partial treatment truthfully records whether upstream applied the requested partial scope or fell back to a full rebuild (`APPLIED_PARTIAL` or `FALLBACK_FULL`); a fallback is not counted as partial-refresh evidence. Three comparisons against `full-refresh` are persisted with retrieval, deterministic fake-agent correctness, and required-file evidence in `incremental-change-staleness-execution.json` (schema `my-dev-kit-lab-incremental-change-staleness-execution-v2`) and the existing `report.json`/`report.txt`/`report.html`; historical v0.6.2 (V1) artifacts stay readable. There is no winner, ranking, or safety claim, and the plugin is self-only (`--target` is not accepted). `--kit-command` also accepts `incremental-change-staleness` and now defaults to my-dev-kit 1.12.5. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) and [docs/METRICS.md](docs/METRICS.md).
19
+ - **Compare no-refresh, partial-refresh, and full-refresh retrieval after a controlled change (v0.6.3, released; retained in the current package):** the `incremental-change-staleness` experiment plugin runs six frozen controlled-change scenario families (unrelated-file, local-implementation, exported-symbol, public-API, import-graph, and test-only changes) against four disposable target copies — `stale-index` (no refresh), `changed-files-refresh` and `affected-neighborhood-refresh` (real partial refreshes performed by my-dev-kit 1.12.5 through `index --incremental --refresh-scope`, selected internally with no Lab-level flag), and `full-refresh` (a complete new post-change index, used as a comparison reference rather than a preferred treatment). Each partial treatment truthfully records whether upstream applied the requested partial scope or fell back to a full rebuild (`APPLIED_PARTIAL` or `FALLBACK_FULL`); a fallback is not counted as partial-refresh evidence. Three comparisons against `full-refresh` are persisted with retrieval, deterministic fake-agent correctness, and required-file evidence in `incremental-change-staleness-execution.json` (schema `my-dev-kit-lab-incremental-change-staleness-execution-v2`) and the existing `report.json`/`report.txt`/`report.html`; historical v0.6.2 (V1) artifacts stay readable. There is no winner, ranking, or safety claim, and the plugin is self-only (`--target` is not accepted). `--kit-command` also accepts `incremental-change-staleness` and now defaults to my-dev-kit 1.12.5. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) and [docs/METRICS.md](docs/METRICS.md).
20
20
  - **Measure context-window scaling (v0.7.0, released):** the self-only `context-window-scaling` plugin compares `raw-full-file` and `my-dev-kit-guided` over 8k, 16k, 32k, 64k, or custom positive-integer estimated-token budgets. It records fit, utilization, deterministic fake-agent correctness/success, and omitted expected relevant files, with JSON/text/HTML reports and three SVG plots. `plots generate` reads `context-window-scaling-execution.json`. Token counts are a character-based estimate, not provider-tokenizer or billing data; fake-agent correctness is context-independent in the current harness. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md), [docs/METRICS.md](docs/METRICS.md), and [docs/COMMANDS.md](docs/COMMANDS.md).
21
- - **Run context-window scaling over synthetic repositories (v0.7.1, current release):** `context-window-scaling` uses the bundled fixed corpus by default, or `--synthetic-config <path>` (a `SyntheticRepositoryConfigV1` JSON file) to generate disposable deterministic TypeScript/Python repositories beneath the run output. The option is a command input selector, not plugin scientific config; it is mutually exclusive with `--case`, and the scientific semantics, execution artifact, reports, and plots are unchanged. See [docs/COMMANDS.md](docs/COMMANDS.md) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md).
21
+ - **Run context-window scaling over synthetic repositories (v0.7.1, released):** `context-window-scaling` uses the bundled fixed corpus by default, or `--synthetic-config <path>` (a `SyntheticRepositoryConfigV1` JSON file) to generate disposable deterministic TypeScript/Python repositories beneath the run output. The option is a command input selector, not plugin scientific config; it is mutually exclusive with `--case`, and the scientific semantics, execution artifact, reports, and plots are unchanged. See [docs/COMMANDS.md](docs/COMMANDS.md) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md).
22
+ - **Run context-window scaling over a local Git repository (v0.7.2, current release):** `context-window-scaling` can also run against an explicitly selected local Git repository with `--target <repository> --local-subject-config <path>` (a `LocalRepositorySubjectConfigV1` JSON file). The Lab treats the repository as read-only and compares its state before and after the run, builds contexts only from files Git does not ignore (ignored, oversized (over 1 MiB), symbolic-link, and other unsafe entries are excluded), requires the output directory to be outside the repository, and keeps the repository path, source text, and file names out of durable artifacts (file identities appear as numbered `<redacted file N>` placeholders). A `local-repository-subject-manifest.json` records the logical subject ID, the full Git commit, the branch, and aggregate size metadata. This measures operational context fit only, not retrieval precision, recall, or ranking quality. It is part of the current package. See [docs/COMMANDS.md](docs/COMMANDS.md) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md).
22
23
  - **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).
23
24
  - **Audit repository health:** run conservative code-rot detectors for TypeScript/JavaScript, Python, Java, and Kotlin, or adapt security findings into the common audit report.
24
25
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
@@ -238,12 +239,12 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
238
239
  - 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)
239
240
  - 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
240
241
  - 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
241
- - The generic experiment-plugin framework has four registered plugins: `context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, and `context-window-scaling`. The v0.7.0 context-window-scaling plugin is released; v0.7.1 lets it also run over caller-supplied synthetic repositories; external/local repository experiments (v0.7.2), retrieval precision/recall, and agent-success plugins remain planned.
242
+ - The generic experiment-plugin framework has four registered plugins: `context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, and `context-window-scaling`. The v0.7.0 context-window-scaling plugin is released; v0.7.1 lets it also run over caller-supplied synthetic repositories; v0.7.2 lets it run over an explicitly selected local Git repository; retrieval precision/recall and agent-success plugins remain planned.
242
243
  - `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
243
244
  - The current release does not guarantee token savings; it produces auditable evidence for specific cases, targets, agents, and strategies
244
245
  - Context-window scaling uses estimated tokens (`ceil(characters / 4)`) and experiment budgets, not actual provider tokenizer or model-window limits. Its deterministic fake-agent correctness check does not measure semantic sensitivity to the supplied context.
245
246
  - Released v0.6.1 affected-neighborhood evidence is bounded to the baseline graph of the prepared index and to the snapshot-represented files: a changed symbol means the symbol was present in a confirmed changed indexed file, not that its own text was proven to change; `not-indicated` means only that this bounded analysis found no supporting evidence for reindexing, not that skipping a reindex is safe; unresolved or ambiguous expected symbols keep a task at `unknown` rather than unrelated
246
- - v0.6.3 (previous release) partial-refresh evidence is scoped to the executed deterministic scenarios and the configured my-dev-kit 1.12.5-or-later tool. A partial treatment can fall back to a full rebuild and is then reported as not comparable as partial refresh, so an applied partial refresh in one run does not guarantee the next; `no-observed-regression-relative-to-full` is not a safety claim, and full refresh is a reference rather than a winner. Correctness remains deterministic fake-agent evidence, and `incremental-change-staleness` accepts no external `--target`.
247
+ - v0.6.3 partial-refresh evidence is scoped to the executed deterministic scenarios and the configured my-dev-kit 1.12.5-or-later tool. A partial treatment can fall back to a full rebuild and is then reported as not comparable as partial refresh, so an applied partial refresh in one run does not guarantee the next; `no-observed-regression-relative-to-full` is not a safety claim, and full refresh is a reference rather than a winner. Correctness remains deterministic fake-agent evidence, and `incremental-change-staleness` accepts no external `--target`.
247
248
  - Provider telemetry dashboards, semantic LLM judging, and cloud API billing integration are not yet implemented
248
249
  - The six new stage-context strategies have no CLI flags yet, are configured programmatically, and do not yet include plots, screenshots, or gallery integration
249
250
  - The published upstream artifacts the stage-context strategies read do not expose considered-but-unselected reads or unnecessary-read evidence; those metrics report `unavailable` rather than zero
@@ -166,15 +166,26 @@ export function renderExperimentRunHelp() {
166
166
  " --context-budgets <values> Comma-separated estimated-context-token budgets: 8k, 16k, 32k,",
167
167
  " 64k (8192/16384/32768/65536), or positive integers such as",
168
168
  " 12000 (default: 8k,16k,32k,64k). Duplicates after",
169
- " normalization are rejected. Only --out, --case,",
170
- " --synthetic-config, --context-budgets, and --kit-command are",
171
- " accepted; --case selects from the bundled four-case catalog.",
172
- " Self target only.",
169
+ " normalization are rejected. Only --out, --target, --case,",
170
+ " --synthetic-config, --local-subject-config, --context-budgets,",
171
+ " and --kit-command are accepted; --case selects from the bundled",
172
+ " four-case catalog. Bundled and synthetic modes run against the",
173
+ " Lab itself (--target is rejected there); an external --target",
174
+ " requires --local-subject-config.",
173
175
  " --synthetic-config <path> Path to a SyntheticRepositoryConfigV1 JSON file; relative paths",
174
176
  " resolve against the invocation directory. Generated",
175
177
  " repositories are written beneath the selected experiment",
176
178
  " output directory (synthetic-repositories/). Mutually exclusive",
177
179
  " with --case. Without it, the bundled four-case catalog is used.",
180
+ " --local-subject-config <path> Path to a LocalRepositorySubjectConfigV1 JSON file; applies to",
181
+ " context-window-scaling with an explicit external --target (a",
182
+ " local Git worktree root). Relative paths resolve against the",
183
+ " invocation directory. The repository is never modified; the",
184
+ " output directory must be outside it. Durable output omits the",
185
+ " repository path, source text, and file names, and adds",
186
+ " local-repository-subject-manifest.json. Mutually exclusive with",
187
+ " --case and --synthetic-config. Measures operational context fit",
188
+ " only, not retrieval precision, recall, or ranking quality.",
178
189
  " Generate plots with \"plots generate --experiment <run dir>\".",
179
190
  "",
180
191
  "warm-index-reuse only:",
@@ -130,6 +130,9 @@ function describePurpose(plugin) {
130
130
  }
131
131
  function describeTargetBehavior(plugin) {
132
132
  const supportsExternal = plugin.metadata.supportedTargets.includes("external-local");
133
+ if (plugin.metadata.id === "context-window-scaling") {
134
+ return "Three subject modes. Bundled (default): the bundled fixed scaling corpus against the Lab itself. Synthetic: caller-supplied deterministic synthetic repositories via --synthetic-config <path> (a SyntheticRepositoryConfigV1 JSON file, mutually exclusive with --case) written beneath the experiment output directory. External local repository: an explicitly selected local Git worktree via --target <path> together with --local-subject-config <path> (a LocalRepositorySubjectConfigV1 JSON file); the repository is never modified, the output directory must be outside it, and durable output omits the repository path, source text, and file names. Operational context-fit measurement only; no retrieval precision, recall, or ranking quality is measured.";
135
+ }
133
136
  if (supportsExternal) {
134
137
  return "Runs against the current lab repository when --target is omitted, or against an explicit local target project when --target <path> is provided. Outputs stay under lab-controlled output directories by default.";
135
138
  }
@@ -153,7 +156,7 @@ function describeExamples(plugin) {
153
156
  `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id}${agentOptions}`
154
157
  ];
155
158
  // Only advertise an explicit --target example for plugins that support external targets.
156
- if (plugin.metadata.supportedTargets.includes("external-local")) {
159
+ if (plugin.metadata.supportedTargets.includes("external-local") && plugin.metadata.id !== "context-window-scaling") {
157
160
  examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --target "Z:\\Users\\newuser\\Projects\\my-dev-kit-v1"${agentOptions}${screenshotOption}`);
158
161
  }
159
162
  // Only advertise campaign examples for plugins that declare the campaignPreset config field.
@@ -161,7 +164,7 @@ function describeExamples(plugin) {
161
164
  examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --campaign codex-full --include-real-agents --out <dir>`, `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --campaign claude-full --include-real-agents --case warm-medium-complete-idempotent --out <dir>`);
162
165
  }
163
166
  if (plugin.metadata.id === "context-window-scaling") {
164
- examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --synthetic-config <path-to-config.json> --out <run-dir>`);
167
+ examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --synthetic-config <path-to-config.json> --out <run-dir>`, `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --target <local-git-repository> --local-subject-config <path-to-local-subject-config.json> --out <run-dir-outside-the-repository>`);
165
168
  }
166
169
  return examples;
167
170
  }
@@ -3,8 +3,11 @@ import path from "node:path";
3
3
  import { parseAgentCommandTemplate } from "../agents/index.js";
4
4
  import { parseAgentId } from "../agents/agentRegistry.js";
5
5
  import { readBenchmarkProjectProfiles, readEvaluationCases } from "../evaluation/index.js";
6
+ import { loadLocalRepositorySubject } from "../evaluation/localRepositorySubject/index.js";
6
7
  import { contextStrategyComparisonPlugin, contextWindowScalingPlugin, createDefaultExperimentPluginRegistry, getWarmIndexCampaignPreset, incrementalChangeStalenessPlugin, parseContextBudgetsCliValue, parseWarmIndexCampaignPresetId, resolveExperimentTarget, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
7
8
  import { prepareSyntheticContextWindowScalingInputs } from "../experiments/plugins/contextWindowScaling/index.js";
9
+ import { projectExternalLocalTarget, projectRunForExternalLocalPersistence, } from "../experiments/plugins/contextWindowScaling/localSubjectPrivacy.js";
10
+ import { assertWorkRootOutsideTarget } from "../experiments/plugins/contextWindowScaling/localSubjectScratch.js";
8
11
  import { buildDefaultExperimentOutputRoot } from "../experiments/outputPaths.js";
9
12
  import { parsePromptComplexityLevel, parsePromptStrategy } from "../prompts/index.js";
10
13
  import { writePluginExperimentReports } from "../report/index.js";
@@ -38,7 +41,16 @@ const DEFAULT_PROJECT_PROFILES_RESOURCE = "benchmarks/contracts/benchmark-projec
38
41
  // catalog's package-relative targetRoot, so both resolve from the package root, never the cwd.
39
42
  const CONTEXT_WINDOW_SCALING_CASES_RESOURCE = "benchmarks/contracts/context-window-scaling-cases.json";
40
43
  // Options the context-window-scaling plugin accepts; every other flag is rejected, not ignored.
41
- const CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS = ["--experiment", "--out", "--case", "--synthetic-config", "--context-budgets", "--kit-command"];
44
+ const CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS = [
45
+ "--experiment",
46
+ "--out",
47
+ "--target",
48
+ "--case",
49
+ "--synthetic-config",
50
+ "--local-subject-config",
51
+ "--context-budgets",
52
+ "--kit-command"
53
+ ];
42
54
  export async function runExperimentRunCommandFromArgs(argv, options = {}) {
43
55
  try {
44
56
  const args = parseRunExperimentArgs(argv);
@@ -53,8 +65,8 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
53
65
  experimentId: args.experimentId,
54
66
  runId
55
67
  });
56
- if (args.syntheticConfigPath !== undefined && outputRoot === undefined) {
57
- // Synthetic generation needs the concrete root before the runner runs; use the exact default the
68
+ if ((args.syntheticConfigPath !== undefined || args.localSubjectConfigPath !== undefined) && outputRoot === undefined) {
69
+ // Synthetic generation and external-local runs need the concrete root before the runner runs; use the exact default the
58
70
  // runner itself would compute for this self-targeted invocation.
59
71
  outputRoot = buildDefaultExperimentOutputRoot({
60
72
  toolRoot,
@@ -74,22 +86,53 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
74
86
  toolRoot,
75
87
  runId
76
88
  });
89
+ const localSubject = readLoadedLocalSubject(inputs);
90
+ // External-local runs persist only a projected run: no target, tool, or output machine-local paths.
91
+ const persistedRun = localSubject
92
+ ? projectRunForExternalLocalPersistence(result, projectExternalLocalTarget(localSubject.manifest))
93
+ : result;
94
+ if (localSubject && result.status === "failed") {
95
+ // A failed local run has no execution evidence, so no report is produced and none could be mistaken for a
96
+ // completed experiment. The safe failure description (codes and kinds only) is the whole outcome.
97
+ console.log([
98
+ `Experiment: ${result.pluginId}`,
99
+ `Run ID: ${result.runId}`,
100
+ "Status: failed",
101
+ "Mode: external-local repository subject",
102
+ `Subject: ${localSubject.subjectId}`,
103
+ ...result.failures.map((failure) => `Failure: ${failure.message}`)
104
+ ].join("\n"));
105
+ return 1;
106
+ }
77
107
  const reports = await writePluginExperimentReports({
78
- run: result,
108
+ run: persistedRun,
79
109
  plugin: registry.describe(result.pluginId),
80
- outputRoot: String(result.metadata?.outputRoot ?? "")
110
+ outputRoot: String(result.metadata?.outputRoot ?? ""),
111
+ redactOutputRoot: localSubject !== undefined
81
112
  });
82
- const outputLines = [
83
- `Experiment: ${result.pluginId}`,
84
- `Run ID: ${result.runId}`,
85
- `Status: ${result.status}`,
86
- `Mode: ${result.target.isSelf ? "self" : "external target"}`,
87
- `Tool root: ${result.target.toolRoot}`,
88
- `Target root: ${result.target.targetRoot}`,
89
- `Output: ${String(result.metadata?.outputRoot ?? "")}`,
90
- `Report JSON: ${reports.outputPaths.jsonPath}`,
91
- `Report HTML: ${reports.outputPaths.htmlPath}`
92
- ];
113
+ const outputLines = localSubject
114
+ ? [
115
+ `Experiment: ${result.pluginId}`,
116
+ `Run ID: ${result.runId}`,
117
+ `Status: ${result.status}`,
118
+ "Mode: external-local repository subject",
119
+ `Subject: ${localSubject.subjectId}`,
120
+ `Output: ${String(result.metadata?.outputRoot ?? "")}`,
121
+ `Report JSON: ${reports.outputPaths.jsonPath}`,
122
+ `Report HTML: ${reports.outputPaths.htmlPath}`,
123
+ ...result.failures.map((failure) => `Failure: ${failure.message}`)
124
+ ]
125
+ : [
126
+ `Experiment: ${result.pluginId}`,
127
+ `Run ID: ${result.runId}`,
128
+ `Status: ${result.status}`,
129
+ `Mode: ${result.target.isSelf ? "self" : "external target"}`,
130
+ `Tool root: ${result.target.toolRoot}`,
131
+ `Target root: ${result.target.targetRoot}`,
132
+ `Output: ${String(result.metadata?.outputRoot ?? "")}`,
133
+ `Report JSON: ${reports.outputPaths.jsonPath}`,
134
+ `Report HTML: ${reports.outputPaths.htmlPath}`
135
+ ];
93
136
  // Automatic presentation (report already produced above) applies only to warm-index-reuse
94
137
  // campaign runs; legacy fake-agent warm-index runs and context-strategy-comparison stay
95
138
  // report-only, unchanged.
@@ -161,6 +204,7 @@ export function parseRunExperimentArgs(argv) {
161
204
  let campaignPreset;
162
205
  let contextBudgets;
163
206
  let syntheticConfigPath;
207
+ let localSubjectConfigPath;
164
208
  const seenFlags = [];
165
209
  const commandTemplates = {};
166
210
  for (let index = 0; index < argv.length; index += 1) {
@@ -188,6 +232,12 @@ export function parseRunExperimentArgs(argv) {
188
232
  }
189
233
  syntheticConfigPath = readRequiredValue(argv, ++index, "--synthetic-config");
190
234
  }
235
+ else if (arg === "--local-subject-config") {
236
+ if (localSubjectConfigPath !== undefined) {
237
+ throw new Error("--local-subject-config may be supplied only once.");
238
+ }
239
+ localSubjectConfigPath = readRequiredValue(argv, ++index, "--local-subject-config");
240
+ }
191
241
  else if (arg === "--case") {
192
242
  caseIds.push(...splitList(readRequiredValue(argv, ++index, "--case")));
193
243
  }
@@ -258,6 +308,9 @@ export function parseRunExperimentArgs(argv) {
258
308
  if (syntheticConfigPath !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
259
309
  throw new Error(`--synthetic-config is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
260
310
  }
311
+ if (localSubjectConfigPath !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
312
+ throw new Error(`--local-subject-config is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
313
+ }
261
314
  if (experimentId === contextWindowScalingPlugin.metadata.id) {
262
315
  const unsupported = [...new Set(seenFlags.filter((flag) => !CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS.includes(flag)))];
263
316
  if (unsupported.length > 0) {
@@ -269,6 +322,23 @@ export function parseRunExperimentArgs(argv) {
269
322
  if (syntheticConfigPath !== undefined && seenFlags.includes("--case")) {
270
323
  throw new Error("--case and --synthetic-config are mutually exclusive; the synthetic config owns the generated case set.");
271
324
  }
325
+ // Subject-mode matrix: bundled (no source flags), synthetic (--synthetic-config), external-local
326
+ // (--target with --local-subject-config). Every other combination is rejected, never given a precedence.
327
+ if (localSubjectConfigPath !== undefined && syntheticConfigPath !== undefined) {
328
+ throw new Error("--synthetic-config and --local-subject-config are mutually exclusive; choose one subject source.");
329
+ }
330
+ if (localSubjectConfigPath !== undefined && seenFlags.includes("--case")) {
331
+ throw new Error("--case cannot be combined with --local-subject-config; the local subject config owns the case set.");
332
+ }
333
+ if (localSubjectConfigPath !== undefined && targetPath === undefined) {
334
+ throw new Error(`--local-subject-config requires an external --target for ${experimentId}.`);
335
+ }
336
+ if (syntheticConfigPath !== undefined && targetPath !== undefined) {
337
+ throw new Error("--synthetic-config cannot be combined with --target; synthetic subjects are generated beneath the experiment output directory.");
338
+ }
339
+ if (targetPath !== undefined && localSubjectConfigPath === undefined) {
340
+ throw new Error(`External ${experimentId} targets require --local-subject-config.`);
341
+ }
272
342
  }
273
343
  if (kitCommand !== undefined && !KIT_COMMAND_PLUGIN_IDS.includes(experimentId)) {
274
344
  throw new Error(`--kit-command is only supported for --experiment ${KIT_COMMAND_PLUGIN_IDS.join(" or ")}.`);
@@ -292,6 +362,7 @@ export function parseRunExperimentArgs(argv) {
292
362
  targetPath,
293
363
  outDir,
294
364
  syntheticConfigPath,
365
+ localSubjectConfigPath,
295
366
  config: withoutUndefined({
296
367
  casesPath,
297
368
  projectProfilesPath,
@@ -370,6 +441,27 @@ async function loadContextWindowScalingInputs(args, toolRoot, context, outputRoo
370
441
  if (!validation.valid || !validation.config) {
371
442
  throw new Error(`Invalid context window scaling config: ${validation.errors.join("; ")}`);
372
443
  }
444
+ if (args.localSubjectConfigPath !== undefined) {
445
+ if (outputRoot === undefined) {
446
+ throw new Error("Internal error: external-local runs require a resolved experiment output root.");
447
+ }
448
+ const target = resolveExperimentTarget(args.targetPath, toolRoot);
449
+ if (target.isSelf || target.kind !== "external-local") {
450
+ throw new Error("Local-repository subject mode requires an external-local target; --target must not be the Lab repository.");
451
+ }
452
+ // Fail closed before anything is created: the output root (and the private scratch beneath it) must be outside the target.
453
+ try {
454
+ await assertWorkRootOutsideTarget(outputRoot, target.targetRoot);
455
+ }
456
+ catch {
457
+ throw new Error("Experiment output root must not be inside the external target project.");
458
+ }
459
+ const configPath = path.resolve(context.invocationCwd, args.localSubjectConfigPath);
460
+ const localSubjectConfig = await readJsonConfigFile("--local-subject-config", configPath, args.localSubjectConfigPath);
461
+ // The selected --target is the physical root handed to the Batch 1 loader; the config carries no path.
462
+ const localSubject = await loadLocalRepositorySubject({ config: localSubjectConfig, repositoryPath: target.targetRoot });
463
+ return { cases: localSubject.evaluationCases, localSubject, env: process.env };
464
+ }
373
465
  if (args.syntheticConfigPath !== undefined) {
374
466
  if (outputRoot === undefined) {
375
467
  throw new Error("Internal error: synthetic generation requires a resolved experiment output root.");
@@ -396,20 +488,27 @@ async function loadContextWindowScalingInputs(args, toolRoot, context, outputRoo
396
488
  }
397
489
  // The command only reads and parses the caller-supplied file; schema validation belongs to the synthetic planner.
398
490
  async function readSyntheticConfigJson(resolvedPath, displayPath) {
491
+ return readJsonConfigFile("--synthetic-config", resolvedPath, displayPath);
492
+ }
493
+ async function readJsonConfigFile(flag, resolvedPath, displayPath) {
399
494
  let text;
400
495
  try {
401
496
  text = await readFile(resolvedPath, "utf8");
402
497
  }
403
498
  catch (error) {
404
- throw new Error(`Cannot read --synthetic-config ${displayPath}: ${error instanceof Error ? error.message : String(error)}`);
499
+ throw new Error(`Cannot read ${flag} ${displayPath}: ${error instanceof Error ? error.message : String(error)}`);
405
500
  }
406
501
  try {
407
502
  return JSON.parse(text);
408
503
  }
409
504
  catch (error) {
410
- throw new Error(`--synthetic-config ${displayPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
505
+ throw new Error(`${flag} ${displayPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
411
506
  }
412
507
  }
508
+ function readLoadedLocalSubject(inputs) {
509
+ const value = inputs?.localSubject;
510
+ return value && typeof value === "object" ? value : undefined;
511
+ }
413
512
  // Shared by both registered plugins: explicit paths resolve against toolRoot, defaults come from
414
513
  // bundled package resources so they do not depend on the invocation cwd.
415
514
  async function loadCasesAndProjectProfiles(args, toolRoot, context) {
@@ -98,3 +98,47 @@ export function collectFilesForGlobs(targetRoot, globs) {
98
98
  .sort(([a], [b]) => a.localeCompare(b))
99
99
  .map(([relativePath, absolutePath]) => ({ relativePath, absolutePath }));
100
100
  }
101
+ /**
102
+ * Pure counterpart of `collectFilesForGlobs` for a caller-supplied, already validated list of repository-relative
103
+ * file paths. It applies the same glob matching and the same walk exclusions (excluded directory names below the
104
+ * glob base directory, temp files) and the same ordering, but never touches the filesystem, so the caller decides
105
+ * which files may be opened at all. Unlike the walking variant it does not require the base directory to exist.
106
+ */
107
+ export function selectRelativePathsForGlobs(relativePaths, globs) {
108
+ const selected = new Set();
109
+ for (const globPattern of globs) {
110
+ if (!globPattern || typeof globPattern !== "string") {
111
+ throw new Error("Invalid glob pattern.");
112
+ }
113
+ const baseDir = baseDirectoryFromGlob(globPattern);
114
+ if (baseDir.split("/").includes("..") || path.isAbsolute(baseDir)) {
115
+ throw new Error(`Resolved path escapes target root: ${globPattern}`);
116
+ }
117
+ // Linear scan for the trailing "/" run (a regular expression here is a polynomial-backtracking risk on
118
+ // glob text that comes from user configuration).
119
+ let baseEnd = baseDir.length;
120
+ while (baseEnd > 0 && baseDir.charCodeAt(baseEnd - 1) === 47)
121
+ baseEnd -= 1;
122
+ const normalizedBase = baseDir === "." ? "" : baseDir.slice(0, baseEnd);
123
+ for (const relativePath of relativePaths) {
124
+ const normalizedPath = relativePath.replace(/\\/g, "/");
125
+ if (normalizedPath === normalizedBase) {
126
+ // An exact file base is used as-is by the walking variant (no walk exclusions apply).
127
+ if (matchesGlob(normalizedPath, globPattern))
128
+ selected.add(normalizedPath);
129
+ continue;
130
+ }
131
+ if (normalizedBase !== "" && !normalizedPath.startsWith(`${normalizedBase}/`))
132
+ continue;
133
+ const belowBase = normalizedBase === "" ? normalizedPath : normalizedPath.slice(normalizedBase.length + 1);
134
+ const directorySegments = belowBase.split("/").slice(0, -1);
135
+ if (directorySegments.some((segment) => excludedDirNames.has(segment)))
136
+ continue;
137
+ if (isTempFile(normalizedPath))
138
+ continue;
139
+ if (matchesGlob(normalizedPath, globPattern))
140
+ selected.add(normalizedPath);
141
+ }
142
+ }
143
+ return [...selected].sort((a, b) => a.localeCompare(b));
144
+ }