@dzhechkov/harness-cli 0.8.25 → 0.8.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -5,8 +5,15 @@
5
5
  */
6
6
 
7
7
  import { parseNpmPackInventory, type InventorySource, type LocalInventoryResult } from '@dzhechkov/harness-core';
8
+ // Fix-round 1 (Codex HIGH-1c, feature recall-short-terms): the ONE place `dz recall` prints an
9
+ // empty result must name WHY — via the shared helper, not by re-deriving the decision. Routed
10
+ // through harness-core's re-export (lead correction) rather than a new direct dependency on
11
+ // `@dzhechkov/memory`: a new package-graph edge is a publishing-surface change outside this
12
+ // feature's scope, and harness-core already depends on memory.
13
+ import { noSearchableTermsReason } from '@dzhechkov/harness-core';
8
14
  import { appendFileSync, chmodSync, closeSync, constants as fsConstants, cpSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, writeSync, type Dirent } from 'node:fs';
9
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
15
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, posix as nodePosixPath } from 'node:path';
16
+ const posixNormalize = nodePosixPath.normalize;
10
17
  import { fileURLToPath } from 'node:url';
11
18
  import { request as httpsRequest } from 'node:https';
12
19
  import { KNOWN_CLI_FLAGS } from './known-flags.js';
@@ -15,7 +22,7 @@ import { resolveInstallSpec } from './install-spec.js';
15
22
  import { dispatchedCommands, documentedCommands } from './command-inventory.js';
16
23
  import { execFile, execFileSync, execSync, spawn, spawnSync, type ChildProcess } from 'node:child_process';
17
24
  import { createHash, randomBytes } from 'node:crypto';
18
- import { homedir, hostname, tmpdir } from 'node:os';
25
+ import { cpus, homedir, hostname, tmpdir } from 'node:os';
19
26
  import { createRequire } from 'node:module';
20
27
  import { isDeepStrictEqual } from 'node:util';
21
28
  import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed, type JournalKind, type JournalIo } from '@dzhechkov/harness-core';
