@dzhechkov/harness-cli 0.8.23 → 0.8.25

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/dist/cli.js CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
- import { appendFileSync, chmodSync, closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
6
+ import { parseNpmPackInventory } from '@dzhechkov/harness-core';
7
+ 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 } from 'node:fs';
7
8
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
9
  import { fileURLToPath } from 'node:url';
9
10
  import { request as httpsRequest } from 'node:https';
@@ -19,11 +20,11 @@ import { isDeepStrictEqual } from 'node:util';
19
20
  import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from '@dzhechkov/harness-core';
20
21
  import { appendRunEvent, readRunRegistry, liveParents, liveness, probePid, settleDeadRuns, planRegistryArchive, planWorktreeCleanup, renderCleanupPlan, worktreeRemovalsToApply } from '@dzhechkov/harness-core';
21
22
  import { openRound, closeRound, listRounds, parseCodexTokens, classifyRoundExecOutcome, buildRoundExecRow, } from '@dzhechkov/harness-core';
22
- import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, resolveSelection, formatSelectRefusal, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
23
+ import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, resolveSelection, formatSelectRefusal, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, memoryBackendSourceLabel, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
23
24
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
24
25
  TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, computeParity, PARITY_FEATURES, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, applyLegStatus, applyLegReasonMessage, resolveAgentdbPath, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeSpendReport, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, spendReport, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, findExactLesson, recordToPattern, bundleSkills, brainHome, brainAgentdbPath, listPreReindexSnapshots, rotatePreReindexSnapshots, scanSnapshotDir, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
25
26
  // guard-promotion (feature guard-promotion, scout idea #1)
26
- assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, hashPackBytes, rewriteWorkspaceSpecs, listPackFiles, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, resolveScanTailTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, runRetroTailScan, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
27
+ assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, hashPackBytes, rewriteWorkspaceSpecs, detectSiblingDrift, planPackedInstallSmoke, judgePackedInstallSmoke, listPackFiles, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, resolveScanTailTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, runRetroTailScan, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
27
28
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
28
29
  replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, readQeGrade, scoreReceiptToAggregateRow, readScoreAggregateRows, dedupeScoreAggregateRows, buildScoreAggregateReport, renderScoreAggregateReport, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, tgPostHtmlIssues, tgVisibleLength, decideTgSend, TG_TEXT_LIMIT, countRecallEventsForRun, unknownFlagNotice, mirrorWriterExplanation, mirrorWriterReason, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
29
30
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
@@ -5443,7 +5444,14 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
5443
5444
  write(`║ 2. Preset: ${preset.padEnd(40)}║`);
5444
5445
  // Step 3: Run setup (hooks + memory + config)
5445
5446
  write(`║ 3. Setting up learning environment... ║`);
5446
- const memoryOpt = options.get('memory');
5447
+ const memoryOptRaw = options.get('memory');
5448
+ // FR-1/T3 (feature `setup-backend-from-config`): pass `--memory` through AS-IS — `agentdb`,
5449
+ // `jsonl`, or `undefined` — never collapsed to `undefined` on anything but agentdb. The prior
5450
+ // `memoryOpt === 'agentdb' ? 'agentdb' : undefined` made an explicit `--memory jsonl` INDISTINCT
5451
+ // from "no flag at all", so `runSetup`'s config-aware default (FR-2's downgrade path) could never
5452
+ // fire from the CLI. An unrecognised value (neither `agentdb` nor `jsonl`) still reads as
5453
+ // "no flag" — the same permissive fallback as before.
5454
+ const memoryOpt = memoryOptRaw === 'agentdb' ? 'agentdb' : memoryOptRaw === 'jsonl' ? 'jsonl' : undefined;
5447
5455
  // ADR-001 Decision 2 (feature setup-installs-apply-leg): bake THIS CLI's own installed
5448
5456
  // @dzhechkov/harness-core into the generated apply-leg hooks — the installation actually running
5449
5457
  // `dz setup` is the one a consumer's project can always reach, unlike a hard-coded npm prefix
@@ -5461,13 +5469,16 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
5461
5469
  projectRoot,
5462
5470
  target,
5463
5471
  preset,
5464
- memory: memoryOpt === 'agentdb' ? 'agentdb' : undefined,
5472
+ memory: memoryOpt,
5465
5473
  noHooks: flags.has('no-hooks'),
5466
5474
  noMemory: flags.has('no-memory'),
5467
5475
  force: flags.has('force'),
5468
5476
  installDriver: flags.has('install-driver'),
5469
5477
  coreDistDir,
5470
5478
  });
