@dzhechkov/harness-cli 0.8.20 → 0.8.22

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
@@ -22,7 +22,7 @@ import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures,
22
22
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
23
23
  TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, 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, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, 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, 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,
24
24
  // guard-promotion (feature guard-promotion, scout idea #1)
25
- assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, 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,
25
+ 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
26
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
27
27
  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, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
28
28
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
@@ -104,7 +104,7 @@ Usage:
104
104
  dz upgrade [--target <name>] [--pubkey <path>] [--require-signing] (a TAMPERED pack aborts the upgrade)
105
105
  dz sign --pack <dir> --key <path-outside-repo> (Ed25519 manifest + CycloneDX SBOM for a pack)
106
106
  dz verify-pack --pack <dir> [--pubkey <path>] (signature check; fail-closed; key from the repo, never the pack)
107
- dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] [--require-signing] [--provenance|--no-provenance] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live; claim-check gate default warn — surfaces README claim findings, never blocks; error fails an offending package)
107
+ dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] [--mirror-cmd <cmd>|--no-mirror] [--require-signing] [--provenance|--no-provenance] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live; claim-check gate default warn — surfaces README claim findings, never blocks; error fails an offending package)
108
108
  dz release [--filter <name>] [--tag] [--publish] [--json] [--dry-run] [--no-issue] (VERIFIED release: 4 HARD gates in FRONT of dz publish — full package test suites, audit >=high, node --check of every dist/bin file, bin smoke-boot via "node <bin> --help" — any red gate STOPS the release (exit 1) + best-effort gh issue; all green ⇒ re-sign reminder, then prints the ready dz publish command (or chains with --publish); never duplicates publish's own gates)
109
109
  dz parity [--target <name>] [--json] (the honest feature×target map, COMPUTED from the capability model — which harness feature is full / manual / absent on each of the ${TARGET_NAMES.length} targets, and via which form)
110
110
  dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] (portable Step-10 Delivery Gate: prints the 4-plane review brief + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked)
@@ -2331,6 +2331,9 @@ function cmdStatusline(options, flags, cwd, write, readStdin, writeErr) {
2331
2331
  line += ` ⛔ STORE UNREADABLE${data.storeHealth.unreadableFiles !== undefined && data.storeHealth.unreadableFiles.length > 0
2332
2332
  ? `: ${data.storeHealth.unreadableFiles.join(', ')}` : ''}`;
2333
2333
  }
2334
+ else if (data.storeHealth?.verdict === 'busy') {
2335
+ line += ' ⏳ STORE BUSY';
2336
+ }
2334
2337
  else if (data.storeHealth?.verdict === 'source-changed') {
2335
2338
  line += ' ⚠ store source changed';
2336
2339
  }
@@ -2940,9 +2943,9 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
2940
2943
  const reason = teachReasonPhrase(target.reason);
2941
2944
  return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
2942
2945
  }
