@dzhechkov/harness-cli 0.3.200 → 0.3.201

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/README.md CHANGED
@@ -132,7 +132,7 @@ point any tool at (a skill is just Markdown). Dedicated targets are on the [road
132
132
 
133
133
  ## User Journey — from install to mastery
134
134
 
135
- All 39 commands mapped to a real workflow:
135
+ All 41 commands mapped to a real workflow:
136
136
 
137
137
  ```
138
138
  DISCOVER → INSTALL → USE → CREATE → MAINTAIN → SHARE
@@ -356,7 +356,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
356
356
 
357
357
  > **A skill and its npx toolkit are not duplicates — they're a graduation.** Several skills (e.g. `feature-adr`, `design-thinking`) exist BOTH as a skill inside a `dz` preset AND as a standalone `npx` package. The preset's SKILL.md is **fully functional on its own** (the whole methodology — modules + references — travels with it, and it auto-activates by description), and it's the only way to compile that capability to the **non-Claude platforms** (Codex/OpenCode/Hermes/OpenClaude) via `dz`. The npx package adds **project-level runtime governance** around the same skill: a slash command, governance rules, a context shard, and (for feature-adr) reward-learning + `/harvest`. So: pick the **skill/preset** for a working capability across platforms; pick the **npx toolkit** when you want it as a governed, command-driven fixture of one project.
358
358
 
359
- ## All Commands (40)
359
+ ## All Commands (41)
360
360
 
361
361
  ```
362
362
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
@@ -377,6 +377,7 @@ dz vector harmonize [--apply] [--threshold <0..1>] [--json] # SEMANTIC me
377
377
  dz teach --harmonize [--apply] [--threshold <0..1>] # alias of `dz vector harmonize`
378
378
  dz statusline [--json] [--install] # compact Claude Code statusline: live self-learning pattern count + brain sources
379
379
  dz usage [--json] [--project <dir>] | --calibrate --session <pct> --weekly <pct> [--model fable=<pct>] # ESTIMATE Claude usage from fixed reset windows; optional per-model weekly binding; exit 0 ALWAYS
380
+ dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>] # enforce the Integrity Rule: flag untagged/overstated accuracy claims; default scan = READMEs + features' 08_qe_report.md; exit 1 only at/above --fail-on (default high)
380
381
  dz brain list [--json] # the durable cross-project knowledge brain
381
382
  dz brain query "<q>" [--source <slug>] [--limit <N>] [--any] [--rerank] [--json] # cross-source recall (--any = OR match; --rerank reorders on-point first). Auto-broadens: if strict all-terms match yields 0 hits, it retries once as OR and labels the result "broadened" (text note + broadened:true in --json) instead of returning empty. Explicit --any is OR from the start (never labeled broadened).
382
383
  dz brain add [--source <slug>] [--project <dir>] [--from-slice <f>|--from-pack <p>|--from-kus <f> --slug <s> --kind <k> --license <spdx> [--override]] [--json] # grow the brain
@@ -400,7 +401,7 @@ dz create-skill --name <id> [--description <text>] [--tier 1|2|3] [--with-r
400
401
  dz registry [search <query>] [--category <cat>]
401
402
  dz benchmark <skill-dir> [--compare <dir>] [--all]
402
403
  dz mcp-scan [path] [--json] (static agent-permission audit; exit 0/1/2 = clean/medium/high)
403
- dz publish [--filter <name>] [--bump-only] (dry-run by default; pass --yes/--confirm to go live)
404
+ dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] (dry-run by default; pass --yes/--confirm to go live; claim-check gate defaults to warn — surfaces README claim findings, never blocks)
404
405
  dz auto-canonicalize --source <github-url> --pack <skills-pack>
405
406
  dz sync-upstream [--package <dir>] [--list] [--all]
406
407
  dz drift-check [--all] [--json] [--project <dir>] # CI gate: exit 1 on NEW shared-skill drift (baseline: .dz/drift-allowlist.json; --all incl .claude dogfood)