5479
+ // FR-3: name the source of the backend actually used — never left to be inferred from the flag
5480
+ // alone, since the backend may now come from `.dz/config.json` or the jsonl default.
5481
+ write(`dz setup: memory backend: ${setupResult.memoryBackend} (${memoryBackendSourceLabel(setupResult.memoryBackendSource)})`);
5471
5482
  for (const step of setupResult.steps) {
5472
5483
  const icon = step.status === 'done' ? '✓' : step.status === 'skipped' ? '○' : '✗';
5473
5484
  write(`║ ${icon} ${step.name.padEnd(25)} ${step.detail.slice(0, 20).padEnd(20)}║`);
@@ -5518,7 +5529,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
5518
5529
  // not from package presence — a skipped hook/MCP step must not let the summary claim a store
5519
5530
  // nothing writes to (audit code#3).
5520
5531
  const wiring = setupResult.steps.find((s) => s.name === 'agentdb wiring');
5521
- const backendLabel = memoryOpt === 'agentdb'
5532
+ // Keyed off the RESOLVED backend (setupResult.memoryBackend), not the raw flag: FR-1 means the
5533
+ // flag can be absent while the actual backend is still agentdb (config-sourced) — the old
5534
+ // `memoryOpt === 'agentdb'` check would have mislabeled that run as jsonl right after fixing the
5535
+ // underlying steps to keep it agentdb.
5536
+ const backendLabel = setupResult.memoryBackend === 'agentdb'
5522
5537
  ? (wiring?.status === 'done' ? 'agentdb (.dz/agentdb.db + .dz/agentdb-mcp.db, separate stores)' : `agentdb INCOMPLETE — see setup steps`)
5523
5538
  : 'sessions.jsonl + patterns.jsonl';
5524
5539
  write(`║ Learning: ${backendLabel.padEnd(41)}║`);
@@ -6208,7 +6223,99 @@ function mirrorFailureMessage(error) {
6208
6223
  return error.message.trim().split(/\r?\n/, 1)[0] ?? 'mirror command failed';
6209
6224
  return String(error);
6210
6225
  }
6211
- function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6226
+ /**
6227
+ * Scratch root for the packed-install smoke's pack/install dirs (feature
6228
+ * publish-sibling-drift-gate). MEASURED 2026-09-13: npm resolves a LOCAL tarball path (`npm
6229
+ * install <path-to.tgz>`) relative to `os.tmpdir()` — not to cwd — whenever that path sits
6230
+ * INSIDE `os.tmpdir()`, and does the same for the install dir; put pack and install dirs both
6231
+ * under `tmpdir()` and the recorded `file:` spec loses its `tmpdir()` prefix entirely (reproducer:
6232
+ * a fresh `npm pack <src> --pack-destination "$T/pack"` + `cd "$T/install" && npm install
6233
+ * "$T/pack/x.tgz"` with `$T` under `/tmp` silently installs NOTHING — "changed 1 package", empty
6234
+ * node_modules, `reify moves {}` in `--loglevel silly`; the identical commands under `/var/tmp`
6235
+ * install correctly). A directory outside `os.tmpdir()` sidesteps the quirk entirely.
6236
+ */
6237
+ function packedInstallScratchRoot() {
6238
+ return existsSync('/var/tmp') ? '/var/tmp' : tmpdir();
6239
+ }
6240
+ /**
6241
+ * FR-1 (feature release-smoke-staged-pack): stage every target's `package.json` exactly like a
6242
+ * live publish packs it — `workspace:*` sibling specs rewritten to the exact sibling version
6243
+ * (`rewriteWorkspaceSpecs`), `scripts.prepublishOnly` dropped — run `fn`, then ALWAYS restore the
6244
+ * original bytes in a `finally`, whatever `fn` does or throws. A restore failure is reported
6245
+ * through `write` (with the path), never swallowed — the "absence of a receipt is not success"
6246
+ * rule this file follows everywhere else.
6247
+ *
6248
+ * `cmdPublish`'s dry-run preview and `cmdRelease`'s packed-install-smoke `pack` steps both go
6249
+ * through this ONE helper, so the two doors that ask "what would the registry receive?" pack the
6250
+ * exact same bytes (MEASURED 2026-09-13 16:05: `dz release` packed the live `workspace:*`
6251
+ * package.json and its smoke install died with EUNSUPPORTEDPROTOCOL — `dz publish`'s preview
6252
+ * already staged around this and release never got that).
6253
+ */
6254
+ function withStagedPackageJson(targets, workspaceVersions, write, fn, label = 'dz') {
6255
+ // Lead edits after Codex review (2026-09-13, findings 1/5/6): every write — the staged text and
6256
+ // the restore — goes through a sibling temp file + rename, so a reader never sees a truncated
6257
+ // package.json; a restore that FAILS is an error the caller must see (thrown after fn, or attached
6258
+ // to fn's own error), never a warning that lets a run "succeed" on a damaged tree; and the
6259
+ // diagnostic keeps the calling command's name (`label`).
6260
+ const atomicWrite = (path, text) => {
6261
+ // Codex round 2: an EXCLUSIVE, randomized sibling temp — never a shared pid-named file
6262
+ const tmp = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.staged.tmp`;
6263
+ writeFileSync(tmp, text, { flag: 'wx' });
6264
+ renameSync(tmp, path);
6265
+ };
6266
+ const stagedOriginals = [];
6267
+ let fnError;
6268
+ let fnThrew = false;
6269
+ try {
6270
+ for (const p of targets) {
6271
+ const pkgJsonPath = join(p.dir, 'package.json');
6272
+ const original = readFileSync(pkgJsonPath, 'utf-8');
6273
+ const rewritten = JSON.parse(rewriteWorkspaceSpecs(original, workspaceVersions));
6274
+ const scripts = rewritten['scripts'];
6275
+ if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts))
6276
+ delete scripts['prepublishOnly'];
6277
+ stagedOriginals.push({ path: pkgJsonPath, text: original });
6278
+ atomicWrite(pkgJsonPath, JSON.stringify(rewritten, null, 2) + '\n');
6279
+ }
6280
+ return fn();
6281
+ }
6282
+ catch (err) {
6283
+ fnThrew = true;
6284
+ fnError = err;
6285
+ throw err;
6286
+ }
6287
+ finally {
6288
+ const restoreFailures = [];
6289
+ for (const o of stagedOriginals) {
6290
+ try {
6291
+ atomicWrite(o.path, o.text);
6292
+ }
6293
+ catch (err) {
6294
+ const msg = `${label}: ✗ could not restore ${o.path} after staged packing: ${formatPublishError(err)} — the tree is left STAGED, restore it by hand`;
6295
+ try {
6296
+ write(msg);
6297
+ }
6298
+ catch { /* a throwing writer must not mask the restore failure */ }
6299
+ restoreFailures.push(msg);
6300
+ }
6301
+ }
6302
+ if (restoreFailures.length > 0) {
6303
+ // Codex round 2: one aggregate error carrying BOTH fn's own failure (if any) and the restore
6304
+ // failures — never a bare message assignment that could itself throw out of finally.
6305
+ const fnPart = fnThrew ? `\n(during: ${fnError instanceof Error ? fnError.message : String(fnError)})` : '';
6306
+ // eslint-disable-next-line no-unsafe-finally -- a damaged tree must not read as success
6307
+ throw new Error(`${restoreFailures.join('\n')}${fnPart}`);
6308
+ }
6309
+ }
6310
+ }
6311
+ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner, siblingDriftFetcher, packedInstallRunner, publishExecRunner, gateAuditFsLayer,
6312
+ /**
6313
+ * AM-5 (feature publish-gate-audit-durable): test seam for the sibling-drift gate's `npm pack
6314
+ * --dry-run --json` call — production leaves it unset (real `execFileSync`). Takes the package
6315
+ * dir, returns raw stdout, or THROWS to simulate a real `npm` failure — a test can then prove the
6316
+ * failure reaches `parseNpmPackInventory`'s caller as `unavailable`, never a real subprocess.
6317
+ */
6318
+ npmPackRunner) {
6212
6319
  const json = flags.has('json');
6213
6320
  // Under --json stdout carries exactly one JSON document, so every human line — guard notes, refusals,
6214
6321
  // progress — goes to stderr instead of being dropped: a refusal that prints nothing is the silent
@@ -6216,9 +6323,9 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6216
6323
  const write = json ? (line) => { process.stderr.write(`${line}\n`); } : writeOutput;
6217
6324
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
6218
6325
  // silently swallowed and flip the command into live-publish mode.
6219
- const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance', 'json', 'no-mirror']);
6326
+ const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance', 'json', 'no-mirror', 'allow-sibling-drift', 'include-drifted']);
6220
6327
  const allowedOptions = new Set(['filter', 'claim-check', 'no-guard', 'sign-key', 'mirror-cmd']);
6221
- const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>, --mirror-cmd <cmd>, --no-mirror, --no-guard "<reason>" (skip the guard pre-flight; logged)';
6328
+ const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>, --mirror-cmd <cmd>, --no-mirror, --no-guard "<reason>" (skip the guard pre-flight; logged), --allow-sibling-drift (override the sibling-drift gate; logged), --include-drifted (auto-extend the batch with a drifted sibling)';
6222
6329
  for (const flag of flags) {
6223
6330
  if (!allowedFlags.has(flag)) {
6224
6331
  write(`dz publish: unknown option --${flag}`);
@@ -6308,8 +6415,370 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6308
6415
  const bumpOnly = flags.has('bump-only');
6309
6416
  // SAFETY: dry-run is the DEFAULT. A real publish requires an EXPLICIT opt-in
6310
6417
  // via --yes, --confirm, or --no-dry-run. Without one, we never bump or publish.
6418
+ // Computed HERE (moved up from below the gates, AM-5) so both gates can see it: a dry run keeps
6419
+ // previewing packed-install with the CURRENT pre-bump tarball (nothing to compare a LIVE publish
6420
+ // against yet), while a live run defers the real packed-install-smoke into `publishPackages`'s
6421
+ // `packedTransport` — the one that tests the ACTUAL bytes about to ship (AM-1).
6311
6422
  const wantsLive = flags.has('yes') || flags.has('confirm') || flags.has('no-dry-run');
6312
6423
  const dryRun = !wantsLive;
6424
+ // ── FR-1..FR-4 — sibling-drift gate, then packed-install smoke (feature
6425
+ // publish-sibling-drift-gate, ADR-001). The sibling-drift gate runs before the signature gate
6426
+ // and the live-publish banner (so a --include-drifted-expanded batch is checked and shown too).
6427
+ // AM-5: on a DRY RUN both gates always print their verdict, even once sibling-drift already
6428
+ // blocks — the whole point of a preview is full information before anything ships. On a LIVE
6429
+ // run, sibling-drift still refuses immediately (packing/installing a doomed batch wastes real
6430
+ // time); its own packed-install smoke is deferred into `publishPackages`'s `packedTransport`
6431
+ // (AM-1) — the one gate that tests the tarball bytes actually handed to `npm publish`.
6432
+ const allowSiblingDrift = flags.has('allow-sibling-drift');
6433
+ const includeDrifted = flags.has('include-drifted');
6434
+ const allPackages = discoverPackages(cwd);
6435
+ const workspaceVersions = new Map(allPackages.map((p) => [p.name, p.version]));
6436
+ const workspaceDirs = new Map(allPackages.map((p) => [p.name, p.dir]));
6437
+ const matchesFilter = (pk) => filter === undefined || filter.length === 0 || filter.some((f) => pk.name.includes(f) || pk.dir.includes(f));
6438
+ let targets = allPackages.filter(matchesFilter);
6439
+ let batchNames = new Set(targets.map((p) => p.name));
6440
+ // Production default: `npm pack <name>@<version>` into a temp dir, extracted. Tests inject a
6441
+ // local directory (ADR-001, "fetchPublished … в тестах — локальный каталог").
6442
+ const fetchPublished = siblingDriftFetcher ??
6443
+ ((name, version) => {
6444
+ try {
6445
+ const tmp = mkdtempSync(join(tmpdir(), 'dz-sibling-drift-'));
6446
+ execSync(`npm pack ${name}@${version} --pack-destination ${JSON.stringify(tmp)}`, {
6447
+ stdio: 'pipe',
6448
+ encoding: 'utf-8',
6449
+ timeout: 60_000,
6450
+ });
6451
+ const tarball = readdirSync(tmp).find((f) => f.endsWith('.tgz'));
6452
+ if (tarball === undefined)
6453
+ return null;
6454
+ execSync(`tar -xzf ${JSON.stringify(join(tmp, tarball))} -C ${JSON.stringify(tmp)}`, { stdio: 'pipe', timeout: 60_000 });
6455
+ return { dir: join(tmp, 'package') };
6456
+ }
6457
+ catch {
6458
+ return null;
6459
+ }
6460
+ });
6461
+ // AM-4: `npm pack --dry-run --json` is a real subprocess — cache it for the lifetime of this
6462
+ // ENTIRE run (keyed by resolved dir), NOT per package being checked (round-1 review, finding 5):
6463
+ // the cache used to be re-created inside the per-package loop body, so two different dependents
6464
+ // of the SAME sibling packed it twice. `npmPackRunner` (AM-5) is a test seam — production leaves
6465
+ // it unset and runs the real subprocess; a test injects a stub that throws to prove a real `npm`
6466
+ // failure reaches the caller as `unavailable`, without spawning anything.
6467
+ // Lead fix after the fix-round's live dry-run (2026-09-14 01:02, MEASURED on the hub): the
6468
+ // workspace side is now PACKED BY THE LIVE TRANSPORT — `pnpm pack` into a per-run temp dir,
6469
+ // unpacked, and handed to core as a `packedDir` that core hashes with the SAME full walk it uses
6470
+ // for the published tarball. `npm pack --dry-run --json` (kept behind the `npmPackRunner` test
6471
+ // seam) never lists the LICENSE pnpm synthesises from the workspace root into a package whose own
6472
+ // tree has none, so two siblings unchanged since publication (harness-presets, scout) read as
6473
+ // "LICENSE only in the published copy" — a false drift the fix-round's inventory could not see.
6474
+ // Honest limit: the seam path (tests) still parses npm's JSON; only production takes the pnpm path.
6475
+ const npmPackInventoryCache = new Map();
6476
+ let packTmpDir;
6477
+ const npmPackInventory = (dir) => {
6478
+ const key = resolve(dir);
6479
+ const hit = npmPackInventoryCache.get(key);
6480
+ if (hit !== undefined)
6481
+ return hit;
6482
+ let out;
6483
+ try {
6484
+ if (npmPackRunner !== undefined) {
6485
+ out = parseNpmPackInventory(npmPackRunner(dir));
6486
+ }
6487
+ else {
6488
+ packTmpDir ??= mkdtempSync(join(tmpdir(), 'dz-drift-pack-'));
6489
+ out = { packedDir: extractIntoTempDir(dir, mkdtempSync(join(packTmpDir, 'p-'))).dir };
6490
+ }
6491
+ }
6492
+ catch (err) {
6493
+ const how = npmPackRunner !== undefined ? 'npm pack --dry-run --json' : 'pnpm pack';
6494
+ out = { unavailable: `${how} failed: ${err.message.split('\n')[0]}` };
6495
+ }
6496
+ npmPackInventoryCache.set(key, out);
6497
+ return out;
6498
+ };
6499
+ const localInventorySource = npmPackRunner !== undefined ? 'npm-pack' : 'pnpm-pack';
6500
+ // AM-3 (Codex round-1 review, finding 4, high): sibling-drift audit records are EXACTLY one per
6501
+ // package per rule per RUN. The old code appended one JSONL record per SIBLING a package depends
6502
+ // on (a package with two drifted deps wrote two rows under the same rule), and the `unavailable`
6503
+ // branch without `--allow-sibling-drift` wrote NO record at all. Every sibling outcome for a
6504
+ // package is now aggregated first (`pkParts`/`pkVerdict`/`pkOverrideUsed`) and written ONCE —
6505
+ // `block` if any sibling blocks, else `warn` if the only issues were resolved via
6506
+ // `--allow-sibling-drift`, else `pass` — with a detail naming every sibling and its status.
6507
+ // `siblingDriftAudited` guarantees the single write even though `--include-drifted`'s fixed-point
6508
+ // loop can revisit the SAME package across rounds: a package's own `dependencies` never change
6509
+ // between rounds, so a later round can only ever re-derive a SUBSET of what the first pass
6510
+ // already covered (its siblings that drifted got folded into the batch and are now skipped).
6511
+ const siblingDriftAudited = new Set();
6512
+ let driftBlocked = 0;
6513
+ const extraBatch = new Set();
6514
+ // AM-2: --include-drifted must reach a FIXED POINT over transitive drifted siblings — a sibling
6515
+ // folded into the batch can itself depend on a drifted sibling outside it, and the round-1 review
6516
+ // (finding 2) showed the single pass never re-checked an EXPANDED batch's own new edges. Capped at
6517
+ // `allPackages.length + 1` rounds (the plan's own "цикл с потолком = число пакетов").
6518
+ const maxRounds = allPackages.length + 1;
6519
+ // Codex round-3 (2026-09-14): the per-run pack scratch is released in a `finally`, so an
6520
+ // exception thrown while hashing or auditing cannot leak a `dz-drift-pack-*` dir under tmpdir.
6521
+ try {
6522
+ for (let round = 0; round < maxRounds; round++) {
6523
+ let addedThisRound = false;
6524
+ for (const pk of targets) {
6525
+ let manifestObj;
6526
+ try {
6527
+ manifestObj = JSON.parse(readFileSync(join(pk.dir, 'package.json'), 'utf-8'));
6528
+ }
6529
+ catch (err) {
6530
+ // AM-3: an unreadable/invalid package.json for a BATCH package is an input this HARD gate
6531
+ // cannot build — it must BLOCK, never silently degrade to "no dependencies" (which used to
6532
+ // read as a clean n/a). AND (finding 4) the non-override branch below used to print a
6533
+ // BLOCKED line with NO audit record behind it — an `unavailable` outcome is logged exactly
6534
+ // like every other outcome, override or not.
6535
+ const reason = `package.json unreadable/invalid (${err.message.split('\n')[0]})`;
6536
+ if (!siblingDriftAudited.has(pk.name)) {
6537
+ siblingDriftAudited.add(pk.name);
6538
+ if (allowSiblingDrift) {
6539
+ 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);
6540
+ if (wrote.logged) {
6541
+ write(`dz publish: ⚠ sibling drift check unavailable for ${pk.name} (${reason}) — allowed via --allow-sibling-drift (logged)`);
6542
+ }
6543
+ else {
6544
+ 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`);
6545
+ driftBlocked++;
6546
+ }
6547
+ }
6548
+ else {
6549
+ const wrote = appendPublishGateAudit(cwd, 'sibling-drift', 'block', `${pk.name}: ${reason}`, [{ name: pk.name, version: pk.version }], undefined, gateAuditFsLayer);
6550
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${reason}); add --allow-sibling-drift to override (logged) or fix the manifest${auditSuffix(wrote)}`);
6551
+ driftBlocked++;
6552
+ }
6553
+ }
6554
+ continue;
6555
+ }
6556
+ const deps = manifestObj?.dependencies ?? {};
6557
+ const peerDeps = manifestObj?.peerDependencies ?? {};
6558
+ const optionalDeps = manifestObj?.optionalDependencies ?? {};
6559
+ // AM-6: a package with no EXTERNAL sibling to check at all is n/a for THIS gate — recorded as
6560
+ // a pass note ("no external siblings"), not silence (FR-6 compatibility: this branch prints
6561
+ // nothing to stdout, matching the pre-existing behavior). "External" covers BOTH "no
6562
+ // workspace: dependency declared" and "every workspace: dependency is inside THIS batch"
6563
+ // (publishing fresh, nothing stale to drift from) — both used to leave this package with no
6564
+ // audit record at all when every dep resolved to the second case.
6565
+ const anyWorkspaceDep = [...Object.values(deps), ...Object.values(peerDeps), ...Object.values(optionalDeps)]
6566
+ .some((spec) => String(spec).startsWith('workspace:'));
6567
+ const pkParts = [];
6568
+ let pkVerdict = 'pass';
6569
+ let pkOverrideUsed = false;
6570
+ if (anyWorkspaceDep) {
6571
+ const drifts = detectSiblingDrift({
6572
+ localInventory: npmPackInventory,
6573
+ localInventorySource,
6574
+ dependencies: deps,
6575
+ peerDependencies: peerDeps,
6576
+ optionalDependencies: optionalDeps,
6577
+ workspaceVersions,
6578
+ workspaceDirs,
6579
+ batch: batchNames,
6580
+ fetchPublished,
6581
+ });
6582
+ for (const r of drifts) {
6583
+ if (r.status === 'same') {
6584
+ pkParts.push(`${r.name}@${r.version}: same`);
6585
+ write(`dz publish: ✓ sibling drift: none (${r.name}@${r.version} = workspace)`);
6586
+ }
6587
+ else if (r.status === 'unavailable') {
6588
+ if (allowSiblingDrift) {
6589
+ pkParts.push(`${r.name}@${r.version}: unavailable (${r.reason}) — allowed via --allow-sibling-drift`);
6590
+ if (pkVerdict !== 'block')
6591
+ pkVerdict = 'warn';
6592
+ pkOverrideUsed = true;
6593
+ }
6594
+ else {
6595
+ pkParts.push(`${r.name}@${r.version}: unavailable (${r.reason})`);
6596
+ pkVerdict = 'block';
6597
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${r.reason}); add --allow-sibling-drift to override (logged) or check network/registry access`);
6598
+ driftBlocked++;
6599
+ }
6600
+ }
6601
+ else if (includeDrifted) {
6602
+ pkParts.push(`${r.name}@${r.version}: drift (${r.changedFiles.length} file(s)) — auto-included via --include-drifted`);
6603
+ if (!batchNames.has(r.name) && !extraBatch.has(r.name)) {
6604
+ extraBatch.add(r.name);
6605
+ addedThisRound = true;
6606
+ 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(', ')})` : ''}`);
6607
+ }
6608
+ }
6609
+ else if (allowSiblingDrift) {
6610
+ pkParts.push(`${r.name}@${r.version}: drift (${r.changedFiles.length} file(s)) — allowed via --allow-sibling-drift`);
6611
+ if (pkVerdict !== 'block')
6612
+ pkVerdict = 'warn';
6613
+ pkOverrideUsed = true;
6614
+ }
6615
+ else {
6616
+ pkParts.push(`${r.name}@${r.version}: drift (${r.changedFiles.length} file(s))`);
6617
+ pkVerdict = 'block';
6618
+ const suggestFilter = filterStr !== undefined ? `${filterStr},${r.name}` : `${pk.name},${r.name}`;
6619
+ 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`);
6620
+ driftBlocked++;
6621
+ }
6622
+ }
6623
+ }
6624
+ // AM-3/AM-6: the single, aggregated audit write for this package — "no external siblings"
6625
+ // when nothing was ever checked, otherwise every sibling's status joined into one detail.
6626
+ if (!siblingDriftAudited.has(pk.name)) {
6627
+ siblingDriftAudited.add(pk.name);
6628
+ const detail = pkParts.length > 0 ? pkParts.join('; ') : 'no external siblings';
6629
+ // AM-6: an override reason is attached only when the FINAL verdict is 'warn' — if some
6630
+ // OTHER sibling still stands as a live block, the override never actually excused the run.
6631
+ const overrideReason = pkOverrideUsed && pkVerdict !== 'block' ? '--allow-sibling-drift' : undefined;
6632
+ const wrote = appendPublishGateAudit(cwd, 'sibling-drift', pkVerdict, detail, [{ name: pk.name, version: pk.version }], overrideReason, gateAuditFsLayer);
6633
+ if (pkOverrideUsed) {
6634
+ // AM-6: the override is only real once ITS audit row is durable — a write failure must
6635
+ // refuse the publish rather than print "(logged)" about a record that never landed.
6636
+ if (wrote.logged) {
6637
+ write(`dz publish: ⚠ sibling drift override recorded for ${pk.name} — allowed via --allow-sibling-drift (logged)`);
6638
+ }
6639
+ else {
6640
+ 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`);
6641
+ driftBlocked++;
6642
+ }
6643
+ }
6644
+ else if (pkParts.length > 0) {
6645
+ write(`dz publish: ℹ sibling drift audit for ${pk.name}${auditSuffix(wrote)}`);
6646
+ }
6647
+ else if (!wrote.logged) {
6648
+ // Codex round-2 (2026-09-14), AM-6 residual: the "no external siblings" pass note stays
6649
+ // silent on stdout ONLY while its record actually landed — a failed audit write is said.
6650
+ write(`dz publish: ℹ sibling drift audit for ${pk.name} (no external siblings)${auditSuffix(wrote)}`);
6651
+ }
6652
+ }
6653
+ }
6654
+ if (driftBlocked > 0)
6655
+ break; // nothing to expand into a run that already refuses
6656
+ if (!includeDrifted || !addedThisRound)
6657
+ break; // no auto-expand requested, or fixed point reached
6658
+ // FR-4: --include-drifted folds the drifted sibling(s) into the batch — they bump patch like
6659
+ // any other package in `publishPackages`' own (unchanged) bump logic. Re-loop: the newly
6660
+ // folded-in sibling(s) may themselves depend on a drifted sibling outside the (now bigger) batch.
6661
+ filter = filter === undefined ? [...batchNames, ...extraBatch] : [...filter, ...extraBatch];
6662
+ targets = allPackages.filter(matchesFilter);
6663
+ batchNames = new Set(targets.map((p) => p.name));
6664
+ }
6665
+ }
6666
+ finally {
6667
+ if (packTmpDir !== undefined)
6668
+ rmSync(packTmpDir, { recursive: true, force: true });
6669
+ }
6670
+ const siblingDriftFailed = driftBlocked > 0;
6671
+ if (siblingDriftFailed && !dryRun) {
6672
+ write(`dz publish: refusing to publish (${driftBlocked} sibling-drift violation(s))`);
6673
+ return 1;
6674
+ }
6675
+ // FR-3 — packed-install smoke: pack the WHOLE (possibly --include-drifted-expanded) batch,
6676
+ // install every tarball together in a CLEAN dir (out-of-batch siblings resolve from the
6677
+ // registry, exactly like a fresh user's install), then boot every bin with --version.
6678
+ // "n/a" (FR-6) when nothing in the batch has a bin. AM-8: a bin is collected here whether or not
6679
+ // its target file exists YET — a manifest that declares one but ships nothing must BLOCK after a
6680
+ // real install, never silently vanish from the plan (which used to read as n/a, or even skip the
6681
+ // whole gate when it was the batch's only bin).
6682
+ const bins = [];
6683
+ for (const pk of targets) {
6684
+ let manifest = {};
6685
+ try {
6686
+ manifest = JSON.parse(readFileSync(join(pk.dir, 'package.json'), 'utf-8'));
6687
+ }
6688
+ catch { /* no bin info available */ }
6689
+ if (typeof manifest.bin === 'string') {
6690
+ bins.push({ pkg: pk.name, binName: pk.name.split('/').pop() ?? pk.name, relPath: manifest.bin.replace(/^\.\//, '') });
6691
+ }
6692
+ else if (manifest.bin !== undefined && manifest.bin !== null && typeof manifest.bin === 'object') {
6693
+ for (const [name, relRaw] of Object.entries(manifest.bin)) {
6694
+ bins.push({ pkg: pk.name, binName: name, relPath: String(relRaw).replace(/^\.\//, '') });
6695
+ }
6696
+ }
6697
+ }
6698
+ // AM-1/AM-5: the packed-install-smoke PREVIEW below runs on a DRY RUN only, against whatever is
6699
+ // CURRENTLY on disk (pre-bump) — it cannot be the "same bytes that ship" gate AM-1 requires,
6700
+ // because a dry run never bumps/builds/packs anything real to compare against. On a LIVE run the
6701
+ // real gate is `packedTransport` (wired at the `publishPackages` call below), which packs ONCE
6702
+ // post-bump and smokes exactly those tarballs — this preview is skipped entirely then, so its
6703
+ // digest is never confused with the one that actually ships.
6704
+ let packedInstallSmokePreviewFailed = false;
6705
+ if (dryRun) {
6706
+ if (bins.length === 0) {
6707
+ 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);
6708
+ write(`dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)${auditSuffix(wrote)}`);
6709
+ }
6710
+ else {
6711
+ const scratchRoot = packedInstallScratchRoot();
6712
+ const packDir = mkdtempSync(join(scratchRoot, 'dz-publish-pack-'));
6713
+ const installDir = mkdtempSync(join(scratchRoot, 'dz-publish-install-'));
6714
+ const runSmoke = packedInstallRunner ??
6715
+ ((cmd, o) => {
6716
+ try {
6717
+ const stdout = execSync(cmd, { cwd: o.cwd, stdio: 'pipe', encoding: 'utf-8', timeout: o.timeoutMs });
6718
+ return { exitCode: 0, stdout: stdout == null ? '' : String(stdout), stderr: '' };
6719
+ }
6720
+ catch (err) {
6721
+ const e = err;
6722
+ const timedOut = (e.status === null || e.status === undefined) && (e.signal != null || e.killed === true);
6723
+ return {
6724
+ exitCode: typeof e.status === 'number' ? e.status : 1,
6725
+ stdout: e.stdout == null ? '' : String(e.stdout),
6726
+ stderr: e.stderr == null || String(e.stderr).trim() === '' ? formatPublishError(e) : String(e.stderr),
6727
+ timedOut,
6728
+ };
6729
+ }
6730
+ });
6731
+ const smokePlan = planPackedInstallSmoke({
6732
+ packages: targets.map((p) => ({ name: p.name, dir: p.dir, version: p.version })),
6733
+ bins,
6734
+ packDir,
6735
+ installDir,
6736
+ });
6737
+ const smokeExecutions = [];
6738
+ // Lead edit after the live dry-run (13.09 12:05): the preview packed the WORKING directory with
6739
+ // `workspace:^` specs still inside, so `npm install <tgz>` died with EUNSUPPORTEDPROTOCOL — the
6740
+ // preview must stage package.json exactly as the live packedTransport does (sibling pins via
6741
+ // rewriteWorkspaceSpecs, prepublishOnly dropped) and restore the originals afterwards. Shared
6742
+ // with `dz release` via `withStagedPackageJson` (feature release-smoke-staged-pack).
6743
+ try {
6744
+ withStagedPackageJson(targets, workspaceVersions, write, () => {
6745
+ for (const step of smokePlan.steps) {
6746
+ const r = runSmoke(step.cmd, { cwd: step.cwd, timeoutMs: step.timeoutMs });
6747
+ smokeExecutions.push({ stepId: step.id, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, ...(r.timedOut !== undefined ? { timedOut: r.timedOut } : {}) });
6748
+ }
6749
+ }, 'dz publish');
6750
+ }
6751
+ catch (err) {
6752
+ // a restore failure is a preview failure (Codex finding 1): never a green preview on a staged tree
6753
+ smokeExecutions.push({ stepId: 'staged-restore', exitCode: 1, stdout: '', stderr: formatPublishError(err) });
6754
+ }
6755
+ const smokeVerdict = judgePackedInstallSmoke(smokePlan, smokeExecutions);
6756
+ try {
6757
+ rmSync(packDir, { recursive: true, force: true });
6758
+ }
6759
+ catch { /* best-effort cleanup */ }
6760
+ try {
6761
+ rmSync(installDir, { recursive: true, force: true });
6762
+ }
6763
+ catch { /* best-effort cleanup */ }
6764
+ if (smokeVerdict.ok) {
6765
+ 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);
6766
+ write(`dz publish: ✓ packed install smoke (preview)${auditSuffix(wrote)}`);
6767
+ }
6768
+ else {
6769
+ const detail = smokeVerdict.failureDetail ?? smokeVerdict.bins.find((b) => !b.ok)?.detail ?? '(no detail)';
6770
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail, targets.map((p) => ({ name: p.name, version: p.version })), undefined, gateAuditFsLayer);
6771
+ write(`dz publish: BLOCKED — packed install smoke failed (preview): ${detail}${auditSuffix(wrote)}`);
6772
+ for (const b of smokeVerdict.bins.filter((b) => !b.ok))
6773
+ write(` ✗ ${b.pkg} (${b.binName}): ${b.detail ?? '(no detail)'}`);
6774
+ packedInstallSmokePreviewFailed = true;
6775
+ }
6776
+ }
6777
+ }
6778
+ if (siblingDriftFailed || packedInstallSmokePreviewFailed) {
6779
+ write(`dz publish: refusing to publish (${driftBlocked} sibling-drift violation(s)${packedInstallSmokePreviewFailed ? ', packed install smoke failed' : ''})`);
6780
+ return 1;
6781
+ }
6313
6782
  if (!dryRun) {
6314
6783
  // Loud confirmation banner listing exactly what is about to be published.
6315
6784
  const targets = discoverPackages(cwd).filter((p) => filter === undefined || filter.length === 0
@@ -6356,23 +6825,37 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6356
6825
  // `pnpm publish` and skipped re-signing (ADR-001, features/publish-gate-verifies-the-tarball).
6357
6826
  let cleanupGate = null;
6358
6827
  try {
6359
- const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
6360
- let extracted;
6361
- try {
6362
- extracted = extractPublishTarball(pk.dir);
6363
- cleanupGate = extracted.cleanup;
6364
- }
6365
- catch (err) {
6366
- // Cross-family review (codex `gpt-5.6-sol`, 2026-08-22): falling back to the working
6367
- // TREE here fails the gate OPEN. The gate's whole claim is "what ships matches the
6368
- // signature"; with no artifact, nothing was compared, and reporting a pass would be a
6369
- // claim about an object that was never built. Say why, and block.
6370
- write(`dz publish: could not pack ${pk.name} (${err.message.split('\n')[0]}) — the artifact was never built, so its signature was not checked`);
6828
+ const parsedManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
6829
+ // FR-4 (feature publish-gate-audit-durable): a manifest that PARSES but is not a plain
6830
+ // object — `null`, an array, a bare string — is UNAVAILABLE, never "no signature". Those
6831
+ // are different failures with different fixes: "no signature" means run `dz sign`; a
6832
+ // malformed manifest means the file itself is corrupt/wrong-shaped and re-signing alone
6833
+ // would silently paper over that. Mirrors `readManifest`'s shape guard in
6834
+ // publish-sibling-drift.ts (null/array/non-object ⇒ cannot be used, say so).
6835
+ if (parsedManifest === null || typeof parsedManifest !== 'object' || Array.isArray(parsedManifest)) {
6836
+ const gotShape = parsedManifest === null ? 'null' : Array.isArray(parsedManifest) ? 'an array' : typeof parsedManifest;
6837
+ write(`dz publish: ${pk.name}'s ${MANIFEST_NAME} is not a JSON object (got ${gotShape}) its signature is unavailable, not merely absent`);
6371
6838
  artifactUnavailable = true;
6372
6839
  }
6373
- verifyOk =
6374
- extracted !== undefined &&
6375
- verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
6840
+ else {
6841
+ const signed = parsedManifest;
6842
+ let extracted;
6843
+ try {
6844
+ extracted = extractPublishTarball(pk.dir);
6845
+ cleanupGate = extracted.cleanup;
6846
+ }
6847
+ catch (err) {
6848
+ // Cross-family review (codex `gpt-5.6-sol`, 2026-08-22): falling back to the working
6849
+ // TREE here fails the gate OPEN. The gate's whole claim is "what ships matches the
6850
+ // signature"; with no artifact, nothing was compared, and reporting a pass would be a
6851
+ // claim about an object that was never built. Say why, and block.
6852
+ write(`dz publish: could not pack ${pk.name} (${err.message.split('\n')[0]}) — the artifact was never built, so its signature was not checked`);
6853
+ artifactUnavailable = true;
6854
+ }
6855
+ verifyOk =
6856
+ extracted !== undefined &&
6857
+ verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
6858
+ }
6376
6859
  }
6377
6860
  catch {
6378
6861
  verifyOk = false;
@@ -6400,12 +6883,83 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6400
6883
  // longer exist. Default to the same path `dz sign --init` writes, so the ordinary operator needs no
6401
6884
  // new flag; `--sign-key` overrides it.
6402
6885
  const signKey = (options.get('sign-key') ?? join(homedir(), '.dz', 'keys', 'dz.key')).trim();
6886
+ // AM-1: the packedTransport smoke closure and the actual `npm publish <tgz>` inside
6887
+ // `publishPackages` both read from THIS SAME directory — created once, cleaned up once, after
6888
+ // publishPackages returns (it needs the tarballs on disk through its own publish step).
6889
+ const packedTransportPackDestDir = mkdtempSync(join(packedInstallScratchRoot(), 'dz-publish-packed-'));
6403
6890
  const publishReport = publishPackages(cwd, {
6404
6891
  provenance,
6405
6892
  dryRun,
6406
6893
  filter,
6407
6894
  bumpOnly,
6408
6895
  claimGate: claimCheckOpt,
6896
+ exec: publishExecRunner,
6897
+ packedTransport: {
6898
+ packDestDir: packedTransportPackDestDir,
6899
+ // AM-1: judged ONCE, over every package's packed artifact — nothing in the batch publishes
6900
+ // until this returns ok:true. `bins` (AM-8-fixed: declared bins are collected whether or not
6901
+ // their target file exists yet) was already computed above from the same `targets` this
6902
+ // batch resolves to.
6903
+ smoke: (artifacts) => {
6904
+ for (const a of artifacts)
6905
+ write(`dz publish: tarball ${a.name}@${a.newVersion} sha256:${a.sha256}`);
6906
+ const auditPackages = artifacts.map((a) => ({ name: a.name, version: a.newVersion, tarballSha256: a.sha256 }));
6907
+ if (bins.length === 0) {
6908
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin', auditPackages, undefined, gateAuditFsLayer);
6909
+ write(`dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)${auditSuffix(wrote)}`);
6910
+ return { ok: true };
6911
+ }
6912
+ const scratchRoot = packedInstallScratchRoot();
6913
+ const installDir = mkdtempSync(join(scratchRoot, 'dz-publish-install-'));
6914
+ const runSmoke = packedInstallRunner ??
6915
+ ((cmd, o) => {
6916
+ try {
6917
+ const stdout = execSync(cmd, { cwd: o.cwd, stdio: 'pipe', encoding: 'utf-8', timeout: o.timeoutMs });
6918
+ return { exitCode: 0, stdout: stdout == null ? '' : String(stdout), stderr: '' };
6919
+ }
6920
+ catch (err) {
6921
+ const e = err;
6922
+ const timedOut = (e.status === null || e.status === undefined) && (e.signal != null || e.killed === true);
6923
+ return {
6924
+ exitCode: typeof e.status === 'number' ? e.status : 1,
6925
+ stdout: e.stdout == null ? '' : String(e.stdout),
6926
+ stderr: e.stderr == null || String(e.stderr).trim() === '' ? formatPublishError(e) : String(e.stderr),
6927
+ timedOut,
6928
+ };
6929
+ }
6930
+ });
6931
+ const smokePlan = planPackedInstallSmoke({
6932
+ // skipPack (AM-1): these tarballs are ALREADY packed (by publishPackages, above) — a
6933
+ // second, different pack here would smoke bytes other than the ones about to publish.
6934
+ packages: artifacts.map((a) => ({ name: a.name, dir: '(packed already — see skipPack)', version: a.newVersion })),
6935
+ bins,
6936
+ packDir: packedTransportPackDestDir,
6937
+ installDir,
6938
+ skipPack: true,
6939
+ });
6940
+ const smokeExecutions = [];
6941
+ for (const step of smokePlan.steps) {
6942
+ const r = runSmoke(step.cmd, { cwd: step.cwd, timeoutMs: step.timeoutMs });
6943
+ smokeExecutions.push({ stepId: step.id, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, ...(r.timedOut !== undefined ? { timedOut: r.timedOut } : {}) });
6944
+ }
6945
+ const verdict = judgePackedInstallSmoke(smokePlan, smokeExecutions);
6946
+ try {
6947
+ rmSync(installDir, { recursive: true, force: true });
6948
+ }
6949
+ catch { /* best-effort cleanup */ }
6950
+ if (verdict.ok) {
6951
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'pack/install/--version all clean (live, packedTransport)', auditPackages, undefined, gateAuditFsLayer);
6952
+ write(`dz publish: ✓ packed install smoke${auditSuffix(wrote)}`);
6953
+ return { ok: true };
6954
+ }
6955
+ const detail = verdict.failureDetail ?? verdict.bins.find((b) => !b.ok)?.detail ?? '(no detail)';
6956
+ const wrote = appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail, auditPackages, undefined, gateAuditFsLayer);
6957
+ write(`dz publish: BLOCKED — packed install smoke failed: ${detail}${auditSuffix(wrote)}`);
6958
+ for (const b of verdict.bins.filter((b) => !b.ok))
6959
+ write(` ✗ ${b.pkg} (${b.binName}): ${b.detail ?? '(no detail)'}`);
6960
+ return { ok: false, reason: detail };
6961
+ },
6962
+ },
6409
6963
  signKey: signKey === '' ? undefined : resolve(cwd, signKey),
6410
6964
  verifyAfterSign: (packDir) => {
6411
6965
  // Verify the OUTCOME against the trust root a CONSUMER would use — an existing key may be the
@@ -6482,6 +7036,10 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6482
7036
  }
6483
7037
  },
6484
7038
  });
7039
+ try {
7040
+ rmSync(packedTransportPackDestDir, { recursive: true, force: true });
7041
+ }
7042
+ catch { /* best-effort cleanup */ }
6485
7043
  const configMirror = mirrorCommandFromConfig(cwd);
6486
7044
  const configuredCommand = (options.get('mirror-cmd') ?? configMirror.command ?? '').trim();
6487
7045
  const publishedVersions = publishReport.packages
@@ -6570,6 +7128,10 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6570
7128
  ? ` (confirmed by registry after ${pkg.registryProbes} probes)`
6571
7129
  : '';
6572
7130
  write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} → ${pkg.newVersion} ${pkg.status}${receipt}${detail}`);
7131
+ // AM-1: the digest of the EXACT tarball bytes that were smoke-tested AND published — present
7132
+ // only for a packedTransport publish, so "the smoke tested what shipped" is checkable here too.
7133
+ if (pkg.status === 'published' && pkg.sha256 !== undefined)
7134
+ write(` sha256:${pkg.sha256}`);
6573
7135
  if (pkg.status === 'error' && pkg.error) {
6574
7136
  for (const line of pkg.error.split('\n'))
6575
7137
  write(` ${line}`);
@@ -6916,13 +7478,36 @@ function cmdRelease(options, flags, cwd, write, runner) {
6916
7478
  say(`dz release --affected: ${selected.length}/${factsList.length} package(s) selected from ${changed.length} changed file(s)${selected.length === factsList.length ? ' (no narrowing — full set)' : ''}`);
6917
7479
  factsList = selected;
6918
7480
  }
7481
+ // FR-6 (feature publish-sibling-drift-gate): real tmp dirs for the packed-install smoke — only
7482
+ // when something in the set actually has a bin to boot (packing bin-less siblings proves
7483
+ // nothing this gate exists to catch). Planning stays pure (planReleaseGates never mkdtemps
7484
+ // itself); these are cleaned up on every exit path below, dry-run included.
7485
+ const packedInstallEligible = factsList.some((f) => f.bins.some((b) => b.exists));
7486
+ const releaseScratchRoot = packedInstallScratchRoot();
7487
+ const packedInstallDirs = packedInstallEligible
7488
+ ? { packDir: mkdtempSync(join(releaseScratchRoot, 'dz-release-pack-')), installDir: mkdtempSync(join(releaseScratchRoot, 'dz-release-install-')) }
7489
+ : undefined;
7490
+ const cleanupPackedInstallDirs = () => {
7491
+ if (packedInstallDirs === undefined)
7492
+ return;
7493
+ try {
7494
+ rmSync(packedInstallDirs.packDir, { recursive: true, force: true });
7495
+ }
7496
+ catch { /* best-effort cleanup */ }
7497
+ try {
7498
+ rmSync(packedInstallDirs.installDir, { recursive: true, force: true });
7499
+ }
7500
+ catch { /* best-effort cleanup */ }
7501
+ };
6919
7502
  const plan = planReleaseGates(factsList, {
6920
7503
  monorepoRoot: cwd,
6921
7504
  pnpmLockPresent: existsSync(join(cwd, 'pnpm-lock.yaml')),
6922
7505
  includeDevDeps: flags.has('audit-dev'),
7506
+ packedInstall: packedInstallDirs,
6923
7507
  });
6924
7508
  // --dry-run: print the full plan, execute NOTHING (deterministic, byte-testable preview).
6925
7509
  if (flags.has('dry-run')) {
7510
+ cleanupPackedInstallDirs();
6926
7511
  if (json) {
6927
7512
  write(JSON.stringify({ dryRun: true, packages: plan.packages, steps: plan.steps, skips: plan.skips, warnings }, null, 2));
6928
7513
  return 0;
@@ -6949,7 +7534,20 @@ function cmdRelease(options, flags, cwd, write, runner) {
6949
7534
  let smokeTmp;
6950
7535
  const execSteps = plan.steps.filter((s) => s.kind !== 'synthetic-fail');
6951
7536
  say(`\ndz release — executing ${execSteps.length} gate step(s) across ${plan.packages.length} package(s)…`);
6952
- for (const step of execSteps) {
7537
+ // FR-1/FR-2 (feature release-smoke-staged-pack): the packed-install smoke's `pack` steps must
7538
+ // pack the SAME staged bytes `dz publish`'s preview does — sibling `workspace:*` deps rewritten
7539
+ // to the exact sibling version, `prepublishOnly` dropped — or `npm install` on the resulting
7540
+ // tarballs dies with EUNSUPPORTEDPROTOCOL (MEASURED 2026-09-13 16:05). `workspaceVersions` is
7541
+ // built like publish's (from the FULL workspace, not just this release's filtered batch — an
7542
+ // out-of-batch sibling still needs its real version). Only the `pack` sub-steps are staged; the
7543
+ // `install`/`bin-exists`/`bin-version` steps already run against the packed tarballs and need no
7544
+ // staging.
7545
+ const allPackages = discoverPackages(cwd);
7546
+ const workspaceVersions = new Map(allPackages.map((p) => [p.name, p.version]));
7547
+ const packStepIds = new Set((plan.packedInstallPlan?.steps ?? [])
7548
+ .filter((s) => s.kind === 'pack')
7549
+ .map((s) => `smoke:packed-install:${s.id}`));
7550
+ const runGateStep = (step) => {
6953
7551
  let stepCwd = step.cwd;
6954
7552
  if (step.tempCwd === true) {
6955
7553
  // AM-4: boot bins in a throwaway cwd so an installer-style bin cannot mutate the workspace.
@@ -6967,6 +7565,37 @@ function cmdRelease(options, flags, cwd, write, runner) {
6967
7565
  durationMs: Date.now() - started,
6968
7566
  timedOut: r.timedOut,
6969
7567
  });
7568
+ };
7569
+ // Plan/execution order is load-bearing (see the plan/execution skew guard test) — steps run in
7570
+ // exactly the order `plan.steps` lists them, one loop, no reordering. Only the `pack` steps are
7571
+ // wrapped in the staged window, individually, so where they fall in that order never changes.
7572
+ let announcedStagedPack = false;
7573
+ for (const step of execSteps) {
7574
+ if (packStepIds.has(step.id)) {
7575
+ if (!announcedStagedPack) {
7576
+ say('dz release: smoke: packed tarballs staged like the live publish (workspace:* → exact sibling versions)');
7577
+ announcedStagedPack = true;
7578
+ }
7579
+ // Codex finding 2: stage ONLY the package this pack step packs — sibling pins come from
7580
+ // workspaceVersions, and `npm pack` reads the packed package's manifest alone.
7581
+ const packed = factsList.filter((f) => f.name === step.pkg);
7582
+ try {
7583
+ withStagedPackageJson(packed, workspaceVersions, write, () => runGateStep(step), 'dz release');
7584
+ }
7585
+ catch (err) {
7586
+ // a staging/restore failure is a FAILED step with the reason, never a silent skip — and ONE
7587
+ // record per step: a run already recorded by runGateStep is replaced, not duplicated (Codex r2)
7588
+ const failed = { stepId: step.id, exitCode: 1, stdout: '', stderr: formatPublishError(err), durationMs: 0, timedOut: false };
7589
+ const at = executions.findIndex((e) => e.stepId === step.id);
7590
+ if (at >= 0)
7591
+ executions[at] = failed;
7592
+ else
7593
+ executions.push(failed);
7594
+ }
7595
+ }
7596
+ else {
7597
+ runGateStep(step);
7598
+ }
6970
7599
  }
6971
7600
  if (smokeTmp !== undefined) {
6972
7601
  try {
@@ -6974,14 +7603,31 @@ function cmdRelease(options, flags, cwd, write, runner) {
6974
7603
  }
6975
7604
  catch { /* best-effort cleanup */ }
6976
7605
  }
7606
+ cleanupPackedInstallDirs();
6977
7607
  const verdict = classifyGateExecutions(plan, executions);
6978
7608
  // Report: per-gate ✓/✗/○ with package granularity + timestamp (NFR-5). Skips are named,
6979
7609
  // never folded into pass wording (AM-2: "N passed, M skipped", not "all tests passed").
6980
7610
  for (const g of verdict.gates) {
6981
7611
  const icon = g.status === 'pass' ? '✓' : g.status === 'fail' ? '✗' : '○';
6982
7612
  say(` ${icon} ${g.gate.padEnd(7)} ${g.status.toUpperCase().padEnd(4)} ${g.passed} passed, ${g.failures.length} failed, ${g.skips.length} skipped`);
6983
- for (const f of g.failures)
7613
+ for (const f of g.failures) {
6984
7614
  say(` [${f.class}] ${f.pkg !== undefined ? `${f.pkg}: ` : ''}${f.reason}`);
7615
+ // FR-2 / AM-4 / AM-6 (feature release-gate-output-tail): print each non-empty stream's
7616
+ // tail under the failure line, labelled `stdout:`/`stderr:` at the failure's own 6-space
7617
+ // indent, with its content lines at an 8-space CONTINUATION indent so a reader can tell a
7618
+ // tail line from a new failure/skip bullet at a glance; --json carries the same `tails`
7619
+ // object as-is (present, possibly with empty strings, on every executed failure).
7620
+ if (f.tails !== undefined) {
7621
+ for (const stream of ['stdout', 'stderr']) {
7622
+ const t = f.tails[stream];
7623
+ if (t.length === 0)
7624
+ continue;
7625
+ say(` ${stream}:`);
7626
+ for (const tailLine of t.split('\n'))
7627
+ say(` ${tailLine}`);
7628
+ }
7629
+ }
7630
+ }
6985
7631
  for (const sk of g.skips)
6986
7632
  say(` [${sk.class}] ${sk.pkg}: ${sk.reason}`);
6987
7633
  }
@@ -9806,6 +10452,156 @@ function runGuardEvaluation(root, op, text, overrideReason, publishFilter) {
9806
10452
  catch { /* audit is best-effort, never blocks the verdict */ }
9807
10453
  return result;
9808
10454
  }
10455
+ /**
10456
+ * AM-2: loop `writeSync` until every byte of `data` has been accepted, checking the RETURNED
10457
+ * length on every call (Codex round-1 review, finding 1: the old code called `writeSync` once and
10458
+ * ignored the return value — a short write followed by `fsyncSync` durably persists a TRUNCATED,
10459
+ * unparseable JSON line into an append-only log every future read walks). A call that reports zero
10460
+ * or negative progress can never complete the buffer and would spin forever — that is treated as a
10461
+ * failure, not a retry target.
10462
+ */
10463
+ function writeAllSync(fsLayer, fd, data) {
10464
+ let remaining = data;
10465
+ while (remaining.length > 0) {
10466
+ const n = fsLayer.writeSync(fd, remaining);
10467
+ if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) {
10468
+ throw new Error(`writeSync made no progress (returned ${n}) with ${remaining.length} byte(s) still pending`);
10469
+ }
10470
+ // Codex round-2 (2026-09-14) finding 3: a layer claiming MORE bytes than it was given is lying
10471
+ // about the record, and must not reach "logged: true" through a subarray that just goes empty.
10472
+ if (n > remaining.length) {
10473
+ throw new Error(`writeSync claimed ${n} byte(s) written but only ${remaining.length} were supplied`);
10474
+ }
10475
+ remaining = remaining.subarray(n);
10476
+ }
10477
+ }
10478
+ const REAL_PUBLISH_GATE_AUDIT_FS = { existsSync, mkdirSync, openSync, writeSync, fsyncSync, closeSync };
10479
+ /**
10480
+ * Feature `publish-sibling-drift-gate` (FR-5/AM-6), durability + per-package identity added by
10481
+ * feature `publish-gate-audit-durable` (FR-1/FR-2): both the sibling-drift and packed-install-smoke
10482
+ * gates write to the SAME append-only, hash-chained `.dz/guard-audit.jsonl` the declarative
10483
+ * `dz guard` rules use — visibility for `dz guard promote`/`dz compounding` never depends on
10484
+ * which mechanism produced the finding. `pass` records go through as an informational `note`
10485
+ * (never a violation, so they can never flip the row's own verdict) so a clean check is ALSO on
10486
+ * the record, not just a block or an override (AM-6: "аудит без записи = не аудит").
10487
+ *
10488
+ * FR-1: one JSONL record PER PACKAGE in `packages`, each naming the package, its version, and the
10489
+ * tarball's sha256 (or the explicit `sha256:n/a` before a tarball exists — a dry-run preview). FR-2:
10490
+ * the write is `openSync('a') → writeSync → fsyncSync(fd) → closeSync`, plus an `fsyncSync` of the
10491
+ * `.dz` directory itself the one time this call CREATES it (a file's own fsync durably persists its
10492
+ * bytes; the directory entry that makes the file findable after a crash needs its own fsync — the
10493
+ * same lesson `integration-apply.ts`'s `fsyncDirectory` already encodes).
10494
+ *
10495
+ * Returns `{ logged, reason? }` — never a bare boolean, so a caller can print WHY a write failed,
10496
+ * not just that it did. Most callers are best-effort (a write failure never blocks a verdict already
10497
+ * decided) — the one exception is an `--allow-sibling-drift` OVERRIDE, whose caller MUST check
10498
+ * `logged`: an override is not real without a durable row behind it (AM-6's load-bearing property —
10499
+ * see `auditedOverride` in `cmdPublish`).
10500
+ */
10501
+ function appendPublishGateAudit(root, rule, verdict, detail, packages, overrideReason, fsLayer = REAL_PUBLISH_GATE_AUDIT_FS) {
10502
+ if (packages.length === 0)
10503
+ return { logged: true }; // nothing to record about — not a failure
10504
+ const dzDir = join(root, '.dz');
10505
+ const auditPath = join(dzDir, 'guard-audit.jsonl');
10506
+ const dzDirExisted = fsLayer.existsSync(dzDir);
10507
+ // AM-2: tracked BEFORE the write — this is what decides whether the append call below is about
10508
+ // to CREATE the file (needing a `.dz` directory-entry fsync afterwards) or extend an existing one
10509
+ // (whose directory entry is already durable from a previous append).
10510
+ const auditFileExisted = fsLayer.existsSync(auditPath);
10511
+ try {
10512
+ fsLayer.mkdirSync(dzDir, { recursive: true });
10513
+ }
10514
+ catch (err) {
10515
+ return { logged: false, reason: `could not create ${dzDir}: ${err.message.split('\n')[0]}` };
10516
+ }
10517
+ const records = packages.map((pkg) => {
10518
+ const sha = pkg.tarballSha256 !== undefined && pkg.tarballSha256 !== '' ? pkg.tarballSha256 : 'n/a';
10519
+ const label = `${rule}: ${pkg.name}@${pkg.version} sha256:${sha} — ${detail}`;
10520
+ return auditRecord(verdict === 'pass'
10521
+ ? { op: 'publish', verdict, violations: [], checked: [rule], notEstablished: [], notes: [label] }
10522
+ : { op: 'publish', verdict, violations: [{ rule, severity: 'hard', detail: label }], checked: [rule], notEstablished: [] }, new Date().toISOString(), overrideReason !== undefined ? { reason: overrideReason } : undefined);
10523
+ });
10524
+ let bytes;
10525
+ try {
10526
+ bytes = appendChainedLines(records, readLogTail(auditPath));
10527
+ }
10528
+ catch (err) {
10529
+ return { logged: false, reason: `could not build the chained record: ${err.message.split('\n')[0]}` };
10530
+ }
10531
+ if (bytes === '')
10532
+ return { logged: true };
10533
+ const buf = Buffer.from(bytes, 'utf-8');
10534
+ let fd;
10535
+ try {
10536
+ fd = fsLayer.openSync(auditPath, fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_WRONLY);
10537
+ writeAllSync(fsLayer, fd, buf); // AM-2: loops on a short write, throws on no progress
10538
+ fsLayer.fsyncSync(fd);
10539
+ fsLayer.closeSync(fd);
10540
+ fd = undefined;
10541
+ }
10542
+ catch (err) {
10543
+ return { logged: false, reason: err.message.split('\n')[0] ?? String(err) };
10544
+ }
10545
+ finally {
10546
+ if (fd !== undefined) {
10547
+ try {
10548
+ fsLayer.closeSync(fd);
10549
+ }
10550
+ catch { /* best-effort */ }
10551
+ }
10552
+ }
10553
+ // AM-2 (Codex round-1 review, finding 1, high): a directory-entry fsync is REQUIRED, not
10554
+ // best-effort, whenever THIS call is the reason the entry needed persisting — swallowing its
10555
+ // failure used to let `logged: true` go out about a record whose directory entry can vanish on a
10556
+ // crash before the next fsck. Two DIFFERENT entries can need persisting, tracked independently:
10557
+ if (!auditFileExisted) {
10558
+ // The audit FILE was just created by the open() above — `.dz` (its containing directory) needs
10559
+ // its own fsync so the new directory entry survives a crash; the file's own fsync (above) only
10560
+ // guarantees the file's DATA, not that anything can find it afterwards.
10561
+ let dfd;
10562
+ try {
10563
+ dfd = fsLayer.openSync(dzDir, fsConstants.O_RDONLY);
10564
+ fsLayer.fsyncSync(dfd);
10565
+ }
10566
+ catch (err) {
10567
+ return { logged: false, reason: `could not fsync ${dzDir} after creating ${auditPath}: ${err.message.split('\n')[0]}` };
10568
+ }
10569
+ finally {
10570
+ if (dfd !== undefined) {
10571
+ try {
10572
+ fsLayer.closeSync(dfd);
10573
+ }
10574
+ catch { /* best-effort, does not affect the verdict already returned */ }
10575
+ }
10576
+ }
10577
+ }
10578
+ if (!dzDirExisted) {
10579
+ // `.dz` ITSELF was just created by `mkdirSync` above — its PARENT (`root`) needs its own fsync
10580
+ // so `.dz`'s OWN directory entry survives a crash (the fsync of `.dz` just above only durably
10581
+ // persists entries INSIDE `.dz`, not the fact that `.dz` exists at all).
10582
+ let pfd;
10583
+ try {
10584
+ pfd = fsLayer.openSync(root, fsConstants.O_RDONLY);
10585
+ fsLayer.fsyncSync(pfd);
10586
+ }
10587
+ catch (err) {
10588
+ return { logged: false, reason: `could not fsync ${root} after creating ${dzDir}: ${err.message.split('\n')[0]}` };
10589
+ }
10590
+ finally {
10591
+ if (pfd !== undefined) {
10592
+ try {
10593
+ fsLayer.closeSync(pfd);
10594
+ }
10595
+ catch { /* best-effort, does not affect the verdict already returned */ }
10596
+ }
10597
+ }
10598
+ }
10599
+ return { logged: true };
10600
+ }
10601
+ /** FR-2: the printed suffix a caller appends to its own verdict line — never silent about logging. */
10602
+ function auditSuffix(wrote) {
10603
+ return wrote.logged ? ' (logged)' : ` (audit NOT logged: ${wrote.reason ?? 'unknown reason'})`;
10604
+ }
9809
10605
  function renderGuardObservation(observation) {
9810
10606
  const tag = observation.status === 'unknown' ? 'note' : 'observe';
9811
10607
  return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
@@ -16445,7 +17241,16 @@ async function cmdQeBridge(options, flags, cwd, write) {
16445
17241
  catch (error) {
16446
17242
  return failRun('audit-write-failed', `the review was PARSED (grade ${signoff.grade}) but its raw stdout could not be retained: ${String(error)} — an unauditable success is not a success, so the run FAILS rather than shipping a verdict nobody can re-derive`, probed, { stdout: review.stdout, stderr: review.stderr });
16447
17243
  }
16448
- const recordText = (reportWritten) => `${JSON.stringify(buildBridgeSignoffRecord(signoff, {
17244
+ // writeSequence (qe-bridge-signoff-order): diagnostic sequencing metadata — a self-reported
17245
+ // process-local trace with monotonic stamps taken at each named event (start of the first record
17246
+ // write; after the report landed; just before the atomic update). It replaces a wall-clock
17247
+ // mtime comparison that was a race (the record is rewritten AFTER the report by design). It does
17248
+ // NOT prove write order or crash safety: those are proven by the failpoint test (R4-1) and the
17249
+ // report-failure test. Lead edit after Codex review 2026-09-13: honest step names.
17250
+ const seq = [
17251
+ { step: 'signoff-write-started', monotonicNs: String(process.hrtime.bigint()) },
17252
+ ];
17253
+ const recordText = (reportWritten, writeSequence) => `${JSON.stringify(buildBridgeSignoffRecord(signoff, {
16449
17254
  runId,
16450
17255
  claudeBin: resolvedBin,
16451
17256
  binOverride,
@@ -16454,11 +17259,12 @@ async function cmdQeBridge(options, flags, cwd, write) {
16454
17259
  rawStdoutFile,
16455
17260
  promptSha256,
16456
17261
  ...(parsed.channels === undefined ? {} : { channels: parsed.channels }),
17262
+ writeSequence,
16457
17263
  }), null, 2)}\n`;
16458
17264
  let signoffPath;
16459
17265
  try {
16460
17266
  signoffPath = uniquePath(join(stateDir, `signoff-${runId}`), '.json');
16461
- writeNewFileOrThrow(signoffPath, recordText(false));
17267
+ writeNewFileOrThrow(signoffPath, recordText(false, seq));
16462
17268
  }
16463
17269
  catch (error) {
16464
17270
  return failRun('audit-write-failed', `the review was PARSED (grade ${signoff.grade}) but the signoff record could not be written: ${String(error)} — the verdict exists and cannot be persisted, so the run FAILS rather than reporting an unrecorded success`, probed, { stdout: review.stdout, stderr: review.stderr });
@@ -16471,6 +17277,10 @@ async function cmdQeBridge(options, flags, cwd, write) {
16471
17277
  reportError = error;
16472
17278
  }
16473
17279
  if (reportError === null) {
17280
+ // the report is on disk: the sequence gains a step BEFORE the `reportWritten:true` record
17281
+ // write, not after — an observer reading the eventual writeSequence must see the report step
17282
+ // land before the record-update step that persists it.
17283
+ seq.push({ step: 'report-written', monotonicNs: String(process.hrtime.bigint()) });
16474
17284
  // the ONLY moment `reportWritten:true` may appear: after the report is on disk
16475
17285
  try {
16476
17286
  // ATOMIC (R4-1): write a sibling temp file, then rename() over the original. On the same
@@ -16479,7 +17289,8 @@ async function cmdQeBridge(options, flags, cwd, write) {
16479
17289
  // which made the "a crash leaves a record that is true or pessimistic" claim untrue in the
16480
17290
  // one case it was about.
16481
17291
  const tmpPath = `${signoffPath}.tmp.${process.pid}`;
16482
- writeNewFileOrThrow(tmpPath, recordText(true));
17292
+ seq.push({ step: 'record-update-prepared', monotonicNs: String(process.hrtime.bigint()) });
17293
+ writeNewFileOrThrow(tmpPath, recordText(true, seq));
16483
17294
  if (process.env[QE_BRIDGE_FAILPOINT_ENV] === 'hang-before-rename') {
16484
17295
  // test-only: stop dead INSIDE the window, so a SIGKILL can prove the property
16485
17296
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 600_000);
@@ -19311,7 +20122,7 @@ export async function runCli(argv, io = {}) {
19311
20122
  case 'auto-canonicalize':
19312
20123
  return await cmdAutoCanonicalize(options, cwd, write);
19313
20124
  case 'publish':
19314
- return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner);
20125
+ return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner, io.publishSiblingDriftFetcher, io.publishPackedInstallRunner, io.publishExecRunner, io.publishGateAuditFsLayer, io.publishNpmPackRunner);
19315
20126
  case 'release':
19316
20127
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
19317
20128
  case 'parity':