@dzhechkov/harness-cli 0.3.199 → 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/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, 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)
@@ -51,7 +51,8 @@ Usage:
51
51
  dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
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
- dz usage [--json] [--project <dir>] (ESTIMATE Claude session (5h-block) + weekly (7d) token usage from local transcripts; exit 0 ALWAYS; pct=null when memory.usage.{sessionTokenLimit,weeklyTokenLimit} unconfigured in .dz/config.json)
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>]
@@ -81,6 +82,7 @@ Presets: ${PRESET_NAMES.join(', ')}`;
81
82
  /** Parse `<command> [--key value] [--flag]` argv. */
82
83
  function parseArgs(argv) {
83
84
  const options = new Map();
85
+ const optionLists = new Map();
84
86
  const flags = new Set();
85
87
  const positional = [];
86
88
  for (let index = 0; index < argv.length; index += 1) {
@@ -90,6 +92,9 @@ function parseArgs(argv) {
90
92
  const next = argv[index + 1];
91
93
  if (next !== undefined && !next.startsWith('--')) {
92
94
  options.set(key, next);
95
+ const list = optionLists.get(key) ?? [];
96
+ list.push(next);
97
+ optionLists.set(key, list);
93
98
  index += 1;
94
99
  }
95
100
  else {
@@ -104,7 +109,7 @@ function parseArgs(argv) {
104
109
  for (let pi = 1; pi < positional.length; pi += 1) {
105
110
  options.set(`_positional_${pi - 1}`, positional[pi] ?? '');
106
111
  }
107
- return { command: positional[0] ?? '', options, flags };
112
+ return { command: positional[0] ?? '', options, optionLists, flags };
108
113
  }
109
114
  /**
110
115
  * Discover skill source directories: explicit `--skills-dir` if given, else
@@ -947,18 +952,165 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
947
952
  return 0;
948
953
  }
949
954
  }
955
+ function isJsonRecord(value) {
956
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
957
+ }
958
+ function usageConfigPath(projectRoot) {
959
+ return join(projectRoot, '.dz', 'config.json');
960
+ }
961
+ function readProjectConfigForUsage(projectRoot) {
962
+ const path = usageConfigPath(projectRoot);
963
+ try {
964
+ if (!existsSync(path))
965
+ return { config: {} };
966
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
967
+ if (isJsonRecord(parsed))
968
+ return { config: parsed };
969
+ return { config: {}, warning: 'existing config is not a JSON object; writing a minimal config' };
970
+ }
971
+ catch {
972
+ return { config: {}, warning: 'existing config could not be parsed; writing a minimal config' };
973
+ }
974
+ }
975
+ function applyUsageCalibrationToConfig(config, plan) {
976
+ const next = { ...config };
977
+ const memory = isJsonRecord(next['memory']) ? { ...next['memory'] } : {};
978
+ const usage = isJsonRecord(memory['usage']) ? { ...memory['usage'] } : {};
979
+ for (const change of plan.changes) {
980
+ if (change.key === 'session') {
981
+ usage['sessionTokenLimit'] = change.after;
982
+ }
983
+ else if (change.key === 'weekly') {
984
+ usage['weeklyTokenLimit'] = change.after;
985
+ }
986
+ else {
987
+ const model = normalizeClaudeUsageModelKey(change.key);
988
+ if (model) {
989
+ const existingByModel = isJsonRecord(usage['weeklyTokenLimitByModel']) ? { ...usage['weeklyTokenLimitByModel'] } : {};
990
+ existingByModel[model] = change.after;
991
+ usage['weeklyTokenLimitByModel'] = existingByModel;
992
+ }
993
+ }
994
+ }
995
+ if (plan.changes.length > 0) {
996
+ usage['calibratedAt'] = plan.after.calibratedAt;
997
+ usage['source'] = plan.after.source;
998
+ }
999
+ memory['usage'] = usage;
1000
+ next['memory'] = memory;
1001
+ return next;
1002
+ }
1003
+ function parseUsageModelArgs(modelArgs) {
1004
+ const modelPct = {};
1005
+ const skipped = [];
1006
+ for (const raw of modelArgs) {
1007
+ const eq = raw.indexOf('=');
1008
+ if (eq <= 0 || eq === raw.length - 1) {
1009
+ skipped.push(`model ${raw}: skipped malformed model=pct argument`);
1010
+ continue;
1011
+ }
1012
+ const modelName = raw.slice(0, eq).trim();
1013
+ const model = normalizeClaudeUsageModelKey(modelName);
1014
+ if (!model) {
1015
+ skipped.push(`model ${modelName}: skipped unknown model`);
1016
+ continue;
1017
+ }
1018
+ modelPct[model] = raw.slice(eq + 1).trim();
1019
+ }
1020
+ return { modelPct, skipped };
1021
+ }
1022
+ function writeUsageCalibrationSummary(opts) {
1023
+ opts.write('usage calibrate: estimated local transcript counts; claude.ai/settings/usage is authoritative');
1024
+ opts.write(`usage calibrate: project ${opts.projectRoot}`);
1025
+ if (opts.configWarning)
1026
+ opts.write(`usage calibrate: ${opts.configWarning}`);
1027
+ for (const change of opts.plan.changes) {
1028
+ opts.write(`usage calibrate: ${change.key} tokens=${change.tokens} pct=${change.pct}% limit ${change.before ?? 'null'} -> ${change.after}`);
1029
+ }
1030
+ const skipped = [...opts.preSkipped, ...opts.plan.skipped];
1031
+ for (const item of skipped)
1032
+ opts.write(`usage calibrate: skipped ${item}`);
1033
+ if (opts.wrote) {
1034
+ opts.write('usage calibrate: wrote .dz/config.json with source claude.ai/settings/usage');
1035
+ }
1036
+ else {
1037
+ opts.write('usage calibrate: no config changes written');
1038
+ }
1039
+ }
1040
+ function cmdUsageCalibrate(options, optionLists, cwd, write) {
1041
+ const projectRoot = resolve(cwd, options.get('project') ?? '.');
1042
+ const suppliedModels = optionLists.get('model') ?? [];
1043
+ const parsedModels = parseUsageModelArgs(suppliedModels);
1044
+ const modelPct = parsedModels.modelPct;
1045
+ const hasModelPct = Object.keys(modelPct).length > 0;
1046
+ const input = {
1047
+ ...(options.has('session') ? { sessionPct: options.get('session') } : {}),
1048
+ ...(options.has('weekly') ? { weeklyPct: options.get('weekly') } : {}),
1049
+ ...(hasModelPct ? { modelPct } : {}),
1050
+ calibratedAt: new Date().toISOString(),
1051
+ source: 'claude.ai/settings/usage',
1052
+ };
1053
+ const missingInputs = [];
1054
+ if (!options.has('session') && !options.has('weekly') && !hasModelPct) {
1055
+ missingInputs.push('no calibration percentages supplied');
1056
+ }
1057
+ try {
1058
+ const current = computeUsage(projectRoot);
1059
+ const before = readUsageLimits(projectRoot);
1060
+ const plan = deriveUsageCalibration(current, before, input);
1061
+ if (plan.changes.length === 0) {
1062
+ writeUsageCalibrationSummary({
1063
+ projectRoot,
1064
+ plan,
1065
+ preSkipped: [...parsedModels.skipped, ...missingInputs],
1066
+ wrote: false,
1067
+ write,
1068
+ });
1069
+ return 0;
1070
+ }
1071
+ const existing = readProjectConfigForUsage(projectRoot);
1072
+ const nextConfig = applyUsageCalibrationToConfig(existing.config, plan);
1073
+ try {
1074
+ mkdirSync(join(projectRoot, '.dz'), { recursive: true });
1075
+ writeFileSync(usageConfigPath(projectRoot), JSON.stringify(nextConfig, null, 2) + '\n');
1076
+ writeUsageCalibrationSummary({
1077
+ projectRoot,
1078
+ plan,
1079
+ preSkipped: [...parsedModels.skipped, ...missingInputs],
1080
+ configWarning: existing.warning,
1081
+ wrote: true,
1082
+ write,
1083
+ });
1084
+ }
1085
+ catch {
1086
+ writeUsageCalibrationSummary({
1087
+ projectRoot,
1088
+ plan,
1089
+ preSkipped: [...parsedModels.skipped, ...missingInputs, 'write failed'],
1090
+ configWarning: existing.warning,
1091
+ wrote: false,
1092
+ write,
1093
+ });
1094
+ }
1095
+ return 0;
1096
+ }
1097
+ catch {
1098
+ write('usage calibrate: skipped internal error; no config changes written');
1099
+ return 0;
1100
+ }
1101
+ }
950
1102
  /**
951
- * `dz usage` — print an ESTIMATE of Claude session (active 5h-block) + weekly (rolling 7d) token
952
- * usage, aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json`
953
- * emits the single-line contract the feature-adr usage-probe agent parses; the human path prints a
954
- * compact `session ~74% (resets 19:00) · week ~52% (resets Sat)` or an unconfigured hint.
1103
+ * `dz usage` — print an ESTIMATE of Claude session + weekly usage from fixed reset windows,
1104
+ * aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json` emits
1105
+ * the single-line contract the feature-adr usage-probe agent parses; `--calibrate` is the only
1106
+ * write path and records human-transcribed claude.ai percentages in `.dz/config.json`.
955
1107
  *
956
1108
  * **Exit code is 0 ALWAYS** — including on internal error the whole body is guarded and prints the
957
1109
  * all-null JSON, so a probe can NEVER distinguish "usage unknown" from "command failed" via a
958
- * non-zero exit (unknown is a first-class value, INV-3). `--project <dir>` scopes ONLY the
959
- * `.dz/config.json` limits read; the measurement is account-wide (all projects, FR-1.6).
1110
+ * non-zero exit. `--project <dir>` scopes ONLY the `.dz/config.json` read/write; measurement is
1111
+ * account-wide (all projects).
960
1112
  */
961
- function cmdUsage(options, flags, cwd, write) {
1113
+ function cmdUsage(options, optionLists, flags, cwd, write) {
962
1114
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
963
1115
  const nullContract = () => JSON.stringify({
964
1116
  sessionPct: null,
@@ -970,18 +1122,28 @@ function cmdUsage(options, flags, cwd, write) {
970
1122
  estimated: true,
971
1123
  });
972
1124
  try {
1125
+ if (flags.has('calibrate'))
1126
+ return cmdUsageCalibrate(options, optionLists, cwd, write);
973
1127
  const u = computeUsage(projectRoot);
974
1128
  const lim = readUsageLimits(projectRoot);
1129
+ const modelLimits = lim.weeklyTokenLimitByModel;
1130
+ const hasModelLimits = modelLimits !== undefined && Object.keys(modelLimits).length > 0;
975
1131
  if (flags.has('json')) {
976
- write(JSON.stringify({
1132
+ const limitsPayload = { session: lim.sessionTokenLimit ?? null, weekly: lim.weeklyTokenLimit ?? null };
1133
+ if (hasModelLimits)
1134
+ limitsPayload.weeklyByModel = { ...modelLimits };
1135
+ const payload = {
977
1136
  sessionPct: u.sessionPct,
978
1137
  weeklyPct: u.weeklyPct,
979
1138
  sessionTokens: u.sessionTokens,
980
1139
  weeklyTokens: u.weeklyTokens,
981
1140
  resetsAt: { session: u.sessionResetsAt, weekly: u.weeklyResetsAt },
982
- limits: { session: lim.sessionTokenLimit ?? null, weekly: lim.weeklyTokenLimit ?? null },
1141
+ limits: limitsPayload,
983
1142
  estimated: true,
984
- }));
1143
+ };
1144
+ if (hasModelLimits && u.weeklyByModel !== undefined)
1145
+ payload.weeklyByModel = u.weeklyByModel;
1146
+ write(JSON.stringify(payload));
985
1147
  return 0;
986
1148
  }
987
1149
  if (u.sessionPct === null && u.weeklyPct === null) {
@@ -1000,7 +1162,8 @@ function cmdUsage(options, flags, cwd, write) {
1000
1162
  }
1001
1163
  };
1002
1164
  const s = u.sessionPct === null ? 'n/a' : '~' + u.sessionPct + '%';
1003
- const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%';
1165
+ const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
1166
+ const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
1004
1167
  write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
1005
1168
  return 0;
1006
1169
  }
@@ -2437,11 +2600,12 @@ function cmdPublish(options, flags, cwd, write) {
2437
2600
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
2438
2601
  // silently swallowed and flip the command into live-publish mode.
2439
2602
  const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help']);
2440
- 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>';
2441
2605
  for (const flag of flags) {
2442
2606
  if (!allowedFlags.has(flag)) {
2443
2607
  write(`dz publish: unknown option --${flag}`);
2444
- write(` allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>`);
2608
+ write(allowedHelp);
2445
2609
  return 1;
2446
2610
  }
2447
2611
  }
@@ -2450,10 +2614,19 @@ function cmdPublish(options, flags, cwd, write) {
2450
2614
  continue;
2451
2615
  if (!allowedOptions.has(key)) {
2452
2616
  write(`dz publish: unknown option --${key}`);
2453
- write(` allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>`);
2617
+ write(allowedHelp);
2454
2618
  return 1;
2455
2619
  }
2456
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';
2457
2630
  const bumpOnly = flags.has('bump-only');
2458
2631
  const filterStr = options.get('filter');
2459
2632
  // SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
@@ -2495,13 +2668,17 @@ function cmdPublish(options, flags, cwd, write) {
2495
2668
  }
2496
2669
  write(`╚══════════════════════════════════════════════════════════════════════╝`);
2497
2670
  }
2498
- const report = publishPackages(cwd, { dryRun, filter, bumpOnly });
2499
- 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}` : ''}`);
2500
2673
  write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
2501
2674
  for (const pkg of report.packages) {
2502
2675
  const icon = pkg.status === 'published' ? '✓' : pkg.status === 'skipped' ? '○' : '✗';
2503
2676
  const detail = pkg.error ? ` (${pkg.error.slice(0, 60)})` : '';
2504
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
+ }
2505
2682
  }
2506
2683
  return report.errors > 0 ? 1 : 0;
2507
2684
  }
@@ -2657,6 +2834,136 @@ function cmdBenchmark(options, flags, cwd, write) {
2657
2834
  write(`\nL0 only. For L1/L2 judge evaluation: /bto-test ${skillDir}`);
2658
2835
  return score.passRate >= 80 ? 0 : 1;
2659
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
+ }
2660
2967
  function cmdMcpScan(options, flags, cwd, write) {
2661
2968
  // The arg parser captures `--boolFlag <next>` as an OPTION value, so a path
2662
2969
  // typed AFTER a boolean flag (e.g. `dz mcp-scan --reconcile .`) lands as that
@@ -3202,7 +3509,7 @@ export async function runCli(argv, io = {}) {
3202
3509
  return '';
3203
3510
  }
3204
3511
  };
3205
- const { command, options, flags } = parseArgs(argv);
3512
+ const { command, options, optionLists, flags } = parseArgs(argv);
3206
3513
  if (command === '' || command === 'help' || flags.has('help')) {
3207
3514
  write(USAGE);
3208
3515
  return 0;
@@ -3221,7 +3528,7 @@ export async function runCli(argv, io = {}) {
3221
3528
  case 'create-skill':
3222
3529
  return cmdCreateSkill(options, flags, cwd, write);
3223
3530
  case 'info':
3224
- return cmdInfo(options, { command, options, flags }, cwd, write);
3531
+ return cmdInfo(options, { command, options, optionLists, flags }, cwd, write);
3225
3532
  case 'scout':
3226
3533
  return await cmdScout(options, flags, cwd, write);
3227
3534
  case 'workflow':
@@ -3247,7 +3554,9 @@ export async function runCli(argv, io = {}) {
3247
3554
  case 'statusline':
3248
3555
  return cmdStatusline(options, flags, cwd, write, readStdin);
3249
3556
  case 'usage':
3250
- return cmdUsage(options, flags, cwd, write);
3557
+ return cmdUsage(options, optionLists, flags, cwd, write);
3558
+ case 'claim-check':
3559
+ return cmdClaimCheck(options, optionLists, flags, cwd, write);
3251
3560
  case 'setup':
3252
3561
  return await cmdSetup(options, flags, cwd, write);
3253
3562
  case 'pretrain':