@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/src/cli.ts CHANGED
@@ -279,6 +279,8 @@ import {
279
279
  isInsideTree,
280
280
  signManifest,
281
281
  verifyManifest,
282
+ hashPackBytes,
283
+ rewriteWorkspaceSpecs,
282
284
  listPackFiles,
283
285
  listSignablePackFiles,
284
286
  assertKeyOutsideTree,
@@ -667,7 +669,7 @@ Usage:
667
669
  dz upgrade [--target <name>] [--pubkey <path>] [--require-signing] (a TAMPERED pack aborts the upgrade)
668
670
  dz sign --pack <dir> --key <path-outside-repo> (Ed25519 manifest + CycloneDX SBOM for a pack)
669
671
  dz verify-pack --pack <dir> [--pubkey <path>] (signature check; fail-closed; key from the repo, never the pack)
670
- 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)
672
+ 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)
671
673
  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)
672
674
  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)
673
675
  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)
@@ -839,6 +841,8 @@ export interface CliIo {
839
841
  * without spawning anything.
840
842
  */
841
843
  readonly releaseRunner?: ReleaseExecRunner;
844
+ /** Post-publish mirror command seam; production uses synchronous shell execution. */
845
+ readonly publishMirrorRunner?: PublishMirrorRunner;
842
846
  /**
843
847
  * Test seam for `dz install`: overrides the `npm install` subprocess (production leaves
844
848
  * it unset → real `execSync`, stdio piped). A stub runner that pre-stages a fixture
@@ -858,6 +862,21 @@ export type ReleaseExecRunner = (
858
862
  opts: { readonly cwd: string; readonly timeoutMs: number },
859
863
  ) => { exitCode: number; stdout: string; stderr: string; timedOut?: boolean };
860
864
 
865
+ export type PublishMirrorRunner = (
866
+ command: string,
867
+ options: { readonly cwd: string; readonly env: NodeJS.ProcessEnv },
868
+ ) => string;
869
+
870
+ type PublishMirrorState = {
871
+ readonly status: 'confirmed' | 'unconfirmed' | 'skipped' | 'not-configured';
872
+ readonly command: string;
873
+ readonly commit?: string;
874
+ readonly receipt?: { readonly manifestUrl: string; readonly confirmedAt: string; readonly waitedMs: number };
875
+ readonly error?: string;
876
+ readonly reason?: string;
877
+ readonly warning?: string;
878
+ };
879
+
861
880
  interface ParsedArgs {
862
881
  readonly command: string;
863
882
  readonly options: Map<string, string>;
@@ -2986,6 +3005,8 @@ function cmdStatusline(
2986
3005
  } else if (data.storeHealth?.verdict === 'unreadable') {
2987
3006
  line += ` ⛔ STORE UNREADABLE${data.storeHealth.unreadableFiles !== undefined && data.storeHealth.unreadableFiles.length > 0
2988
3007
  ? `: ${data.storeHealth.unreadableFiles.join(', ')}` : ''}`;
3008
+ } else if (data.storeHealth?.verdict === 'busy') {
3009
+ line += ' ⏳ STORE BUSY';
2989
3010
  } else if (data.storeHealth?.verdict === 'source-changed') {
2990
3011
  line += ' ⚠ store source changed';
2991
3012
  }
@@ -3634,13 +3655,16 @@ function learningStoreLine(
3634
3655
  ) + (reason ? ' [' + reason + ']' : '');
3635
3656
  }
3636
3657
 
3637
- function inspectLearningStore(projectRoot: string): {
3658
+ function inspectLearningStore(
3659
+ projectRoot: string,
3660
+ countOptions?: { readonly busyTimeoutMs?: number; readonly attempts?: number },
3661
+ ): {
3638
3662
  mark: StoreMark | undefined;
3639
3663
  health: StoreHealth;
3640
3664
  rows: ReturnType<typeof countLearningStoreRowsReadonly>;
3641
3665
  } {
3642
3666
  const mark = readStoreMark(projectRoot);
3643
- const rows = countLearningStoreRowsReadonly(projectRoot);
3667
+ const rows = countLearningStoreRowsReadonly(projectRoot, countOptions);
3644
3668
  return { mark, rows, health: checkStoreHealth({ projectRoot, ...rows, mark }) };
3645
3669
  }
3646
3670
 
@@ -3737,7 +3761,8 @@ function refreshLearningStoreMark(
3737
3761
  const rows = countLearningStoreRowsReadonly(projectRoot);
3738
3762
  const counts = observedRows(rows);
3739
3763
  if (counts === undefined) {
3740
- writeErr(`⚠ dz store guard: ${options.reader ? 'reader observation' : 'store operation'} completed but the external mark was not updated — a store tier is unreadable`);
3764
+ const busy = rows.lexicalRows === 'busy' || rows.vectorRows === 'busy';
3765
+ 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'}`);
3741
3766
  return;
3742
3767
  }
3743
3768
  writeStoreMark(projectRoot, {
@@ -3771,7 +3796,7 @@ function allowLearningStoreWrite(
3771
3796
  ): boolean {
3772
3797
  let inspection: ReturnType<typeof inspectLearningStore>;
3773
3798
  try {
3774
- inspection = inspectLearningStore(projectRoot);
3799
+ inspection = inspectLearningStore(projectRoot, { busyTimeoutMs: 250, attempts: 3 });
3775
3800
  } catch (error) {
3776
3801
  const health: StoreHealth = {
3777
3802
  verdict: 'unreadable',
@@ -3787,6 +3812,10 @@ function allowLearningStoreWrite(
3787
3812
  // own row had landed.
3788
3813
  return true;
3789
3814
  }
3815
+ if (inspection.health.verdict === 'busy') {
3816
+ writeErr(`dz store guard: NOT MEASURED — ${inspection.health.reason}`);
3817
+ return true;
3818
+ }
3790
3819
  if (inspection.health.verdict === 'source-changed') {
3791
3820
  const counts = observedRows(inspection.rows);
3792
3821
  if (counts === undefined) return false;
@@ -3826,6 +3855,7 @@ function warnLearningStoreRead(projectRoot: string, writeErr: WriteErr, command:
3826
3855
  for (const line of lexicalSourceLines(rows)) writeErr(line);
3827
3856
  return;
3828
3857
  }
3858
+ if (health.verdict === 'busy') return;
3829
3859
  const counts = observedRows(rows);
3830
3860
  if (counts !== undefined && counts.lexicalRows + counts.vectorRows > 0
3831
3861
  && (mark === undefined || mark.lexicalLast !== counts.lexicalRows || mark.vectorLast !== counts.vectorRows
@@ -6855,7 +6885,37 @@ function cmdSbom(options: Map<string, string>, flags: Set<string>, cwd: string,
6855
6885
  return 0;
6856
6886
  }
6857
6887
 
6858
- function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: string, writeOutput: Write): number {
6888
+ function mirrorCommandFromConfig(cwd: string): { command?: string; warning?: string } {
6889
+ const configPath = join(cwd, '.dz', 'config.json');
6890
+ if (!existsSync(configPath)) return {};
6891
+ try {
6892
+ const config = JSON.parse(readFileSync(configPath, 'utf8')) as { publish?: { mirrorCommand?: unknown } };
6893
+ const command = typeof config.publish?.mirrorCommand === 'string' ? config.publish.mirrorCommand.trim() : '';
6894
+ return command === '' ? {} : { command };
6895
+ } catch (error) {
6896
+ const reason = error instanceof Error ? error.message : String(error);
6897
+ return { warning: `.dz/config.json unreadable — ${reason}` };
6898
+ }
6899
+ }
6900
+
6901
+ function mirrorShellToken(value: string): string {
6902
+ if (value === '') return "''";
6903
+ if (!/^[A-Za-z0-9@/.,+_-]+$/.test(value)) throw new Error('published package/version list is not shell-safe');
6904
+ return value;
6905
+ }
6906
+
6907
+ function mirrorFailureMessage(error: unknown): string {
6908
+ if (error instanceof Error && error.message.trim() !== '') return error.message.trim().split(/\r?\n/, 1)[0] ?? 'mirror command failed';
6909
+ return String(error);
6910
+ }
6911
+
6912
+ function cmdPublish(
6913
+ options: Map<string, string>,
6914
+ flags: Set<string>,
6915
+ cwd: string,
6916
+ writeOutput: Write,
6917
+ mirrorRunner?: PublishMirrorRunner,
6918
+ ): number {
6859
6919
  const json = flags.has('json');
6860
6920
  // Under --json stdout carries exactly one JSON document, so every human line — guard notes, refusals,
6861
6921
  // progress — goes to stderr instead of being dropped: a refusal that prints nothing is the silent
@@ -6863,9 +6923,9 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
6863
6923
  const write: Write = json ? (line) => { process.stderr.write(`${line}\n`); } : writeOutput;
6864
6924
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
6865
6925
  // silently swallowed and flip the command into live-publish mode.
6866
- const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance', 'json']);
6867
- const allowedOptions = new Set(['filter', 'claim-check', 'no-guard', 'sign-key']);
6868
- 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)';
6926
+ const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance', 'json', 'no-mirror']);
6927
+ const allowedOptions = new Set(['filter', 'claim-check', 'no-guard', 'sign-key', 'mirror-cmd']);
6928
+ 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)';
6869
6929
  for (const flag of flags) {
6870
6930
  if (!allowedFlags.has(flag)) {
6871
6931
  write(`dz publish: unknown option --${flag}`);
@@ -7045,7 +7105,7 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
7045
7105
  // longer exist. Default to the same path `dz sign --init` writes, so the ordinary operator needs no
7046
7106
  // new flag; `--sign-key` overrides it.
7047
7107
  const signKey = (options.get('sign-key') ?? join(homedir(), '.dz', 'keys', 'dz.key')).trim();
7048
- const report = publishPackages(cwd, {
7108
+ const publishReport = publishPackages(cwd, {
7049
7109
  provenance,
7050
7110
  dryRun,
7051
7111
  filter,
@@ -7125,9 +7185,82 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
7125
7185
  },
7126
7186
  });
7127
7187
 
7188
+ const configMirror = mirrorCommandFromConfig(cwd);
7189
+ const configuredCommand = (options.get('mirror-cmd') ?? configMirror.command ?? '').trim();
7190
+ const publishedVersions = publishReport.packages
7191
+ .filter((pkg) => pkg.status === 'published')
7192
+ .map((pkg) => `${pkg.name}@${pkg.newVersion}`);
7193
+ const expected = publishedVersions.join(',');
7194
+ const fullMirrorCommand = configuredCommand === ''
7195
+ ? ''
7196
+ : `${configuredCommand} --expect ${mirrorShellToken(expected)} --json`;
7197
+ let mirror: PublishMirrorState;
7198
+
7199
+ // This exact conjunction is the Step-7/8 mutation anchor: an epilogue is eligible only after a
7200
+ // live sweep that actually landed at least one package. Other explicit skip states are handled
7201
+ // before command resolution so each reason remains distinguishable in text and JSON.
7202
+ const mirrorEligible = !dryRun && publishReport.published >= 1;
7203
+ if (bumpOnly) {
7204
+ mirror = { status: 'skipped', command: fullMirrorCommand, reason: 'bump-only' };
7205
+ } else if (!mirrorEligible) {
7206
+ mirror = {
7207
+ status: 'skipped',
7208
+ command: fullMirrorCommand,
7209
+ reason: dryRun ? 'dry-run' : 'published=0',
7210
+ };
7211
+ } else if (flags.has('no-mirror')) {
7212
+ mirror = { status: 'skipped', command: fullMirrorCommand, reason: '--no-mirror' };
7213
+ } else if (configMirror.warning !== undefined && options.get('mirror-cmd') === undefined) {
7214
+ mirror = { status: 'not-configured', command: '', warning: configMirror.warning };
7215
+ } else if (configuredCommand === '') {
7216
+ mirror = { status: 'not-configured', command: '' };
7217
+ } else {
7218
+ const runMirror: PublishMirrorRunner = mirrorRunner
7219
+ ?? ((command, runnerOptions) => execSync(command, {
7220
+ cwd: runnerOptions.cwd,
7221
+ env: runnerOptions.env,
7222
+ encoding: 'utf8',
7223
+ stdio: 'pipe',
7224
+ }));
7225
+ try {
7226
+ const stdout = runMirror(fullMirrorCommand, {
7227
+ cwd,
7228
+ env: { ...process.env, DZ_PUBLISHED: expected },
7229
+ });
7230
+ const parsed = JSON.parse(stdout) as {
7231
+ ok?: unknown;
7232
+ commit?: unknown;
7233
+ receipt?: { manifestUrl?: unknown; confirmedAt?: unknown; waitedMs?: unknown };
7234
+ error?: unknown;
7235
+ };
7236
+ if (parsed.ok !== true) throw new Error(typeof parsed.error === 'string' ? parsed.error : 'mirror command returned ok:false');
7237
+ if (typeof parsed.commit !== 'string' || parsed.commit === '') throw new Error('mirror command returned no commit');
7238
+ if (typeof parsed.receipt?.manifestUrl !== 'string'
7239
+ || typeof parsed.receipt.confirmedAt !== 'string'
7240
+ || typeof parsed.receipt.waitedMs !== 'number') {
7241
+ throw new Error('mirror command returned no live-manifest receipt');
7242
+ }
7243
+ mirror = {
7244
+ status: 'confirmed',
7245
+ command: fullMirrorCommand,
7246
+ commit: parsed.commit,
7247
+ receipt: {
7248
+ manifestUrl: parsed.receipt.manifestUrl,
7249
+ confirmedAt: parsed.receipt.confirmedAt,
7250
+ waitedMs: parsed.receipt.waitedMs,
7251
+ },
7252
+ };
7253
+ } catch (error) {
7254
+ mirror = { status: 'unconfirmed', command: fullMirrorCommand, error: mirrorFailureMessage(error) };
7255
+ }
7256
+ }
7257
+
7258
+ const report = { ...publishReport, mirror };
7259
+ const exitCode = report.errors > 0 ? 1 : mirror.status === 'unconfirmed' ? 3 : 0;
7260
+
7128
7261
  if (json) {
7129
7262
  writeOutput(JSON.stringify(report));
7130
- return report.errors > 0 ? 1 : 0;
7263
+ return exitCode;
7131
7264
  }
7132
7265
 
7133
7266
  write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
@@ -7177,7 +7310,21 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
7177
7310
  for (const item of pkg.notVerified) write(` · ${item}`);
7178
7311
  }
7179
7312
  }
7180
- return report.errors > 0 ? 1 : 0;
7313
+ for (const warning of report.warnings ?? []) write(` ⚠ warning: ${warning}`);
7314
+ for (const path of report.releaseLineSynced ?? []) write(` ↳ release line synced: ${path}`);
7315
+ if (mirror.status === 'confirmed') {
7316
+ write(` ✓ mirror: confirmed — ${mirror.commit} (${mirror.receipt?.manifestUrl})`);
7317
+ } else if (mirror.status === 'not-configured') {
7318
+ if (mirror.warning !== undefined) write(` ⚠ mirror: ${mirror.warning}`);
7319
+ else write(' ℹ mirror: not configured');
7320
+ } else if (mirror.status === 'skipped') {
7321
+ write(` ℹ mirror: skipped (${mirror.reason})`);
7322
+ } else {
7323
+ write(` ✗ mirror: unconfirmed — ${mirror.error}`);
7324
+ const rerun = mirror.command || 'configure publish.mirrorCommand, then run it';
7325
+ write(`dz publish: published, mirror NOT confirmed — ${mirror.error}; re-run: ${rerun}`);
7326
+ }
7327
+ return exitCode;
7181
7328
  }
7182
7329
 
7183
7330
  /* ------------------------------------------------------------------ */
@@ -9635,21 +9782,52 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
9635
9782
  // a resolvable workspace dep becomes its real semver (safe → the rule passes); an UNRESOLVABLE one (points at
9636
9783
  // no workspace package, or not a pnpm workspace) stays `workspace:*` so the rule catches a dep that WOULD ship
9637
9784
  // raw. That is the genuinely dangerous case the rule exists for.
9638
- type Manifest = { name?: string; version?: string; private?: boolean; license?: string; licenseHold?: unknown; dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
9785
+ type Manifest = { name?: string; version?: string; private?: boolean; license?: string; licenseHold?: unknown; dependencies?: Record<string, string>; devDependencies?: Record<string, string>; peerDependencies?: Record<string, string>; optionalDependencies?: Record<string, string> };
9639
9786
  const manifests: Manifest[] = [];
9640
- const located: { dir: string; m: Manifest }[] = [];
9787
+ const located: { dir: string; m: Manifest; text: string }[] = [];
9641
9788
  try {
9642
9789
  const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
9643
9790
  for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
9644
9791
  try {
9645
- const m = JSON.parse(readFileSync(join(root, rel), 'utf8')) as Manifest;
9792
+ const packageJsonText = readFileSync(join(root, rel), 'utf8');
9793
+ const m = JSON.parse(packageJsonText) as Manifest;
9646
9794
  manifests.push(m);
9647
- located.push({ dir: rel.replace(/\/package\.json$/, ''), m });
9795
+ located.push({ dir: rel.replace(/\/package\.json$/, ''), m, text: packageJsonText });
9648
9796
  } catch { /* skip unreadable */ }
9649
9797
  }
9650
9798
  } catch { /* not a git repo */ }
9651
9799
  const versionByName = new Map<string, string>();
9652
9800
  for (const m of manifests) if (m.name && typeof m.version === 'string') versionByName.set(m.name, m.version);
9801
+ // release-line-in-sync: filesystem access belongs in the CLI gatherer. The core evaluator receives
9802
+ // only immutable evidence, so evaluateGuard(facts) stays deterministic and independent of cwd.
9803
+ const coreManifestPath = join(root, 'packages', '@dzhechkov', 'harness-core', 'package.json');
9804
+ const cliManifestPath = join(root, 'packages', '@dzhechkov', 'harness-cli', 'package.json');
9805
+ if (existsSync(coreManifestPath) || existsSync(cliManifestPath)) {
9806
+ const readText = (path: string): string | null => {
9807
+ try { return readFileSync(path, 'utf8'); } catch { return null; }
9808
+ };
9809
+ const readVersion = (path: string): string | null => {
9810
+ const raw = readText(path);
9811
+ if (raw === null) return null;
9812
+ try {
9813
+ const parsed = JSON.parse(raw) as { version?: unknown };
9814
+ return typeof parsed.version === 'string' && /^\d+\.\d+\.\d+$/.test(parsed.version)
9815
+ ? parsed.version
9816
+ : null;
9817
+ } catch { return null; }
9818
+ };
9819
+ facts['releaseLines'] = {
9820
+ coreVersion: readVersion(coreManifestPath),
9821
+ cliVersion: readVersion(cliManifestPath),
9822
+ readmes: [
9823
+ { path: 'README.md', text: readText(join(root, 'README.md')) },
9824
+ {
9825
+ path: 'packages/@dzhechkov/harness-cli/README.md',
9826
+ text: readText(join(root, 'packages', '@dzhechkov', 'harness-cli', 'README.md')),
9827
+ },
9828
+ ],
9829
+ };
9830
+ }
9653
9831
  publishPackageRoots.push(...located
9654
9832
  .filter(({ dir, m }) => m.private !== true && (
9655
9833
  publishFilter === undefined
@@ -9811,22 +9989,136 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
9811
9989
  }
9812
9990
  } catch { /* нечитаемо — правило молчит */ }
9813
9991
  facts['counts'] = gatherReadmeCounts(root);
9814
- // readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: does the
9815
- // change set contain its package.json (the version-bump signal) without its README.md?
9992
+ // One package-scoped WORKING-TREE diff feeds both readme-first and signature-fresh. A second
9993
+ // porcelain call could observe a different tree and let the two publish guards disagree.
9994
+ let changedPackageFiles: string[] | undefined;
9995
+ // readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: did
9996
+ // the version FIELD change from HEAD without its README.md changing? A package.json edit alone
9997
+ // says nothing about version; repository/metadata-only edits must not manufacture a bump.
9816
9998
  try {
9817
- const status = execSync('git status --porcelain -- "packages/@dzhechkov/"', { cwd: root, encoding: 'utf-8' });
9818
- const perPack = new Map<string, { pkgJson: boolean; readme: boolean }>();
9819
- for (const line of status.split('\n')) {
9820
- const m = line.match(/packages\/@dzhechkov\/([^/]+)\/(.+)$/);
9999
+ const status = execSync('git status --porcelain -uall -- "packages/@dzhechkov/"', { cwd: root, encoding: 'utf-8' });
10000
+ changedPackageFiles = status
10001
+ .split('\n')
10002
+ .map((line) => line.slice(3).trim())
10003
+ .map((path) => (path.includes(' -> ') ? path.split(' -> ')[1]!.trim() : path))
10004
+ .filter(Boolean);
10005
+ const perPack = new Map<string, { readme: boolean }>();
10006
+ for (const rel of changedPackageFiles) {
10007
+ const m = rel.match(/^packages\/@dzhechkov\/([^/]+)\/(.+)$/);
9821
10008
  if (!m || !m[1] || !m[2]) continue;
9822
- const e = perPack.get(m[1]) ?? { pkgJson: false, readme: false };
9823
- if (m[2] === 'package.json') e.pkgJson = true;
10009
+ const e = perPack.get(m[1]) ?? { readme: false };
9824
10010
  if (m[2] === 'README.md') e.readme = true;
9825
10011
  perPack.set(m[1], e);
9826
10012
  }
9827
- facts['readmeFirst'] = [...perPack.entries()].map(([name, e]) => ({ name: '@dzhechkov/' + name, versionBumped: e.pkgJson, readmeChanged: e.readme }));
10013
+ const readVersion = (raw: string): string => {
10014
+ const version = (JSON.parse(raw) as { version?: unknown }).version;
10015
+ if (typeof version !== 'string') throw new Error('package version is unreadable');
10016
+ return version;
10017
+ };
10018
+ facts['readmeFirst'] = [...perPack.entries()].map(([name, e]) => {
10019
+ const packagePath = `packages/@dzhechkov/${name}/package.json`;
10020
+ let versionBumped = false;
10021
+ let versionUnknown = false;
10022
+ try {
10023
+ const treeVersion = readVersion(readFileSync(join(root, packagePath), 'utf8'));
10024
+ try {
10025
+ const headVersion = readVersion(execFileSync('git', ['show', `HEAD:${packagePath}`], {
10026
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
10027
+ }));
10028
+ versionBumped = treeVersion !== headVersion;
10029
+ } catch {
10030
+ try {
10031
+ const trackedAtHead = execFileSync('git', ['ls-tree', '--name-only', 'HEAD', '--', packagePath], {
10032
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
10033
+ }).trim() === packagePath;
10034
+ if (trackedAtHead) versionUnknown = true;
10035
+ else versionBumped = true;
10036
+ } catch {
10037
+ versionUnknown = true;
10038
+ }
10039
+ }
10040
+ } catch {
10041
+ versionUnknown = true;
10042
+ }
10043
+ return {
10044
+ name: '@dzhechkov/' + name,
10045
+ versionBumped,
10046
+ readmeChanged: e.readme,
10047
+ ...(versionUnknown ? { versionUnknown: true } : {}),
10048
+ };
10049
+ });
9828
10050
  } catch { /* not a git repo — rule skips */ }
9829
10051
 
10052
+ // signature-fresh: verify only signed packs whose non-signature files changed in the same
10053
+ // porcelain listing above. Manifest/SBOM churn is the remedy, not another reason to verify.
10054
+ // The trust key and every byte read here stay in the CLI; the guard core receives facts only.
10055
+ const trustRootPath = join(root, TRUST_ROOT_REL);
10056
+ if (changedPackageFiles !== undefined && existsSync(trustRootPath)) {
10057
+ try {
10058
+ const trustRootPem = readFileSync(trustRootPath, 'utf8');
10059
+ const signedPacks: {
10060
+ name: string;
10061
+ dir: string;
10062
+ changed: boolean;
10063
+ ok: boolean | null;
10064
+ failures: string[];
10065
+ note?: string;
10066
+ }[] = [];
10067
+ for (const { dir, m, text: packageJsonText } of located) {
10068
+ const manifestPath = join(root, dir, MANIFEST_NAME);
10069
+ if (!existsSync(manifestPath)) continue;
10070
+ const prefix = dir + '/';
10071
+ const changedPackFiles = new Set(changedPackageFiles
10072
+ .filter((rel) => rel.startsWith(prefix))
10073
+ .map((rel) => rel.slice(prefix.length))
10074
+ .filter((packRel) => packRel !== MANIFEST_NAME && packRel !== SBOM_NAME));
10075
+ const changed = changedPackFiles.size > 0;
10076
+ let ok: boolean | null = null;
10077
+ let failures: string[] = [];
10078
+ let note: string | undefined;
10079
+ if (changed) {
10080
+ try {
10081
+ const signed = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
10082
+ manifest?: { files?: readonly { path?: unknown; sha256?: unknown }[] };
10083
+ };
10084
+ const verified = verifyManifest(join(root, dir), signed as never, trustRootPem);
10085
+ // A source checkout contains dev files outside package.json#files. They are absent
10086
+ // from the tarball by design, so an unchanged "present but not signed" source file
10087
+ // is not evidence about signature freshness. A changed/new unsigned file stays loud.
10088
+ const relevantFailures = verified.failures.filter((failure) =>
10089
+ failure.reason !== 'present in the pack but not signed' || changedPackFiles.has(failure.path));
10090
+ ok = verified.ok || relevantFailures.length === 0;
10091
+ if (!ok && relevantFailures.length > 0 && relevantFailures.every(({ path }) => path === 'package.json')) {
10092
+ const signedPackageHash = signed.manifest?.files?.find(({ path }) => path === 'package.json')?.sha256;
10093
+ if (typeof signedPackageHash === 'string') {
10094
+ const rewritten = rewriteWorkspaceSpecs(packageJsonText, versionByName);
10095
+ // MEASURED 2026-09-10 on keysarium/skills-meta/core: pnpm also removes only
10096
+ // prepublishOnly from the packed scripts table. Keep that I/O-adapter concern
10097
+ // outside the workspace-only core helper.
10098
+ const packedShape = JSON.parse(rewritten) as Record<string, unknown>;
10099
+ const scripts = packedShape['scripts'];
10100
+ if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
10101
+ delete (scripts as Record<string, unknown>)['prepublishOnly'];
10102
+ }
10103
+ const rewrittenHash = hashPackBytes('package.json', Buffer.from(JSON.stringify(packedShape, null, 2) + '\n'), (signed.manifest as { version?: number } | undefined)?.version);
10104
+ if (rewrittenHash === signedPackageHash) {
10105
+ ok = true;
10106
+ note = 'package.json matches after workspace rewrite';
10107
+ }
10108
+ }
10109
+ }
10110
+ failures = relevantFailures.slice(0, 3).map((failure) => `${failure.path}: ${failure.reason}`);
10111
+ } catch (error) {
10112
+ ok = false;
10113
+ failures = [`${MANIFEST_NAME}: ${error instanceof Error ? error.message : String(error)}`];
10114
+ }
10115
+ }
10116
+ signedPacks.push({ name: m.name ?? dir, dir, changed, ok, failures, ...(note !== undefined ? { note } : {}) });
10117
+ }
10118
+ facts['signedPacks'] = signedPacks;
10119
+ } catch { /* unreadable trust root — absence of evidence, so the rule is not established */ }
10120
+ }
10121
+
9830
10122
  // review-round: the same WORKING-TREE diff, asked a different question — does a package that
9831
10123
  // bumps its version and changes SOURCE bring a GRADED QE report with it? Scoped to source so a
9832
10124
  // docs-only republish is never blocked (ADR-001, features/publish-needs-a-review). A throw here
@@ -17958,7 +18250,10 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
17958
18250
  // `dz recap` — a refusal built on either list would reject working commands, which is a worse
17959
18251
  // failure than the one being fixed. Goes to STDERR so a `--json` consumer's stdout stays clean.
17960
18252
  if (command !== 'contract-check') {
17961
- for (const notice of unknownFlagNotice([...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')), KNOWN_CLI_FLAGS)) {
18253
+ for (const notice of unknownFlagNotice(
18254
+ [...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')),
18255
+ KNOWN_CLI_FLAGS,
18256
+ )) {
17962
18257
  writeErr(notice.line);
17963
18258
  }
17964
18259
  }
@@ -18115,7 +18410,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
18115
18410
  case 'auto-canonicalize':
18116
18411
  return await cmdAutoCanonicalize(options, cwd, write);
18117
18412
  case 'publish':
18118
- return cmdPublish(options, flags, cwd, write);
18413
+ return cmdPublish(options, flags, cwd, write, io.publishMirrorRunner);
18119
18414
  case 'release':
18120
18415
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
18121
18416
  case 'parity':
@@ -324,4 +324,5 @@ export const KNOWN_CLI_FLAGS: readonly string[] = [
324
324
  'write',
325
325
  'year',
326
326
  'yes',
327
+ 'mirror-cmd', // dz publish epilogue (mirror-push-on-publish)
327
328
  ];