@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/.dz-manifest.json +16 -16
- package/README.md +116 -6
- package/dist/boolean-flags.d.ts.map +1 -1
- package/dist/boolean-flags.js +2 -0
- package/dist/boolean-flags.js.map +1 -1
- package/dist/cli.d.ts +28 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +458 -12
- package/dist/cli.js.map +1 -1
- package/dist/known-flags.d.ts.map +1 -1
- package/dist/known-flags.js +2 -0
- package/dist/known-flags.js.map +1 -1
- package/package.json +27 -27
- package/sbom.json +15 -15
- package/src/boolean-flags.ts +2 -0
- package/src/cli.ts +474 -11
- package/src/known-flags.ts +2 -0
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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':
|