@dzhechkov/harness-cli 0.7.3 → 0.7.5

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
@@ -6,7 +6,11 @@
6
6
  import { appendFileSync, chmodSync, closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
7
7
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
9
+ import { request as httpsRequest } from 'node:https';
10
+ import { KNOWN_CLI_FLAGS } from './known-flags.js';
11
+ import { isBooleanFlag } from './boolean-flags.js';
12
+ import { resolveInstallSpec } from './install-spec.js';
13
+ import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
10
14
  import { createHash, randomBytes } from 'node:crypto';
11
15
  import { homedir, tmpdir } from 'node:os';
12
16
  import { createRequire } from 'node:module';
@@ -16,13 +20,13 @@ TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher
16
20
  // guard-promotion (feature guard-promotion, scout idea #1)
17
21
  assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, 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, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, 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,
18
22
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
19
- 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, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, mirrorWriterExplanation, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
23
+ 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, 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,
20
24
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
21
25
  readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, ensureBacklogEmbedForm, readBacklogEmbedFormVersion, recordAbsorption, DEDUP_EMBED_FORM_VERSION, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, transitionIdeas, editIdea, clearEmbedStale, BACKLOG_BACKENDS, applyDomainBoost, DZ_OWNED_TASK_TYPES, applyExportHoldout, DEFAULT_HELD_OUT_DOMAINS, canonicalDomainKey, readAgentdbRowsByTaskType, heldOutAfterOptIn, renderHoldoutNote, renderSharedStoreAdvice, decideVectorExport, countDisplacedByCut, renderDomainBoostNote, renderDomainCutNote, parseReqeDebt,
22
26
  // qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
23
27
  KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
24
28
  // Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
25
- parseMutationRegistry, applyMutationToText, countFailingTests, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, } from '@dzhechkov/harness-core';
29
+ parseMutationRegistry, applyMutationToText, countFailingTests, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, CADENCE_WINDOW_DAYS, } from '@dzhechkov/harness-core';
26
30
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
27
31
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
28
32
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -63,6 +67,8 @@ Usage:
63
67
  dz epoch-replay --score <judgments.json> --work-order <file> [--slice <name>] [--json] (un-blind against the pre-registered assignment → SUPPORTED only when the two 95% Wilson CIs are DISJOINT, else FALSIFIED / INCONCLUSIVE)
64
68
  dz score --slug <feature> [--project <dir>] [--json] (process scorecard for ONE feature-adr run, from its artifacts: ADR confirmation, discrimination, cross-model QE grade, live verification, README-first, learning loop, amendments — descriptive-only, a low score exits 0)
65
69
  dz recap [--day|--week|--month] [--at <ISO date>] [--project <dir>] [--json] (what was done over a window, from records only: deliveries with the grade an independent review STATED — a report naming two grades is reported ambiguous, never guessed — registry publishes, gate verdicts, knowledge reuse. --quarter/--half-year/--year are RECOGNISED and REFUSED with the real span in days: there is one complete quarter and the longest record is 174 days. Every section carries its own data-start date, and "the source was not read" never prints as zero. Contaminated measures — commit count, lines, tokens, learning-event volume, inventory counts, lesson count — are not computed, and the report says so. exit 0 reported / 2 refused)