@@ -65,6 +72,7 @@ import {
65
72
  runDoctor,
66
73
  runInit,
67
74
  discoverSkillIds,
75
+ loadSkillFromDir,
68
76
  resolveSelection,
69
77
  formatSelectRefusal,
70
78
  runIntegrationsVerify,
@@ -139,6 +147,7 @@ import {
139
147
  TARGET_SHORT_LABELS,
140
148
  applyLegStatus,
141
149
  applyLegReasonMessage,
150
+ probeApplyLeg,
142
151
  resolveAgentdbPath,
143
152
  WORKFLOW_TEMPLATES_RETIRED_MESSAGE,
144
153
  parsePlan,
@@ -558,6 +567,7 @@ import {
558
567
  REGISTRY_SELFCHECK_TESTS,
559
568
  buildMutationTestCommand,
560
569
  parseMutationRegistry,
570
+ registryEntriesAddedSince,
561
571
  applyMutationToText,
562
572
  attributeBaselineRedness,
563
573
  countFailingTests,
@@ -630,7 +640,7 @@ import {
630
640
  import type { OperatorProfile, Domain as ProfileDomain, Register as ProfileRegister } from '@dzhechkov/harness-core';
631
641
  import type { RecordKind, RecordDecision } from '@dzhechkov/harness-core';
632
642
  import type { CadenceWindow } from '@dzhechkov/harness-core';
633
- import type { MutationEntryResult, MutationObservation, MutationRegistryEntry } from '@dzhechkov/harness-core';
643
+ import type { MutationEntryResult, MutationObservation, MutationRegistryEntry, MutationRegistry } from '@dzhechkov/harness-core';
634
644
  import type { SkillApplyFailure, SkillLoadFailure } from '@dzhechkov/harness-core';
635
645
  import type { ReqeDebt } from '@dzhechkov/harness-core';
636
646
  import type { ClassifyResultRow, ExecutionEvidence } from '@dzhechkov/harness-core';
@@ -752,7 +762,7 @@ Usage:
752
762
  dz profile [init|show|set|sync] [--json] (WHO the assistant is talking to — per-user store at ~/.dz/profile.json (0600, NEVER in a project), delivered as a marked block in ~/.claude/CLAUDE.md so it loads in EVERY project, dz installed or not. init = five questions (language, register, deep/weak domains as comma lists — "networking (CCIE; NSX)" keeps the parenthetical as the note, Enter skips — teaches y/n with one re-ask, never a silent default); show ALWAYS prints the store path + age + drift verdict + the rendered block; set register|language|teaches <v> or set deep|weak add|rm <tag> [note] — register accepts the owner's own words (профи / профи лайт / просто), an unknown value is REFUSED naming the accepted set; sync re-writes the block (runs automatically after init/set; foreign content byte-for-byte, timestamped backup before every modifying write). The register changes FORM, never FACTS, and governs dialogue only — never ADRs/commits/QE reports; both rules are baked into the rendered block at every level. exit 0 done / 1 no profile or failed / 2 refused input)
753
763
  dz reqe [--slug <feature> [--done --report <f>]] [--json] (the re-QE debt ledger: a usage-switched run whose Step-8 QE ran on the coder's OWN family records a debt; list debts, print the cross-family review brief, settle FAIL-CLOSED against a graded report — the settlement lands in 08_qe_report.md)
754
764
  dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of 17 NAMED failures with an audit record under features/<slug>/.fa-state/qe-bridge/ (runId, resolved executable + binOverride, prompt sha256, channel offsets, requestedOut, reportWritten, retained raw stdout; 0600 files in a 0700 dir), never a clean review. A --coder-family that contradicts the recorded reqe debt is refused. Writes features/<slug>/08b_reqe_report.md, which dz reqe --done settles unchanged. DISCLOSURE: the extracts you scope are sent to the Claude runtime; the bridge cannot classify secrets. DZ_QE_BRIDGE_CLAUDE_BIN is a TEST SEAM, not a flag. exit 0 signoff parsed (ANY grade — it reports, it does not gate) / 1 named failure / 2 usage)
755
- dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--timeout <ms>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: copy the package to a scratch dir, verify the baseline suite is green, apply each registry mutation, run the suite, REQUIRE red, restore. The red must be BEHAVIOURAL: a mutation that no longer parses is MUTATION_UNPARSEABLE; a red run whose OWN output reports a test FILE failing to load (node --test file-level not-ok with exitCode, vitest Failed Suites) is MUTATION_LOAD_FATAL — the signal comes from the same run as the failing count, never from a separate isolated import; red output whose shape matches no known runner is INCONCLUSIVE (a runner-coverage gap, loud, never PROVEN); a count far above the entry's bound is OVER_FAILING; a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky). Mutation writes are realpath-contained to the scratch copy: a symlink escape or a node_modules/ target is refused (exit 2), the real tree is never written. A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error)
765
+ dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--touched <path[,path]>] [--added-since <git-ref>] [--timeout <ms>] [--max-workers <n>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: '--max-workers' resolves flag > the registry's own 'maxWorkers' field > 'min(4, max(1, floor(cpus/2)))' (an invalid flag value — 0, negative, fractional, non-numeric — is a usage error, exit 2, never a silent default; fix-round 1), injects '--maxWorkers=<n>' right after 'vitest run' inside its own compound-command segment (unless that segment already names the flag; fix-round 1 — scoped detection, not a whole-command substring check) and sets 'VITEST_MAX_WORKERS=<n>' in the env regardless — an uncapped full-suite baseline/mutant run at vitest's default worker count (= cpu cores) has measured load 62-358 and <2GB free on an 8-core/16GB box under embedding-daemon tests, killing full overnight gate runs (0bb74d66); printed as 'mutation-gate: workers: <n> (<flag|registry|default>)', or 'mutation-gate: workers: n/a — test command is not vitest' when the command is not recognised as vitest. '--touched' selects entries whose 'file' matches one of the given paths, accepted in ANY of package-relative, './'-prefixed, absolute-inside-the-package, repo-relative, or backslash-separated form — all normalized to package-relative POSIX before matching (fix-round 1, AM-1); a path that resolves OUTSIDE the package is counted, never silently dropped, as '<K> outside package' in the 'selected N of M' line; '--added-since <ref>' selects entries whose id is not present in the registry as it read at that ref ('git show <ref>:<registry path>'): a registry genuinely ABSENT at that ref means every current entry counts as added (said explicitly); an unresolvable ref, any OTHER git failure, or an invalid/malformed base registry at that ref is a usage error (exit 2), never folded into "absent" (AM-3) — a feature scopes the gate to its own touched files and any entries it just added instead of the whole registry (MEASURED: an unscoped run over 358 entries on this repo's core package ran 30-40 minutes and hit the timeout wall, INCONCLUSIVE every time). The two selectors UNION and the result INTERSECTS with '--only' when both are given; an empty selection prints 'selected 0 of M entries (…)' and exits 0 — never a silent skip; '--json' always carries a 'selection' object ({selected, total, touched, addedSince, base, outsidePackage}) on every scoped run (AM-4). copy the package to a scratch dir, verify the baseline suite is green, apply each registry mutation, run the suite, REQUIRE red, restore. The red must be BEHAVIOURAL: a mutation that no longer parses is MUTATION_UNPARSEABLE; a red run whose OWN output reports a test FILE failing to load (node --test file-level not-ok with exitCode, vitest Failed Suites) is MUTATION_LOAD_FATAL — the signal comes from the same run as the failing count, never from a separate isolated import; red output whose shape matches no known runner is INCONCLUSIVE (a runner-coverage gap, loud, never PROVEN); a count far above the entry's bound is OVER_FAILING; a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky). Mutation writes are realpath-contained to the scratch copy: a symlink escape or a node_modules/ target is refused (exit 2), the real tree is never written. A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error)
756
766
  dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--allow-cold-start] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
757
767
  dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
758
768
  dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
@@ -855,6 +865,13 @@ export type MutationGateRunner = (
855
865
  readonly timeoutMs: number;
856
866
  readonly phase: 'baseline' | 'mutation' | 'rebaseline' | 'final-rebaseline';
857
867
  readonly entryId?: string;
868
+ /**
869
+ * mutation-gate-baseline-honesty FR-3: the extra env vars the REAL executor would set on top of
870
+ * `process.env` for this run — currently just `VITEST_MAX_WORKERS`, set unconditionally
871
+ * regardless of whether the test command is recognised as vitest (a non-vitest command still
872
+ * gets the env var; only the command-string injection is vitest-gated).
873
+ */
874
+ readonly env: Readonly<Record<string, string>>;
858
875
  },
859
876
  ) => MutationGateRunnerObservation;
860
877
 
@@ -2599,6 +2616,41 @@ async function cmdInstall(
2599
2616
  if (root.layout === 'npx-template' && root.hasCompanionAssets) {
2600
2617
  write(` note: ${pkg} also ships commands/hooks/agents — \`npx -y ${pkg} init\` installs the full kit.`);
2601
2618
  }
2619
+ // Junk-skip summary (feature skills-walk-symlinks-and-junk, FR-2/AC-3). `report.skills[].skipped`
2620
+ // above is a WRITE outcome (an existing file not overwritten without --force) — a different
2621
+ // question from "did this skill directory contain build/cache junk that never became an asset at
2622
+ // all". Re-walk each discovered skill (a cheap second READ — `loadSkillFromDir` already did this
2623
+ // once inside `runInit`'s `adapter.compile`, this adds no write) to surface that count without
2624
+ // reshaping `InitReport`. Silent at N=0 (AC-3): most packages ship no junk and must print nothing.
2625
+ // fix-round 1 MEDIUM-4: this counts ENTRIES, not files. A skipped junk DIRECTORY (e.g.
2626
+ // `__pycache__`) is exactly one entry here even though it may hold many files underneath —
2627
+ // `walkFiles` never descends into a skipped junk directory to count those (see skills.ts),
2628
+ // so a per-file count would be a number this code cannot honestly produce. The wording and
2629
+ // the trailing `/` on directory paths say so, instead of implying "file" for something that
2630
+ // may be a whole tree.
2631
+ let junkSkippedCount = 0;
2632
+ const junkSkippedPaths: string[] = [];
2633
+ for (const id of discoverSkillIds(root.dir)) {
2634
+ let skill;
2635
+ try {
2636
+ skill = loadSkillFromDir(root.dir, id);
2637
+ } catch {
2638
+ continue; // unparseable skills are reported separately via report.failures below
2639
+ }
2640
+ for (const entry of skill.skipped ?? []) {
2641
+ if (!entry.reason.startsWith('junk')) continue; // broken-symlink/cycle/escape skips are a different concern (FR-1/AM-8)
2642
+ junkSkippedCount += 1;
2643
+ const shownPath = relative(pkgDir, entry.path).split('\\').join('/');
2644
+ const isDir = entry.reason.startsWith('junk directory');
2645
+ junkSkippedPaths.push(isDir ? `${shownPath}/` : shownPath);
2646
+ }
2647
+ }
2648
+ if (junkSkippedCount > 0) {
2649
+ const shown = junkSkippedPaths.slice(0, 5).join(', ');
2650
+ const more = junkSkippedPaths.length > 5 ? `, … (+${junkSkippedPaths.length - 5} more)` : '';
2651
+ const noun = junkSkippedCount === 1 ? 'entry' : 'entries';
2652
+ write(`skills: skipped ${junkSkippedCount} junk ${noun} (${shown}${more})`);
2653
+ }
2602
2654
  // Skip-and-collect at install time (D1 / the report's D2 amendment): the offending
2603
2655
  // SKILL.md came out of the DOWNLOADED TARBALL, so the path is rendered relative to
2604
2656
  // the package root (a `node_modules/**` absolute path is not actionable) and the
@@ -5061,7 +5113,18 @@ async function cmdRecall(
5061
5113
 
5062
5114
  if (hits.length === 0) {
5063
5115
  write(`dz recall "${shownQuery}"`);
5064
- write(` No matching patterns (teach some with \`dz teach\`, or consolidate sessions).`);
5116
+ // Fix-round 1 (Codex HIGH-1c): a query that tokenizes to literally nothing (pure
5117
+ // punctuation/whitespace) is a DIFFERENT empty result than a query with real terms that
5118
+ // simply matched no record — FR-3 requires the reason to be named, not folded into the same
5119
+ // generic "no matching patterns" line. `noSearchableTermsReason` is the single source of
5120
+ // truth for the decision (memory/src/tokenize.ts); this print site calls it rather than
5121
+ // re-deriving "no searchable terms" from the query text itself.
5122
+ const noTermsReason = noSearchableTermsReason(query);
5123
+ if (noTermsReason !== undefined) {
5124
+ write(` no searchable terms in "${shownQuery}" (only punctuation/whitespace) — reason: ${noTermsReason}`);
5125
+ } else {
5126
+ write(` No matching patterns (teach some with \`dz teach\`, or consolidate sessions).`);
5127
+ }
5065
5128
  // The domain note must print here too (Codex QE #10): a --domain run with zero hits
5066
5129
  // silently said nothing about the domain, so the reader could not tell whether the
5067
5130
  // boost had been applied and found nothing, or had not run at all.
@@ -7989,7 +8052,7 @@ function cmdPublish(
7989
8052
  /* ADR-001): computed from the declarative model, never hand-written */
7990
8053
  /* ------------------------------------------------------------------ */
7991
8054
 
7992
- function cmdParity(options: Map<string, string>, flags: Set<string>, write: Write, writeErr: WriteErr, cwd: string): number {
8055
+ async function cmdParity(options: Map<string, string>, flags: Set<string>, write: Write, writeErr: WriteErr, cwd: string): Promise<number> {
7993
8056
  const json = flags.has('json');
7994
8057
  if (flags.has('help')) {
7995
8058
  write('dz parity [--target <name>] [--json] — the computed feature×target map (never hand-written)');
@@ -8017,30 +8080,53 @@ function cmdParity(options: Map<string, string>, flags: Set<string>, write: Writ
8017
8080
  }
8018
8081
  }
8019
8082
 
8020
- // ADR-001 Decision 3 (feature setup-installs-apply-leg): `learning-apply` on `claude-code` is
8021
- // MEASURED, not declared — `hooks-prompt` is present for that ONE target only when
8022
- // `applyLegStatus(root).installed`. `computeParity` itself is untouched (FR-5); only the
8023
- // capability SET fed into it for this one cell differs from the static `TARGET_CAPABILITIES`.
8024
- // `applyLegStatus` never throws (fix round 1, Q3 finding: an unreadable helper used to be able to
8025
- // crash this command rather than degrade to a named remedy).
8083
+ // ADR-001 Decision 3 (feature setup-installs-apply-leg), extended by `apply-leg-never-silent`
8084
+ // Decision 3 (FR-4): `learning-apply` on `claude-code` is MEASURED, not declared — `hooks-prompt`
8085
+ // is present for that ONE target only when the leg is OBSERVED to inject, not merely installed.
8086
+ // Issue #2 was exactly this cell reading `full` while the leg injected nothing in every session
8087
+ // but one: `applyLegStatus(root).installed` alone (file presence + structural wiring) is
8088
+ // necessary but not sufficient `probeApplyLeg` is the live end-to-end proof. `computeParity`
8089
+ // itself is untouched (FR-5 of the earlier feature); only the capability SET fed into it for this
8090
+ // one cell differs from the static `TARGET_CAPABILITIES`, exactly as before this feature.
8091
+ // `applyLegStatus` never throws (fix round 1, Q3 finding); `probeApplyLeg` is only invoked when
8092
+ // `installed` is true — a leg with missing/stale/unreadable helpers has nothing live to probe,
8093
+ // and the pre-existing remedy for that state is unchanged.
8026
8094
  const applyLegStatusVal = applyLegStatus(cwd);
8027
8095
  const applyLegInstalled = applyLegStatusVal.installed;
8096
+ // Codex round-2: `probeApplyLeg` may REJECT (temp dir, dynamic import, a throwing remover) — doctor
8097
+ // catches that; parity must too, or a probe crash would crash `dz parity` instead of reading as
8098
+ // "installed but silent: probe-error".
8099
+ let applyLegProbe: Awaited<ReturnType<typeof probeApplyLeg>> | undefined;
8100
+ if (applyLegInstalled) {
8101
+ try {
8102
+ applyLegProbe = await probeApplyLeg(cwd);
8103
+ } catch (err) {
8104
+ applyLegProbe = { ok: false, reason: `probe-error: ${err instanceof Error ? err.message : String(err)}`, elapsedMs: 0 };
8105
+ }
8106
+ }
8107
+ const applyLegWorking = applyLegProbe?.ok === true;
8028
8108
  const matrix = buildParityMatrix().map((row) => {
8029
- if (row.feature.id !== 'learning-apply' || applyLegInstalled) return row;
8109
+ if (row.feature.id !== 'learning-apply' || applyLegWorking) return row;
8030
8110
  const claudeCodeCaps = TARGET_CAPABILITIES['claude-code'].filter((c) => c !== 'hooks-prompt');
8031
8111
  return { feature: row.feature, cells: { ...row.cells, 'claude-code': computeParity(row.feature, claudeCodeCaps) } };
8032
8112
  });
8033
- // The "not installed" remedy — named ONLY for the one cell whose grant is a live measurement,
8034
- // never a blanket note for every `manual` cell (most targets are manual by DESIGN, not absence).
8035
- // `stale-version`/`unreadable` route through `applyLegReasonMessage` — the SAME text-producing
8036
- // function `dz doctor` uses for those two reasons (fix round 1, HIGH finding 2 / Q3 finding 7), so
8037
- // the two instruments cannot disagree about WHY a stale or broken install is not "full".
8113
+ // The remedy — named ONLY for the one cell whose grant is a live measurement, never a blanket
8114
+ // note for every `manual` cell (most targets are manual by DESIGN, not absence). Three distinct
8115
+ // states, never conflated: NOT installed (stale-version/unreadable route through
8116
+ // `applyLegReasonMessage`, the SAME text-producing function `dz doctor` uses for those two
8117
+ // reasons, so the two instruments cannot disagree about WHY); installed but the live probe did
8118
+ // NOT observe injection (FR-4's new "installed but silent" case, reason from `probeApplyLeg`
8119
+ // itself — the SAME reason `dz doctor`'s live-probe row prints, so doctor and parity cannot
8120
+ // disagree about a dead leg either); and the working case, where this function returns ''.
8038
8121
  const applyLegRemedy = (featureId: string, t: TargetName): string => {
8039
- if (featureId !== 'learning-apply' || t !== 'claude-code' || applyLegInstalled) return '';
8040
- if (applyLegStatusVal.reason === 'stale-version' || applyLegStatusVal.reason === 'unreadable') {
8041
- return ` ${applyLegReasonMessage(applyLegStatusVal)}`;
8122
+ if (featureId !== 'learning-apply' || t !== 'claude-code' || applyLegWorking) return '';
8123
+ if (!applyLegInstalled) {
8124
+ if (applyLegStatusVal.reason === 'stale-version' || applyLegStatusVal.reason === 'unreadable') {
8125
+ return ` — ${applyLegReasonMessage(applyLegStatusVal)}`;
8126
+ }
8127
+ return ' — not installed — run dz setup --target claude-code --memory agentdb';
8042
8128
  }
8043
- return 'not installed run dz setup --target claude-code --memory agentdb';
8129
+ return ` — installed but silent: ${applyLegProbe?.reason ?? 'unknown'}`;
8044
8130
  };
8045
8131
  // EVIDENCE staleness, folded into the report (fix round 2, R2-3). Derived from the records
8046
8132
  // themselves — no `codex --version`, no subprocess, so `dz parity` stays a deterministic function
@@ -12955,6 +13041,46 @@ function splitMutationGateOutputWrite(
12955
13041
  return 'path' in result ? { outputPath: result.path } : { outputError: result.error };
12956
13042
  }
12957
13043
 
13044
+ /** Fix-round 1 (AM-1, feature qe-step-gate-scoped-to-feature): a `--touched` path arrives in one of
13045
+ * several shapes — package-relative POSIX (the common case, already correct as-is), with a leading
13046
+ * `./`, as an ABSOLUTE path inside the package, or REPO-relative (e.g.
13047
+ * `packages/@dzhechkov/harness-cli/src/x.ts`) — and on a POSIX host a caller may hand a backslash
13048
+ * path too (a Windows-authored change list). MEASURED (Codex round-1 review): the pre-fix
13049
+ * normalization only converted the native path separator, so any of the other shapes matched ZERO
13050
+ * registry entries and the gate silently reported `selected 0` / exit 0 — the run looked clean while
13051
+ * defending nothing. This resolves EVERY shape to the package-relative POSIX candidate(s) a registry
13052
+ * entry's `file` is written in, and — when a path genuinely resolves outside `pkgDir` (an absolute
13053
+ * path elsewhere, or a `../` that still escapes the package after a repo-relative reinterpretation)
13054
+ * — reports it as `outside`, never a silent non-match indistinguishable from "the path doesn't
13055
+ * exist".
13056
+ */
13057
+ function normalizeTouchedPath(
13058
+ raw: string,
13059
+ pkgDir: string,
13060
+ repoTop: string | null,
13061
+ ): { readonly candidates: readonly string[]; readonly outside: boolean } {
13062
+ // Codex round-2: normalize lexically FIRST so `lib/../../x` is seen as the traversal it is, and treat a
13063
+ // Windows-drive path (`C:/…`) on a POSIX host as outside the package (counted, said) rather than as a
13064
+ // package-relative name that silently matches nothing. Symlinked package dirs stay a named limit: the
13065
+ // containment check is lexical, not realpath-based.
13066
+ const p = posixNormalize(raw.replace(/\\/g, '/')).replace(/^(?:\.\/)+/, '');
13067
+ if (p === '' || p === '.') return { candidates: [], outside: false };
13068
+ if (/^[A-Za-z]:\//.test(p) && !isAbsolute(p)) return { candidates: [], outside: true };
13069
+
13070
+ if (isAbsolute(p)) {
13071
+ const rel = relative(pkgDir, resolve(p)).split(sep).join('/');
13072
+ return rel === '' || rel.startsWith('..') ? { candidates: [], outside: true } : { candidates: [rel], outside: false };
13073
+ }
13074
+
13075
+ const candidates = new Set<string>();
13076
+ if (p !== '..' && !p.startsWith('../')) candidates.add(p); // already package-relative, as given
13077
+ if (repoTop !== null) {
13078
+ const rel = relative(pkgDir, resolve(repoTop, p)).split(sep).join('/');
13079
+ if (rel !== '' && !rel.startsWith('..')) candidates.add(rel);
13080
+ }
13081
+ return candidates.size > 0 ? { candidates: [...candidates], outside: false } : { candidates: [], outside: true };
13082
+ }
13083
+
12958
13084
  function cmdMutationGate(
12959
13085
  options: Map<string, string>,
12960
13086
  flags: Set<string>,
@@ -12988,7 +13114,9 @@ function cmdMutationGate(
12988
13114
 
12989
13115
  let entries: readonly MutationRegistryEntry[] = parsed.registry.entries;
12990
13116
  let entryResults: readonly MutationEntryResult[] = parsed.entryResults;
13117
+ const totalRegistryEntries = entries.length;
12991
13118
  const only = options.get('only');
13119
+ let onlyIds: Set<string> | null = null;
12992
13120
  if (only !== undefined) {
12993
13121
  const ids = only.split(',').map((s) => s.trim()).filter(Boolean);
12994
13122
  const known = new Set([
@@ -12997,16 +13125,138 @@ function cmdMutationGate(
12997
13125
  ]);
12998
13126
  const unknown = ids.filter((id) => !known.has(id));
12999
13127
  if (unknown.length > 0) return fail(`--only names unknown entry id(s): ${unknown.join(', ')}`);
13128
+ onlyIds = new Set(ids);
13000
13129
  entries = entries.filter((e) => ids.includes(e.id));
13001
13130
  entryResults = entryResults.filter((result) => ids.includes(result.id));
13002
13131
  }
13003
13132
 
13133
+ // Feature qe-step-gate-scoped-to-feature (FR-1/FR-2/FR-3): scope the gate to the files a FEATURE
13134
+ // actually touched and/or entries added since a base ref, instead of the whole registry — MEASURED
13135
+ // 2026-09-12, 358 entries on this repo's core package ran 30-40 minutes and hit the timeout wall,
13136
+ // INCONCLUSIVE every time, though a feature owns only its own touched files. `--touched` and
13137
+ // `--added-since` UNION (a file changed by the feature OR an entry it newly added is in scope);
13138
+ // that union then INTERSECTS with `--only` when both are given, same algebra as an ordinary filter
13139
+ // chain. Both selectors are computed against the FULL (pre-`--only`) registry so their reported
13140
+ // counts describe what THEY matched, independent of any `--only` narrowing applied on top.
13141
+ const touchedRaw = options.get('touched');
13142
+ const addedSinceRaw = options.get('added-since');
13143
+ let selectionMeta: {
13144
+ readonly selected: number;
13145
+ readonly total: number;
13146
+ readonly touched: readonly string[] | null;
13147
+ readonly addedSince: readonly string[] | null;
13148
+ readonly base: string | null;
13149
+ readonly baseAbsent: boolean;
13150
+ readonly outsidePackage: number;
13151
+ } | null = null;
13152
+ if (touchedRaw !== undefined || addedSinceRaw !== undefined) {
13153
+ let repoTop: string | null = null;
13154
+ try { repoTop = execSync('git rev-parse --show-toplevel', { cwd: pkgDir, stdio: 'pipe', encoding: 'utf-8' }).trim() || null; } catch { /* not in a git repo */ }
13155
+
13156
+ const touchedIds = new Set<string>();
13157
+ let outsidePackageCount = 0;
13158
+ if (touchedRaw !== undefined) {
13159
+ const touchedPaths = touchedRaw.split(',').map((s) => s.trim()).filter(Boolean);
13160
+ const normalizedCandidates = new Set<string>();
13161
+ for (const raw of touchedPaths) {
13162
+ const { candidates, outside } = normalizeTouchedPath(raw, pkgDir, repoTop);
13163
+ if (outside) outsidePackageCount++;
13164
+ for (const c of candidates) normalizedCandidates.add(c);
13165
+ }
13166
+ for (const entry of parsed.registry.entries) {
13167
+ if (normalizedCandidates.has(entry.file)) touchedIds.add(entry.id);
13168
+ }
13169
+ }
13170
+
13171
+ const addedSinceIds = new Set<string>();
13172
+ let baseAbsentMessage: string | null = null;
13173
+ if (addedSinceRaw !== undefined) {
13174
+ if (repoTop === null) return fail(`--added-since requires ${pkgDir} to be inside a git repository`);
13175
+ try {
13176
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', `${addedSinceRaw}^{commit}`], { cwd: repoTop, stdio: 'pipe' });
13177
+ } catch {
13178
+ return fail(`--added-since names an unknown git ref: ${addedSinceRaw}`);
13179
+ }
13180
+ const registryRelPath = relative(repoTop, registryPath).split(sep).join('/');
13181
+ let baseRegistry: MutationRegistry | null = null;
13182
+ // Fix-round 1 (AM-3): only a genuinely ABSENT registry at that ref means "all entries are
13183
+ // new". Any OTHER `git show` failure (a bad path that isn't a missing-registry case, git
13184
+ // itself missing, a timeout) is a usage error, and a registry that parses to invalid JSON /
13185
+ // an invalid registry shape at that ref is a distinct usage error too — collapsing all three
13186
+ // into "absent" used to hide real failures behind a silently-too-generous selection.
13187
+ try {
13188
+ // stdio: registry-absent-at-ref is an EXPECTED outcome (git's own "fatal: path … does not
13189
+ // exist" on stderr would otherwise leak to the terminal by node's default inherit-stderr
13190
+ // behaviour) — pipe it into the caught error instead of printing it at the user.
13191
+ const baseText = execFileSync('git', ['show', `${addedSinceRaw}:${registryRelPath}`], { cwd: repoTop, encoding: 'utf-8', env: { ...process.env, LC_ALL: 'C', LANG: 'C' }, maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
13192
+ const baseParsed = parseMutationRegistry(baseText);
13193
+ if (baseParsed.registry === null) {
13194
+ return fail(`base registry at ${addedSinceRaw} is not a valid registry: ${baseParsed.errors.join('; ')}`);
13195
+ }
13196
+ baseRegistry = baseParsed.registry;
13197
+ } catch (e) {
13198
+ const stderr = typeof (e as { stderr?: unknown })?.stderr === 'string'
13199
+ ? (e as { stderr: string }).stderr
13200
+ : String((e as Error)?.message ?? e);
13201
+ if (/does not exist in|exists on disk, but not in|Path .* does not exist/i.test(stderr)) {
13202
+ baseRegistry = null; // registry absent at that ref → every current entry counts as added
13203
+ baseAbsentMessage = `base registry absent at ${addedSinceRaw} — all ${totalRegistryEntries} entries count as added`;
13204
+ } else {
13205
+ return fail(`git show ${addedSinceRaw}:${registryRelPath} failed: ${stderr.trim()}`);
13206
+ }
13207
+ }
13208
+ if (baseAbsentMessage !== null && !json) write(`mutation-gate: ${baseAbsentMessage}`);
13209
+ for (const id of registryEntriesAddedSince(baseRegistry, parsed.registry)) addedSinceIds.add(id);
13210
+ }
13211
+
13212
+ let unionIds = new Set<string>([...touchedIds, ...addedSinceIds]);
13213
+ if (onlyIds !== null) {
13214
+ const only2 = onlyIds;
13215
+ unionIds = new Set([...unionIds].filter((id) => only2.has(id)));
13216
+ }
13217
+ const selectorParts: string[] = [];
13218
+ if (touchedRaw !== undefined) selectorParts.push(`touched: ${touchedIds.size}${outsidePackageCount > 0 ? ` (${outsidePackageCount} outside package)` : ''}`);
13219
+ if (addedSinceRaw !== undefined) selectorParts.push(`added-since ${addedSinceRaw}: ${addedSinceIds.size}`);
13220
+ if (!json) write(`mutation-gate: selected ${unionIds.size} of ${totalRegistryEntries} entries (${selectorParts.join('; ')})`);
13221
+ entries = entries.filter((e) => unionIds.has(e.id));
13222
+ entryResults = entryResults.filter((result) => unionIds.has(result.id));
13223
+
13224
+ // AM-4: every scoped run — empty or not — carries a machine-readable `selection` object in the
13225
+ // JSON contract, not just the empty-selection early return.
13226
+ selectionMeta = {
13227
+ selected: unionIds.size,
13228
+ total: totalRegistryEntries,
13229
+ touched: touchedRaw !== undefined ? [...touchedIds].sort() : null,
13230
+ addedSince: addedSinceRaw !== undefined ? [...addedSinceIds].sort() : null,
13231
+ base: addedSinceRaw ?? null,
13232
+ baseAbsent: baseAbsentMessage !== null,
13233
+ outsidePackage: outsidePackageCount,
13234
+ };
13235
+
13236
+ if (unionIds.size === 0) {
13237
+ const msg = `0 entries match ${selectorParts.join(', ')} — nothing to run`;
13238
+ if (json) {
13239
+ write(JSON.stringify({
13240
+ packageDir: pkgDir,
13241
+ registryPath,
13242
+ selection: selectionMeta,
13243
+ results: [],
13244
+ summary: summarizeMutationResults([]),
13245
+ exitCode: 0,
13246
+ }, null, 2));
13247
+ } else {
13248
+ write(`mutation-gate: ${msg}`);
13249
+ }
13250
+ return 0;
13251
+ }
13252
+ }
13253
+
13004
13254
  if (entries.length === 0) {
13005
13255
  const scope = only === undefined ? 'registry' : 'selected registry entries';
13006
13256
  const error = `${scope} has no runnable entries after validation — nothing can be run; the registry is unusable`;
13007
13257
  const summary = summarizeMutationResults(entryResults);
13008
13258
  if (json) {
13009
- write(JSON.stringify({ error, registryPath, results: entryResults, summary, exitCode: 2 }, null, 2));
13259
+ write(JSON.stringify({ error, registryPath, ...(selectionMeta !== null ? { selection: selectionMeta } : {}), results: entryResults, summary, exitCode: 2 }, null, 2));
13010
13260
  } else {
13011
13261
  write(`dz mutation-gate: ${error}`);
13012
13262
  for (const result of entryResults) {
@@ -13018,7 +13268,7 @@ function cmdMutationGate(
13018
13268
 
13019
13269
  const testCmdRaw = options.get('test-cmd') ?? parsed.registry.testCommand ?? 'npm test';
13020
13270
  if (/[\0\n\r]/.test(testCmdRaw)) return fail('--test-cmd may not contain NUL or newline characters');
13021
- const testCmd = testCmdRaw;
13271
+ let testCmd = testCmdRaw;
13022
13272
  const excludedSelfChecks = REGISTRY_SELFCHECK_TESTS.filter((testFile) =>
13023
13273
  entries.some((entry) => buildMutationTestCommand(testCmd, entry).excluded.includes(testFile)),
13024
13274
  );
@@ -13026,8 +13276,88 @@ function cmdMutationGate(
13026
13276
  write(`mutation-gate: self-check excluded from mutant runs: ${excludedSelfChecks.join(', ') || '(none)'}`);
13027
13277
  }
13028
13278
 
13029
- const timeoutOpt = Number(options.get('timeout') ?? '300000');
13030
- const timeout = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
13279
+ // mutation-gate-timeout-verdict FR-3/FR-4: precedence is the `--timeout` flag > the registry's
13280
+ // own `timeoutMs` field > the 300000ms default. A package whose real baseline run is longer than
13281
+ // the default (this repo's core package, MEASURED ≈5-8 min) declares its floor in the registry so
13282
+ // a bare `dz mutation-gate` — no flag — still succeeds (AC-5).
13283
+ const timeoutFlagRaw = options.get('timeout');
13284
+ let timeout: number;
13285
+ let timeoutSource: 'flag' | 'registry' | 'default';
13286
+ if (timeoutFlagRaw !== undefined) {
13287
+ const timeoutOpt = Number(timeoutFlagRaw);
13288
+ if (Number.isFinite(timeoutOpt) && timeoutOpt > 0) {
13289
+ timeout = timeoutOpt;
13290
+ timeoutSource = 'flag';
13291
+ } else {
13292
+ timeout = 300000;
13293
+ timeoutSource = 'default';
13294
+ }
13295
+ } else if (parsed.registry.timeoutMs !== undefined) {
13296
+ timeout = parsed.registry.timeoutMs;
13297
+ timeoutSource = 'registry';
13298
+ } else {
13299
+ timeout = 300000;
13300
+ timeoutSource = 'default';
13301
+ }
13302
+ if (!json) write(`mutation-gate: timeout: ${timeout} ms (${timeoutSource})`);
13303
+
13304
+ // mutation-gate-baseline-honesty FR-2/FR-3: precedence is the `--max-workers` flag > the
13305
+ // registry's own `maxWorkers` field > `min(4, max(1, floor(cpus/2)))`. Baseline and mutant runs
13306
+ // spawn the package's FULL testCommand at vitest's DEFAULT worker count (= cpu cores) unless
13307
+ // capped — MEASURED: under embedding-daemon tests (0.7-3.5 GB/process) this repo's core package
13308
+ // hit load 62-358 and 0.4-1.8 GB free on an 8-core/16GB box, killing three full overnight runs
13309
+ // (0bb74d66); the same suite at `--maxWorkers=2` passed 6909/6909.
13310
+ const maxWorkersFlagRaw = options.get('max-workers');
13311
+ const defaultMaxWorkers = Math.min(4, Math.max(1, Math.floor(cpus().length / 2)));
13312
+ let maxWorkers: number;
13313
+ let maxWorkersSource: 'flag' | 'registry' | 'default';
13314
+ if (maxWorkersFlagRaw !== undefined) {
13315
+ // fix-round 1, AM-1: an invalid `--max-workers` value (0, negative, fractional, `NaN`,
13316
+ // non-numeric) is a USAGE ERROR (exit 2) — mirroring the `--only` unknown-id fail() style —
13317
+ // never a silent fallback to the default. A silent fallback would make a typo'd flag run
13318
+ // uncapped-by-mistake while LOOKING capped (the source line still says "(default)").
13319
+ const maxWorkersOpt = Number(maxWorkersFlagRaw);
13320
+ if (!Number.isInteger(maxWorkersOpt) || maxWorkersOpt < 1) {
13321
+ return fail(`--max-workers must be a positive integer, got '${maxWorkersFlagRaw}'`);
13322
+ }
13323
+ maxWorkers = maxWorkersOpt;
13324
+ maxWorkersSource = 'flag';
13325
+ } else if (parsed.registry.maxWorkers !== undefined) {
13326
+ maxWorkers = parsed.registry.maxWorkers;
13327
+ maxWorkersSource = 'registry';
13328
+ } else {
13329
+ maxWorkers = defaultMaxWorkers;
13330
+ maxWorkersSource = 'default';
13331
+ }
13332
+ // FR-3: inject the ceiling into the command ONLY when it is (detectably) a vitest run and does
13333
+ // not already name the flag itself — an arbitrary testCommand cannot be assumed to accept
13334
+ // `--maxWorkers`. VITEST_MAX_WORKERS is set in the env unconditionally (below, at spawn time)
13335
+ // regardless of this detection, so a vitest command reached indirectly (e.g. through a package
13336
+ // script) is still capped.
13337
+ // fix-round 1, AM-3: detection AND injection are scoped to the VITEST SEGMENT — from the first
13338
+ // `vitest run` token to the next `&&`/`||`/`;`/`|` (or end of string) — and the flag lands right
13339
+ // after `vitest run`, never appended to the tail of a whole (possibly compound) command. A
13340
+ // raw-substring append over the FULL command turned `vitest run … && cleanup` into
13341
+ // `vitest run … && cleanup --maxWorkers=2` (silently handed to `cleanup`, not vitest), and the
13342
+ // existing-flag check could be suppressed by an unrelated `--maxWorkers` substring living outside
13343
+ // the vitest segment entirely (e.g. inside `cleanup`'s own args, or before `vitest run` in the
13344
+ // same command).
13345
+ const vitestRunIdx = testCmd.indexOf('vitest run');
13346
+ const isVitestCommand = vitestRunIdx !== -1;
13347
+ if (isVitestCommand) {
13348
+ const tailFromRun = testCmd.slice(vitestRunIdx);
13349
+ const terminator = /&&|\|\||;|\|/.exec(tailFromRun);
13350
+ const vitestSegment = terminator !== null ? tailFromRun.slice(0, terminator.index) : tailFromRun;
13351
+ if (!vitestSegment.includes('--maxWorkers')) {
13352
+ const insertAt = vitestRunIdx + 'vitest run'.length;
13353
+ testCmd = `${testCmd.slice(0, insertAt)} --maxWorkers=${maxWorkers}${testCmd.slice(insertAt)}`;
13354
+ }
13355
+ }
13356
+ if (!json) {
13357
+ write(isVitestCommand
13358
+ ? `mutation-gate: workers: ${maxWorkers} (${maxWorkersSource})`
13359
+ : 'mutation-gate: workers: n/a — test command is not vitest');
13360
+ }
13031
13361
 
13032
13362
  // Route-b guard mode: `per-entry` (default, strongest — each red entry re-baselines the restored
13033
13363
  // tree, so a flaky neighbour flips THAT entry to INCONCLUSIVE) or `final` (cheap — one re-run at
@@ -13108,22 +13438,29 @@ function cmdMutationGate(
13108
13438
  phase: 'baseline' | 'mutation' | 'rebaseline' | 'final-rebaseline',
13109
13439
  entryId?: string,
13110
13440
  ): MutationGateRunnerObservation => {
13441
+ // FR-3: VITEST_MAX_WORKERS is set in any case — regardless of whether the command was
13442
+ // recognised as vitest and got the `--maxWorkers=<n>` flag injected — so a vitest command
13443
+ // reached indirectly (a wrapper script) is still capped.
13444
+ const extraEnv: Readonly<Record<string, string>> = { VITEST_MAX_WORKERS: String(maxWorkers) };
13111
13445
  if (injectedRunner !== undefined) {
13112
13446
  return injectedRunner(suiteCommand, {
13113
13447
  cwd: copyDir,
13114
13448
  timeoutMs: timeout,
13115
13449
  phase,
13450
+ env: extraEnv,
13116
13451
  ...(entryId !== undefined ? { entryId } : {}),
13117
13452
  });
13118
13453
  }
13454
+ const startedAt = performance.now();
13119
13455
  const run = spawnSync(suiteCommand, {
13120
13456
  cwd: copyDir,
13121
13457
  shell: true,
13122
13458
  encoding: 'utf-8',
13123
13459
  timeout,
13124
13460
  maxBuffer: 64 * 1024 * 1024,
13125
- env: { ...process.env, FORCE_COLOR: '0' },
13461
+ env: { ...process.env, FORCE_COLOR: '0', ...extraEnv },
13126
13462
  });
13463
+ const elapsedMs = Math.round(performance.now() - startedAt);
13127
13464
  const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
13128
13465
  ? run.error.code
13129
13466
  : undefined;
@@ -13134,10 +13471,28 @@ function cmdMutationGate(
13134
13471
  throw run.error;
13135
13472
  }
13136
13473
  const signal = typeof run.signal === 'string' ? run.signal : undefined;
13474
+ // mutation-gate-timeout-verdict FR-1/FR-2: a genuine ETIMEDOUT must NEVER be read as a real
13475
+ // suite verdict, even when the killed child intercepted the kill signal and exited with its
13476
+ // OWN status (e.g. a SIGTERM handler calling `process.exit(1)`) — MEASURED: node still sets
13477
+ // `run.error.code === 'ETIMEDOUT'` in that case, but a numeric `run.status` used to win the
13478
+ // `typeof run.status !== 'number'` check below, so the timeout was silently reported as
13479
+ // "baseline suite RED (exit 1)", indistinguishable from a real red suite. ETIMEDOUT now short
13480
+ // -circuits to `exitCode: null` unconditionally, before that check runs.
13481
+ if (errorCode === 'ETIMEDOUT') {
13482
+ // Codex round-1 (2026-09-14): report BOTH facts when both exist — a trapped SIGTERM that
13483
+ // exits 1 shows `child exit 1; signal SIGTERM`, a plain kill shows `no exit code; signal …`.
13484
+ const exitPart = typeof run.status === 'number' ? `child exit ${run.status}` : 'child produced no exit code';
13485
+ const childExit = signal === undefined ? exitPart : `${exitPart}; signal ${signal}`;
13486
+ const suggestedMs = Math.max(timeout + 1, Math.ceil(elapsedMs * 2));
13487
+ return {
13488
+ exitCode: null,
13489
+ output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
13490
+ failureReason: `timeout after ${timeout}ms (elapsed ${elapsedMs}ms); ${childExit}; try --timeout ${suggestedMs} or the registry's timeoutMs field`,
13491
+ };
13492
+ }
13137
13493
  let failureReason: string | undefined;
13138
13494
  if (typeof run.status !== 'number') {
13139
- if (errorCode === 'ETIMEDOUT') failureReason = `timeout after ${timeout}ms${signal === undefined ? '' : `; signal=${signal}`}`;
13140
- else if (errorCode === 'ENOBUFS') failureReason = 'maxBuffer exceeded (ENOBUFS; 67108864-byte output ceiling)';
13495
+ if (errorCode === 'ENOBUFS') failureReason = 'maxBuffer exceeded (ENOBUFS; 67108864-byte output ceiling)';
13141
13496
  else if (signal !== undefined) failureReason = `child killed by signal ${signal}`;
13142
13497
  else if (errorCode !== undefined) failureReason = `spawn failure code ${errorCode}`;
13143
13498
  else failureReason = 'spawn failure with no error code or signal';
@@ -13389,7 +13744,7 @@ function cmdMutationGate(
13389
13744
 
13390
13745
  const exitCode = mutationGateExitCode(results, baseline.ok);
13391
13746
  if (json) {
13392
- write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, exitCode }, null, 2));
13747
+ write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, ...(selectionMeta !== null ? { selection: selectionMeta } : {}), exitCode }, null, 2));
13393
13748
  return exitCode;
13394
13749
  }
13395
13750
  write(renderMutationReport(results, baseline, pkgDir));
@@ -20541,7 +20896,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
20541
20896
  case 'release':
20542
20897
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
20543
20898
  case 'parity':
20544
- return cmdParity(options, flags, write, writeErr, cwd);
20899
+ return await cmdParity(options, flags, write, writeErr, cwd);
20545
20900
  case 'registry':
20546
20901
  return cmdRegistry(options, cwd, write);
20547
20902
  case 'benchmark':
@@ -10,6 +10,7 @@
10
10
  * See harness-core/src/cli-flag-notice.ts for the full reasoning and the honest limit.
11
11
  */
12
12
  export const KNOWN_CLI_FLAGS: readonly string[] = [
13
+ 'added-since',
13
14
  'affected',
14
15
  'all',
15
16
  'allow-cold-start',
@@ -162,6 +163,7 @@ export const KNOWN_CLI_FLAGS: readonly string[] = [
162
163
  'max',
163
164
  'max-per-day',
164
165
  'max-wall-clock',
166
+ 'max-workers',
165
167
  'memory',
166
168
  'mock',
167
169
  'mode',
@@ -239,6 +241,10 @@ export const KNOWN_CLI_FLAGS: readonly string[] = [
239
241
  'pubkey',
240
242
  'publish',
241
243
  'quarter',
244
+ // Not a dz flag: `git rev-parse --verify --quiet` is a git argument this CLI passes through
245
+ // (mutation-gate --added-since ref validation) — same pass-through class as the git-diff volume
246
+ // counters noted above.
247
+ 'quiet',
242
248
  'reason',
243
249
  'rebaseline',
244
250
  'recalled',
@@ -324,6 +330,7 @@ export const KNOWN_CLI_FLAGS: readonly string[] = [
324
330
  'topic',
325
331
  'topics',
326
332
  'tokens',
333
+ 'touched',
327
334
  'transcript',
328
335
  'type',
329
336
  'untracked-files', // git status argument