@dzhechkov/harness-cli 0.8.24 → 0.8.26

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
@@ -4,8 +4,16 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
 
7
- import { appendFileSync, chmodSync, closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, type Dirent } from 'node:fs';
8
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
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';
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';
15
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, posix as nodePosixPath } from 'node:path';
16
+ const posixNormalize = nodePosixPath.normalize;
9
17
  import { fileURLToPath } from 'node:url';
10
18
  import { request as httpsRequest } from 'node:https';
11
19
  import { KNOWN_CLI_FLAGS } from './known-flags.js';
@@ -14,7 +22,7 @@ import { resolveInstallSpec } from './install-spec.js';
14
22
  import { dispatchedCommands, documentedCommands } from './command-inventory.js';
15
23
  import { execFile, execFileSync, execSync, spawn, spawnSync, type ChildProcess } from 'node:child_process';
16
24
  import { createHash, randomBytes } from 'node:crypto';
17
- import { homedir, hostname, tmpdir } from 'node:os';
25
+ import { cpus, homedir, hostname, tmpdir } from 'node:os';
18
26
  import { createRequire } from 'node:module';
19
27
  import { isDeepStrictEqual } from 'node:util';
20
28
  import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed, type JournalKind, type JournalIo } from '@dzhechkov/harness-core';
@@ -64,6 +72,7 @@ import {
64
72
  runDoctor,
65
73
  runInit,
66
74
  discoverSkillIds,
75
+ loadSkillFromDir,
67
76
  resolveSelection,
68
77
  formatSelectRefusal,
69
78
  runIntegrationsVerify,
@@ -138,6 +147,7 @@ import {
138
147
  TARGET_SHORT_LABELS,
139
148
  applyLegStatus,
140
149
  applyLegReasonMessage,
150
+ probeApplyLeg,
141
151
  resolveAgentdbPath,
142
152
  WORKFLOW_TEMPLATES_RETIRED_MESSAGE,
143
153
  parsePlan,
@@ -557,6 +567,7 @@ import {
557
567
  REGISTRY_SELFCHECK_TESTS,
558
568
  buildMutationTestCommand,
559
569
  parseMutationRegistry,
570
+ registryEntriesAddedSince,
560
571
  applyMutationToText,
561
572
  attributeBaselineRedness,
562
573
  countFailingTests,
@@ -629,7 +640,7 @@ import {
629
640
  import type { OperatorProfile, Domain as ProfileDomain, Register as ProfileRegister } from '@dzhechkov/harness-core';
630
641
  import type { RecordKind, RecordDecision } from '@dzhechkov/harness-core';
631
642
  import type { CadenceWindow } from '@dzhechkov/harness-core';
632
- import type { MutationEntryResult, MutationObservation, MutationRegistryEntry } from '@dzhechkov/harness-core';
643
+ import type { MutationEntryResult, MutationObservation, MutationRegistryEntry, MutationRegistry } from '@dzhechkov/harness-core';
633
644
  import type { SkillApplyFailure, SkillLoadFailure } from '@dzhechkov/harness-core';
634
645
  import type { ReqeDebt } from '@dzhechkov/harness-core';
635
646
  import type { ClassifyResultRow, ExecutionEvidence } from '@dzhechkov/harness-core';
@@ -751,7 +762,7 @@ Usage:
751
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)
752
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)
753
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)
754
- 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)
755
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)
756
767
  dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
757
768
  dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
@@ -854,6 +865,13 @@ export type MutationGateRunner = (
854
865
  readonly timeoutMs: number;
855
866
  readonly phase: 'baseline' | 'mutation' | 'rebaseline' | 'final-rebaseline';
856
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>>;
857
875
  },
858
876
  ) => MutationGateRunnerObservation;
859
877
 
@@ -955,6 +973,20 @@ export interface CliIo {
955
973
  command: string,
956
974
  options: { cwd?: string | URL | undefined; stdio?: unknown; encoding?: unknown; timeout?: number | undefined; env?: NodeJS.ProcessEnv | undefined },
957
975
  ) => string;