70
+ dz cadence [--window day|week|month|quarter|halfyear|year] [--json] (the WHAT-SHIPPED aggregator: graded-shipment cadence by ISO week + npm-publish cadence (recap cache) + guard repeat decay on the FIXED rule set + recall reuse; a window deeper than 2× the record is REFUSED with the depth named (ADR: a cadence from one point is scale forgery); exit 0 report / 2 refused-window / 1 usage)
71
+ dz tg-post --draft <file.html> [--manifest <sources.json>] [--channel <@name|id>] [--send --yes] [--night] [--preview] [--json] (the sender for an APPROVED channel post, per the accepted genai-tweets-channel ADRs: HTML mode only — never MarkdownV2; link preview OFF by default (x.com previews in Telegram are broken); the 00:00-06:00 MSK quiet window refuses without an explicit --night. DEFAULT IS A DRY-RUN: it validates the draft (tag balance, allowed tags, bare &/<, the 4096 visible-character limit with the overshoot counted) and runs the provenance gate over --manifest IN-PROCESS — a draft with no manifest is refused as unchecked, and anything but ALLOWED refuses. A real send needs --send --yes, stating ADR-004's manual-publishing decision out loud each time. The token comes from TELEGRAM_BOT_TOKEN or telegram.tokenFile in .dz/config.json and is never printed. exit 0 sent or clean dry-run / 1 refused or Telegram error / 2 usage)
66
72
  dz name-check [--command <n>] [--module <basename>] [--export <a,b>] [--project <dir>] [--json] (is this name free, BEFORE a line of code? Scans workspace SOURCE — never dist, because a stale build answers 'free' confidently. Checks a dz command name against the dispatcher AND the help block, a module basename against every package's src/, and exported identifiers against every declaration in the workspace. exit 0 all free / 1 at least one taken, naming where / 2 nothing asked or the scan did not run — an empty sweep is never a clean bill. Honest limit, printed on the passing path: it reads declarations, so a re-export under a different name stays the build's job)
67
73
  dz provenance-check --manifest <sources.json> [--project <dir>] [--json] (nothing goes out citing a source that may not leave this machine. Checks PROVENANCE, not words: every claim names its source, and only a KNOWN kind that resolves safely is cleared. Repo paths go through 'git -C <root> check-ignore' over the RESOLVED path — a symlink into an ignored directory is REFUSED (git classifies the string and never dereferences, MEASURED), and the verdict does not change with your working directory. Store records must be named in the git-TRACKED provenance-public.json, so declaring one public is a reviewable commit rather than a field inside an ignored store. An undeclared kind is refused, never inferred from the path's shape. exit 0 allowed / 1 blocked / 3 NOT ESTABLISHED — an empty manifest, an unreadable one, or an oracle that did not run is never a pass. It proves what was CITED: it cannot see a paraphrase with no citation, nor confidential text pasted by hand into an allowed file)
68
74
  dz amendment-check --slug <slug> | --feature-dir <dir> | --all [--json] (the deterministic Step-8 amendment gate: every AM-N row must resolve to a test found INSIDE the file the row names; the PLAN is authoritative when it carries rows, and an ideation amendment the plan drops is a failure. exit 0 pass/skip, 1 fail, 3 NOT-ESTABLISHED — a section that parsed ZERO rows is never a pass. --all is a CENSUS and always exits 0. Does NOT prove non-vacuity — that is dz discrimination-check)
@@ -156,10 +162,24 @@ function parseArgs(argv) {
156
162
  const optionLists = new Map();
157
163
  const flags = new Set();
158
164
  const positional = [];
165
+ // The command decides `--force`'s arity (valued only under `dz guard`); it is always the first
166
+ // token when present — a leading `--flag` means there is no command at all.
167
+ const command = (argv[0] ?? '').startsWith('--') ? '' : argv[0] ?? '';
159
168
  for (let index = 0; index < argv.length; index += 1) {
160
169
  const arg = argv[index] ?? '';
161
- if (arg.startsWith('--')) {
170
+ // A token that BEGINS with -- but contains whitespace cannot be a flag — no real flag carries a
171
+ // space (values arrive as separate argv tokens). It is quoted TEXT that happens to open with
172
+ // dashes: `dz backlog add "--semantic идея…"` was rejected twice with «an idea text is
173
+ // required» while the text sat right there, eaten as an option (f18fc89e).
174
+ if (arg.startsWith('--') && !/\s/.test(arg)) {
162
175
  const key = arg.slice(2);
176
+ // A DECLARED boolean flag never swallows the next token (backlog 247ddcfa: `dz sync
177
+ // --dry-run .` used to disarm the safety flag silently and then WRITE). The declaration is
178
+ // data with a two-way drift test — see src/boolean-flags.ts.
179
+ if (isBooleanFlag(key, command)) {
180
+ flags.add(key);
181
+ continue;
182
+ }
163
183
  const next = argv[index + 1];
164
184
  if (next !== undefined && !next.startsWith('--')) {
165
185
  options.set(key, next);
@@ -400,7 +420,15 @@ async function cmdSync(options, flags, cwd, write, writeErr) {
400
420
  // Auto-discover all skills-* packs, or use explicit --canonical
401
421
  let canonicalDirs;
402
422
  if (canonicalArg !== undefined) {
403
- canonicalDirs = [resolve(cwd, canonicalArg)];
423
+ const dir = resolve(cwd, canonicalArg);
424
+ if (!existsSync(dir)) {
425
+ // MEASURED 2026-08-24: a TYPO here used to print `0/0 in sync` and exit 0 — green exactly
426
+ // when nothing was compared, and in CI that reads as "all skills healthy". An explicit path
427
+ // that does not exist is an answer about the INVOCATION, not about the skills.
428
+ write(`dz sync: --canonical ${dir} does not exist — nothing was compared, and nothing-compared is not a clean sync`);
429
+ return 3;
430
+ }
431
+ canonicalDirs = [dir];
404
432
  }
405
433
  else {
406
434
  const baseDir = join(projectRoot, 'packages', '@dzhechkov');
@@ -417,7 +445,18 @@ async function cmdSync(options, flags, cwd, write, writeErr) {
417
445
  force: flags.has('force'),
418
446
  });
419
447
  const { total, inSync, missing, drift } = report.summary;
420
- write(`dz sync${report.dryRun ? ' --dry-run' : ''}: ${inSync}/${total} in sync, ${missing} missing, ${drift} drift`);
448
+ // The DONE half, beside the SEEN half: a run that wrote its fixes used to print only what it had
449
+ // seen before writing, and exit 1 — "returns 0 when it did nothing and 1 when it worked". Status
450
+ // counts describe the tree as found; `wrote` describes what this run changed about it.
451
+ const wrote = report.skills.filter((skill) => skill.written.length > 0);
452
+ write(`dz sync${report.dryRun ? ' --dry-run' : ''}: ${inSync}/${total} in sync, ${missing} missing, ${drift} drift${wrote.length > 0 ? ` — wrote ${wrote.length} skill(s)` : ''}`);
453
+ if (total === 0) {
454
+ // Nothing was compared. Exit 3, the house not-established convention (`dz workflow-lint`,
455
+ // `check-plan-completeness`) — never 0: a gate that is green when it never ran is the defect
456
+ // this repo keeps finding elsewhere (the publish gate, the scout 401, the recall log).
457
+ write(`dz sync: 0 canonical skill(s) found under ${canonicalDirs.join(', ')} — nothing was compared, and nothing-compared is not a clean sync`);
458
+ return 3;
459
+ }
421
460
  // Skip-and-collect (D1): the broken canonical skills are NAMED on stderr, and their
422
461
  // presence keeps the exit code non-zero — a partial sync is not a clean sync.
423
462
  if (report.failures.length > 0) {
@@ -427,7 +466,10 @@ async function cmdSync(options, flags, cwd, write, writeErr) {
427
466
  writeErr(line);
428
467
  return 1;
429
468
  }
430
- return missing === 0 && drift === 0 ? 0 : 1;
469
+ // The exit reflects the OUTCOME: a missing/drift skill this run wrote is resolved, not pending.
470
+ // A dry run writes nothing, so its exit keeps the CI meaning — 1 whenever work exists.
471
+ const unresolved = report.skills.filter((skill) => skill.status !== 'in-sync' && skill.written.length === 0);
472
+ return unresolved.length === 0 ? 0 : 1;
431
473
  }
432
474
  function cmdCreateSkill(options, flags, cwd, write) {
433
475
  const name = options.get('name');
@@ -1409,6 +1451,47 @@ async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
1409
1451
  write('dz install: package name required (e.g., dz install @dzhechkov/skills-devops)');
1410
1452
  return 1;
1411
1453
  }
1454
+ // install-spec-honesty (43a52cf2 + c999786b): resolve the spec to {npmSpec, dirName} BEFORE any
1455
+ // npm process runs — the versioned form used to npm-install SUCCESSFULLY (mutating the project's
1456
+ // package.json) and then die on an invented node_modules path.
1457
+ const specResolution = resolveInstallSpec(pkg, (p) => resolve(cwd, p), {
1458
+ isFile: (p) => { try {
1459
+ return statSync(p).isFile();
1460
+ }
1461
+ catch {
1462
+ return false;
1463
+ } },
1464
+ isDir: (p) => { try {
1465
+ return statSync(p).isDirectory();
1466
+ }
1467
+ catch {
1468
+ return false;
1469
+ } },
1470
+ readTarballName: (p) => {
1471
+ try {
1472
+ const out = execSync(`tar -xzOf ${JSON.stringify(p)} package/package.json`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
1473
+ const name = JSON.parse(out).name;
1474
+ return typeof name === 'string' && name !== '' ? name : null;
1475
+ }
1476
+ catch {
1477
+ return null;
1478
+ }
1479
+ },
1480
+ readDirName: (p) => {
1481
+ try {
1482
+ const name = JSON.parse(readFileSync(join(p, 'package.json'), 'utf-8')).name;
1483
+ return typeof name === 'string' && name !== '' ? name : null;
1484
+ }
1485
+ catch {
1486
+ return null;
1487
+ }
1488
+ },
1489
+ });
1490
+ if (specResolution.kind === 'refused') {
1491
+ write(`dz install: ${specResolution.reason}`);
1492
+ write(` ${specResolution.hint}`);
1493
+ return 1;
1494
+ }
1412
1495
  const targetOpt = options.get('target') ?? 'claude-code';
1413
1496
  const targetResolution = resolveTargetName(targetOpt);
1414
1497
  if (targetResolution.kind === 'unknown') {
@@ -1421,8 +1504,8 @@ async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
1421
1504
  writeErr(formatTargetAliasNote('dz install', targetOpt, target));
1422
1505
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1423
1506
  // Step 1: npm install the package (installRunner is the CliIo test seam — unset in production)
1424
- write(`Installing ${pkg}...`);
1425
- const installCmd = `npm install ${pkg} --save-dev --no-fund --no-audit`;
1507
+ write(`Installing ${specResolution.npmSpec}${specResolution.kind === 'name' ? '' : ` (${specResolution.kind} → node_modules/${specResolution.dirName})`}...`);
1508
+ const installCmd = `npm install ${JSON.stringify(specResolution.npmSpec)} --save-dev --no-fund --no-audit`;
1426
1509
  try {
1427
1510
  if (installRunner)
1428
1511
  installRunner(installCmd, projectRoot);
@@ -1433,8 +1516,9 @@ async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
1433
1516
  write(`dz install: npm install failed — ${err instanceof Error ? err.message : String(err)}`);
1434
1517
  return 1;
1435
1518
  }
1436
- // Step 2: Find SKILL.md files in the installed package
1437
- const pkgDir = join(projectRoot, 'node_modules', pkg);
1519
+ // Step 2: Find SKILL.md files in the installed package — under the RESOLVED dir name, never the
1520
+ // raw spec (path.join concatenates an absolute segment; a version suffix invents a dir).
1521
+ const pkgDir = join(projectRoot, 'node_modules', specResolution.dirName);
1438
1522
  if (!existsSync(pkgDir)) {
1439
1523
  write(`dz install: package not found at ${pkgDir}`);
1440
1524
  return 1;
@@ -1683,6 +1767,30 @@ function cmdStatuslineInstall(options, cwd, write) {
1683
1767
  function cmdStatuslineFaRecord(options, cwd, write) {
1684
1768
  const slug = (options.get('slug') ?? '').trim();
1685
1769
  const step = (options.get('step') ?? '').trim();
1770
+ // `--recalled auto` derives the count from the recall-usage log for `--run <id>`, replacing the
1771
+ // pipeline's hardcoded `--recalled 3` (a literal at three call sites — the fallback writer that
1772
+ // lights the panel had nowhere to get a real number until `dz recall` began recording itself).
1773
+ if ((options.get('recalled') ?? '').trim() === 'auto') {
1774
+ const runKey = (options.get('run') ?? '').trim();
1775
+ if (runKey === '') {
1776
+ write('dz statusline --fa-record: --recalled auto needs --run <id> — without a run key there is nothing to count');
1777
+ return 2;
1778
+ }
1779
+ // The COUNT's source and the PANEL's home are two different addresses. The pipeline records
1780
+ // recalls in the canonical BRAIN store while the panel state lives in the project — with a
1781
+ // separate `args.brain` the count read from `--project` would be a silent zero over the wrong
1782
+ // log (the dz sync 0/0 class, again). `--count-project` names the log's home explicitly and
1783
+ // defaults to `--project` for the common single-root case.
1784
+ const countRoot = resolve((options.get('count-project') ?? options.get('project') ?? cwd).trim() || cwd);
1785
+ const derived = countRecallEventsForRun(countRoot, runKey);
1786
+ if (derived === null) {
1787
+ // An unreadable log is not zero. Refusing keeps the panel's PREVIOUS state, which is the
1788
+ // honest outcome: no new claim, rather than a wrong one (the dz sync 0/0 class).
1789
+ write('dz statusline --fa-record: the recall-usage log could not be read — recording nothing rather than a wrong number');
1790
+ return 2;
1791
+ }
1792
+ options.set('recalled', String(derived));
1793
+ }
1686
1794
  if (slug === '' || step === '') {
1687
1795
  write('dz statusline --fa-record: --slug and --step are both required');
1688
1796
  write(' Example: dz statusline --fa-record --slug add-user-auth --step "Step 0" --recalled 5 --stored 2');
@@ -1822,6 +1930,9 @@ function applyUsageCalibrationToConfig(config, plan) {
1822
1930
  if (plan.changes.length > 0) {
1823
1931
  usage['calibratedAt'] = plan.after.calibratedAt;
1824
1932
  usage['source'] = plan.after.source;
1933
+ // A fresh calibration re-arms routing for THIS account and clears the legacy free-text switch:
1934
+ // the calibration is the very act the disable-note demanded.
1935
+ usage['calibrationAccount'] = plan.after.calibrationAccount ?? null;
1825
1936
  }
1826
1937
  memory['usage'] = usage;
1827
1938
  next['memory'] = memory;
@@ -1988,6 +2099,43 @@ function cmdUsageByStage(options, flags, write) {
1988
2099
  * non-zero exit. `--project <dir>` scopes ONLY the `.dz/config.json` read/write; measurement is
1989
2100
  * account-wide (all projects).
1990
2101
  */
2102
+ function cmdCadence(options, flags, cwd, write) {
2103
+ const root = resolve(cwd, options.get('project') ?? '.');
2104
+ const windowRaw = (options.get('window') ?? 'week').trim();
2105
+ if (!(windowRaw in CADENCE_WINDOW_DAYS)) {
2106
+ write('dz cadence: --window must be one of ' + Object.keys(CADENCE_WINDOW_DAYS).join('|'));
2107
+ return 1;
2108
+ }
2109
+ const r = buildCadenceReport(root, windowRaw);
2110
+ if (flags.has('json')) {
2111
+ write(JSON.stringify(r));
2112
+ return r.decision.ok ? 0 : 2;
2113
+ }
2114
+ write('dz cadence — window ' + r.window + ', record depth ' + r.depthDays + ' day(s)');
2115
+ if (!r.decision.ok) {
2116
+ write(' ' + r.decision.reason);
2117
+ if (r.decision.largestAllowed)
2118
+ write(' cure: dz cadence --window ' + r.decision.largestAllowed);
2119
+ return 2;
2120
+ }
2121
+ const weeks = [...new Set([...Object.keys(r.shipments.graded), ...Object.keys(r.npmPublishes.weekly), ...Object.keys(r.recalls.weekly)])].sort();
2122
+ write(' week shipped(graded) npm-publishes recalls');
2123
+ for (const w of weeks) {
2124
+ write(' ' + w.padEnd(12) + String(r.shipments.graded[w] ?? 0).padStart(15) + String(r.npmPublishes.weekly[w] ?? 0).padStart(15) + String(r.recalls.weekly[w] ?? 0).padStart(9));
2125
+ }
2126
+ write(' graded ' + r.shipments.gradedTotal + ' (' + Object.entries(r.shipments.byGrade).sort().map(([g, n]) => g + '×' + n).join(', ') + ') · UNGRADED ' + r.shipments.ungraded + ' (named, not hidden)');
2127
+ if (r.guard.decay.length > 0) {
2128
+ write(' guard repeat decay (FIXED set — rules with pre-window history only):');
2129
+ for (const d of r.guard.decay.slice(0, 8))
2130
+ write(' ' + d.rule.padEnd(28) + 'before×' + d.before + ' → in-window×' + d.inWindow);
2131
+ }
2132
+ if (r.guard.excludedNewborn.length > 0)
2133
+ write(' excluded newborn rule(s) (no pre-window history — a zero here would be youth, not virtue): ' + r.guard.excludedNewborn.join(', '));
2134
+ for (const dgr of [r.npmPublishes.degraded, r.guard.degraded, r.recalls.degraded])
2135
+ if (dgr)
2136
+ write(' DEGRADED: ' + dgr);
2137
+ return 0;
2138
+ }
1991
2139
  function cmdUsage(options, optionLists, flags, cwd, write) {
1992
2140
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1993
2141
  const nullContract = () => JSON.stringify({
@@ -2021,6 +2169,12 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
2021
2169
  limits: limitsPayload,
2022
2170
  estimated: true,
2023
2171
  };
2172
+ // ADR-001 usage-honesty: a consumer that reads null pcts deserves the WHY (closed reason
2173
+ // set), and a human deserves the raw estimates when POLICY (not measurement) nulled them.
2174
+ if (u.notEstablished.length > 0)
2175
+ payload.notEstablished = u.notEstablished;
2176
+ if (u.estimatesNotForRouting !== undefined)
2177
+ payload.estimatesNotForRouting = u.estimatesNotForRouting;
2024
2178
  if (hasModelLimits && u.weeklyByModel !== undefined)
2025
2179
  payload.weeklyByModel = u.weeklyByModel;
2026
2180
  // re-QE debt surfacing (backlog 6b40e667 — QE #9: the json contract must carry the debt too,
@@ -2035,6 +2189,25 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
2035
2189
  write(JSON.stringify(payload));
2036
2190
  return 0;
2037
2191
  }
2192
+ if (u.sessionPct === null && u.weeklyPct === null && u.notEstablished.length > 0) {
2193
+ // Limits may be fully configured and the pcts STILL null — that is the honesty, not a config
2194
+ // gap. Say why, and show the human the raw estimates when only policy nulled them.
2195
+ write('usage: not established — ' + u.notEstablished.join(', '));
2196
+ if (u.estimatesNotForRouting !== undefined) {
2197
+ const e = u.estimatesNotForRouting;
2198
+ write(' estimates (NOT for routing): session ~' + (e.sessionPct ?? '?') + '% · week ~' + (e.weeklyPct ?? '?') + '% — recalibrate on THIS account: dz usage --calibrate --session <pct> --weekly <pct>');
2199
+ }
2200
+ else {
2201
+ write(' the scan established nothing (' + u.sessionTokens + ' session / ' + u.weeklyTokens + ' weekly tokens counted) — a percentage would be a guess, and routing must not eat guesses');
2202
+ }
2203
+ try {
2204
+ const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
2205
+ if (reqe.debts.length > 0)
2206
+ write('re-QE due: ' + reqe.debts.length + ' usage-switched run(s) kept same-family QE — run `dz reqe` for the cross-family pass');
2207
+ }
2208
+ catch { /* advisory only */ }
2209
+ return 0;
2210
+ }
2038
2211
  if (u.sessionPct === null && u.weeklyPct === null) {
2039
2212
  write('usage: unconfigured — set memory.usage.sessionTokenLimit / weeklyTokenLimit in .dz/config.json (percentages are ESTIMATES calibrated from observed exhaustion)');
2040
2213
  try {
@@ -2780,14 +2953,39 @@ async function cmdRecall(options, flags, cwd, write) {
2780
2953
  limit,
2781
2954
  ...(bookFilter !== undefined ? { book: bookFilter } : {}),
2782
2955
  });
2783
- const { hits, error } = asJson ? await withForeignStdoutOnStderr(runBooks) : await runBooks();
2956
+ let booksResult;
2957
+ try {
2958
+ booksResult = asJson ? await withForeignStdoutOnStderr(runBooks) : await runBooks();
2959
+ }
2960
+ catch (err) {
2961
+ // A corrupted store used to surface as a bare top-level `dz: file is not a database` — exit 1
2962
+ // (honest) but with no path and no cure (a2a574a9/c7aec002). Name both.
2963
+ const msg = err instanceof Error ? err.message : String(err);
2964
+ write(`dz recall --books: the book store is unreadable — ${msg}`);
2965
+ write(` store: ${join(projectRoot, '.dz', 'memory', 'books.sqlite')}`);
2966
+ write(' cure: restore it from a backup, or delete the file and re-digitize (dz brain add --from-slice / book-digitizer) — deleting loses only this project\'s local shelf, the machine-wide brain is separate');
2967
+ return 1;
2968
+ }
2969
+ const { hits, error } = booksResult;
2970
+ // f1451a6a: an EXPLICIT --semantic request whose vector instrument did not run must not read as
2971
+ // a clean 0-hit search. error!==undefined here means the INSTRUMENT failed (embedder/sqlite
2972
+ // unavailable), not that the shelf is empty — house convention exit 3 = not-established.
2973
+ const semanticNotEstablished = flags.has('semantic') && error !== undefined;
2784
2974
  if (asJson) {
2975
+ if (semanticNotEstablished) {
2976
+ write(JSON.stringify({ hits, notEstablished: error }));
2977
+ return 3;
2978
+ }
2785
2979
  write(JSON.stringify(hits));
2786
2980
  return 0;
2787
2981
  }
2788
2982
  write(`dz recall "${query}" --books${bookFilter !== undefined ? ` --book ${bookFilter}` : ''} — ${hits.length} KU hit(s)`);
2789
2983
  if (error !== undefined)
2790
2984
  write(` (${error})`);
2985
+ if (semanticNotEstablished) {
2986
+ write(' --semantic was EXPLICIT and the vector instrument did not run — nothing was established (exit 3, not a clean 0)');
2987
+ return 3;
2988
+ }
2791
2989
  for (const h of hits) {
2792
2990
  const src = h.chapter !== undefined ? ` [${h.book} гл.${h.chapter}${h.pages ? ` с.${h.pages[0]}-${h.pages[1]}` : ''}]` : ` [${h.book}]`;
2793
2991
  write(` (${h.type}) ${h.name}${src}`);
@@ -2902,6 +3100,12 @@ async function cmdRecall(options, flags, cwd, write) {
2902
3100
  write(JSON.stringify(hits.map((h) => ({
2903
3101
  ...h.pattern,
2904
3102
  relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
3103
+ // The TRUE cosine, as a second companion key — the design named it and the first ship missed
3104
+ // it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
3105
+ // every probe returned "no similarity" through --json while the human output showed sim=).
3106
+ // Unlike `relevance` it SURVIVES a domain boost: closeness is order-independent (ADR-001 of
3107
+ // recall-true-closeness), and null means "not measured for this hit", never zero.
3108
+ similarity: 'similarity' in h && typeof h.similarity === 'number' ? h.similarity : null,
2905
3109
  }))));
2906
3110
  // The honesty notes go to STDERR here rather than being skipped: the JSON branch
2907
3111
  // used to return before them, so a scripted caller was told nothing about a boost
@@ -2987,9 +3191,13 @@ async function cmdRecall(options, flags, cwd, write) {
2987
3191
  if (vectorOn && hits.length > 0) {
2988
3192
  const measured = hits.filter((h) => typeof h.similarity === 'number' && Number.isFinite(h.similarity));
2989
3193
  if (measured.length > 0) {
3194
+ const runId = (options.get('run') ?? '').trim();
2990
3195
  appendRecallUsage({
2991
3196
  projectRoot: projectRoot,
2992
3197
  query: shownQuery,
3198
+ // `--run` threads a caller's run key into the log, so a later `--recalled auto` can count
3199
+ // THIS run's events instead of asserting a literal (the /feature-adr panel's `--recalled 3`).
3200
+ ...(runId === '' ? {} : { runId }),
2993
3201
  hits: measured.map((h) => ({ dzId: patternRecordId(h.pattern), score: h.similarity })),
2994
3202
  });
2995
3203
  }
@@ -9143,6 +9351,30 @@ function packNpmName(packDir) {
9143
9351
  return undefined;
9144
9352
  }
9145
9353
  }
9354
+ /**
9355
+ * Parse `pnpm pack --json` STDOUT robustly: a package with a `prepack` script echoes lifecycle
9356
+ * banners first, and the banner text itself may contain '[' or '{' (skills-feature-adr's guard
9357
+ * does) — so candidates are tried from the LAST line-start opener backwards; pnpm's JSON is the
9358
+ * final thing on stdout. MEASURED 2026-08-25: byte-0 parse failed on the banner, first-opener
9359
+ * parse failed on the banner's own array literal.
9360
+ */
9361
+ function parsePnpmPackJson(out) {
9362
+ const starts = [];
9363
+ for (let li = 0; li < out.length; li = out.indexOf('\n', li) + 1) {
9364
+ const ch = out[li];
9365
+ if (ch === '{' || ch === '[')
9366
+ starts.push(li);
9367
+ if (out.indexOf('\n', li) === -1)
9368
+ break;
9369
+ }
9370
+ for (let ci = starts.length - 1; ci >= 0; ci--) {
9371
+ try {
9372
+ return JSON.parse(out.slice(starts[ci]));
9373
+ }
9374
+ catch { /* try an earlier candidate */ }
9375
+ }
9376
+ throw new Error('pnpm pack emitted no parseable JSON');
9377
+ }
9146
9378
  function npmPackedPaths(packDir) {
9147
9379
  // `pnpm`, not `npm`: the PUBLISHER is `pnpm publish` (see `publishArgv`), and the two packers do not
9148
9380
  // agree. MEASURED 2026-08-21 on `skills-news`: `npm pack` emits a 1051-byte package.json identical
@@ -9154,7 +9386,7 @@ function npmPackedPaths(packDir) {
9154
9386
  encoding: 'utf-8',
9155
9387
  maxBuffer: 64 * 1024 * 1024,
9156
9388
  });
9157
- const parsed = JSON.parse(out);
9389
+ const parsed = parsePnpmPackJson(out);
9158
9390
  const entry = Array.isArray(parsed) ? parsed[0] : parsed;
9159
9391
  const files = entry?.files ?? [];
9160
9392
  return files.map((f) => f.path.replace(/^package\//, '')).sort();
@@ -9186,7 +9418,7 @@ function extractIntoTempDir(packDir, tmp) {
9186
9418
  encoding: 'utf-8',
9187
9419
  maxBuffer: 64 * 1024 * 1024,
9188
9420
  });
9189
- const parsed = JSON.parse(out);
9421
+ const parsed = parsePnpmPackJson(out);
9190
9422
  const entry = Array.isArray(parsed) ? parsed[0] : parsed;
9191
9423
  const tgz = entry?.filename;
9192
9424
  if (tgz === undefined)
@@ -9366,7 +9598,12 @@ function recapPublishes(repo) {
9366
9598
  }
9367
9599
  }
9368
9600
  const sorted = items.map((i) => i.iso).sort();
9369
- return { dataStart: sorted.length > 0 ? sorted[0].slice(0, 10) : null, items };
9601
+ return {
9602
+ dataStart: sorted.length > 0 ? sorted[0].slice(0, 10) : null,
9603
+ items,
9604
+ ...(typeof raw.fetchedAt === 'string' ? { fetchedAt: raw.fetchedAt } : {}),
9605
+ ...(Array.isArray(raw.failed) && raw.failed.length > 0 ? { failed: raw.failed.filter((f) => typeof f === 'string') } : {}),
9606
+ };
9370
9607
  }
9371
9608
  catch {
9372
9609
  return null;
@@ -9680,9 +9917,303 @@ function cmdNameCheck(options, flags, cwd, write) {
9680
9917
  write(line);
9681
9918
  return decision.exit;
9682
9919
  }
9683
- function cmdRecap(options, flags, cwd, write) {
9920
+ // ── recap publish-times cache (feature recap-publish-cache) ──────────────────
9921
+ //
9922
+ // The Publishes section is the one recap section whose timestamps a third party holds — and the one
9923
+ // that was always `unavailable`, because nothing ever filled its cache. The fetch lives HERE, behind
9924
+ // an explicit flag, never inside the report: 51 packages cost 18.3s sequentially (measured in the
9925
+ // recap design), and a report that does network I/O is a report that fails when the network does.
9926
+ const RECAP_NPM_CACHE_REL = '.dz/recap/npm-times.json';
9927
+ /**
9928
+ * Ask the registry when every non-private workspace package's versions were published.
9929
+ *
9930
+ * Genuinely parallel in batches of 8 (`execFile`, not `spawnSync` — a sync call in a loop stays
9931
+ * sequential whatever the batch size claims, which is exactly the kind of false label this pipeline
9932
+ * keeps removing): measured in the recap design at 5.7s against 18.3s sequential over 51 packages.
9933
+ * A package whose lookup fails lands in `failed`, never silently absent — the same class as the
9934
+ * scout 401 that printed `github: 0` and read as "nothing new exists".
9935
+ */
9936
+ async function fetchNpmPublishTimes(repoRoot, write) {
9937
+ const pkgsRoot = join(repoRoot, 'packages', '@dzhechkov');
9938
+ const names = [];
9939
+ if (existsSync(pkgsRoot)) {
9940
+ for (const e of readdirSync(pkgsRoot, { withFileTypes: true })) {
9941
+ if (!e.isDirectory())
9942
+ continue;
9943
+ const pj = join(pkgsRoot, e.name, 'package.json');
9944
+ if (!existsSync(pj))
9945
+ continue;
9946
+ try {
9947
+ const meta = JSON.parse(readFileSync(pj, 'utf-8'));
9948
+ if (meta.private === true || typeof meta.name !== 'string' || meta.name === '')
9949
+ continue;
9950
+ // Deduplicated: two workspace directories can declare one npm name, and a failure would
9951
+ // then count twice — "2 FAILED" about one distinct package (review round 2).
9952
+ if (!names.includes(meta.name))
9953
+ names.push(meta.name);
9954
+ }
9955
+ catch { /* an unreadable package.json is not a publishable package */ }
9956
+ }
9957
+ }
9958
+ const packages = {};
9959
+ const failed = [];
9960
+ const one = (name) => new Promise((done) => {
9961
+ execFile('npm', ['view', name, 'time', '--json'], { timeout: 30_000, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
9962
+ if (err !== null) {
9963
+ failed.push(name);
9964
+ done();
9965
+ return;
9966
+ }
9967
+ try {
9968
+ const times = JSON.parse(stdout);
9969
+ const versions = {};
9970
+ for (const [k, v] of Object.entries(times)) {
9971
+ if (k === 'created' || k === 'modified' || typeof v !== 'string')
9972
+ continue;
9973
+ versions[k] = v;
9974
+ }
9975
+ // A published package has at least one version by construction — npm will not host an
9976
+ // empty one. Valid-but-empty JSON is therefore an ANOMALY, not a zero: recording it as a
9977
+ // success would let the report read "0 publishes" about a package whose answer was broken
9978
+ // (cross-family review, codex gpt-5.6-sol, 2026-08-24, grade B).
9979
+ if (Object.keys(versions).length === 0)
9980
+ failed.push(name);
9981
+ else
9982
+ packages[name] = { versions };
9983
+ }
9984
+ catch {
9985
+ failed.push(name);
9986
+ }
9987
+ done();
9988
+ });
9989
+ });
9990
+ const BATCH = 8;
9991
+ for (let i = 0; i < names.length; i += BATCH) {
9992
+ await Promise.all(names.slice(i, i + BATCH).map(one));
9993
+ write(`dz recap: registry queried ${Math.min(i + BATCH, names.length)}/${names.length}…`);
9994
+ }
9995
+ // `failed` sorted so the cache is deterministic for a given outcome set.
9996
+ return { version: 1, fetchedAt: new Date().toISOString(), packages, failed: [...failed].sort() };
9997
+ }
9998
+ // ── `dz tg-post` (feature genai-tweets-channel, стадия 0→1) ──────────────────
9999
+ //
10000
+ // The sender for an APPROVED draft, implementing the channel's own accepted ADRs: HTML mode (never
10001
+ // MarkdownV2 — 18 escapes against 3), link preview off by default (x.com previews in Telegram have
10002
+ // been broken since 2022), the 00:00-06:00 MSK quiet window, and ADR-004's standing order that
10003
+ // publishing stays MANUAL — the default run is a dry-run, and a real send needs --send --yes.
10004
+ //
10005
+ // The provenance gate runs IN-PROCESS before any send: the draft's sources.json goes through the
10006
+ // same classification `dz provenance-check` uses, and anything but ALLOWED refuses. A draft with no
10007
+ // manifest is refused too — unchecked is not approved.
10008
+ function tgReadToken(repoRoot, write) {
10009
+ // Token sources, in order: env, then a tokenFile named in .dz/config.json. The token itself is
10010
+ // NEVER printed, logged, or included in any error — only where it was looked for.
10011
+ const env = process.env['TELEGRAM_BOT_TOKEN'];
10012
+ if (env !== undefined && env.trim() !== '')
10013
+ return env.trim();
10014
+ try {
10015
+ const cfg = JSON.parse(readFileSync(join(repoRoot, '.dz', 'config.json'), 'utf-8'));
10016
+ const file = cfg.telegram?.tokenFile;
10017
+ if (typeof file === 'string' && file.trim() !== '' && existsSync(file)) {
10018
+ const text = readFileSync(file, 'utf-8');
10019
+ const m = /TELEGRAM_BOT_TOKEN\s*=\s*"?([^"\n]+)"?/.exec(text);
10020
+ if (m?.[1] !== undefined)
10021
+ return m[1].trim();
10022
+ const bare = text.trim();
10023
+ if (bare !== '' && !bare.includes('\n'))
10024
+ return bare;
10025
+ }
10026
+ }
10027
+ catch { /* fall through to the honest null */ }
10028
+ write('dz tg-post: no bot token — set TELEGRAM_BOT_TOKEN or telegram.tokenFile in .dz/config.json');
10029
+ return null;
10030
+ }
10031
+ function tgApi(token, method, body) {
10032
+ return new Promise((done) => {
10033
+ const payload = JSON.stringify(body);
10034
+ const req = httpsRequest({
10035
+ hostname: 'api.telegram.org',
10036
+ path: `/bot${token}/${method}`,
10037
+ method: 'POST',
10038
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) },
10039
+ timeout: 30_000,
10040
+ }, (res) => {
10041
+ let data = '';
10042
+ res.on('data', (c) => { data += c.toString('utf-8'); });
10043
+ res.on('end', () => {
10044
+ try {
10045
+ const parsed = JSON.parse(data);
10046
+ done({ ok: parsed.ok === true, ...(typeof parsed.description === 'string' ? { description: parsed.description } : {}), ...(typeof parsed.result?.message_id === 'number' ? { messageId: parsed.result.message_id } : {}) });
10047
+ }
10048
+ catch {
10049
+ done({ ok: false, description: `unparseable response (HTTP ${res.statusCode ?? '?'})` });
10050
+ }
10051
+ });
10052
+ });
10053
+ req.on('error', (e) => done({ ok: false, description: e.message }));
10054
+ req.on('timeout', () => { req.destroy(); done({ ok: false, description: 'timeout after 30s' }); });
10055
+ req.write(payload);
10056
+ req.end();
10057
+ });
10058
+ }
10059
+ async function cmdTgPost(options, flags, cwd, write) {
10060
+ const repoRoot = resolve(options.get('project') ?? cwd);
10061
+ const json = flags.has('json');
10062
+ const draftPath = (options.get('draft') ?? '').trim();
10063
+ if (draftPath === '') {
10064
+ write('dz tg-post: --draft <file.html> is required');
10065
+ return 2;
10066
+ }
10067
+ let draft;
10068
+ try {
10069
+ draft = readFileSync(resolve(cwd, draftPath), 'utf-8');
10070
+ }
10071
+ catch {
10072
+ write(`dz tg-post: cannot read draft ${draftPath}`);
10073
+ return 2;
10074
+ }
10075
+ const issues = tgPostHtmlIssues(draft);
10076
+ // Provenance, in-process. `--manifest` names the draft's sources; its absence is a refusal at the
10077
+ // decision layer, not a quiet pass here.
10078
+ let provenanceOutcome = 'skipped';
10079
+ const manifestPath = (options.get('manifest') ?? '').trim();
10080
+ if (manifestPath !== '') {
10081
+ let manifest = null;
10082
+ try {
10083
+ manifest = parseSourceManifest(readFileSync(resolve(cwd, manifestPath), 'utf-8'));
10084
+ }
10085
+ catch {
10086
+ manifest = null;
10087
+ }
10088
+ const publicRecords = new Set();
10089
+ const show = spawnSync('git', ['-C', repoRoot, 'show', `HEAD:${PROVENANCE_PUBLIC_REL}`], { encoding: 'utf-8', maxBuffer: 8 * 1024 * 1024 });
10090
+ if (show.error === undefined && show.status === 0) {
10091
+ try {
10092
+ const raw = JSON.parse(show.stdout ?? '');
10093
+ if (Array.isArray(raw.records))
10094
+ for (const r of raw.records)
10095
+ if (typeof r === 'string')
10096
+ publicRecords.add(r);
10097
+ }
10098
+ catch { /* an unparseable committed list clears nothing */ }
10099
+ }
10100
+ const resolved = new Map();
10101
+ const toAsk = [];
10102
+ for (const c of manifest?.claims ?? []) {
10103
+ if (c.kind !== 'path' || typeof c.source !== 'string' || c.source.trim() === '')
10104
+ continue;
10105
+ const src = c.source.trim();
10106
+ if (resolved.has(src))
10107
+ continue;
10108
+ const real = provenanceResolve(repoRoot, src);
10109
+ resolved.set(src, real);
10110
+ if (real !== null)
10111
+ toAsk.push(real);
10112
+ }
10113
+ const tracked = new Set();
10114
+ const dirty = new Set();
10115
+ if (toAsk.length > 0) {
10116
+ const ls = spawnSync('git', ['-C', repoRoot, 'ls-files', '-z', '--', ...toAsk], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
10117
+ if (ls.error === undefined && ls.status === 0) {
10118
+ for (const rel of (ls.stdout ?? '').split('\0'))
10119
+ if (rel !== '')
10120
+ tracked.add(resolve(repoRoot, rel));
10121
+ }
10122
+ const onDisk = spawnSync('git', ['-C', repoRoot, 'hash-object', '--no-filters', '--', ...toAsk], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
10123
+ const inHead = spawnSync('git', ['-C', repoRoot, 'ls-tree', '-z', 'HEAD', '--', ...toAsk], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
10124
+ if (onDisk.error !== undefined || onDisk.status !== 0 || inHead.error !== undefined || inHead.status !== 0) {
10125
+ for (const p of toAsk)
10126
+ dirty.add(p);
10127
+ }
10128
+ else {
10129
+ const diskHashes = (onDisk.stdout ?? '').split('\n').map((l) => l.trim()).filter((l) => l !== '');
10130
+ const headHash = new Map();
10131
+ for (const rec of (inHead.stdout ?? '').split('\0')) {
10132
+ if (rec === '')
10133
+ continue;
10134
+ const tab = rec.indexOf('\t');
10135
+ if (tab < 0)
10136
+ continue;
10137
+ const parts = rec.slice(0, tab).split(/\s+/);
10138
+ headHash.set(resolve(repoRoot, rec.slice(tab + 1)), parts[2] ?? '');
10139
+ }
10140
+ toAsk.forEach((abs, i) => {
10141
+ const committed = headHash.get(abs);
10142
+ if (committed === undefined || committed !== diskHashes[i])
10143
+ dirty.add(abs);
10144
+ });
10145
+ }
10146
+ }
10147
+ const decision = decideSourceProvenance(manifest, {
10148
+ ignoredPaths: provenanceIgnored(repoRoot, toAsk),
10149
+ publicRecords,
10150
+ resolved,
10151
+ trackedPaths: tracked,
10152
+ dirtyPaths: dirty,
10153
+ });
10154
+ provenanceOutcome = decision.outcome;
10155
+ if (!json)
10156
+ for (const line of renderSourceProvenance(decision))
10157
+ write(line);
10158
+ }
10159
+ const decision = decideTgSend({
10160
+ issues,
10161
+ provenanceOutcome,
10162
+ confirmed: flags.has('send') && flags.has('yes'),
10163
+ nowUtcIso: new Date().toISOString(),
10164
+ nightOverride: flags.has('night'),
10165
+ });
10166
+ const visible = tgVisibleLength(draft);
10167
+ if (!json) {
10168
+ for (const i of issues)
10169
+ write(` [${i.kind}] ${i.detail}`);
10170
+ write(`dz tg-post: ${decision.action === 'send' ? 'SENDING' : 'DRY-RUN / REFUSED'} — ${decision.reason}`);
10171
+ write(` ${visible} visible character(s) of ${TG_TEXT_LIMIT}; provenance: ${provenanceOutcome}`);
10172
+ }
10173
+ if (decision.action !== 'send') {
10174
+ if (json)
10175
+ write(JSON.stringify({ ok: false, action: decision.action, reason: decision.reason, issues, visible, provenance: provenanceOutcome }));
10176
+ // A refused send exits 1; a clean DRY-RUN (no --send asked) exits 0 — asking to see is not a failure.
10177
+ return flags.has('send') ? 1 : issues.length > 0 ? 1 : 0;
10178
+ }
10179
+ const token = tgReadToken(repoRoot, write);
10180
+ if (token === null)
10181
+ return 2;
10182
+ const channel = (options.get('channel') ?? '').trim();
10183
+ if (channel === '') {
10184
+ write('dz tg-post: --channel @name (or a chat id) is required to send');
10185
+ return 2;
10186
+ }
10187
+ const sent = await tgApi(token, 'sendMessage', {
10188
+ chat_id: channel,
10189
+ text: draft,
10190
+ parse_mode: 'HTML',
10191
+ // ADR-004: x.com previews in Telegram are broken — never rely on them.
10192
+ link_preview_options: { is_disabled: !flags.has('preview') },
10193
+ });
10194
+ if (!sent.ok) {
10195
+ write(`dz tg-post: Telegram refused — ${sent.description ?? 'no description'}`);
10196
+ if (json)
10197
+ write(JSON.stringify({ ok: false, action: 'send', reason: sent.description ?? 'refused', visible }));
10198
+ return 1;
10199
+ }
10200
+ write(`dz tg-post: sent to ${channel}${sent.messageId !== undefined ? ` (message ${sent.messageId})` : ''}`);
10201
+ if (json)
10202
+ write(JSON.stringify({ ok: true, action: 'send', channel, messageId: sent.messageId ?? null, visible }));
10203
+ return 0;
10204
+ }
10205
+ async function cmdRecap(options, flags, cwd, write) {
9684
10206
  const repo = resolve(options.get('project') ?? cwd);
9685
10207
  const json = flags.has('json');
10208
+ if (flags.has('refresh-publishes')) {
10209
+ const cache = await fetchNpmPublishTimes(repo, json ? () => { } : write);
10210
+ const cachePath = join(repo, ...RECAP_NPM_CACHE_REL.split('/'));
10211
+ mkdirSync(dirname(cachePath), { recursive: true });
10212
+ writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}\n`);
10213
+ if (!json) {
10214
+ write(`dz recap: publish-times cache written — ${Object.keys(cache.packages).length} package(s)${cache.failed.length > 0 ? `, ${cache.failed.length} FAILED: ${cache.failed.join(', ')}` : ''}`);
10215
+ }
10216
+ }
9686
10217
  // Every horizon is RECOGNISED, including the refused ones — swallowing `--year` silently would be
9687
10218
  // the same defect as a gate that passes because it never ran.
9688
10219
  const asked = ['day', 'week', 'month', ...REFUSED_HORIZONS].filter((h) => flags.has(h));
@@ -9718,11 +10249,23 @@ function cmdRecap(options, flags, cwd, write) {
9718
10249
  reuse: recapReuse(repo, window),
9719
10250
  uncommittedSlugs: deliveries.uncommitted.sort(),
9720
10251
  });
9721
- if (json)
9722
- write(JSON.stringify(report));
9723
- else
10252
+ if (json) {
10253
+ write(JSON.stringify({ ...report, publishesMeta: publishes === null ? null : { fetchedAt: publishes.fetchedAt ?? null, failed: publishes.failed ?? [] } }));
10254
+ }
10255
+ else {
9724
10256
  for (const line of renderRecap(report))
9725
10257
  write(line);
10258
+ // The cache's AGE and its failures print beside the numbers they qualify: third-party
10259
+ // timestamps are only as fresh as the last explicit --refresh-publishes, and a package the
10260
+ // registry did not answer for is MISSING from the section above — said, not implied.
10261
+ if (publishes?.fetchedAt !== undefined) {
10262
+ const days = Math.floor((Date.parse(`${at.slice(0, 10)}T00:00:00Z`) - Date.parse(publishes.fetchedAt)) / 86_400_000);
10263
+ write(` ℹ publish-times cache fetched ${publishes.fetchedAt.slice(0, 10)}${Number.isFinite(days) && days > 0 ? ` — ${days} day(s) before this report's anchor` : ''}; refresh with --refresh-publishes`);
10264
+ }
10265
+ if (publishes?.failed !== undefined) {
10266
+ write(` ⚠ the registry did not answer for ${publishes.failed.length} package(s) at the last refresh: ${publishes.failed.join(', ')} — their publishes are MISSING above, not zero`);
10267
+ }
10268
+ }
9726
10269
  return 0;
9727
10270
  }
9728
10271
  function cmdFeatureAdrRecord(options, flags, cwd, write) {
@@ -12649,6 +13192,15 @@ export async function runCli(argv, io = {}) {
12649
13192
  }
12650
13193
  };
12651
13194
  const { command, options, optionLists, flags } = parseArgs(argv);
13195
+ // An unrecognised `--flag` must not pass in silence. MEASURED 2026-08-24: `dz recall "x" --breif
13196
+ // --limit 2` printed the full ordinary output and exited 0, so a typo read as "the mode worked".
13197
+ // It WARNS rather than refuses, and the reason is measured, not cautious: 53 of the names this CLI
13198
+ // reads appear nowhere in help, and static extraction over the dispatch table lost `--week` from
13199
+ // `dz recap` — a refusal built on either list would reject working commands, which is a worse
13200
+ // failure than the one being fixed. Goes to STDERR so a `--json` consumer's stdout stays clean.
13201
+ for (const notice of unknownFlagNotice([...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')), KNOWN_CLI_FLAGS)) {
13202
+ writeErr(notice.line);
13203
+ }
12652
13204
  // ── `dz --version` / `dz -v` / `dz version` — PRE-DISPATCH, before the help branch ──
12653
13205
  //
12654
13206
  // Until now `dz --version` printed the whole USAGE manual and exited 0 (MEASURED 2026-08-17,
@@ -12801,6 +13353,10 @@ export async function runCli(argv, io = {}) {
12801
13353
  return cmdScore(options, flags, cwd, write);
12802
13354
  case 'recap':
12803
13355
  return cmdRecap(options, flags, cwd, write);
13356
+ case 'cadence':
13357
+ return cmdCadence(options, flags, cwd, write);
13358
+ case 'tg-post':
13359
+ return cmdTgPost(options, flags, cwd, write);
12804
13360
  case 'name-check':
12805
13361
  return cmdNameCheck(options, flags, cwd, write);
12806
13362
  case 'provenance-check':