@@ -528,6 +529,43 @@ phase boundary and pre-emptively routes the remaining stages to Codex when `sess
528
529
  dispatch dies (`codex:codex-rescue` is a Claude wrapper), so the switch must happen BEFORE — the
529
530
  pre-emptive probe, not reactive detection, is the real defense.
530
531
 
532
+ ### Claim check (`dz claim-check`)
533
+
534
+ Makes the Integrity Rule — *"no fake claims; verify before claiming success"* — a runnable check
535
+ instead of prose. Every quantitative claim (coverage, test/skill/command counts, benchmarks) must be
536
+ tagged `MEASURED` / `CLAIMED` / `SYNTHETIC` / `ESTIMATED` / `UNVALIDATED` / `BASELINE`, and a
537
+ `MEASURED` claim must name its reproducer (`npm test`, a coverage report, `npm view`, a git ref).
538
+ "100% / perfect" framing is always high severity.
539
+
540
+ ```bash
541
+ dz claim-check # scans root README, every package README, features/*/08_qe_report.md
542
+ dz claim-check docs/perf.md --json # {"ok":false,"findings":[…],"scanned":[…]}
543
+ dz claim-check --fail-on medium # stricter gate for CI
544
+ ```
545
+
546
+ **Exit-code contract:** `0` when ok; `1` only when a finding at or above `--fail-on` (default `high`)
547
+ exists. `--json` always emits valid JSON, even on the failure path, and never throws on an
548
+ unreadable, binary, or missing file — those are skipped and reported in `scanned`.
549
+
550
+ `dz publish` runs the same check over each package's README. The gate defaults to `warn`: findings
551
+ are surfaced but publish status is **never** changed. Pass `--claim-check error` to fail a package
552
+ that carries a high-severity claim, or `--claim-check off` to disable it.
553
+
554
+ ```bash
555
+ dz publish --filter harness-cli # warn (default): prints "⚠ claim-check: N finding(s) (M high)"
556
+ dz publish --filter harness-cli --claim-check error # a high finding fails THIS package only
557
+ dz publish --filter harness-cli --claim-check off # disable the gate entirely
558
+ ```
559
+
560
+ Ported from rUv's [`ruview`](https://www.npmjs.com/package/@ruvnet/ruview) `src/guardrails.js` (MIT),
561
+ which was itself written after a documented AI-slop incident. The detection semantics are kept
562
+ verbatim — including a deliberately `\b`-free `100%` regex that looks like a bug and is not.
563
+
564
+ **Honest note:** run on this repo it reports hundreds of medium findings and a handful of high ones
565
+ (reproducer: `dz claim-check --json`, which prints the exact current counts — deliberately not frozen
566
+ into this README, since any doc edit moves them). That is the point of the tool, not a defect in it:
567
+ the high findings are real untagged perfect-score claims in our own documentation.
568
+
531
569
  ### Targets (10 platforms — 5 lossless + Copilot + AGENTS.md + Cursor + Gemini + Windsurf)
532
570
 
533
571
  Five platforms natively support the [agentskills.io](https://agentskills.io) `SKILL.md` format:
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2JH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAq5GD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgH5E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA8JH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAyiHD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkH5E"}
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'node:child_process';
9
9
  import { homedir } from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, recallHybrid, teachGuard, mirrorPatternsToVector, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, queryBookKnowledge, loadStorePatternsSync, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, } from '@dzhechkov/harness-core';
12
+ import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, recallHybrid, teachGuard, mirrorPatternsToVector, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, } from '@dzhechkov/harness-core';
13
13
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
14
14
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
15
15
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -28,7 +28,7 @@ Usage:
28
28
  dz workflow <task> [--dry-run]
29
29
  dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
30
30
  dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
31
- dz publish [--filter <name>] [--bump-only] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live)
31
+ dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] (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)
32
32
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
33
33
  dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz, not the cwd — pin to a canonical brain)
34
34
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
@@ -52,6 +52,7 @@ Usage:
52
52
  dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
53
53
  dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
54
54
  dz usage [--json] [--project <dir>] | dz usage --calibrate --session <pct> --weekly <pct> [--model fable=<pct>] [--project <dir>] (ESTIMATE Claude usage from fixed reset windows; optional per-model weekly binding; exit 0 ALWAYS; pct=null when limits unconfigured)
55
+ dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>] (enforce the Integrity Rule: flag untagged/overstated accuracy claims; default scan = root README.md + every discovered package's README.md + features/*/08_qe_report.md; exit 1 only at/above --fail-on, default high)
55
56
  dz pretrain [--project <dir>]
56
57
  dz recommend "<task description>"
57
58
  dz compose <preset1+preset2+...> [--target <name>]
@@ -2599,11 +2600,12 @@ function cmdPublish(options, flags, cwd, write) {
2599
2600
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
2600
2601
  // silently swallowed and flip the command into live-publish mode.
2601
2602
  const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help']);
2602
- const allowedOptions = new Set(['filter']);
2603
+ const allowedOptions = new Set(['filter', 'claim-check']);
2604
+ const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>';
2603
2605
  for (const flag of flags) {
2604
2606
  if (!allowedFlags.has(flag)) {
2605
2607
  write(`dz publish: unknown option --${flag}`);
2606
- write(` allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>`);
2608
+ write(allowedHelp);
2607
2609
  return 1;
2608
2610
  }
2609
2611
  }
@@ -2612,10 +2614,19 @@ function cmdPublish(options, flags, cwd, write) {
2612
2614
  continue;
2613
2615
  if (!allowedOptions.has(key)) {
2614
2616
  write(`dz publish: unknown option --${key}`);
2615
- write(` allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>`);
2617
+ write(allowedHelp);
2616
2618
  return 1;
2617
2619
  }
2618
2620
  }
2621
+ // Pre-publish claim-check gate strictness: reject (never coerce) an invalid value. Default 'warn'
2622
+ // per ADR-001 — findings are SURFACED on every publish, but 'warn' never changes publish status,
2623
+ // so the success path is unchanged. 'off' disables the gate; 'error' fails an offending package.
2624
+ const claimCheckRaw = options.get('claim-check');
2625
+ if (claimCheckRaw !== undefined && !['off', 'warn', 'error'].includes(claimCheckRaw)) {
2626
+ write(`dz publish: invalid --claim-check '${claimCheckRaw}' (expected off|warn|error)`);
2627
+ return 1;
2628
+ }
2629
+ const claimCheckOpt = claimCheckRaw ?? 'warn';
2619
2630
  const bumpOnly = flags.has('bump-only');
2620
2631
  const filterStr = options.get('filter');
2621
2632
  // SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
@@ -2657,13 +2668,17 @@ function cmdPublish(options, flags, cwd, write) {
2657
2668
  }
2658
2669
  write(`╚══════════════════════════════════════════════════════════════════════╝`);
2659
2670
  }
2660
- const report = publishPackages(cwd, { dryRun, filter, bumpOnly });
2661
- write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}`);
2671
+ const report = publishPackages(cwd, { dryRun, filter, bumpOnly, claimGate: claimCheckOpt });
2672
+ write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
2662
2673
  write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
