@dzhechkov/harness-cli 0.8.23 → 0.8.24

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
@@ -19,11 +19,11 @@ import { isDeepStrictEqual } from 'node:util';
19
19
  import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from '@dzhechkov/harness-core';
20
20
  import { appendRunEvent, readRunRegistry, liveParents, liveness, probePid, settleDeadRuns, planRegistryArchive, planWorktreeCleanup, renderCleanupPlan, worktreeRemovalsToApply } from '@dzhechkov/harness-core';
21
21
  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,
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, memoryBackendSourceLabel, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
23
23
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
24
24
  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
25
  // 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,
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, 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
27
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
28
28
  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
29
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
@@ -5443,7 +5443,14 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
5443
5443
  write(`║ 2. Preset: ${preset.padEnd(40)}║`);
5444
5444
  // Step 3: Run setup (hooks + memory + config)
5445
5445
  write(`║ 3. Setting up learning environment... ║`);
5446
- const memoryOpt = options.get('memory');
5446
+ const memoryOptRaw = options.get('memory');
5447
+ // FR-1/T3 (feature `setup-backend-from-config`): pass `--memory` through AS-IS — `agentdb`,
5448
+ // `jsonl`, or `undefined` — never collapsed to `undefined` on anything but agentdb. The prior
5449
+ // `memoryOpt === 'agentdb' ? 'agentdb' : undefined` made an explicit `--memory jsonl` INDISTINCT
5450
+ // from "no flag at all", so `runSetup`'s config-aware default (FR-2's downgrade path) could never
5451
+ // fire from the CLI. An unrecognised value (neither `agentdb` nor `jsonl`) still reads as
5452
+ // "no flag" — the same permissive fallback as before.
5453
+ const memoryOpt = memoryOptRaw === 'agentdb' ? 'agentdb' : memoryOptRaw === 'jsonl' ? 'jsonl' : undefined;
5447
5454
  // ADR-001 Decision 2 (feature setup-installs-apply-leg): bake THIS CLI's own installed
5448
5455
  // @dzhechkov/harness-core into the generated apply-leg hooks — the installation actually running
5449
5456
  // `dz setup` is the one a consumer's project can always reach, unlike a hard-coded npm prefix
@@ -5461,13 +5468,16 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
5461
5468
  projectRoot,
5462
5469
  target,
5463
5470
  preset,
5464
- memory: memoryOpt === 'agentdb' ? 'agentdb' : undefined,
5471
+ memory: memoryOpt,
5465
5472
  noHooks: flags.has('no-hooks'),
5466
5473
  noMemory: flags.has('no-memory'),
5467
5474
  force: flags.has('force'),
5468
5475
  installDriver: flags.has('install-driver'),
5469
5476
  coreDistDir,
5470
5477
  });
5478
+ // FR-3: name the source of the backend actually used — never left to be inferred from the flag
5479
+ // alone, since the backend may now come from `.dz/config.json` or the jsonl default.
5480
+ write(`dz setup: memory backend: ${setupResult.memoryBackend} (${memoryBackendSourceLabel(setupResult.memoryBackendSource)})`);
5471
5481
  for (const step of setupResult.steps) {
5472
5482
  const icon = step.status === 'done' ? '✓' : step.status === 'skipped' ? '○' : '✗';
5473
5483
  write(`║ ${icon} ${step.name.padEnd(25)} ${step.detail.slice(0, 20).padEnd(20)}║`);
@@ -5518,7 +5528,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
5518
5528
  // not from package presence — a skipped hook/MCP step must not let the summary claim a store
5519
5529
  // nothing writes to (audit code#3).
5520
5530
  const wiring = setupResult.steps.find((s) => s.name === 'agentdb wiring');
5521
- const backendLabel = memoryOpt === 'agentdb'
5531
+ // Keyed off the RESOLVED backend (setupResult.memoryBackend), not the raw flag: FR-1 means the
5532
+ // flag can be absent while the actual backend is still agentdb (config-sourced) — the old
5533
+ // `memoryOpt === 'agentdb'` check would have mislabeled that run as jsonl right after fixing the
5534
+ // underlying steps to keep it agentdb.
5535
+ const backendLabel = setupResult.memoryBackend === 'agentdb'
5522
5536
  ? (wiring?.status === 'done' ? 'agentdb (.dz/agentdb.db + .dz/agentdb-mcp.db, separate stores)' : `agentdb INCOMPLETE — see setup steps`)
5523
5537
  : 'sessions.jsonl + patterns.jsonl';
5524
5538
  write(`║ Learning: ${backendLabel.padEnd(41)}║`);
@@ -6208,7 +6222,21 @@ function mirrorFailureMessage(error) {
6208
6222
  return error.message.trim().split(/\r?\n/, 1)[0] ?? 'mirror command failed';
6209
6223
  return String(error);
6210
6224
  }
6211
- function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6225
+ /**
6226
+ * Scratch root for the packed-install smoke's pack/install dirs (feature
6227
+ * publish-sibling-drift-gate). MEASURED 2026-09-13: npm resolves a LOCAL tarball path (`npm
6228
+ * install <path-to.tgz>`) relative to `os.tmpdir()` — not to cwd — whenever that path sits
6229
+ * INSIDE `os.tmpdir()`, and does the same for the install dir; put pack and install dirs both
6230
+ * under `tmpdir()` and the recorded `file:` spec loses its `tmpdir()` prefix entirely (reproducer:
6231
+ * a fresh `npm pack <src> --pack-destination "$T/pack"` + `cd "$T/install" && npm install
6232
+ * "$T/pack/x.tgz"` with `$T` under `/tmp` silently installs NOTHING — "changed 1 package", empty
6233
+ * node_modules, `reify moves {}` in `--loglevel silly`; the identical commands under `/var/tmp`
6234
+ * install correctly). A directory outside `os.tmpdir()` sidesteps the quirk entirely.
6235
+ */
6236
+ function packedInstallScratchRoot() {
6237
+ return existsSync('/var/tmp') ? '/var/tmp' : tmpdir();
6238
+ }
6239
+ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner, siblingDriftFetcher, packedInstallRunner, publishExecRunner) {
6212
6240
  const json = flags.has('json');
6213
6241
  // Under --json stdout carries exactly one JSON document, so every human line — guard notes, refusals,
6214
6242
  // progress — goes to stderr instead of being dropped: a refusal that prints nothing is the silent
@@ -6216,9 +6244,9 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6216
6244
  const write = json ? (line) => { process.stderr.write(`${line}\n`); } : writeOutput;
6217
6245
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
6218
6246
  // 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']);
6247
+ 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
6248
  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)';