976
+ /**
977
+ * Test seam for `dz publish`'s gate-audit writer (feature `publish-gate-audit-durable`, FR-2):
978
+ * overrides the fs primitives `appendPublishGateAudit` uses for its durable append (production
979
+ * leaves it unset → the real `node:fs` functions). Lets a test make `fsyncSync` throw to prove
980
+ * `(audit NOT logged: …)` is printed and the write is reported as failed, without touching any
981
+ * other seam's filesystem.
982
+ */
983
+ readonly publishGateAuditFsLayer?: PublishGateAuditFsLayer;
984
+ /**
985
+ * AM-5 (feature publish-gate-audit-durable): test seam for the sibling-drift gate's `npm pack
986
+ * --dry-run --json` call (production leaves it unset → real `execFileSync`). Takes the package
987
+ * dir, returns raw stdout, or throws to simulate a real `npm` failure without spawning anything.
988
+ */
989
+ readonly publishNpmPackRunner?: (dir: string) => string;
958
990
  /**
959
991
  * Test seam for `dz install`: overrides the `npm install` subprocess (production leaves
960
992
  * it unset → real `execSync`, stdio piped). A stub runner that pre-stages a fixture
@@ -2584,6 +2616,41 @@ async function cmdInstall(
2584
2616
  if (root.layout === 'npx-template' && root.hasCompanionAssets) {
2585
2617
  write(` note: ${pkg} also ships commands/hooks/agents — \`npx -y ${pkg} init\` installs the full kit.`);
2586
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
+ }
2587
2654
  // Skip-and-collect at install time (D1 / the report's D2 amendment): the offending
2588
2655
  // SKILL.md came out of the DOWNLOADED TARBALL, so the path is rendered relative to
2589
2656
  // the package root (a `node_modules/**` absolute path is not actionable) and the
@@ -5046,7 +5113,18 @@ async function cmdRecall(
5046
5113
 
5047
5114
  if (hits.length === 0) {
5048
5115
  write(`dz recall "${shownQuery}"`);
5049
- 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
+ }
5050
5128
  // The domain note must print here too (Codex QE #10): a --domain run with zero hits
5051
5129
  // silently said nothing about the domain, so the reader could not tell whether the
5052
5130
  // boost had been applied and found nothing, or had not run at all.
@@ -7035,6 +7113,77 @@ function packedInstallScratchRoot(): string {
7035
7113
  return existsSync('/var/tmp') ? '/var/tmp' : tmpdir();
7036
7114
  }
7037
7115
 
7116
+ /**
7117
+ * FR-1 (feature release-smoke-staged-pack): stage every target's `package.json` exactly like a
7118
+ * live publish packs it — `workspace:*` sibling specs rewritten to the exact sibling version
7119
+ * (`rewriteWorkspaceSpecs`), `scripts.prepublishOnly` dropped — run `fn`, then ALWAYS restore the
7120
+ * original bytes in a `finally`, whatever `fn` does or throws. A restore failure is reported
7121
+ * through `write` (with the path), never swallowed — the "absence of a receipt is not success"
7122
+ * rule this file follows everywhere else.
7123
+ *
7124
+ * `cmdPublish`'s dry-run preview and `cmdRelease`'s packed-install-smoke `pack` steps both go
7125
+ * through this ONE helper, so the two doors that ask "what would the registry receive?" pack the
7126
+ * exact same bytes (MEASURED 2026-09-13 16:05: `dz release` packed the live `workspace:*`
7127
+ * package.json and its smoke install died with EUNSUPPORTEDPROTOCOL — `dz publish`'s preview
7128
+ * already staged around this and release never got that).
7129
+ */
7130
+ function withStagedPackageJson<T>(
7131
+ targets: readonly { readonly dir: string }[],
7132
+ workspaceVersions: ReadonlyMap<string, string>,
7133
+ write: Write,
7134
+ fn: () => T,
7135
+ label = 'dz',
7136
+ ): T {
7137
+ // Lead edits after Codex review (2026-09-13, findings 1/5/6): every write — the staged text and
7138
+ // the restore — goes through a sibling temp file + rename, so a reader never sees a truncated
7139
+ // package.json; a restore that FAILS is an error the caller must see (thrown after fn, or attached
7140
+ // to fn's own error), never a warning that lets a run "succeed" on a damaged tree; and the
7141
+ // diagnostic keeps the calling command's name (`label`).
7142
+ const atomicWrite = (path: string, text: string): void => {
7143
+ // Codex round 2: an EXCLUSIVE, randomized sibling temp — never a shared pid-named file
7144
+ const tmp = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.staged.tmp`;
7145
+ writeFileSync(tmp, text, { flag: 'wx' });
7146
+ renameSync(tmp, path);
7147
+ };
7148
+ const stagedOriginals: Array<{ path: string; text: string }> = [];
7149
+ let fnError: unknown;
7150
+ let fnThrew = false;
7151
+ try {
7152
+ for (const p of targets) {
7153
+ const pkgJsonPath = join(p.dir, 'package.json');
7154
+ const original = readFileSync(pkgJsonPath, 'utf-8');
7155
+ const rewritten = JSON.parse(rewriteWorkspaceSpecs(original, workspaceVersions)) as Record<string, unknown>;
7156
+ const scripts = rewritten['scripts'];
7157
+ if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) delete (scripts as Record<string, unknown>)['prepublishOnly'];
7158
+ stagedOriginals.push({ path: pkgJsonPath, text: original });
7159
+ atomicWrite(pkgJsonPath, JSON.stringify(rewritten, null, 2) + '\n');
7160
+ }
7161
+ return fn();
7162
+ } catch (err) {
7163
+ fnThrew = true;
7164
+ fnError = err;
7165
+ throw err;
7166
+ } finally {
7167
+ const restoreFailures: string[] = [];
7168
+ for (const o of stagedOriginals) {
7169
+ try {
7170
+ atomicWrite(o.path, o.text);
7171
+ } catch (err) {
7172
+ const msg = `${label}: ✗ could not restore ${o.path} after staged packing: ${formatPublishError(err)} — the tree is left STAGED, restore it by hand`;
7173
+ try { write(msg); } catch { /* a throwing writer must not mask the restore failure */ }
7174
+ restoreFailures.push(msg);
7175
+ }
7176
+ }
7177
+ if (restoreFailures.length > 0) {
7178
+ // Codex round 2: one aggregate error carrying BOTH fn's own failure (if any) and the restore
7179
+ // failures — never a bare message assignment that could itself throw out of finally.
7180
+ const fnPart = fnThrew ? `\n(during: ${fnError instanceof Error ? fnError.message : String(fnError)})` : '';
7181
+ // eslint-disable-next-line no-unsafe-finally -- a damaged tree must not read as success
7182
+ throw new Error(`${restoreFailures.join('\n')}${fnPart}`);
7183
+ }
7184
+ }
7185
+ }
7186
+
7038
7187
  function cmdPublish(
7039
7188
  options: Map<string, string>,
7040
7189
  flags: Set<string>,
@@ -7047,6 +7196,14 @@ function cmdPublish(
7047
7196
  command: string,
7048
7197
  options: { cwd?: string | URL | undefined; stdio?: unknown; encoding?: unknown; timeout?: number | undefined; env?: NodeJS.ProcessEnv | undefined },
7049
7198
  ) => string,
7199
+ gateAuditFsLayer?: PublishGateAuditFsLayer,
7200
+ /**
7201
+ * AM-5 (feature publish-gate-audit-durable): test seam for the sibling-drift gate's `npm pack
7202
+ * --dry-run --json` call — production leaves it unset (real `execFileSync`). Takes the package
7203
+ * dir, returns raw stdout, or THROWS to simulate a real `npm` failure — a test can then prove the
7204
+ * failure reaches `parseNpmPackInventory`'s caller as `unavailable`, never a real subprocess.
7205
+ */
7206
+ npmPackRunner?: (dir: string) => string,
7050
7207
  ): number {
7051
7208
  const json = flags.has('json');
7052
7209
  // Under --json stdout carries exactly one JSON document, so every human line — guard notes, refusals,
@@ -7187,19 +7344,55 @@ function cmdPublish(
7187
7344
  }
7188
7345
  });
7189
7346
 
7190
- // AM-6: an override (--allow-sibling-drift) is only real once its audit row is DURABLE. A write
7191
- // failure must refuse the publish rather than print "(logged)" about a log entry that never
7192
- // landed the same "absence of a receipt is not success" lesson the registry-probe gate already
7193
- // enforces for a publish's own confirmation.
7194
- const auditedOverride = (detail: string, humanMessage: string, pkgNameForBlock: string): boolean => {
7195
- const wrote = appendPublishGateAudit(cwd, 'sibling-drift', 'warn', detail, '--allow-sibling-drift');
7196
- if (wrote) {
7197
- write(`dz publish: ${humanMessage}allowed via --allow-sibling-drift (logged)`);
7198
- return false;
7347
+ // AM-4: `npm pack --dry-run --json` is a real subprocess cache it for the lifetime of this
7348
+ // ENTIRE run (keyed by resolved dir), NOT per package being checked (round-1 review, finding 5):
7349
+ // the cache used to be re-created inside the per-package loop body, so two different dependents
7350
+ // of the SAME sibling packed it twice. `npmPackRunner` (AM-5) is a test seam — production leaves
7351
+ // it unset and runs the real subprocess; a test injects a stub that throws to prove a real `npm`
7352
+ // failure reaches the caller as `unavailable`, without spawning anything.
7353
+ // Lead fix after the fix-round's live dry-run (2026-09-14 01:02, MEASURED on the hub): the
7354
+ // workspace side is now PACKED BY THE LIVE TRANSPORT `pnpm pack` into a per-run temp dir,
7355
+ // unpacked, and handed to core as a `packedDir` that core hashes with the SAME full walk it uses
7356
+ // for the published tarball. `npm pack --dry-run --json` (kept behind the `npmPackRunner` test
7357
+ // seam) never lists the LICENSE pnpm synthesises from the workspace root into a package whose own
7358
+ // tree has none, so two siblings unchanged since publication (harness-presets, scout) read as
7359
+ // "LICENSE only in the published copy" — a false drift the fix-round's inventory could not see.
7360
+ // Honest limit: the seam path (tests) still parses npm's JSON; only production takes the pnpm path.
7361
+ const npmPackInventoryCache = new Map<string, LocalInventoryResult>();
7362
+ let packTmpDir: string | undefined;
7363
+ const npmPackInventory = (dir: string): LocalInventoryResult => {
7364
+ const key = resolve(dir);
7365
+ const hit = npmPackInventoryCache.get(key);
7366
+ if (hit !== undefined) return hit;
7367
+ let out: LocalInventoryResult;
7368
+ try {
7369
+ if (npmPackRunner !== undefined) {
7370
+ out = parseNpmPackInventory(npmPackRunner(dir));
7371
+ } else {
7372
+ packTmpDir ??= mkdtempSync(join(tmpdir(), 'dz-drift-pack-'));
7373
+ out = { packedDir: extractIntoTempDir(dir, mkdtempSync(join(packTmpDir, 'p-'))).dir };
7374
+ }
7375
+ } catch (err) {
7376
+ const how = npmPackRunner !== undefined ? 'npm pack --dry-run --json' : 'pnpm pack';
7377
+ out = { unavailable: `${how} failed: ${(err as Error).message.split('\n')[0]}` };
7199
7378
  }
7200
- write(`dz publish: BLOCKED ${pkgNameForBlock} — ${humanMessage}, and the override could not be recorded (audit write failed); refusing rather than proceeding unlogged`);
7201
- return true;
7379
+ npmPackInventoryCache.set(key, out);
7380
+ return out;
7202
7381
  };
7382
+ const localInventorySource: InventorySource = npmPackRunner !== undefined ? 'npm-pack' : 'pnpm-pack';
7383
+
7384
+ // AM-3 (Codex round-1 review, finding 4, high): sibling-drift audit records are EXACTLY one per
7385
+ // package per rule per RUN. The old code appended one JSONL record per SIBLING a package depends
7386
+ // on (a package with two drifted deps wrote two rows under the same rule), and the `unavailable`
7387
+ // branch without `--allow-sibling-drift` wrote NO record at all. Every sibling outcome for a
7388
+ // package is now aggregated first (`pkParts`/`pkVerdict`/`pkOverrideUsed`) and written ONCE —
7389
+ // `block` if any sibling blocks, else `warn` if the only issues were resolved via
7390
+ // `--allow-sibling-drift`, else `pass` — with a detail naming every sibling and its status.
7391
+ // `siblingDriftAudited` guarantees the single write even though `--include-drifted`'s fixed-point
7392
+ // loop can revisit the SAME package across rounds: a package's own `dependencies` never change
7393
+ // between rounds, so a later round can only ever re-derive a SUBSET of what the first pass
7394
+ // already covered (its siblings that drifted got folded into the batch and are now skipped).
7395
+ const siblingDriftAudited = new Set<string>();
7203
7396
 
7204
7397
  let driftBlocked = 0;
7205
7398
  const extraBatch = new Set<string>();
@@ -7208,6 +7401,9 @@ function cmdPublish(
7208
7401
  // (finding 2) showed the single pass never re-checked an EXPANDED batch's own new edges. Capped at
7209
7402
  // `allPackages.length + 1` rounds (the plan's own "цикл с потолком = число пакетов").
7210
7403
  const maxRounds = allPackages.length + 1;
7404
+ // Codex round-3 (2026-09-14): the per-run pack scratch is released in a `finally`, so an
7405
+ // exception thrown while hashing or auditing cannot leak a `dz-drift-pack-*` dir under tmpdir.
7406
+ try {
7211
7407
  for (let round = 0; round < maxRounds; round++) {
7212
7408
  let addedThisRound = false;
7213
7409
  for (const pk of targets) {
@@ -7221,13 +7417,25 @@ function cmdPublish(
7221
7417
  } catch (err) {
7222
7418
  // AM-3: an unreadable/invalid package.json for a BATCH package is an input this HARD gate
7223
7419
  // cannot build — it must BLOCK, never silently degrade to "no dependencies" (which used to
7224
- // read as a clean n/a).
7420
+ // read as a clean n/a). AND (finding 4) the non-override branch below used to print a
7421
+ // BLOCKED line with NO audit record behind it — an `unavailable` outcome is logged exactly
7422
+ // like every other outcome, override or not.
7225
7423
  const reason = `package.json unreadable/invalid (${(err as Error).message.split('\n')[0]})`;
7226
- if (allowSiblingDrift) {
7227
- if (auditedOverride(`${pk.name}: ${reason}`, `sibling drift check unavailable for ${pk.name} (${reason})`, pk.name)) driftBlocked++;
7228
- } else {
7229
- write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${reason}); add --allow-sibling-drift to override (logged) or fix the manifest`);
7230
- driftBlocked++;
7424
+ if (!siblingDriftAudited.has(pk.name)) {
7425
+ siblingDriftAudited.add(pk.name);
7426
+ if (allowSiblingDrift) {
7427
+ const wrote = appendPublishGateAudit(cwd, 'sibling-drift', 'warn', `${pk.name}: ${reason} allowed via --allow-sibling-drift`, [{ name: pk.name, version: pk.version }], '--allow-sibling-drift', gateAuditFsLayer);
7428
+ if (wrote.logged) {
7429
+ write(`dz publish: ⚠ sibling drift check unavailable for ${pk.name} (${reason}) — allowed via --allow-sibling-drift (logged)`);
7430
+ } else {
7431
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable for ${pk.name} (${reason}), and the override could not be recorded (audit write failed: ${wrote.reason ?? 'unknown reason'}); refusing rather than proceeding unlogged`);
7432
+ driftBlocked++;
7433
+ }
7434
+ } else {
7435
+ const wrote = appendPublishGateAudit(cwd, 'sibling-drift', 'block', `${pk.name}: ${reason}`, [{ name: pk.name, version: pk.version }], undefined, gateAuditFsLayer);
7436
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${reason}); add --allow-sibling-drift to override (logged) or fix the manifest${auditSuffix(wrote)}`);
7437
+ driftBlocked++;
7438
+ }
7231
7439
  }
7232
7440
  continue;
7233
7441
  }