2663
2674
  for (const pkg of report.packages) {
2664
2675
  const icon = pkg.status === 'published' ? '✓' : pkg.status === 'skipped' ? '○' : '✗';
2665
2676
  const detail = pkg.error ? ` (${pkg.error.slice(0, 60)})` : '';
2666
2677
  write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} → ${pkg.newVersion} ${pkg.status}${detail}`);
2678
+ // Surface warn-mode findings that did not block the publish.
2679
+ if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
2680
+ write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
2681
+ }
2667
2682
  }
2668
2683
  return report.errors > 0 ? 1 : 0;
2669
2684
  }
@@ -2819,6 +2834,136 @@ function cmdBenchmark(options, flags, cwd, write) {
2819
2834
  write(`\nL0 only. For L1/L2 judge evaluation: /bto-test ${skillDir}`);
2820
2835
  return score.passRate >= 80 ? 0 : 1;
2821
2836
  }
2837
+ /**
2838
+ * Exit-code contract for `dz claim-check` (named in the ADR, locked by tests):
2839
+ * exit 0 when no finding at/above `failOn` exists; exit 1 only when one does.
2840
+ * `--fail-on none` never exits non-zero. Severity order: high > medium > none.
2841
+ */
2842
+ function computeClaimExit(findings, failOn) {
2843
+ const rank = { none: 0, medium: 1, high: 2 };
2844
+ if (failOn === 'none')
2845
+ return 0;
2846
+ return findings.some((f) => rank[f.severity] >= rank[failOn]) ? 1 : 0;
2847
+ }
2848
+ /**
2849
+ * Default scan set when no paths are given: the repo root README, every published
2850
+ * package README under packages/@dzhechkov, and every feature's 08_qe_report.md. Each
2851
+ * entry is guarded by existsSync; a missing packages/ or features/ dir is skipped, never fatal.
2852
+ */
2853
+ function defaultClaimScanSet(root) {
2854
+ const set = [];
2855
+ const rootReadme = join(root, 'README.md');
2856
+ if (existsSync(rootReadme))
2857
+ set.push(rootReadme);
2858
+ try {
2859
+ for (const p of discoverPackages(root)) {
2860
+ const readme = join(p.dir, 'README.md');
2861
+ if (existsSync(readme))
2862
+ set.push(readme);
2863
+ }
2864
+ }
2865
+ catch { /* no packages/@dzhechkov dir — skip */ }
2866
+ try {
2867
+ const featuresDir = join(root, 'features');
2868
+ if (existsSync(featuresDir)) {
2869
+ for (const e of readdirSync(featuresDir, { withFileTypes: true })) {
2870
+ if (!e.isDirectory())
2871
+ continue;
2872
+ const qe = join(featuresDir, e.name, '08_qe_report.md');
2873
+ if (existsSync(qe))
2874
+ set.push(qe);
2875
+ }
2876
+ }
2877
+ }
2878
+ catch { /* no features dir — skip */ }
2879
+ return set;
2880
+ }
2881
+ /** Cheap binary sniff: a NUL byte in the first 512 chars ⇒ skip (never scan binaries). */
2882
+ function looksBinaryText(text) {
2883
+ const n = Math.min(text.length, 512);
2884
+ for (let i = 0; i < n; i += 1)
2885
+ if (text.charCodeAt(i) === 0)
2886
+ return true;
2887
+ return false;
2888
+ }
2889
+ /**
2890
+ * `dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>]`
2891
+ *
2892
+ * I/O adapter over the pure `claimCheck` engine: resolves the scan set, reads each file
2893
+ * never-throw (unreadable/binary/missing files are skipped and reported in `scanned`), merges
2894
+ * per-file findings (each enriched with its `file`), and applies the exit-code contract.
2895
+ * `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
2896
+ */
2897
+ function cmdClaimCheck(options, _optionLists, flags, cwd, write) {
2898
+ // Reject (never silently coerce) an invalid --fail-on.
2899
+ const failOnRaw = options.get('fail-on') ?? 'high';
2900
+ if (!['high', 'medium', 'none'].includes(failOnRaw)) {
2901
+ write(`dz claim-check: invalid --fail-on '${failOnRaw}' (expected high|medium|none)`);
2902
+ return 1;
2903
+ }
2904
+ const failOn = failOnRaw;
2905
+ const root = resolve(cwd, options.get('project') ?? '.');
2906
+ // `--json <path>` is captured by parseArgs as the OPTION `json=<path>` (the boolean flag ate the
2907
+ // next token — the same gotcha cmdMcpScan recovers). Recover both: mark json, adopt the eaten
2908
+ // token as the first path. `--json` alone (at end / before another --flag) lands as a bare flag.
2909
+ let json = flags.has('json');
2910
+ const paths = [];
2911
+ const jsonConsumed = options.get('json');
2912
+ if (jsonConsumed !== undefined) {
2913
+ json = true;
2914
+ if (jsonConsumed !== 'true')
2915
+ paths.push(jsonConsumed);
2916
+ }
2917
+ // Variadic positional paths land as _positional_0, _positional_1, … (see parseArgs).
2918
+ for (let i = 0;; i += 1) {
2919
+ const p = options.get(`_positional_${i}`);
2920
+ if (p === undefined)
2921
+ break;
2922
+ paths.push(p);
2923
+ }
2924
+ const scanSet = paths.length > 0 ? paths.map((p) => resolve(root, p)) : defaultClaimScanSet(root);
2925
+ const findings = [];
2926
+ const scanned = [];
2927
+ for (const abs of scanSet) {
2928
+ // Show a repo-relative path for in-tree files; fall back to the absolute path for
2929
+ // anything outside root (avoids an ugly ../../.. chain for an explicit external path).
2930
+ const relRaw = relative(root, abs);
2931
+ const rel = relRaw && !relRaw.startsWith('..') ? relRaw : abs;
2932
+ let text;
2933
+ try {
2934
+ text = readFileSync(abs, 'utf-8');
2935
+ }
2936
+ catch (err) {
2937
+ scanned.push({ path: rel, status: 'skipped', reason: err instanceof Error ? err.message : 'not found' });
2938
+ continue;
2939
+ }
2940
+ if (looksBinaryText(text)) {
2941
+ scanned.push({ path: rel, status: 'skipped', reason: 'binary' });
2942
+ continue;
2943
+ }
2944
+ const result = claimCheck(text);
2945
+ for (const f of result.findings)
2946
+ findings.push({ ...f, file: rel });
2947
+ scanned.push({ path: rel, status: 'scanned', findings: result.findings.length });
2948
+ }
2949
+ const ok = findings.length === 0;
2950
+ if (json) {
2951
+ write(JSON.stringify({ ok, findings, scanned })); // ALWAYS valid JSON, pass or fail
2952
+ return computeClaimExit(findings, failOn);
2953
+ }
2954
+ // Human output.
2955
+ write(summarize({ ok, findings }));
2956
+ for (const f of findings) {
2957
+ write(` [${f.severity}] ${f.file}:${f.line} — ${f.reason}`);
2958
+ write(` ${f.excerpt}`);
2959
+ write(` ↳ ${f.suggestion}`);
2960
+ }
2961
+ const skipped = scanned.filter((s) => s.status === 'skipped');
2962
+ write(`\n ${scanned.length} file(s) in scan set, ${skipped.length} skipped.`);
2963
+ for (const s of skipped)
2964
+ write(` skipped ${s.path} (${s.reason})`);
2965
+ return computeClaimExit(findings, failOn);
2966
+ }
2822
2967
  function cmdMcpScan(options, flags, cwd, write) {
2823
2968
  // The arg parser captures `--boolFlag <next>` as an OPTION value, so a path
2824
2969
  // typed AFTER a boolean flag (e.g. `dz mcp-scan --reconcile .`) lands as that
@@ -3410,6 +3555,8 @@ export async function runCli(argv, io = {}) {
3410
3555
  return cmdStatusline(options, flags, cwd, write, readStdin);
3411
3556
  case 'usage':
3412
3557
  return cmdUsage(options, optionLists, flags, cwd, write);
3558
+ case 'claim-check':
3559
+ return cmdClaimCheck(options, optionLists, flags, cwd, write);
3413
3560
  case 'setup':
3414
3561
  return await cmdSetup(options, flags, cwd, write);
3415
3562
  case 'pretrain':