6249
+ 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
6250
  for (const flag of flags) {
6223
6251
  if (!allowedFlags.has(flag)) {
6224
6252
  write(`dz publish: unknown option --${flag}`);
@@ -6308,8 +6336,282 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6308
6336
  const bumpOnly = flags.has('bump-only');
6309
6337
  // SAFETY: dry-run is the DEFAULT. A real publish requires an EXPLICIT opt-in
6310
6338
  // via --yes, --confirm, or --no-dry-run. Without one, we never bump or publish.
6339
+ // Computed HERE (moved up from below the gates, AM-5) so both gates can see it: a dry run keeps
6340
+ // previewing packed-install with the CURRENT pre-bump tarball (nothing to compare a LIVE publish
6341
+ // against yet), while a live run defers the real packed-install-smoke into `publishPackages`'s
6342
+ // `packedTransport` — the one that tests the ACTUAL bytes about to ship (AM-1).
6311
6343
  const wantsLive = flags.has('yes') || flags.has('confirm') || flags.has('no-dry-run');
6312
6344
  const dryRun = !wantsLive;
6345
+ // ── FR-1..FR-4 — sibling-drift gate, then packed-install smoke (feature
6346
+ // publish-sibling-drift-gate, ADR-001). The sibling-drift gate runs before the signature gate
6347
+ // and the live-publish banner (so a --include-drifted-expanded batch is checked and shown too).
6348
+ // AM-5: on a DRY RUN both gates always print their verdict, even once sibling-drift already
6349
+ // blocks — the whole point of a preview is full information before anything ships. On a LIVE
6350
+ // run, sibling-drift still refuses immediately (packing/installing a doomed batch wastes real
6351
+ // time); its own packed-install smoke is deferred into `publishPackages`'s `packedTransport`
6352
+ // (AM-1) — the one gate that tests the tarball bytes actually handed to `npm publish`.
6353
+ const allowSiblingDrift = flags.has('allow-sibling-drift');
6354
+ const includeDrifted = flags.has('include-drifted');
6355
+ const allPackages = discoverPackages(cwd);
6356
+ const workspaceVersions = new Map(allPackages.map((p) => [p.name, p.version]));
6357
+ const workspaceDirs = new Map(allPackages.map((p) => [p.name, p.dir]));
6358
+ const matchesFilter = (pk) => filter === undefined || filter.length === 0 || filter.some((f) => pk.name.includes(f) || pk.dir.includes(f));
6359
+ let targets = allPackages.filter(matchesFilter);
6360
+ let batchNames = new Set(targets.map((p) => p.name));
6361
+ // Production default: `npm pack <name>@<version>` into a temp dir, extracted. Tests inject a
6362
+ // local directory (ADR-001, "fetchPublished … в тестах — локальный каталог").
6363
+ const fetchPublished = siblingDriftFetcher ??
6364
+ ((name, version) => {
6365
+ try {
6366
+ const tmp = mkdtempSync(join(tmpdir(), 'dz-sibling-drift-'));
6367
+ execSync(`npm pack ${name}@${version} --pack-destination ${JSON.stringify(tmp)}`, {
6368
+ stdio: 'pipe',
6369
+ encoding: 'utf-8',
6370
+ timeout: 60_000,
6371
+ });
6372
+ const tarball = readdirSync(tmp).find((f) => f.endsWith('.tgz'));
6373
+ if (tarball === undefined)
6374
+ return null;
6375
+ execSync(`tar -xzf ${JSON.stringify(join(tmp, tarball))} -C ${JSON.stringify(tmp)}`, { stdio: 'pipe', timeout: 60_000 });
6376
+ return { dir: join(tmp, 'package') };
6377
+ }
6378
+ catch {
6379
+ return null;
6380
+ }
6381
+ });
6382
+ // AM-6: an override (--allow-sibling-drift) is only real once its audit row is DURABLE. A write
6383
+ // failure must refuse the publish rather than print "(logged)" about a log entry that never
6384
+ // landed — the same "absence of a receipt is not success" lesson the registry-probe gate already
6385
+ // enforces for a publish's own confirmation.
6386
+ const auditedOverride = (detail, humanMessage, pkgNameForBlock) => {
6387
+ const wrote = appendPublishGateAudit(cwd, 'sibling-drift', 'warn', detail, '--allow-sibling-drift');
6388
+ if (wrote) {
6389
+ write(`dz publish: ⚠ ${humanMessage} — allowed via --allow-sibling-drift (logged)`);
6390
+ return false;
6391
+ }
6392
+ write(`dz publish: BLOCKED ${pkgNameForBlock} — ${humanMessage}, and the override could not be recorded (audit write failed); refusing rather than proceeding unlogged`);
6393
+ return true;
6394
+ };
6395
+ let driftBlocked = 0;
6396
+ const extraBatch = new Set();
6397
+ // AM-2: --include-drifted must reach a FIXED POINT over transitive drifted siblings — a sibling
6398
+ // folded into the batch can itself depend on a drifted sibling outside it, and the round-1 review
6399
+ // (finding 2) showed the single pass never re-checked an EXPANDED batch's own new edges. Capped at
6400
+ // `allPackages.length + 1` rounds (the plan's own "цикл с потолком = число пакетов").
6401
+ const maxRounds = allPackages.length + 1;
6402
+ for (let round = 0; round < maxRounds; round++) {
6403
+ let addedThisRound = false;
6404
+ for (const pk of targets) {
6405
+ let manifestObj;
6406
+ try {
6407
+ manifestObj = JSON.parse(readFileSync(join(pk.dir, 'package.json'), 'utf-8'));
6408
+ }
6409
+ catch (err) {
6410
+ // AM-3: an unreadable/invalid package.json for a BATCH package is an input this HARD gate
6411
+ // cannot build — it must BLOCK, never silently degrade to "no dependencies" (which used to
6412
+ // read as a clean n/a).
6413
+ const reason = `package.json unreadable/invalid (${err.message.split('\n')[0]})`;
6414
+ if (allowSiblingDrift) {
6415
+ if (auditedOverride(`${pk.name}: ${reason}`, `sibling drift check unavailable for ${pk.name} (${reason})`, pk.name))
6416
+ driftBlocked++;
6417
+ }
6418
+ else {
6419
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${reason}); add --allow-sibling-drift to override (logged) or fix the manifest`);
6420
+ driftBlocked++;
6421
+ }
6422
+ continue;
6423
+ }
6424
+ const deps = manifestObj?.dependencies ?? {};
6425
+ const peerDeps = manifestObj?.peerDependencies ?? {};
6426
+ const optionalDeps = manifestObj?.optionalDependencies ?? {};
6427
+ // AM-6: a package with no workspace: dependency at all is n/a for THIS gate — recorded as a
6428
+ // pass note, not silence (FR-6 compatibility: output stays unchanged for such a batch).
6429
+ const anyWorkspaceDep = [...Object.values(deps), ...Object.values(peerDeps), ...Object.values(optionalDeps)]
6430
+ .some((spec) => String(spec).startsWith('workspace:'));
6431
+ if (!anyWorkspaceDep) {
6432
+ appendPublishGateAudit(cwd, 'sibling-drift', 'pass', `${pk.name}: n/a — no workspace: dependency declared`);
6433
+ continue;
6434
+ }
6435
+ const drifts = detectSiblingDrift({
6436
+ dependencies: deps,
6437
+ peerDependencies: peerDeps,
6438
+ optionalDependencies: optionalDeps,
6439
+ workspaceVersions,
6440
+ workspaceDirs,
6441
+ batch: batchNames,
6442
+ fetchPublished,
6443
+ });
6444
+ for (const r of drifts) {
6445
+ if (r.status === 'same') {
6446
+ appendPublishGateAudit(cwd, 'sibling-drift', 'pass', `${r.name}@${r.version} = workspace (dependent: ${pk.name})`);
6447
+ write(`dz publish: ✓ sibling drift: none (${r.name}@${r.version} = workspace)`);
6448
+ }
6449
+ else if (r.status === 'unavailable') {
6450
+ if (allowSiblingDrift) {
6451
+ if (auditedOverride(`${r.name}@${r.version}: ${r.reason}`, `sibling drift check unavailable for ${r.name}@${r.version} (${r.reason})`, pk.name))
6452
+ driftBlocked++;
6453
+ }
6454
+ else {
6455
+ write(`dz publish: BLOCKED ${pk.name} — sibling drift check unavailable (${r.reason}); add --allow-sibling-drift to override (logged) or check network/registry access`);
6456
+ driftBlocked++;
6457
+ }
6458
+ }
6459
+ else if (includeDrifted) {
6460
+ if (!batchNames.has(r.name) && !extraBatch.has(r.name)) {
6461
+ extraBatch.add(r.name);
6462
+ addedThisRound = true;
6463
+ 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(', ')})` : ''}`);
6464
+ }
6465
+ }
6466
+ else if (allowSiblingDrift) {
6467
+ 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))
6468
+ driftBlocked++;
6469
+ }
6470
+ else {
6471
+ appendPublishGateAudit(cwd, 'sibling-drift', 'block', `${pk.name} depends on ${r.name}@${r.version}; ${r.changedFiles.length} file(s) differ from the workspace`);
6472
+ const suggestFilter = filterStr !== undefined ? `${filterStr},${r.name}` : `${pk.name},${r.name}`;
6473
+ 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`);
6474
+ driftBlocked++;
6475
+ }
6476
+ }
6477
+ }
6478
+ if (driftBlocked > 0)
6479
+ break; // nothing to expand into a run that already refuses
6480
+ if (!includeDrifted || !addedThisRound)
6481
+ break; // no auto-expand requested, or fixed point reached
6482
+ // FR-4: --include-drifted folds the drifted sibling(s) into the batch — they bump patch like
6483
+ // any other package in `publishPackages`' own (unchanged) bump logic. Re-loop: the newly
6484
+ // folded-in sibling(s) may themselves depend on a drifted sibling outside the (now bigger) batch.
6485
+ filter = filter === undefined ? [...batchNames, ...extraBatch] : [...filter, ...extraBatch];
6486
+ targets = allPackages.filter(matchesFilter);
6487
+ batchNames = new Set(targets.map((p) => p.name));
6488
+ }
6489
+ const siblingDriftFailed = driftBlocked > 0;
6490
+ if (siblingDriftFailed && !dryRun) {
6491
+ write(`dz publish: refusing to publish (${driftBlocked} sibling-drift violation(s))`);
6492
+ return 1;
6493
+ }
6494
+ // FR-3 — packed-install smoke: pack the WHOLE (possibly --include-drifted-expanded) batch,
6495
+ // install every tarball together in a CLEAN dir (out-of-batch siblings resolve from the
6496
+ // registry, exactly like a fresh user's install), then boot every bin with --version.
6497
+ // "n/a" (FR-6) when nothing in the batch has a bin. AM-8: a bin is collected here whether or not
6498
+ // its target file exists YET — a manifest that declares one but ships nothing must BLOCK after a
6499
+ // real install, never silently vanish from the plan (which used to read as n/a, or even skip the
6500
+ // whole gate when it was the batch's only bin).
6501
+ const bins = [];
6502
+ for (const pk of targets) {
6503
+ let manifest = {};
6504
+ try {
6505
+ manifest = JSON.parse(readFileSync(join(pk.dir, 'package.json'), 'utf-8'));
6506
+ }
6507
+ catch { /* no bin info available */ }
6508
+ if (typeof manifest.bin === 'string') {
6509
+ bins.push({ pkg: pk.name, binName: pk.name.split('/').pop() ?? pk.name, relPath: manifest.bin.replace(/^\.\//, '') });
6510
+ }
6511
+ else if (manifest.bin !== undefined && manifest.bin !== null && typeof manifest.bin === 'object') {
6512
+ for (const [name, relRaw] of Object.entries(manifest.bin)) {
6513
+ bins.push({ pkg: pk.name, binName: name, relPath: String(relRaw).replace(/^\.\//, '') });
6514
+ }
6515
+ }
6516
+ }
6517
+ // AM-1/AM-5: the packed-install-smoke PREVIEW below runs on a DRY RUN only, against whatever is
6518
+ // CURRENTLY on disk (pre-bump) — it cannot be the "same bytes that ship" gate AM-1 requires,
6519
+ // because a dry run never bumps/builds/packs anything real to compare against. On a LIVE run the
6520
+ // real gate is `packedTransport` (wired at the `publishPackages` call below), which packs ONCE
6521
+ // post-bump and smokes exactly those tarballs — this preview is skipped entirely then, so its
6522
+ // digest is never confused with the one that actually ships.
6523
+ let packedInstallSmokePreviewFailed = false;
6524
+ if (dryRun) {
6525
+ if (bins.length === 0) {
6526
+ appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin');
6527
+ write('dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)');
6528
+ }
6529
+ else {
6530
+ const scratchRoot = packedInstallScratchRoot();
6531
+ const packDir = mkdtempSync(join(scratchRoot, 'dz-publish-pack-'));
6532
+ const installDir = mkdtempSync(join(scratchRoot, 'dz-publish-install-'));
6533
+ const runSmoke = packedInstallRunner ??
6534
+ ((cmd, o) => {
6535
+ try {
6536
+ const stdout = execSync(cmd, { cwd: o.cwd, stdio: 'pipe', encoding: 'utf-8', timeout: o.timeoutMs });
6537
+ return { exitCode: 0, stdout: stdout == null ? '' : String(stdout), stderr: '' };
6538
+ }
6539
+ catch (err) {
6540
+ const e = err;
6541
+ const timedOut = (e.status === null || e.status === undefined) && (e.signal != null || e.killed === true);
6542
+ return {
6543
+ exitCode: typeof e.status === 'number' ? e.status : 1,
6544
+ stdout: e.stdout == null ? '' : String(e.stdout),
6545
+ stderr: e.stderr == null || String(e.stderr).trim() === '' ? formatPublishError(e) : String(e.stderr),
6546
+ timedOut,
6547
+ };
6548
+ }
6549
+ });
6550
+ const smokePlan = planPackedInstallSmoke({
6551
+ packages: targets.map((p) => ({ name: p.name, dir: p.dir, version: p.version })),
6552
+ bins,
6553
+ packDir,
6554
+ installDir,
6555
+ });
6556
+ const smokeExecutions = [];
6557
+ // Lead edit after the live dry-run (13.09 12:05): the preview packed the WORKING directory with
6558
+ // `workspace:^` specs still inside, so `npm install <tgz>` died with EUNSUPPORTEDPROTOCOL — the
6559
+ // preview must stage package.json exactly as the live packedTransport does (sibling pins via
6560
+ // rewriteWorkspaceSpecs, prepublishOnly dropped) and restore the originals afterwards.
6561
+ const stagedOriginals = [];
6562
+ try {
6563
+ for (const p of targets) {
6564
+ const pkgJsonPath = join(p.dir, 'package.json');
6565
+ const original = readFileSync(pkgJsonPath, 'utf-8');
6566
+ const rewritten = JSON.parse(rewriteWorkspaceSpecs(original, workspaceVersions));
6567
+ const scripts = rewritten['scripts'];
6568
+ if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts))
6569
+ delete scripts['prepublishOnly'];
6570
+ stagedOriginals.push({ path: pkgJsonPath, text: original });
6571
+ writeFileSync(pkgJsonPath, JSON.stringify(rewritten, null, 2) + '\n');
6572
+ }
6573
+ for (const step of smokePlan.steps) {
6574
+ const r = runSmoke(step.cmd, { cwd: step.cwd, timeoutMs: step.timeoutMs });
6575
+ smokeExecutions.push({ stepId: step.id, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, ...(r.timedOut !== undefined ? { timedOut: r.timedOut } : {}) });
6576
+ }
6577
+ }
6578
+ finally {
6579
+ for (const o of stagedOriginals) {
6580
+ try {
6581
+ writeFileSync(o.path, o.text);
6582
+ }
6583
+ catch (err) {
6584
+ write(`dz publish: ⚠ could not restore ${o.path} after the preview smoke: ${formatPublishError(err)}`);
6585
+ }
6586
+ }
6587
+ }
6588
+ const smokeVerdict = judgePackedInstallSmoke(smokePlan, smokeExecutions);
6589
+ try {
6590
+ rmSync(packDir, { recursive: true, force: true });
6591
+ }
6592
+ catch { /* best-effort cleanup */ }
6593
+ try {
6594
+ rmSync(installDir, { recursive: true, force: true });
6595
+ }
6596
+ catch { /* best-effort cleanup */ }
6597
+ if (smokeVerdict.ok) {
6598
+ appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'preview: pack/install/--version all clean');
6599
+ write('dz publish: ✓ packed install smoke (preview)');
6600
+ }
6601
+ else {
6602
+ const detail = smokeVerdict.failureDetail ?? smokeVerdict.bins.find((b) => !b.ok)?.detail ?? '(no detail)';
6603
+ appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail);
6604
+ write(`dz publish: BLOCKED — packed install smoke failed (preview): ${detail}`);
6605
+ for (const b of smokeVerdict.bins.filter((b) => !b.ok))
6606
+ write(` ✗ ${b.pkg} (${b.binName}): ${b.detail ?? '(no detail)'}`);
6607
+ packedInstallSmokePreviewFailed = true;
6608
+ }
6609
+ }
6610
+ }
6611
+ if (siblingDriftFailed || packedInstallSmokePreviewFailed) {
6612
+ write(`dz publish: refusing to publish (${driftBlocked} sibling-drift violation(s)${packedInstallSmokePreviewFailed ? ', packed install smoke failed' : ''})`);
6613
+ return 1;
6614
+ }
6313
6615
  if (!dryRun) {
6314
6616
  // Loud confirmation banner listing exactly what is about to be published.
6315
6617
  const targets = discoverPackages(cwd).filter((p) => filter === undefined || filter.length === 0
@@ -6400,12 +6702,82 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6400
6702
  // longer exist. Default to the same path `dz sign --init` writes, so the ordinary operator needs no
6401
6703
  // new flag; `--sign-key` overrides it.
6402
6704
  const signKey = (options.get('sign-key') ?? join(homedir(), '.dz', 'keys', 'dz.key')).trim();
6705
+ // AM-1: the packedTransport smoke closure and the actual `npm publish <tgz>` inside
6706
+ // `publishPackages` both read from THIS SAME directory — created once, cleaned up once, after
6707
+ // publishPackages returns (it needs the tarballs on disk through its own publish step).
6708
+ const packedTransportPackDestDir = mkdtempSync(join(packedInstallScratchRoot(), 'dz-publish-packed-'));
6403
6709
  const publishReport = publishPackages(cwd, {
6404
6710
  provenance,
6405
6711
  dryRun,
6406
6712
  filter,
6407
6713
  bumpOnly,
6408
6714
  claimGate: claimCheckOpt,
6715
+ exec: publishExecRunner,
6716
+ packedTransport: {
6717
+ packDestDir: packedTransportPackDestDir,
6718
+ // AM-1: judged ONCE, over every package's packed artifact — nothing in the batch publishes
6719
+ // until this returns ok:true. `bins` (AM-8-fixed: declared bins are collected whether or not
6720
+ // their target file exists yet) was already computed above from the same `targets` this
6721
+ // batch resolves to.
6722
+ smoke: (artifacts) => {
6723
+ for (const a of artifacts)
6724
+ write(`dz publish: tarball ${a.name}@${a.newVersion} sha256:${a.sha256}`);
6725
+ if (bins.length === 0) {
6726
+ appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'n/a — nothing in the batch declares a bin');
6727
+ write('dz publish: ○ packed install smoke: n/a (nothing in the batch declares a bin)');
6728
+ return { ok: true };
6729
+ }
6730
+ const scratchRoot = packedInstallScratchRoot();
6731
+ const installDir = mkdtempSync(join(scratchRoot, 'dz-publish-install-'));
6732
+ const runSmoke = packedInstallRunner ??
6733
+ ((cmd, o) => {
6734
+ try {
6735
+ const stdout = execSync(cmd, { cwd: o.cwd, stdio: 'pipe', encoding: 'utf-8', timeout: o.timeoutMs });
6736
+ return { exitCode: 0, stdout: stdout == null ? '' : String(stdout), stderr: '' };
6737
+ }
6738
+ catch (err) {
6739
+ const e = err;
6740
+ const timedOut = (e.status === null || e.status === undefined) && (e.signal != null || e.killed === true);
6741
+ return {
6742
+ exitCode: typeof e.status === 'number' ? e.status : 1,
6743
+ stdout: e.stdout == null ? '' : String(e.stdout),
6744
+ stderr: e.stderr == null || String(e.stderr).trim() === '' ? formatPublishError(e) : String(e.stderr),
6745
+ timedOut,
6746
+ };
6747
+ }
6748
+ });
6749
+ const smokePlan = planPackedInstallSmoke({
6750
+ // skipPack (AM-1): these tarballs are ALREADY packed (by publishPackages, above) — a
6751
+ // second, different pack here would smoke bytes other than the ones about to publish.
6752
+ packages: artifacts.map((a) => ({ name: a.name, dir: '(packed already — see skipPack)', version: a.newVersion })),
6753
+ bins,
6754
+ packDir: packedTransportPackDestDir,
6755
+ installDir,
6756
+ skipPack: true,
6757
+ });
6758
+ const smokeExecutions = [];
6759
+ for (const step of smokePlan.steps) {
6760
+ const r = runSmoke(step.cmd, { cwd: step.cwd, timeoutMs: step.timeoutMs });
6761
+ smokeExecutions.push({ stepId: step.id, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, ...(r.timedOut !== undefined ? { timedOut: r.timedOut } : {}) });
6762
+ }
6763
+ const verdict = judgePackedInstallSmoke(smokePlan, smokeExecutions);
6764
+ try {
6765
+ rmSync(installDir, { recursive: true, force: true });
6766
+ }
6767
+ catch { /* best-effort cleanup */ }
6768
+ if (verdict.ok) {
6769
+ appendPublishGateAudit(cwd, 'packed-install-smoke', 'pass', 'pack/install/--version all clean (live, packedTransport)');
6770
+ write('dz publish: ✓ packed install smoke');
6771
+ return { ok: true };
6772
+ }
6773
+ const detail = verdict.failureDetail ?? verdict.bins.find((b) => !b.ok)?.detail ?? '(no detail)';
6774
+ appendPublishGateAudit(cwd, 'packed-install-smoke', 'block', detail);
6775
+ write(`dz publish: BLOCKED — packed install smoke failed: ${detail}`);
6776
+ for (const b of verdict.bins.filter((b) => !b.ok))
6777
+ write(` ✗ ${b.pkg} (${b.binName}): ${b.detail ?? '(no detail)'}`);
6778
+ return { ok: false, reason: detail };
6779
+ },
6780
+ },
6409
6781
  signKey: signKey === '' ? undefined : resolve(cwd, signKey),
6410
6782
  verifyAfterSign: (packDir) => {
6411
6783
  // Verify the OUTCOME against the trust root a CONSUMER would use — an existing key may be the
@@ -6482,6 +6854,10 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6482
6854
  }
6483
6855
  },
6484
6856
  });
6857
+ try {
6858
+ rmSync(packedTransportPackDestDir, { recursive: true, force: true });
6859
+ }
6860
+ catch { /* best-effort cleanup */ }
6485
6861
  const configMirror = mirrorCommandFromConfig(cwd);
6486
6862
  const configuredCommand = (options.get('mirror-cmd') ?? configMirror.command ?? '').trim();
6487
6863
  const publishedVersions = publishReport.packages
@@ -6570,6 +6946,10 @@ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6570
6946
  ? ` (confirmed by registry after ${pkg.registryProbes} probes)`
6571
6947
  : '';
6572
6948
  write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} → ${pkg.newVersion} ${pkg.status}${receipt}${detail}`);
6949
+ // AM-1: the digest of the EXACT tarball bytes that were smoke-tested AND published — present
6950
+ // only for a packedTransport publish, so "the smoke tested what shipped" is checkable here too.
6951
+ if (pkg.status === 'published' && pkg.sha256 !== undefined)
6952
+ write(` sha256:${pkg.sha256}`);
6573
6953
  if (pkg.status === 'error' && pkg.error) {
6574
6954
  for (const line of pkg.error.split('\n'))
6575
6955
  write(` ${line}`);
@@ -6916,13 +7296,36 @@ function cmdRelease(options, flags, cwd, write, runner) {
6916
7296
  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
7297
  factsList = selected;
6918
7298
  }
7299
+ // FR-6 (feature publish-sibling-drift-gate): real tmp dirs for the packed-install smoke — only
7300
+ // when something in the set actually has a bin to boot (packing bin-less siblings proves
7301
+ // nothing this gate exists to catch). Planning stays pure (planReleaseGates never mkdtemps
7302
+ // itself); these are cleaned up on every exit path below, dry-run included.
7303
+ const packedInstallEligible = factsList.some((f) => f.bins.some((b) => b.exists));
7304
+ const releaseScratchRoot = packedInstallScratchRoot();
7305
+ const packedInstallDirs = packedInstallEligible
7306
+ ? { packDir: mkdtempSync(join(releaseScratchRoot, 'dz-release-pack-')), installDir: mkdtempSync(join(releaseScratchRoot, 'dz-release-install-')) }
7307
+ : undefined;
7308
+ const cleanupPackedInstallDirs = () => {
7309
+ if (packedInstallDirs === undefined)
7310
+ return;
7311
+ try {
7312
+ rmSync(packedInstallDirs.packDir, { recursive: true, force: true });
7313
+ }
7314
+ catch { /* best-effort cleanup */ }
7315
+ try {
7316
+ rmSync(packedInstallDirs.installDir, { recursive: true, force: true });
7317
+ }
7318
+ catch { /* best-effort cleanup */ }
7319
+ };
6919
7320
  const plan = planReleaseGates(factsList, {
6920
7321
  monorepoRoot: cwd,
6921
7322
  pnpmLockPresent: existsSync(join(cwd, 'pnpm-lock.yaml')),
6922
7323
  includeDevDeps: flags.has('audit-dev'),
7324
+ packedInstall: packedInstallDirs,
6923
7325
  });
6924
7326
  // --dry-run: print the full plan, execute NOTHING (deterministic, byte-testable preview).
6925
7327
  if (flags.has('dry-run')) {
7328
+ cleanupPackedInstallDirs();
6926
7329
  if (json) {
6927
7330
  write(JSON.stringify({ dryRun: true, packages: plan.packages, steps: plan.steps, skips: plan.skips, warnings }, null, 2));
6928
7331
  return 0;
@@ -6974,6 +7377,7 @@ function cmdRelease(options, flags, cwd, write, runner) {
6974
7377
  }
6975
7378
  catch { /* best-effort cleanup */ }
6976
7379
  }
7380
+ cleanupPackedInstallDirs();
6977
7381
  const verdict = classifyGateExecutions(plan, executions);
6978
7382
  // Report: per-gate ✓/✗/○ with package granularity + timestamp (NFR-5). Skips are named,
6979
7383
  // never folded into pass wording (AM-2: "N passed, M skipped", not "all tests passed").
@@ -9806,6 +10210,33 @@ function runGuardEvaluation(root, op, text, overrideReason, publishFilter) {
9806
10210
  catch { /* audit is best-effort, never blocks the verdict */ }
9807
10211
  return result;
9808
10212
  }
10213
+ /**
10214
+ * Feature `publish-sibling-drift-gate` (FR-5/AM-6): both the sibling-drift and packed-install-smoke
10215
+ * gates write to the SAME append-only, hash-chained `.dz/guard-audit.jsonl` the declarative
10216
+ * `dz guard` rules use — visibility for `dz guard promote`/`dz compounding` never depends on
10217
+ * which mechanism produced the finding. `pass` records go through as an informational `note`
10218
+ * (never a violation, so they can never flip the row's own verdict) so a clean check is ALSO on
10219
+ * the record, not just a block or an override (AM-6: "аудит без записи = не аудит").
10220
+ *
10221
+ * Returns whether the write actually landed. Most callers are best-effort (a write failure never
10222
+ * blocks a verdict already decided) — the one exception is an `--allow-sibling-drift` OVERRIDE,
10223
+ * whose caller MUST check this return value: an override is not real without a durable row behind
10224
+ * it (AM-6's load-bearing property — see `auditedOverride` in `cmdPublish`).
10225
+ */
10226
+ function appendPublishGateAudit(root, rule, verdict, detail, overrideReason) {
10227
+ try {
10228
+ const rec = auditRecord(verdict === 'pass'
10229
+ ? { op: 'publish', verdict, violations: [], checked: [rule], notEstablished: [], notes: [`${rule}: ${detail}`] }
10230
+ : { op: 'publish', verdict, violations: [{ rule, severity: 'hard', detail }], checked: [rule], notEstablished: [] }, new Date().toISOString(), overrideReason !== undefined ? { reason: overrideReason } : undefined);
10231
+ mkdirSync(join(root, '.dz'), { recursive: true });
10232
+ const auditPath = join(root, '.dz', 'guard-audit.jsonl');
10233
+ writeFileSync(auditPath, appendChainedLines([rec], readLogTail(auditPath)), { flag: 'a' });
10234
+ return true;
10235
+ }
10236
+ catch {
10237
+ return false; // audit write failed — the caller decides whether that itself is refusable (AM-6)
10238
+ }
10239
+ }
9809
10240
  function renderGuardObservation(observation) {
9810
10241
  const tag = observation.status === 'unknown' ? 'note' : 'observe';
9811
10242
  return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
@@ -16445,7 +16876,16 @@ async function cmdQeBridge(options, flags, cwd, write) {
16445
16876
  catch (error) {
16446
16877
  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
16878
  }
16448
- const recordText = (reportWritten) => `${JSON.stringify(buildBridgeSignoffRecord(signoff, {
16879
+ // writeSequence (qe-bridge-signoff-order): diagnostic sequencing metadata — a self-reported
16880
+ // process-local trace with monotonic stamps taken at each named event (start of the first record
16881
+ // write; after the report landed; just before the atomic update). It replaces a wall-clock
16882
+ // mtime comparison that was a race (the record is rewritten AFTER the report by design). It does
16883
+ // NOT prove write order or crash safety: those are proven by the failpoint test (R4-1) and the
16884
+ // report-failure test. Lead edit after Codex review 2026-09-13: honest step names.
16885
+ const seq = [
16886
+ { step: 'signoff-write-started', monotonicNs: String(process.hrtime.bigint()) },
16887
+ ];
16888
+ const recordText = (reportWritten, writeSequence) => `${JSON.stringify(buildBridgeSignoffRecord(signoff, {
16449
16889
  runId,
16450
16890
  claudeBin: resolvedBin,
16451
16891
  binOverride,
@@ -16454,11 +16894,12 @@ async function cmdQeBridge(options, flags, cwd, write) {
16454
16894
  rawStdoutFile,
16455
16895
  promptSha256,
16456
16896
  ...(parsed.channels === undefined ? {} : { channels: parsed.channels }),
16897
+ writeSequence,
16457
16898
  }), null, 2)}\n`;
16458
16899
  let signoffPath;
16459
16900
  try {
16460
16901
  signoffPath = uniquePath(join(stateDir, `signoff-${runId}`), '.json');
16461
- writeNewFileOrThrow(signoffPath, recordText(false));
16902
+ writeNewFileOrThrow(signoffPath, recordText(false, seq));
16462
16903
  }
16463
16904
  catch (error) {
16464
16905
  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 +16912,10 @@ async function cmdQeBridge(options, flags, cwd, write) {
16471
16912
  reportError = error;
16472
16913
  }
16473
16914
  if (reportError === null) {
16915
+ // the report is on disk: the sequence gains a step BEFORE the `reportWritten:true` record
16916
+ // write, not after — an observer reading the eventual writeSequence must see the report step
16917
+ // land before the record-update step that persists it.
16918
+ seq.push({ step: 'report-written', monotonicNs: String(process.hrtime.bigint()) });
16474
16919
  // the ONLY moment `reportWritten:true` may appear: after the report is on disk
16475
16920
  try {
16476
16921
  // ATOMIC (R4-1): write a sibling temp file, then rename() over the original. On the same
@@ -16479,7 +16924,8 @@ async function cmdQeBridge(options, flags, cwd, write) {
16479
16924
  // which made the "a crash leaves a record that is true or pessimistic" claim untrue in the
16480
16925
  // one case it was about.
16481
16926
  const tmpPath = `${signoffPath}.tmp.${process.pid}`;
16482
- writeNewFileOrThrow(tmpPath, recordText(true));
16927
+ seq.push({ step: 'record-update-prepared', monotonicNs: String(process.hrtime.bigint()) });
16928
+ writeNewFileOrThrow(tmpPath, recordText(true, seq));
16483
16929
  if (process.env[QE_BRIDGE_FAILPOINT_ENV] === 'hang-before-rename') {
16484
16930
  // test-only: stop dead INSIDE the window, so a SIGKILL can prove the property
16485
16931
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 600_000);
@@ -19311,7 +19757,7 @@ export async function runCli(argv, io = {}) {
19311
19757
  case 'auto-canonicalize':
19312
19758
  return await cmdAutoCanonicalize(options, cwd, write);
19313
19759
  case 'publish':
19314
- return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner);
19760
+ return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner, io.publishSiblingDriftFetcher, io.publishPackedInstallRunner, io.publishExecRunner);
19315
19761
  case 'release':
19316
19762
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
19317
19763
  case 'parity':