@@ -7235,49 +7443,92 @@ function cmdPublish(
7235
7443
  const peerDeps = manifestObj?.peerDependencies ?? {};
7236
7444
  const optionalDeps = manifestObj?.optionalDependencies ?? {};
7237
7445
 
7238
- // AM-6: a package with no workspace: dependency at all is n/a for THIS gate — recorded as a
7239
- // pass note, not silence (FR-6 compatibility: output stays unchanged for such a batch).
7446
+ // AM-6: a package with no EXTERNAL sibling to check at all is n/a for THIS gate — recorded as
7447
+ // a pass note ("no external siblings"), not silence (FR-6 compatibility: this branch prints
7448
+ // nothing to stdout, matching the pre-existing behavior). "External" covers BOTH "no
7449
+ // workspace: dependency declared" and "every workspace: dependency is inside THIS batch"
7450
+ // (publishing fresh, nothing stale to drift from) — both used to leave this package with no
7451
+ // audit record at all when every dep resolved to the second case.
7240
7452
  const anyWorkspaceDep = [...Object.values(deps), ...Object.values(peerDeps), ...Object.values(optionalDeps)]
7241
7453
  .some((spec) => String(spec).startsWith('workspace:'));
7242
- if (!anyWorkspaceDep) {
7243
- appendPublishGateAudit(cwd, 'sibling-drift', 'pass', `${pk.name}: n/a — no workspace: dependency declared`);
7244
- continue;
7245
- }
7246
7454
 
7247
- const drifts = detectSiblingDrift({
7248
- dependencies: deps,
7249
- peerDependencies: peerDeps,
7250
- optionalDependencies: optionalDeps,
7251
- workspaceVersions,
7252
- workspaceDirs,
7253
- batch: batchNames,
7254
- fetchPublished,
7255
- });
7455
+ const pkParts: string[] = [];
7456
+ let pkVerdict: 'pass' | 'warn' | 'block' = 'pass';
7457
+ let pkOverrideUsed = false;
7458
+
7459
+ if (anyWorkspaceDep) {
7460
+ const drifts = detectSiblingDrift({
7461
+ localInventory: npmPackInventory,
7462
+ localInventorySource,
7463
+ dependencies: deps,
7464
+ peerDependencies: peerDeps,
7465
+ optionalDependencies: optionalDeps,
7466
+ workspaceVersions,
7467
+ workspaceDirs,
7468
+ batch: batchNames,
7469
+ fetchPublished,
7470
+ });
7256
7471
 
7257
- for (const r of drifts) {
7258
- if (r.status === 'same') {
7259
- appendPublishGateAudit(cwd, 'sibling-drift', 'pass', `${r.name}@${r.version} = workspace (dependent: ${pk.name})`);
7260
- write(`dz publish: ✓ sibling drift: none (${r.name}@${r.version} = workspace)`);
7261
- } else if (r.status === 'unavailable') {
7262
- if (allowSiblingDrift) {
7263
- if (auditedOverride(`${r.name}@${r.version}: ${r.reason}`, `sibling drift check unavailable for ${r.name}@${r.version} (${r.reason})`, pk.name)) driftBlocked++;
7472
+ for (const r of drifts) {
7473
+ if (r.status === 'same') {
7474
+ pkParts.push(`${r.name}@${r.version}: same`);
7475
+ write(`dz publish: ✓ sibling drift: none (${r.name}@${r.version} = workspace)`);
7476
+ } else if (r.status === 'unavailable') {
7477
+ if (allowSiblingDrift) {
7478
+ pkParts.push(`${r.name}@${r.version}: unavailable (${r.reason}) — allowed via --allow-sibling-drift`);
7479
+ if (pkVerdict !== 'block') pkVerdict = 'warn';
7480
+ pkOverrideUsed = true;
7481
+ } else {
7482
+ pkParts.push(`${r.name}@${r.version}: unavailable (${r.reason})`);
7483
+ pkVerdict = 'block';
7484
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${r.reason}); add --allow-sibling-drift to override (logged) or check network/registry access`);
7485
+ driftBlocked++;
7486
+ }
7487
+ } else if (includeDrifted) {
7488
+ pkParts.push(`${r.name}@${r.version}: drift (${r.changedFiles.length} file(s)) — auto-included via --include-drifted`);
7489
+ if (!batchNames.has(r.name) && !extraBatch.has(r.name)) {
7490
+ extraBatch.add(r.name);
7491
+ addedThisRound = true;
7492
+ write(`dz publish: → sibling drift: ${r.name}@${r.version} differs from the workspace (${r.changedFiles.length} file(s)) — adding to the batch via --include-drifted${r.missingExports.length > 0 ? ` (missing exports: ${r.missingExports.join(', ')})` : ''}`);
7493
+ }
7494
+ } else if (allowSiblingDrift) {
7495
+ pkParts.push(`${r.name}@${r.version}: drift (${r.changedFiles.length} file(s)) — allowed via --allow-sibling-drift`);
7496
+ if (pkVerdict !== 'block') pkVerdict = 'warn';
7497
+ pkOverrideUsed = true;
7264
7498
  } else {
7265
- write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${r.reason}); add --allow-sibling-drift to override (logged) or check network/registry access`);
7499
+ pkParts.push(`${r.name}@${r.version}: drift (${r.changedFiles.length} file(s))`);
7500
+ pkVerdict = 'block';
7501
+ const suggestFilter = filterStr !== undefined ? `${filterStr},${r.name}` : `${pk.name},${r.name}`;
7502
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift: @dzhechkov/${r.name.replace(/^@dzhechkov\//, '')}@${r.version} on the registry differs from the workspace (${r.changedFiles.length} file(s)); add ${r.name} to the batch (--filter ${suggestFilter}) or publish it first`);
7266
7503
  driftBlocked++;
7267
7504
  }
7268
- } else if (includeDrifted) {
7269
- if (!batchNames.has(r.name) && !extraBatch.has(r.name)) {
7270
- extraBatch.add(r.name);
7271
- addedThisRound = true;
7272
- write(`dz publish: sibling drift: ${r.name}@${r.version} differs from the workspace (${r.changedFiles.length} file(s)) adding to the batch via --include-drifted${r.missingExports.length > 0 ? ` (missing exports: ${r.missingExports.join(', ')})` : ''}`);
7505
+ }
7506
+ }
7507
+
7508
+ // AM-3/AM-6: the single, aggregated audit write for this package — "no external siblings"
7509
+ // when nothing was ever checked, otherwise every sibling's status joined into one detail.
7510
+ if (!siblingDriftAudited.has(pk.name)) {
7511
+ siblingDriftAudited.add(pk.name);
7512
+ const detail = pkParts.length > 0 ? pkParts.join('; ') : 'no external siblings';
7513
+ // AM-6: an override reason is attached only when the FINAL verdict is 'warn' — if some
7514
+ // OTHER sibling still stands as a live block, the override never actually excused the run.
7515
+ const overrideReason = pkOverrideUsed && pkVerdict !== 'block' ? '--allow-sibling-drift' : undefined;
7516
+ const wrote = appendPublishGateAudit(cwd, 'sibling-drift', pkVerdict, detail, [{ name: pk.name, version: pk.version }], overrideReason, gateAuditFsLayer);
7517
+ if (pkOverrideUsed) {
7518
+ // AM-6: the override is only real once ITS audit row is durable — a write failure must
7519
+ // refuse the publish rather than print "(logged)" about a record that never landed.
7520
+ if (wrote.logged) {
7521
+ write(`dz publish: ⚠ sibling drift override recorded for ${pk.name} — allowed via --allow-sibling-drift (logged)`);
7522
+ } else {
7523
+ write(`dz publish: BLOCKED ${pk.name} — the --allow-sibling-drift override could not be recorded (audit write failed: ${wrote.reason ?? 'unknown reason'}); refusing rather than proceeding unlogged`);
7524
+ driftBlocked++;
7273
7525
  }
7274
- } else if (allowSiblingDrift) {
7275
- if (auditedOverride(`${r.name}@${r.version}: ${r.changedFiles.length} file(s) differ from the workspace`, `sibling drift: ${r.name}@${r.version} differs from the workspace (${r.changedFiles.length} file(s))`, pk.name)) driftBlocked++;
7276
- } else {
7277
- appendPublishGateAudit(cwd, 'sibling-drift', 'block', `${pk.name} depends on ${r.name}@${r.version}; ${r.changedFiles.length} file(s) differ from the workspace`);
7278
- const suggestFilter = filterStr !== undefined ? `${filterStr},${r.name}` : `${pk.name},${r.name}`;
7279
- write(`dz publish: BLOCKED ${pk.name} — sibling drift: @dzhechkov/${r.name.replace(/^@dzhechkov\//, '')}@${r.version} on the registry differs from the workspace (${r.changedFiles.length} file(s)); add ${r.name} to the batch (--filter ${suggestFilter}) or publish it first`);
7280
- driftBlocked++;
7526
+ } else if (pkParts.length > 0) {
7527
+ write(`dz publish: sibling drift audit for ${pk.name}${auditSuffix(wrote)}`);
7528
+ } else if (!wrote.logged) {
7529
+ // Codex round-2 (2026-09-14), AM-6 residual: the "no external siblings" pass note stays
7530
+ // silent on stdout ONLY while its record actually landed — a failed audit write is said.
7531
+ write(`dz publish: sibling drift audit for ${pk.name} (no external siblings)${auditSuffix(wrote)}`);
7281
7532
  }
7282
7533
  }
7283
7534
  }
@@ -7292,6 +7543,9 @@ function cmdPublish(
7292
7543
  targets = allPackages.filter(matchesFilter);
7293
7544
  batchNames = new Set(targets.map((p) => p.name));
7294
7545
  }
7546
+ } finally {
7547
+ if (packTmpDir !== undefined) rmSync(packTmpDir, { recursive: true, force: true });
7548
+ }
7295
7549
 
7296
7550
  const siblingDriftFailed = driftBlocked > 0;
7297
7551
  if (siblingDriftFailed && !dryRun) {
@@ -7328,8 +7582,8 @@ function cmdPublish(
7328
7582
  let packedInstallSmokePreviewFailed = false;
7329
7583
  if (dryRun) {
7330
7584
  if (bins.length === 0) {
7331
- appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin');
7332
- write('dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)');
7585
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin', targets.map((p) => ({ name: p.name, version: p.version })), undefined, gateAuditFsLayer);
7586
+ write(`dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)${auditSuffix(wrote)}`);
7333
7587
  } else {
7334
7588
  const scratchRoot = packedInstallScratchRoot();
7335
7589
  const packDir = mkdtempSync(join(scratchRoot, 'dz-publish-pack-'));
@@ -7361,36 +7615,30 @@ function cmdPublish(
7361
7615
  // Lead edit after the live dry-run (13.09 12:05): the preview packed the WORKING directory with
7362
7616
  // `workspace:^` specs still inside, so `npm install <tgz>` died with EUNSUPPORTEDPROTOCOL — the
7363
7617
  // preview must stage package.json exactly as the live packedTransport does (sibling pins via
7364
- // rewriteWorkspaceSpecs, prepublishOnly dropped) and restore the originals afterwards.
7365
- const stagedOriginals: Array<{ path: string; text: string }> = [];
7618
+ // rewriteWorkspaceSpecs, prepublishOnly dropped) and restore the originals afterwards. Shared
7619
+ // with `dz release` via `withStagedPackageJson` (feature release-smoke-staged-pack).
7366
7620
  try {
7367
- for (const p of targets) {
7368
- const pkgJsonPath = join(p.dir, 'package.json');
7369
- const original = readFileSync(pkgJsonPath, 'utf-8');
7370
- const rewritten = JSON.parse(rewriteWorkspaceSpecs(original, workspaceVersions)) as Record<string, unknown>;
7371
- const scripts = rewritten['scripts'];
7372
- if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) delete (scripts as Record<string, unknown>)['prepublishOnly'];
7373
- stagedOriginals.push({ path: pkgJsonPath, text: original });
7374
- writeFileSync(pkgJsonPath, JSON.stringify(rewritten, null, 2) + '\n');
7375
- }
7376
- for (const step of smokePlan.steps) {
7377
- const r = runSmoke(step.cmd, { cwd: step.cwd, timeoutMs: step.timeoutMs });
7378
- smokeExecutions.push({ stepId: step.id, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, ...(r.timedOut !== undefined ? { timedOut: r.timedOut } : {}) });
7379
- }
7380
- } finally {
7381
- for (const o of stagedOriginals) { try { writeFileSync(o.path, o.text); } catch (err) { write(`dz publish: ⚠ could not restore ${o.path} after the preview smoke: ${formatPublishError(err)}`); } }
7621
+ withStagedPackageJson(targets, workspaceVersions, write, () => {
7622
+ for (const step of smokePlan.steps) {
7623
+ const r = runSmoke(step.cmd, { cwd: step.cwd, timeoutMs: step.timeoutMs });
7624
+ smokeExecutions.push({ stepId: step.id, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, ...(r.timedOut !== undefined ? { timedOut: r.timedOut } : {}) });
7625
+ }
7626
+ }, 'dz publish');
7627
+ } catch (err) {
7628
+ // a restore failure is a preview failure (Codex finding 1): never a green preview on a staged tree
7629
+ smokeExecutions.push({ stepId: 'staged-restore', exitCode: 1, stdout: '', stderr: formatPublishError(err) });
7382
7630
  }
7383
7631
  const smokeVerdict = judgePackedInstallSmoke(smokePlan, smokeExecutions);
7384
7632
  try { rmSync(packDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
7385
7633
  try { rmSync(installDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
7386
7634
 
7387
7635
  if (smokeVerdict.ok) {
7388
- appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'preview: pack/install/--version all clean');
7389
- write('dz publish: ✓ packed install smoke (preview)');
7636
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'preview: pack/install/--version all clean', targets.map((p) => ({ name: p.name, version: p.version })), undefined, gateAuditFsLayer);
7637
+ write(`dz publish: ✓ packed install smoke (preview)${auditSuffix(wrote)}`);
7390
7638
  } else {
7391
7639
  const detail = smokeVerdict.failureDetail ?? smokeVerdict.bins.find((b) => !b.ok)?.detail ?? '(no detail)';
7392
- appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail);
7393
- write(`dz publish: BLOCKED — packed install smoke failed (preview): ${detail}`);
7640
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail, targets.map((p) => ({ name: p.name, version: p.version })), undefined, gateAuditFsLayer);
7641
+ write(`dz publish: BLOCKED — packed install smoke failed (preview): ${detail}${auditSuffix(wrote)}`);
7394
7642
  for (const b of smokeVerdict.bins.filter((b) => !b.ok)) write(` ✗ ${b.pkg} (${b.binName}): ${b.detail ?? '(no detail)'}`);
7395
7643
  packedInstallSmokePreviewFailed = true;
7396
7644
  }
@@ -7453,22 +7701,35 @@ function cmdPublish(
7453
7701
  // `pnpm publish` and skipped re-signing (ADR-001, features/publish-gate-verifies-the-tarball).
7454
7702
  let cleanupGate: (() => void) | null = null;
7455
7703
  try {
7456
- const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
7457
- let extracted: { dir: string; cleanup: () => void } | undefined;
7458
- try {
7459
- extracted = extractPublishTarball(pk.dir);
7460
- cleanupGate = extracted.cleanup;
7461
- } catch (err) {
7462
- // Cross-family review (codex `gpt-5.6-sol`, 2026-08-22): falling back to the working
7463
- // TREE here fails the gate OPEN. The gate's whole claim is "what ships matches the
7464
- // signature"; with no artifact, nothing was compared, and reporting a pass would be a
7465
- // claim about an object that was never built. Say why, and block.
7466
- write(`dz publish: could not pack ${pk.name} (${(err as Error).message.split('\n')[0]}) — the artifact was never built, so its signature was not checked`);
7704
+ const parsedManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
7705
+ // FR-4 (feature publish-gate-audit-durable): a manifest that PARSES but is not a plain
7706
+ // object — `null`, an array, a bare string — is UNAVAILABLE, never "no signature". Those
7707
+ // are different failures with different fixes: "no signature" means run `dz sign`; a
7708
+ // malformed manifest means the file itself is corrupt/wrong-shaped and re-signing alone
7709
+ // would silently paper over that. Mirrors `readManifest`'s shape guard in
7710
+ // publish-sibling-drift.ts (null/array/non-object cannot be used, say so).
7711
+ if (parsedManifest === null || typeof parsedManifest !== 'object' || Array.isArray(parsedManifest)) {
7712
+ const gotShape = parsedManifest === null ? 'null' : Array.isArray(parsedManifest) ? 'an array' : typeof parsedManifest;
7713
+ write(`dz publish: ${pk.name}'s ${MANIFEST_NAME} is not a JSON object (got ${gotShape}) its signature is unavailable, not merely absent`);
7467
7714
  artifactUnavailable = true;
7715
+ } else {
7716
+ const signed = parsedManifest;
7717
+ let extracted: { dir: string; cleanup: () => void } | undefined;
7718
+ try {
7719
+ extracted = extractPublishTarball(pk.dir);
7720
+ cleanupGate = extracted.cleanup;
7721
+ } catch (err) {
7722
+ // Cross-family review (codex `gpt-5.6-sol`, 2026-08-22): falling back to the working
7723
+ // TREE here fails the gate OPEN. The gate's whole claim is "what ships matches the
7724
+ // signature"; with no artifact, nothing was compared, and reporting a pass would be a
7725
+ // claim about an object that was never built. Say why, and block.
7726
+ write(`dz publish: could not pack ${pk.name} (${(err as Error).message.split('\n')[0]}) — the artifact was never built, so its signature was not checked`);
7727
+ artifactUnavailable = true;
7728
+ }
7729
+ verifyOk =
7730
+ extracted !== undefined &&
7731
+ verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
7468
7732
  }
7469
- verifyOk =
7470
- extracted !== undefined &&
7471
- verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
7472
7733
  } catch {
7473
7734
  verifyOk = false;
7474
7735
  } finally {
@@ -7513,9 +7774,10 @@ function cmdPublish(
7513
7774
  // batch resolves to.
7514
7775
  smoke: (artifacts): { ok: boolean; reason?: string } => {
7515
7776
  for (const a of artifacts) write(`dz publish: tarball ${a.name}@${a.newVersion} sha256:${a.sha256}`);
7777
+ const auditPackages = artifacts.map((a) => ({ name: a.name, version: a.newVersion, tarballSha256: a.sha256 }));
7516
7778
  if (bins.length === 0) {
7517
- appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin');
7518
- write('dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)');
7779
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin', auditPackages, undefined, gateAuditFsLayer);
7780
+ write(`dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)${auditSuffix(wrote)}`);
7519
7781
  return { ok: true };
7520
7782
  }
7521
7783
  const scratchRoot = packedInstallScratchRoot();
@@ -7554,13 +7816,13 @@ function cmdPublish(
7554
7816
  const verdict = judgePackedInstallSmoke(smokePlan, smokeExecutions);
7555
7817
  try { rmSync(installDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
7556
7818
  if (verdict.ok) {
7557
- appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'pack/install/--version all clean (live, packedTransport)');
7558
- write('dz publish: ✓ packed install smoke');
7819
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'pack/install/--version all clean (live, packedTransport)', auditPackages, undefined, gateAuditFsLayer);
7820
+ write(`dz publish: ✓ packed install smoke${auditSuffix(wrote)}`);
7559
7821
  return { ok: true };
7560
7822
  }
7561
7823
  const detail = verdict.failureDetail ?? verdict.bins.find((b) => !b.ok)?.detail ?? '(no detail)';
7562
- appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail);
7563
- write(`dz publish: BLOCKED — packed install smoke failed: ${detail}`);
7824
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail, auditPackages, undefined, gateAuditFsLayer);
7825
+ write(`dz publish: BLOCKED — packed install smoke failed: ${detail}${auditSuffix(wrote)}`);
7564
7826
  for (const b of verdict.bins.filter((b) => !b.ok)) write(` ✗ ${b.pkg} (${b.binName}): ${b.detail ?? '(no detail)'}`);
7565
7827
  return { ok: false, reason: detail };
7566
7828
  },
@@ -7790,7 +8052,7 @@ function cmdPublish(
7790
8052
  /* ADR-001): computed from the declarative model, never hand-written */
7791
8053
  /* ------------------------------------------------------------------ */
7792
8054
 
7793
- 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> {
7794
8056
  const json = flags.has('json');
7795
8057
  if (flags.has('help')) {
7796
8058
  write('dz parity [--target <name>] [--json] — the computed feature×target map (never hand-written)');
@@ -7818,30 +8080,53 @@ function cmdParity(options: Map<string, string>, flags: Set<string>, write: Writ
7818
8080
  }
7819
8081
  }
7820
8082
 
7821
- // ADR-001 Decision 3 (feature setup-installs-apply-leg): `learning-apply` on `claude-code` is
7822
- // MEASURED, not declared — `hooks-prompt` is present for that ONE target only when
7823
- // `applyLegStatus(root).installed`. `computeParity` itself is untouched (FR-5); only the
7824
- // capability SET fed into it for this one cell differs from the static `TARGET_CAPABILITIES`.
7825
- // `applyLegStatus` never throws (fix round 1, Q3 finding: an unreadable helper used to be able to
7826
- // 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.
7827
8094
  const applyLegStatusVal = applyLegStatus(cwd);
7828
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;
7829
8108
  const matrix = buildParityMatrix().map((row) => {
7830
- if (row.feature.id !== 'learning-apply' || applyLegInstalled) return row;
8109
+ if (row.feature.id !== 'learning-apply' || applyLegWorking) return row;
7831
8110
  const claudeCodeCaps = TARGET_CAPABILITIES['claude-code'].filter((c) => c !== 'hooks-prompt');
7832
8111
  return { feature: row.feature, cells: { ...row.cells, 'claude-code': computeParity(row.feature, claudeCodeCaps) } };
7833
8112
  });
7834
- // The "not installed" remedy — named ONLY for the one cell whose grant is a live measurement,
7835
- // never a blanket note for every `manual` cell (most targets are manual by DESIGN, not absence).
7836
- // `stale-version`/`unreadable` route through `applyLegReasonMessage` — the SAME text-producing
7837
- // function `dz doctor` uses for those two reasons (fix round 1, HIGH finding 2 / Q3 finding 7), so
7838
- // 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 ''.
7839
8121
  const applyLegRemedy = (featureId: string, t: TargetName): string => {
7840
- if (featureId !== 'learning-apply' || t !== 'claude-code' || applyLegInstalled) return '';
7841
- if (applyLegStatusVal.reason === 'stale-version' || applyLegStatusVal.reason === 'unreadable') {
7842
- 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';
7843
8128
  }
7844
- return 'not installed run dz setup --target claude-code --memory agentdb';
8129
+ return ` — installed but silent: ${applyLegProbe?.reason ?? 'unknown'}`;
7845
8130
  };
7846
8131
  // EVIDENCE staleness, folded into the report (fix round 2, R2-3). Derived from the records
7847
8132
  // themselves — no `codex --version`, no subprocess, so `dz parity` stays a deterministic function
@@ -8116,7 +8401,24 @@ function cmdRelease(options: Map<string, string>, flags: Set<string>, cwd: strin
8116
8401
  let smokeTmp: string | undefined;
8117
8402
  const execSteps: GateStep[] = plan.steps.filter((s) => s.kind !== 'synthetic-fail');
8118
8403
  say(`\ndz release — executing ${execSteps.length} gate step(s) across ${plan.packages.length} package(s)…`);
8119
- for (const step of execSteps) {
8404
+
8405
+ // FR-1/FR-2 (feature release-smoke-staged-pack): the packed-install smoke's `pack` steps must
8406
+ // pack the SAME staged bytes `dz publish`'s preview does — sibling `workspace:*` deps rewritten
8407
+ // to the exact sibling version, `prepublishOnly` dropped — or `npm install` on the resulting
8408
+ // tarballs dies with EUNSUPPORTEDPROTOCOL (MEASURED 2026-09-13 16:05). `workspaceVersions` is
8409
+ // built like publish's (from the FULL workspace, not just this release's filtered batch — an
8410
+ // out-of-batch sibling still needs its real version). Only the `pack` sub-steps are staged; the
8411
+ // `install`/`bin-exists`/`bin-version` steps already run against the packed tarballs and need no
8412
+ // staging.
8413
+ const allPackages = discoverPackages(cwd);
8414
+ const workspaceVersions = new Map(allPackages.map((p) => [p.name, p.version]));
8415
+ const packStepIds = new Set(
8416
+ (plan.packedInstallPlan?.steps ?? [])
8417
+ .filter((s) => s.kind === 'pack')
8418
+ .map((s) => `smoke:packed-install:${s.id}`),
8419
+ );
8420
+
8421
+ const runGateStep = (step: GateStep): void => {
8120
8422
  let stepCwd = step.cwd;
8121
8423
  if (step.tempCwd === true) {
8122
8424
  // AM-4: boot bins in a throwaway cwd so an installer-style bin cannot mutate the workspace.
@@ -8133,7 +8435,35 @@ function cmdRelease(options: Map<string, string>, flags: Set<string>, cwd: strin
8133
8435
  durationMs: Date.now() - started,
8134
8436
  timedOut: r.timedOut,
8135
8437
  });
8438
+ };
8439
+
8440
+ // Plan/execution order is load-bearing (see the plan/execution skew guard test) — steps run in
8441
+ // exactly the order `plan.steps` lists them, one loop, no reordering. Only the `pack` steps are
8442
+ // wrapped in the staged window, individually, so where they fall in that order never changes.
8443
+ let announcedStagedPack = false;
8444
+ for (const step of execSteps) {
8445
+ if (packStepIds.has(step.id)) {
8446
+ if (!announcedStagedPack) {
8447
+ say('dz release: smoke: packed tarballs staged like the live publish (workspace:* → exact sibling versions)');
8448
+ announcedStagedPack = true;
8449
+ }
8450
+ // Codex finding 2: stage ONLY the package this pack step packs — sibling pins come from
8451
+ // workspaceVersions, and `npm pack` reads the packed package's manifest alone.
8452
+ const packed = factsList.filter((f) => f.name === step.pkg);
8453
+ try {
8454
+ withStagedPackageJson(packed, workspaceVersions, write, () => runGateStep(step), 'dz release');
8455
+ } catch (err) {
8456
+ // a staging/restore failure is a FAILED step with the reason, never a silent skip — and ONE
8457
+ // record per step: a run already recorded by runGateStep is replaced, not duplicated (Codex r2)
8458
+ const failed = { stepId: step.id, exitCode: 1, stdout: '', stderr: formatPublishError(err), durationMs: 0, timedOut: false };
8459
+ const at = executions.findIndex((e) => e.stepId === step.id);
8460
+ if (at >= 0) executions[at] = failed; else executions.push(failed);
8461
+ }
8462
+ } else {
8463
+ runGateStep(step);
8464
+ }
8136
8465
  }
8466
+
8137
8467
  if (smokeTmp !== undefined) {
8138
8468
  try { rmSync(smokeTmp, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
8139
8469
  }
@@ -8146,7 +8476,22 @@ function cmdRelease(options: Map<string, string>, flags: Set<string>, cwd: strin
8146
8476
  for (const g of verdict.gates) {
8147
8477
  const icon = g.status === 'pass' ? '✓' : g.status === 'fail' ? '✗' : '○';
8148
8478
  say(` ${icon} ${g.gate.padEnd(7)} ${g.status.toUpperCase().padEnd(4)} ${g.passed} passed, ${g.failures.length} failed, ${g.skips.length} skipped`);
8149
- for (const f of g.failures) say(` [${f.class}] ${f.pkg !== undefined ? `${f.pkg}: ` : ''}${f.reason}`);
8479
+ for (const f of g.failures) {
8480
+ say(` [${f.class}] ${f.pkg !== undefined ? `${f.pkg}: ` : ''}${f.reason}`);
8481
+ // FR-2 / AM-4 / AM-6 (feature release-gate-output-tail): print each non-empty stream's
8482
+ // tail under the failure line, labelled `stdout:`/`stderr:` at the failure's own 6-space
8483
+ // indent, with its content lines at an 8-space CONTINUATION indent so a reader can tell a
8484
+ // tail line from a new failure/skip bullet at a glance; --json carries the same `tails`
8485
+ // object as-is (present, possibly with empty strings, on every executed failure).
8486
+ if (f.tails !== undefined) {
8487
+ for (const stream of ['stdout', 'stderr'] as const) {
8488
+ const t = f.tails[stream];
8489
+ if (t.length === 0) continue;
8490
+ say(` ${stream}:`);
8491
+ for (const tailLine of t.split('\n')) say(` ${tailLine}`);
8492
+ }
8493
+ }
8494
+ }
8150
8495
  for (const sk of g.skips) say(` [${sk.class}] ${sk.pkg}: ${sk.reason}`);
8151
8496
  }
8152
8497
  say(` verdict at ${verdict.timestamp}: ${verdict.publishAction === 'proceed' ? '✓ all gates green' : '✗ RELEASE BLOCKED'}`);
@@ -10865,41 +11210,175 @@ function runGuardEvaluation(root: string, op: string, text: string | undefined,
10865
11210
  return result;
10866
11211
  }
10867
11212
 
11213
+ /** A package this audit call concerns (feature `publish-gate-audit-durable`, FR-1). */
11214
+ interface PublishGateAuditPackage {
11215
+ readonly name: string;
11216
+ readonly version: string;
11217
+ /** Present once a tarball has actually been built (packedTransport/smoke); absent → `sha256:n/a`. */
11218
+ readonly tarballSha256?: string;
11219
+ }
11220
+
11221
+ /**
11222
+ * fs primitives `appendPublishGateAudit` needs for its durable write (FR-2), injectable so a test
11223
+ * can make `fsyncSync` throw without touching the real filesystem underneath every OTHER seam this
11224
+ * function shares with production. Left unset in production → the real `node:fs` functions above.
11225
+ */
11226
+ interface PublishGateAuditFsLayer {
11227
+ readonly existsSync: (path: string) => boolean;
11228
+ readonly mkdirSync: (path: string, opts: { recursive: boolean }) => void;
11229
+ readonly openSync: (path: string, flags: number) => number;
11230
+ /**
11231
+ * AM-2 (Codex round-1 review, finding 2, high): `Buffer`, not `string` — a SHORT write must
11232
+ * resume at the exact BYTE it stopped at, and a string-based API cannot express that safely once
11233
+ * the data contains any multi-byte UTF-8 character (re-encoding a slice of an already-partial
11234
+ * string can silently produce different bytes than the ones actually pending). The real
11235
+ * `node:fs.writeSync` accepts a `Buffer` directly (no re-encoding), so this changes nothing about
11236
+ * what production writes.
11237
+ */
11238
+ readonly writeSync: (fd: number, data: Buffer) => number;
11239
+ readonly fsyncSync: (fd: number) => void;
11240
+ readonly closeSync: (fd: number) => void;
11241
+ }
11242
+
11243
+ /**
11244
+ * AM-2: loop `writeSync` until every byte of `data` has been accepted, checking the RETURNED
11245
+ * length on every call (Codex round-1 review, finding 1: the old code called `writeSync` once and
11246
+ * ignored the return value — a short write followed by `fsyncSync` durably persists a TRUNCATED,
11247
+ * unparseable JSON line into an append-only log every future read walks). A call that reports zero
11248
+ * or negative progress can never complete the buffer and would spin forever — that is treated as a
11249
+ * failure, not a retry target.
11250
+ */
11251
+ function writeAllSync(fsLayer: PublishGateAuditFsLayer, fd: number, data: Buffer): void {
11252
+ let remaining = data;
11253
+ while (remaining.length > 0) {
11254
+ const n = fsLayer.writeSync(fd, remaining);
11255
+ if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) {
11256
+ throw new Error(`writeSync made no progress (returned ${n}) with ${remaining.length} byte(s) still pending`);
11257
+ }
11258
+ // Codex round-2 (2026-09-14) finding 3: a layer claiming MORE bytes than it was given is lying
11259
+ // about the record, and must not reach "logged: true" through a subarray that just goes empty.
11260
+ if (n > remaining.length) {
11261
+ throw new Error(`writeSync claimed ${n} byte(s) written but only ${remaining.length} were supplied`);
11262
+ }
11263
+ remaining = remaining.subarray(n);
11264
+ }
11265
+ }
11266
+
11267
+ const REAL_PUBLISH_GATE_AUDIT_FS: PublishGateAuditFsLayer = { existsSync, mkdirSync, openSync, writeSync, fsyncSync, closeSync };
11268
+
10868
11269
  /**
10869
- * Feature `publish-sibling-drift-gate` (FR-5/AM-6): both the sibling-drift and packed-install-smoke
11270
+ * Feature `publish-sibling-drift-gate` (FR-5/AM-6), durability + per-package identity added by
11271
+ * feature `publish-gate-audit-durable` (FR-1/FR-2): both the sibling-drift and packed-install-smoke
10870
11272
  * gates write to the SAME append-only, hash-chained `.dz/guard-audit.jsonl` the declarative
10871
11273
  * `dz guard` rules use — visibility for `dz guard promote`/`dz compounding` never depends on
10872
11274
  * which mechanism produced the finding. `pass` records go through as an informational `note`
10873
11275
  * (never a violation, so they can never flip the row's own verdict) so a clean check is ALSO on
10874
11276
  * the record, not just a block or an override (AM-6: "аудит без записи = не аудит").
10875
11277
  *
10876
- * Returns whether the write actually landed. Most callers are best-effort (a write failure never
10877
- * blocks a verdict already decided) the one exception is an `--allow-sibling-drift` OVERRIDE,
10878
- * whose caller MUST check this return value: an override is not real without a durable row behind
10879
- * it (AM-6's load-bearing property see `auditedOverride` in `cmdPublish`).
11278
+ * FR-1: one JSONL record PER PACKAGE in `packages`, each naming the package, its version, and the
11279
+ * tarball's sha256 (or the explicit `sha256:n/a` before a tarball exists a dry-run preview). FR-2:
11280
+ * the write is `openSync('a') writeSync fsyncSync(fd) closeSync`, plus an `fsyncSync` of the
11281
+ * `.dz` directory itself the one time this call CREATES it (a file's own fsync durably persists its
11282
+ * bytes; the directory entry that makes the file findable after a crash needs its own fsync — the
11283
+ * same lesson `integration-apply.ts`'s `fsyncDirectory` already encodes).
11284
+ *
11285
+ * Returns `{ logged, reason? }` — never a bare boolean, so a caller can print WHY a write failed,
11286
+ * not just that it did. Most callers are best-effort (a write failure never blocks a verdict already
11287
+ * decided) — the one exception is an `--allow-sibling-drift` OVERRIDE, whose caller MUST check
11288
+ * `logged`: an override is not real without a durable row behind it (AM-6's load-bearing property —
11289
+ * see `auditedOverride` in `cmdPublish`).
10880
11290
  */
10881
11291
  function appendPublishGateAudit(
10882
11292
  root: string,
10883
11293
  rule: 'sibling-drift' | 'packed-install-smoke',
10884
11294
  verdict: 'pass' | 'warn' | 'block',
10885
11295
  detail: string,
11296
+ packages: readonly PublishGateAuditPackage[],
10886
11297
  overrideReason?: string,
10887
- ): boolean {
11298
+ fsLayer: PublishGateAuditFsLayer = REAL_PUBLISH_GATE_AUDIT_FS,
11299
+ ): { logged: boolean; reason?: string } {
11300
+ if (packages.length === 0) return { logged: true }; // nothing to record about — not a failure
11301
+ const dzDir = join(root, '.dz');
11302
+ const auditPath = join(dzDir, 'guard-audit.jsonl');
11303
+ const dzDirExisted = fsLayer.existsSync(dzDir);
11304
+ // AM-2: tracked BEFORE the write — this is what decides whether the append call below is about
11305
+ // to CREATE the file (needing a `.dz` directory-entry fsync afterwards) or extend an existing one
11306
+ // (whose directory entry is already durable from a previous append).
11307
+ const auditFileExisted = fsLayer.existsSync(auditPath);
10888
11308
  try {
10889
- const rec = auditRecord(
11309
+ fsLayer.mkdirSync(dzDir, { recursive: true });
11310
+ } catch (err) {
11311
+ return { logged: false, reason: `could not create ${dzDir}: ${(err as Error).message.split('\n')[0]}` };
11312
+ }
11313
+ const records = packages.map((pkg) => {
11314
+ const sha = pkg.tarballSha256 !== undefined && pkg.tarballSha256 !== '' ? pkg.tarballSha256 : 'n/a';
11315
+ const label = `${rule}: ${pkg.name}@${pkg.version} sha256:${sha} — ${detail}`;
11316
+ return auditRecord(
10890
11317
  verdict === 'pass'
10891
- ? { op: 'publish', verdict, violations: [], checked: [rule], notEstablished: [], notes: [`${rule}: ${detail}`] }
10892
- : { op: 'publish', verdict, violations: [{ rule, severity: 'hard', detail }], checked: [rule], notEstablished: [] },
11318
+ ? { op: 'publish', verdict, violations: [], checked: [rule], notEstablished: [], notes: [label] }
11319
+ : { op: 'publish', verdict, violations: [{ rule, severity: 'hard', detail: label }], checked: [rule], notEstablished: [] },
10893
11320
  new Date().toISOString(),
10894
11321
  overrideReason !== undefined ? { reason: overrideReason } : undefined,
10895
11322
  );
10896
- mkdirSync(join(root, '.dz'), { recursive: true });
10897
- const auditPath = join(root, '.dz', 'guard-audit.jsonl');
10898
- writeFileSync(auditPath, appendChainedLines([rec], readLogTail(auditPath)), { flag: 'a' });
10899
- return true;
10900
- } catch {
10901
- return false; // audit write failed the caller decides whether that itself is refusable (AM-6)
11323
+ });
11324
+ let bytes: string;
11325
+ try {
11326
+ bytes = appendChainedLines(records, readLogTail(auditPath));
11327
+ } catch (err) {
11328
+ return { logged: false, reason: `could not build the chained record: ${(err as Error).message.split('\n')[0]}` };
10902
11329
  }
11330
+ if (bytes === '') return { logged: true };
11331
+ const buf = Buffer.from(bytes, 'utf-8');
11332
+ let fd: number | undefined;
11333
+ try {
11334
+ fd = fsLayer.openSync(auditPath, fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_WRONLY);
11335
+ writeAllSync(fsLayer, fd, buf); // AM-2: loops on a short write, throws on no progress
11336
+ fsLayer.fsyncSync(fd);
11337
+ fsLayer.closeSync(fd);
11338
+ fd = undefined;
11339
+ } catch (err) {
11340
+ return { logged: false, reason: (err as Error).message.split('\n')[0] ?? String(err) };
11341
+ } finally {
11342
+ if (fd !== undefined) { try { fsLayer.closeSync(fd); } catch { /* best-effort */ } }
11343
+ }
11344
+ // AM-2 (Codex round-1 review, finding 1, high): a directory-entry fsync is REQUIRED, not
11345
+ // best-effort, whenever THIS call is the reason the entry needed persisting — swallowing its
11346
+ // failure used to let `logged: true` go out about a record whose directory entry can vanish on a
11347
+ // crash before the next fsck. Two DIFFERENT entries can need persisting, tracked independently:
11348
+ if (!auditFileExisted) {
11349
+ // The audit FILE was just created by the open() above — `.dz` (its containing directory) needs
11350
+ // its own fsync so the new directory entry survives a crash; the file's own fsync (above) only
11351
+ // guarantees the file's DATA, not that anything can find it afterwards.
11352
+ let dfd: number | undefined;
11353
+ try {
11354
+ dfd = fsLayer.openSync(dzDir, fsConstants.O_RDONLY);
11355
+ fsLayer.fsyncSync(dfd);
11356
+ } catch (err) {
11357
+ return { logged: false, reason: `could not fsync ${dzDir} after creating ${auditPath}: ${(err as Error).message.split('\n')[0]}` };
11358
+ } finally {
11359
+ if (dfd !== undefined) { try { fsLayer.closeSync(dfd); } catch { /* best-effort, does not affect the verdict already returned */ } }
11360
+ }
11361
+ }
11362
+ if (!dzDirExisted) {
11363
+ // `.dz` ITSELF was just created by `mkdirSync` above — its PARENT (`root`) needs its own fsync
11364
+ // so `.dz`'s OWN directory entry survives a crash (the fsync of `.dz` just above only durably
11365
+ // persists entries INSIDE `.dz`, not the fact that `.dz` exists at all).
11366
+ let pfd: number | undefined;
11367
+ try {
11368
+ pfd = fsLayer.openSync(root, fsConstants.O_RDONLY);
11369
+ fsLayer.fsyncSync(pfd);
11370
+ } catch (err) {
11371
+ return { logged: false, reason: `could not fsync ${root} after creating ${dzDir}: ${(err as Error).message.split('\n')[0]}` };
11372
+ } finally {
11373
+ if (pfd !== undefined) { try { fsLayer.closeSync(pfd); } catch { /* best-effort, does not affect the verdict already returned */ } }
11374
+ }
11375
+ }
11376
+ return { logged: true };
11377
+ }
11378
+
11379
+ /** FR-2: the printed suffix a caller appends to its own verdict line — never silent about logging. */
11380
+ function auditSuffix(wrote: { logged: boolean; reason?: string }): string {
11381
+ return wrote.logged ? ' (logged)' : ` (audit NOT logged: ${wrote.reason ?? 'unknown reason'})`;
10903
11382
  }
10904
11383
 
10905
11384
  function renderGuardObservation(observation: GuardObservation): string {
@@ -12562,6 +13041,46 @@ function splitMutationGateOutputWrite(
12562
13041
  return 'path' in result ? { outputPath: result.path } : { outputError: result.error };
12563
13042
  }
12564
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
+
12565
13084
  function cmdMutationGate(
12566
13085
  options: Map<string, string>,
12567
13086
  flags: Set<string>,
@@ -12595,7 +13114,9 @@ function cmdMutationGate(
12595
13114
 
12596
13115
  let entries: readonly MutationRegistryEntry[] = parsed.registry.entries;
12597
13116
  let entryResults: readonly MutationEntryResult[] = parsed.entryResults;
13117
+ const totalRegistryEntries = entries.length;
12598
13118
  const only = options.get('only');
13119
+ let onlyIds: Set<string> | null = null;
12599
13120
  if (only !== undefined) {
12600
13121
  const ids = only.split(',').map((s) => s.trim()).filter(Boolean);
12601
13122
  const known = new Set([
@@ -12604,16 +13125,138 @@ function cmdMutationGate(
12604
13125
  ]);
12605
13126
  const unknown = ids.filter((id) => !known.has(id));
12606
13127
  if (unknown.length > 0) return fail(`--only names unknown entry id(s): ${unknown.join(', ')}`);
13128
+ onlyIds = new Set(ids);
12607
13129
  entries = entries.filter((e) => ids.includes(e.id));
12608
13130
  entryResults = entryResults.filter((result) => ids.includes(result.id));
12609
13131
  }
12610
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
+
12611
13254
  if (entries.length === 0) {
12612
13255
  const scope = only === undefined ? 'registry' : 'selected registry entries';
12613
13256
  const error = `${scope} has no runnable entries after validation — nothing can be run; the registry is unusable`;
12614
13257
  const summary = summarizeMutationResults(entryResults);
12615
13258
  if (json) {
12616
- 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));
12617
13260
  } else {
12618
13261
  write(`dz mutation-gate: ${error}`);
12619
13262
  for (const result of entryResults) {
@@ -12625,7 +13268,7 @@ function cmdMutationGate(
12625
13268
 
12626
13269
  const testCmdRaw = options.get('test-cmd') ?? parsed.registry.testCommand ?? 'npm test';
12627
13270
  if (/[\0\n\r]/.test(testCmdRaw)) return fail('--test-cmd may not contain NUL or newline characters');
12628
- const testCmd = testCmdRaw;
13271
+ let testCmd = testCmdRaw;
12629
13272
  const excludedSelfChecks = REGISTRY_SELFCHECK_TESTS.filter((testFile) =>
12630
13273
  entries.some((entry) => buildMutationTestCommand(testCmd, entry).excluded.includes(testFile)),
12631
13274
  );
@@ -12633,8 +13276,88 @@ function cmdMutationGate(
12633
13276
  write(`mutation-gate: self-check excluded from mutant runs: ${excludedSelfChecks.join(', ') || '(none)'}`);
12634
13277
  }
12635
13278
 
12636
- const timeoutOpt = Number(options.get('timeout') ?? '300000');
12637
- 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
+ }
12638
13361
 
12639
13362
  // Route-b guard mode: `per-entry` (default, strongest — each red entry re-baselines the restored
12640
13363
  // tree, so a flaky neighbour flips THAT entry to INCONCLUSIVE) or `final` (cheap — one re-run at
@@ -12715,22 +13438,29 @@ function cmdMutationGate(
12715
13438
  phase: 'baseline' | 'mutation' | 'rebaseline' | 'final-rebaseline',
12716
13439
  entryId?: string,
12717
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) };
12718
13445
  if (injectedRunner !== undefined) {
12719
13446
  return injectedRunner(suiteCommand, {
12720
13447
  cwd: copyDir,
12721
13448
  timeoutMs: timeout,
12722
13449
  phase,
13450
+ env: extraEnv,
12723
13451
  ...(entryId !== undefined ? { entryId } : {}),
12724
13452
  });
12725
13453
  }
13454
+ const startedAt = performance.now();
12726
13455
  const run = spawnSync(suiteCommand, {
12727
13456
  cwd: copyDir,
12728
13457
  shell: true,
12729
13458
  encoding: 'utf-8',
12730
13459
  timeout,
12731
13460
  maxBuffer: 64 * 1024 * 1024,
12732
- env: { ...process.env, FORCE_COLOR: '0' },
13461
+ env: { ...process.env, FORCE_COLOR: '0', ...extraEnv },
12733
13462
  });
13463
+ const elapsedMs = Math.round(performance.now() - startedAt);
12734
13464
  const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
12735
13465
  ? run.error.code
12736
13466
  : undefined;
@@ -12741,10 +13471,28 @@ function cmdMutationGate(
12741
13471
  throw run.error;
12742
13472
  }
12743
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
+ }
12744
13493
  let failureReason: string | undefined;
12745
13494
  if (typeof run.status !== 'number') {
12746
- if (errorCode === 'ETIMEDOUT') failureReason = `timeout after ${timeout}ms${signal === undefined ? '' : `; signal=${signal}`}`;
12747
- else if (errorCode === 'ENOBUFS') failureReason = 'maxBuffer exceeded (ENOBUFS; 67108864-byte output ceiling)';
13495
+ if (errorCode === 'ENOBUFS') failureReason = 'maxBuffer exceeded (ENOBUFS; 67108864-byte output ceiling)';
12748
13496
  else if (signal !== undefined) failureReason = `child killed by signal ${signal}`;
12749
13497
  else if (errorCode !== undefined) failureReason = `spawn failure code ${errorCode}`;
12750
13498
  else failureReason = 'spawn failure with no error code or signal';
@@ -12996,7 +13744,7 @@ function cmdMutationGate(
12996
13744
 
12997
13745
  const exitCode = mutationGateExitCode(results, baseline.ok);
12998
13746
  if (json) {
12999
- 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));
13000
13748
  return exitCode;
13001
13749
  }
13002
13750
  write(renderMutationReport(results, baseline, pkgDir));
@@ -20144,11 +20892,11 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
20144
20892
  case 'auto-canonicalize':
20145
20893
  return await cmdAutoCanonicalize(options, cwd, write);
20146
20894
  case 'publish':
20147
- return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner, io.publishSiblingDriftFetcher, io.publishPackedInstallRunner, io.publishExecRunner);
20895
+ return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner, io.publishSiblingDriftFetcher, io.publishPackedInstallRunner, io.publishExecRunner, io.publishGateAuditFsLayer, io.publishNpmPackRunner);
20148
20896
  case 'release':
20149
20897
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
20150
20898
  case 'parity':
20151
- return cmdParity(options, flags, write, writeErr, cwd);
20899
+ return await cmdParity(options, flags, write, writeErr, cwd);
20152
20900
  case 'registry':
20153
20901
  return cmdRegistry(options, cwd, write);
20154
20902
  case 'benchmark':