2943
- function inspectLearningStore(projectRoot) {
2946
+ function inspectLearningStore(projectRoot, countOptions) {
2944
2947
  const mark = readStoreMark(projectRoot);
2945
- const rows = countLearningStoreRowsReadonly(projectRoot);
2948
+ const rows = countLearningStoreRowsReadonly(projectRoot, countOptions);
2946
2949
  return { mark, rows, health: checkStoreHealth({ projectRoot, ...rows, mark }) };
2947
2950
  }
2948
2951
  function shellQuote(value) {
@@ -3014,7 +3017,8 @@ function refreshLearningStoreMark(projectRoot, writeErr, command, options = {})
3014
3017
  const rows = countLearningStoreRowsReadonly(projectRoot);
3015
3018
  const counts = observedRows(rows);
3016
3019
  if (counts === undefined) {
3017
- writeErr(`⚠ dz store guard: ${options.reader ? 'reader observation' : 'store operation'} completed but the external mark was not updated — a store tier is unreadable`);
3020
+ const busy = rows.lexicalRows === 'busy' || rows.vectorRows === 'busy';
3021
+ writeErr(`⚠ dz store guard: ${options.reader ? 'reader observation' : 'store operation'} completed but the external mark was not updated — ${busy ? 'store busy; health not measured this run' : 'a store tier is unreadable'}`);
3018
3022
  return;
3019
3023
  }
3020
3024
  writeStoreMark(projectRoot, {
@@ -3045,7 +3049,7 @@ function storeGuardResetReminder(projectRoot, writeErr, command) {
3045
3049
  function allowLearningStoreWrite(projectRoot, flags, writeErr, command) {
3046
3050
  let inspection;
3047
3051
  try {
3048
- inspection = inspectLearningStore(projectRoot);
3052
+ inspection = inspectLearningStore(projectRoot, { busyTimeoutMs: 250, attempts: 3 });
3049
3053
  }
3050
3054
  catch (error) {
3051
3055
  const health = {
@@ -3063,6 +3067,10 @@ function allowLearningStoreWrite(projectRoot, flags, writeErr, command) {
3063
3067
  // own row had landed.
3064
3068
  return true;
3065
3069
  }
3070
+ if (inspection.health.verdict === 'busy') {
3071
+ writeErr(`dz store guard: NOT MEASURED — ${inspection.health.reason}`);
3072
+ return true;
3073
+ }
3066
3074
  if (inspection.health.verdict === 'source-changed') {
3067
3075
  const counts = observedRows(inspection.rows);
3068
3076
  if (counts === undefined)
@@ -3105,6 +3113,8 @@ function warnLearningStoreRead(projectRoot, writeErr, command) {
3105
3113
  writeErr(line);
3106
3114
  return;
3107
3115
  }
3116
+ if (health.verdict === 'busy')
3117
+ return;
3108
3118
  const counts = observedRows(rows);
3109
3119
  if (counts !== undefined && counts.lexicalRows + counts.vectorRows > 0
3110
3120
  && (mark === undefined || mark.lexicalLast !== counts.lexicalRows || mark.vectorLast !== counts.vectorRows
@@ -6140,7 +6150,33 @@ function cmdSbom(options, flags, cwd, write) {
6140
6150
  write(out);
6141
6151
  return 0;
6142
6152
  }
6143
- function cmdPublish(options, flags, cwd, writeOutput) {
6153
+ function mirrorCommandFromConfig(cwd) {
6154
+ const configPath = join(cwd, '.dz', 'config.json');
6155
+ if (!existsSync(configPath))
6156
+ return {};
6157
+ try {
6158
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
6159
+ const command = typeof config.publish?.mirrorCommand === 'string' ? config.publish.mirrorCommand.trim() : '';
6160
+ return command === '' ? {} : { command };
6161
+ }
6162
+ catch (error) {
6163
+ const reason = error instanceof Error ? error.message : String(error);
6164
+ return { warning: `.dz/config.json unreadable — ${reason}` };
6165
+ }
6166
+ }
6167
+ function mirrorShellToken(value) {
6168
+ if (value === '')
6169
+ return "''";
6170
+ if (!/^[A-Za-z0-9@/.,+_-]+$/.test(value))
6171
+ throw new Error('published package/version list is not shell-safe');
6172
+ return value;
6173
+ }
6174
+ function mirrorFailureMessage(error) {
6175
+ if (error instanceof Error && error.message.trim() !== '')
6176
+ return error.message.trim().split(/\r?\n/, 1)[0] ?? 'mirror command failed';
6177
+ return String(error);
6178
+ }
6179
+ function cmdPublish(options, flags, cwd, writeOutput, mirrorRunner) {
6144
6180
  const json = flags.has('json');
6145
6181
  // Under --json stdout carries exactly one JSON document, so every human line — guard notes, refusals,
6146
6182
  // progress — goes to stderr instead of being dropped: a refusal that prints nothing is the silent
@@ -6148,9 +6184,9 @@ function cmdPublish(options, flags, cwd, writeOutput) {
6148
6184
  const write = json ? (line) => { process.stderr.write(`${line}\n`); } : writeOutput;
6149
6185
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
6150
6186
  // silently swallowed and flip the command into live-publish mode.
6151
- const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance', 'json']);
6152
- const allowedOptions = new Set(['filter', 'claim-check', 'no-guard', 'sign-key']);
6153
- const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>, --no-guard "<reason>" (skip the guard pre-flight; logged)';
6187
+ const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance', 'json', 'no-mirror']);
6188
+ const allowedOptions = new Set(['filter', 'claim-check', 'no-guard', 'sign-key', 'mirror-cmd']);
6189
+ 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)';
6154
6190
  for (const flag of flags) {
6155
6191
  if (!allowedFlags.has(flag)) {
6156
6192
  write(`dz publish: unknown option --${flag}`);
@@ -6332,7 +6368,7 @@ function cmdPublish(options, flags, cwd, writeOutput) {
6332
6368
  // longer exist. Default to the same path `dz sign --init` writes, so the ordinary operator needs no
6333
6369
  // new flag; `--sign-key` overrides it.
6334
6370
  const signKey = (options.get('sign-key') ?? join(homedir(), '.dz', 'keys', 'dz.key')).trim();
6335
- const report = publishPackages(cwd, {
6371
+ const publishReport = publishPackages(cwd, {
6336
6372
  provenance,
6337
6373
  dryRun,
6338
6374
  filter,
@@ -6414,9 +6450,82 @@ function cmdPublish(options, flags, cwd, writeOutput) {
6414
6450
  }
6415
6451
  },
6416
6452
  });
6453
+ const configMirror = mirrorCommandFromConfig(cwd);
6454
+ const configuredCommand = (options.get('mirror-cmd') ?? configMirror.command ?? '').trim();
6455
+ const publishedVersions = publishReport.packages
6456
+ .filter((pkg) => pkg.status === 'published')
6457
+ .map((pkg) => `${pkg.name}@${pkg.newVersion}`);
6458
+ const expected = publishedVersions.join(',');
6459
+ const fullMirrorCommand = configuredCommand === ''
6460
+ ? ''
6461
+ : `${configuredCommand} --expect ${mirrorShellToken(expected)} --json`;
6462
+ let mirror;
6463
+ // This exact conjunction is the Step-7/8 mutation anchor: an epilogue is eligible only after a
6464
+ // live sweep that actually landed at least one package. Other explicit skip states are handled
6465
+ // before command resolution so each reason remains distinguishable in text and JSON.
6466
+ const mirrorEligible = !dryRun && publishReport.published >= 1;
6467
+ if (bumpOnly) {
6468
+ mirror = { status: 'skipped', command: fullMirrorCommand, reason: 'bump-only' };
6469
+ }
6470
+ else if (!mirrorEligible) {
6471
+ mirror = {
6472
+ status: 'skipped',
6473
+ command: fullMirrorCommand,
6474
+ reason: dryRun ? 'dry-run' : 'published=0',
6475
+ };
6476
+ }
6477
+ else if (flags.has('no-mirror')) {
6478
+ mirror = { status: 'skipped', command: fullMirrorCommand, reason: '--no-mirror' };
6479
+ }
6480
+ else if (configMirror.warning !== undefined && options.get('mirror-cmd') === undefined) {
6481
+ mirror = { status: 'not-configured', command: '', warning: configMirror.warning };
6482
+ }
6483
+ else if (configuredCommand === '') {
6484
+ mirror = { status: 'not-configured', command: '' };
6485
+ }
6486
+ else {
6487
+ const runMirror = mirrorRunner
6488
+ ?? ((command, runnerOptions) => execSync(command, {
6489
+ cwd: runnerOptions.cwd,
6490
+ env: runnerOptions.env,
6491
+ encoding: 'utf8',
6492
+ stdio: 'pipe',
6493
+ }));
6494
+ try {
6495
+ const stdout = runMirror(fullMirrorCommand, {
6496
+ cwd,
6497
+ env: { ...process.env, DZ_PUBLISHED: expected },
6498
+ });
6499
+ const parsed = JSON.parse(stdout);
6500
+ if (parsed.ok !== true)
6501
+ throw new Error(typeof parsed.error === 'string' ? parsed.error : 'mirror command returned ok:false');
6502
+ if (typeof parsed.commit !== 'string' || parsed.commit === '')
6503
+ throw new Error('mirror command returned no commit');
6504
+ if (typeof parsed.receipt?.manifestUrl !== 'string'
6505
+ || typeof parsed.receipt.confirmedAt !== 'string'
6506
+ || typeof parsed.receipt.waitedMs !== 'number') {
6507
+ throw new Error('mirror command returned no live-manifest receipt');
6508
+ }
6509
+ mirror = {
6510
+ status: 'confirmed',
6511
+ command: fullMirrorCommand,
6512
+ commit: parsed.commit,
6513
+ receipt: {
6514
+ manifestUrl: parsed.receipt.manifestUrl,
6515
+ confirmedAt: parsed.receipt.confirmedAt,
6516
+ waitedMs: parsed.receipt.waitedMs,
6517
+ },
6518
+ };
6519
+ }
6520
+ catch (error) {
6521
+ mirror = { status: 'unconfirmed', command: fullMirrorCommand, error: mirrorFailureMessage(error) };
6522
+ }
6523
+ }
6524
+ const report = { ...publishReport, mirror };
6525
+ const exitCode = report.errors > 0 ? 1 : mirror.status === 'unconfirmed' ? 3 : 0;
6417
6526
  if (json) {
6418
6527
  writeOutput(JSON.stringify(report));
6419
- return report.errors > 0 ? 1 : 0;
6528
+ return exitCode;
6420
6529
  }
6421
6530
  write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
6422
6531
  write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
@@ -6459,7 +6568,28 @@ function cmdPublish(options, flags, cwd, writeOutput) {
6459
6568
  write(` · ${item}`);
6460
6569
  }
6461
6570
  }
6462
- return report.errors > 0 ? 1 : 0;
6571
+ for (const warning of report.warnings ?? [])
6572
+ write(` ⚠ warning: ${warning}`);
6573
+ for (const path of report.releaseLineSynced ?? [])
6574
+ write(` ↳ release line synced: ${path}`);
6575
+ if (mirror.status === 'confirmed') {
6576
+ write(` ✓ mirror: confirmed — ${mirror.commit} (${mirror.receipt?.manifestUrl})`);
6577
+ }
6578
+ else if (mirror.status === 'not-configured') {
6579
+ if (mirror.warning !== undefined)
6580
+ write(` ⚠ mirror: ${mirror.warning}`);
6581
+ else
6582
+ write(' ℹ mirror: not configured');
6583
+ }
6584
+ else if (mirror.status === 'skipped') {
6585
+ write(` ℹ mirror: skipped (${mirror.reason})`);
6586
+ }
6587
+ else {
6588
+ write(` ✗ mirror: unconfirmed — ${mirror.error}`);
6589
+ const rerun = mirror.command || 'configure publish.mirrorCommand, then run it';
6590
+ write(`dz publish: published, mirror NOT confirmed — ${mirror.error}; re-run: ${rerun}`);
6591
+ }
6592
+ return exitCode;
6463
6593
  }
6464
6594
  /* ------------------------------------------------------------------ */
6465
6595
  /* dz parity — the honest feature×target map (target-parity-matrix, */
@@ -8936,9 +9066,10 @@ function gatherGuardFacts(op, root, text, storeCap, publishFilter) {
8936
9066
  const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
8937
9067
  for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
8938
9068
  try {
8939
- const m = JSON.parse(readFileSync(join(root, rel), 'utf8'));
9069
+ const packageJsonText = readFileSync(join(root, rel), 'utf8');
9070
+ const m = JSON.parse(packageJsonText);
8940
9071
  manifests.push(m);
8941
- located.push({ dir: rel.replace(/\/package\.json$/, ''), m });
9072
+ located.push({ dir: rel.replace(/\/package\.json$/, ''), m, text: packageJsonText });
8942
9073
  }
8943
9074
  catch { /* skip unreadable */ }
8944
9075
  }
@@ -8948,6 +9079,45 @@ function gatherGuardFacts(op, root, text, storeCap, publishFilter) {
8948
9079
  for (const m of manifests)
8949
9080
  if (m.name && typeof m.version === 'string')
8950
9081
  versionByName.set(m.name, m.version);
9082
+ // release-line-in-sync: filesystem access belongs in the CLI gatherer. The core evaluator receives
9083
+ // only immutable evidence, so evaluateGuard(facts) stays deterministic and independent of cwd.
9084
+ const coreManifestPath = join(root, 'packages', '@dzhechkov', 'harness-core', 'package.json');
9085
+ const cliManifestPath = join(root, 'packages', '@dzhechkov', 'harness-cli', 'package.json');
9086
+ if (existsSync(coreManifestPath) || existsSync(cliManifestPath)) {
9087
+ const readText = (path) => {
9088
+ try {
9089
+ return readFileSync(path, 'utf8');
9090
+ }
9091
+ catch {
9092
+ return null;
9093
+ }
9094
+ };
9095
+ const readVersion = (path) => {
9096
+ const raw = readText(path);
9097
+ if (raw === null)
9098
+ return null;
9099
+ try {
9100
+ const parsed = JSON.parse(raw);
9101
+ return typeof parsed.version === 'string' && /^\d+\.\d+\.\d+$/.test(parsed.version)
9102
+ ? parsed.version
9103
+ : null;
9104
+ }
9105
+ catch {
9106
+ return null;
9107
+ }
9108
+ };
9109
+ facts['releaseLines'] = {
9110
+ coreVersion: readVersion(coreManifestPath),
9111
+ cliVersion: readVersion(cliManifestPath),
9112
+ readmes: [
9113
+ { path: 'README.md', text: readText(join(root, 'README.md')) },
9114
+ {
9115
+ path: 'packages/@dzhechkov/harness-cli/README.md',
9116
+ text: readText(join(root, 'packages', '@dzhechkov', 'harness-cli', 'README.md')),
9117
+ },
9118
+ ],
9119
+ };
9120
+ }
8951
9121
  publishPackageRoots.push(...located
8952
9122
  .filter(({ dir, m }) => m.private !== true && (publishFilter === undefined
8953
9123
  || publishFilter.length === 0
@@ -9142,25 +9312,136 @@ function gatherGuardFacts(op, root, text, storeCap, publishFilter) {
9142
9312
  }
9143
9313
  catch { /* нечитаемо — правило молчит */ }
9144
9314
  facts['counts'] = gatherReadmeCounts(root);
9145
- // readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: does the
9146
- // change set contain its package.json (the version-bump signal) without its README.md?
9315
+ // One package-scoped WORKING-TREE diff feeds both readme-first and signature-fresh. A second
9316
+ // porcelain call could observe a different tree and let the two publish guards disagree.
9317
+ let changedPackageFiles;
9318
+ // readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: did
9319
+ // the version FIELD change from HEAD without its README.md changing? A package.json edit alone
9320
+ // says nothing about version; repository/metadata-only edits must not manufacture a bump.
9147
9321
  try {
9148
- const status = execSync('git status --porcelain -- "packages/@dzhechkov/"', { cwd: root, encoding: 'utf-8' });
9322
+ const status = execSync('git status --porcelain -uall -- "packages/@dzhechkov/"', { cwd: root, encoding: 'utf-8' });
9323
+ changedPackageFiles = status
9324
+ .split('\n')
9325
+ .map((line) => line.slice(3).trim())
9326
+ .map((path) => (path.includes(' -> ') ? path.split(' -> ')[1].trim() : path))
9327
+ .filter(Boolean);
9149
9328
  const perPack = new Map();
9150
- for (const line of status.split('\n')) {
9151
- const m = line.match(/packages\/@dzhechkov\/([^/]+)\/(.+)$/);
9329
+ for (const rel of changedPackageFiles) {
9330
+ const m = rel.match(/^packages\/@dzhechkov\/([^/]+)\/(.+)$/);
9152
9331
  if (!m || !m[1] || !m[2])
9153
9332
  continue;
9154
- const e = perPack.get(m[1]) ?? { pkgJson: false, readme: false };
9155
- if (m[2] === 'package.json')
9156
- e.pkgJson = true;
9333
+ const e = perPack.get(m[1]) ?? { readme: false };
9157
9334
  if (m[2] === 'README.md')
9158
9335
  e.readme = true;
9159
9336
  perPack.set(m[1], e);
9160
9337
  }
9161
- facts['readmeFirst'] = [...perPack.entries()].map(([name, e]) => ({ name: '@dzhechkov/' + name, versionBumped: e.pkgJson, readmeChanged: e.readme }));
9338
+ const readVersion = (raw) => {
9339
+ const version = JSON.parse(raw).version;
9340
+ if (typeof version !== 'string')
9341
+ throw new Error('package version is unreadable');
9342
+ return version;
9343
+ };
9344
+ facts['readmeFirst'] = [...perPack.entries()].map(([name, e]) => {
9345
+ const packagePath = `packages/@dzhechkov/${name}/package.json`;
9346
+ let versionBumped = false;
9347
+ let versionUnknown = false;
9348
+ try {
9349
+ const treeVersion = readVersion(readFileSync(join(root, packagePath), 'utf8'));
9350
+ try {
9351
+ const headVersion = readVersion(execFileSync('git', ['show', `HEAD:${packagePath}`], {
9352
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
9353
+ }));
9354
+ versionBumped = treeVersion !== headVersion;
9355
+ }
9356
+ catch {
9357
+ try {
9358
+ const trackedAtHead = execFileSync('git', ['ls-tree', '--name-only', 'HEAD', '--', packagePath], {
9359
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
9360
+ }).trim() === packagePath;
9361
+ if (trackedAtHead)
9362
+ versionUnknown = true;
9363
+ else
9364
+ versionBumped = true;
9365
+ }
9366
+ catch {
9367
+ versionUnknown = true;
9368
+ }
9369
+ }
9370
+ }
9371
+ catch {
9372
+ versionUnknown = true;
9373
+ }
9374
+ return {
9375
+ name: '@dzhechkov/' + name,
9376
+ versionBumped,
9377
+ readmeChanged: e.readme,
9378
+ ...(versionUnknown ? { versionUnknown: true } : {}),
9379
+ };
9380
+ });
9162
9381
  }
9163
9382
  catch { /* not a git repo — rule skips */ }
9383
+ // signature-fresh: verify only signed packs whose non-signature files changed in the same
9384
+ // porcelain listing above. Manifest/SBOM churn is the remedy, not another reason to verify.
9385
+ // The trust key and every byte read here stay in the CLI; the guard core receives facts only.
9386
+ const trustRootPath = join(root, TRUST_ROOT_REL);
9387
+ if (changedPackageFiles !== undefined && existsSync(trustRootPath)) {
9388
+ try {
9389
+ const trustRootPem = readFileSync(trustRootPath, 'utf8');
9390
+ const signedPacks = [];
9391
+ for (const { dir, m, text: packageJsonText } of located) {
9392
+ const manifestPath = join(root, dir, MANIFEST_NAME);
9393
+ if (!existsSync(manifestPath))
9394
+ continue;
9395
+ const prefix = dir + '/';
9396
+ const changedPackFiles = new Set(changedPackageFiles
9397
+ .filter((rel) => rel.startsWith(prefix))
9398
+ .map((rel) => rel.slice(prefix.length))
9399
+ .filter((packRel) => packRel !== MANIFEST_NAME && packRel !== SBOM_NAME));
9400
+ const changed = changedPackFiles.size > 0;
9401
+ let ok = null;
9402
+ let failures = [];
9403
+ let note;
9404
+ if (changed) {
9405
+ try {
9406
+ const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
9407
+ const verified = verifyManifest(join(root, dir), signed, trustRootPem);
9408
+ // A source checkout contains dev files outside package.json#files. They are absent
9409
+ // from the tarball by design, so an unchanged "present but not signed" source file
9410
+ // is not evidence about signature freshness. A changed/new unsigned file stays loud.
9411
+ const relevantFailures = verified.failures.filter((failure) => failure.reason !== 'present in the pack but not signed' || changedPackFiles.has(failure.path));
9412
+ ok = verified.ok || relevantFailures.length === 0;
9413
+ if (!ok && relevantFailures.length > 0 && relevantFailures.every(({ path }) => path === 'package.json')) {
9414
+ const signedPackageHash = signed.manifest?.files?.find(({ path }) => path === 'package.json')?.sha256;
9415
+ if (typeof signedPackageHash === 'string') {
9416
+ const rewritten = rewriteWorkspaceSpecs(packageJsonText, versionByName);
9417
+ // MEASURED 2026-09-10 on keysarium/skills-meta/core: pnpm also removes only
9418
+ // prepublishOnly from the packed scripts table. Keep that I/O-adapter concern
9419
+ // outside the workspace-only core helper.
9420
+ const packedShape = JSON.parse(rewritten);
9421
+ const scripts = packedShape['scripts'];
9422
+ if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
9423
+ delete scripts['prepublishOnly'];
9424
+ }
9425
+ const rewrittenHash = hashPackBytes('package.json', Buffer.from(JSON.stringify(packedShape, null, 2) + '\n'), signed.manifest?.version);
9426
+ if (rewrittenHash === signedPackageHash) {
9427
+ ok = true;
9428
+ note = 'package.json matches after workspace rewrite';
9429
+ }
9430
+ }
9431
+ }
9432
+ failures = relevantFailures.slice(0, 3).map((failure) => `${failure.path}: ${failure.reason}`);
9433
+ }
9434
+ catch (error) {
9435
+ ok = false;
9436
+ failures = [`${MANIFEST_NAME}: ${error instanceof Error ? error.message : String(error)}`];
9437
+ }
9438
+ }
9439
+ signedPacks.push({ name: m.name ?? dir, dir, changed, ok, failures, ...(note !== undefined ? { note } : {}) });
9440
+ }
9441
+ facts['signedPacks'] = signedPacks;
9442
+ }
9443
+ catch { /* unreadable trust root — absence of evidence, so the rule is not established */ }
9444
+ }
9164
9445
  // review-round: the same WORKING-TREE diff, asked a different question — does a package that
9165
9446
  // bumps its version and changes SOURCE bring a GRADED QE report with it? Scoped to source so a
9166
9447
  // docs-only republish is never blocked (ADR-001, features/publish-needs-a-review). A throw here
@@ -17835,7 +18116,7 @@ export async function runCli(argv, io = {}) {
17835
18116
  case 'auto-canonicalize':
17836
18117
  return await cmdAutoCanonicalize(options, cwd, write);
17837
18118
  case 'publish':
17838
- return cmdPublish(options, flags, cwd, write);
18119
+ return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner);
17839
18120
  case 'release':
17840
18121
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
17841
18122
  case 'parity':