@dzhechkov/harness-cli 0.3.200 → 0.3.202

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.3.200",
3
+ "version": "0.3.202",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@
54
54
  "@dzhechkov/skills-reverse-engineering": "^0.1.0",
55
55
  "@dzhechkov/skills-presentation-storyteller": "^0.1.0",
56
56
  "@dzhechkov/skills-website-cloner": "^0.1.0",
57
- "@dzhechkov/harness-core": "0.3.100"
57
+ "@dzhechkov/harness-core": "0.3.101"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.6.0",
package/src/cli.ts CHANGED
@@ -70,6 +70,8 @@ import {
70
70
  deriveUsageCalibration,
71
71
  normalizeClaudeUsageModelKey,
72
72
  readUsageLimits,
73
+ claimCheck,
74
+ summarize,
73
75
  queryBookKnowledge,
74
76
  loadStorePatternsSync,
75
77
  bundleSkills,
@@ -86,7 +88,7 @@ import {
86
88
  importBrainSlice,
87
89
  registerKusToBrain,
88
90
  } from '@dzhechkov/harness-core';
89
- import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan } from '@dzhechkov/harness-core';
91
+ import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding } from '@dzhechkov/harness-core';
90
92
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
91
93
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
92
94
 
@@ -106,7 +108,7 @@ Usage:
106
108
  dz workflow <task> [--dry-run]
107
109
  dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
108
110
  dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
109
- dz publish [--filter <name>] [--bump-only] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live)
111
+ 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)
110
112
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
111
113
  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)
112
114
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
@@ -130,6 +132,7 @@ Usage:
130
132
  dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
131
133
  dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
132
134
  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)
135
+ 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)
133
136
  dz pretrain [--project <dir>]
134
137
  dz recommend "<task description>"
135
138
  dz compose <preset1+preset2+...> [--target <name>]
@@ -2826,11 +2829,12 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
2826
2829
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
2827
2830
  // silently swallowed and flip the command into live-publish mode.
2828
2831
  const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help']);
2829
- const allowedOptions = new Set(['filter']);
2832
+ const allowedOptions = new Set(['filter', 'claim-check']);
2833
+ const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>';
2830
2834
  for (const flag of flags) {
2831
2835
  if (!allowedFlags.has(flag)) {
2832
2836
  write(`dz publish: unknown option --${flag}`);
2833
- write(` allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>`);
2837
+ write(allowedHelp);
2834
2838
  return 1;
2835
2839
  }
2836
2840
  }
@@ -2838,11 +2842,21 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
2838
2842
  if (key.startsWith('_positional_')) continue;
2839
2843
  if (!allowedOptions.has(key)) {
2840
2844
  write(`dz publish: unknown option --${key}`);
2841
- write(` allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>`);
2845
+ write(allowedHelp);
2842
2846
  return 1;
2843
2847
  }
2844
2848
  }
2845
2849
 
2850
+ // Pre-publish claim-check gate strictness: reject (never coerce) an invalid value. Default 'warn'
2851
+ // per ADR-001 — findings are SURFACED on every publish, but 'warn' never changes publish status,
2852
+ // so the success path is unchanged. 'off' disables the gate; 'error' fails an offending package.
2853
+ const claimCheckRaw = options.get('claim-check');
2854
+ if (claimCheckRaw !== undefined && !['off', 'warn', 'error'].includes(claimCheckRaw)) {
2855
+ write(`dz publish: invalid --claim-check '${claimCheckRaw}' (expected off|warn|error)`);
2856
+ return 1;
2857
+ }
2858
+ const claimCheckOpt = (claimCheckRaw as 'off' | 'warn' | 'error' | undefined) ?? 'warn';
2859
+
2846
2860
  const bumpOnly = flags.has('bump-only');
2847
2861
  const filterStr = options.get('filter');
2848
2862
  // SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
@@ -2888,14 +2902,18 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
2888
2902
  write(`╚══════════════════════════════════════════════════════════════════════╝`);
2889
2903
  }
2890
2904
 
2891
- const report = publishPackages(cwd, { dryRun, filter, bumpOnly });
2905
+ const report = publishPackages(cwd, { dryRun, filter, bumpOnly, claimGate: claimCheckOpt });
2892
2906
 
2893
- write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}`);
2907
+ write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
2894
2908
  write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
2895
2909
  for (const pkg of report.packages) {
2896
2910
  const icon = pkg.status === 'published' ? '✓' : pkg.status === 'skipped' ? '○' : '✗';
2897
2911
  const detail = pkg.error ? ` (${pkg.error.slice(0, 60)})` : '';
2898
2912
  write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} → ${pkg.newVersion} ${pkg.status}${detail}`);
2913
+ // Surface warn-mode findings that did not block the publish.
2914
+ if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
2915
+ write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
2916
+ }
2899
2917
  }
2900
2918
  return report.errors > 0 ? 1 : 0;
2901
2919
  }
@@ -3069,6 +3087,139 @@ function cmdBenchmark(options: Map<string, string>, flags: Set<string>, cwd: str
3069
3087
  return score.passRate >= 80 ? 0 : 1;
3070
3088
  }
3071
3089
 
3090
+ /**
3091
+ * Exit-code contract for `dz claim-check` (named in the ADR, locked by tests):
3092
+ * exit 0 when no finding at/above `failOn` exists; exit 1 only when one does.
3093
+ * `--fail-on none` never exits non-zero. Severity order: high > medium > none.
3094
+ */
3095
+ function computeClaimExit(findings: readonly { severity: 'high' | 'medium' }[], failOn: 'high' | 'medium' | 'none'): number {
3096
+ const rank = { none: 0, medium: 1, high: 2 } as const;
3097
+ if (failOn === 'none') return 0;
3098
+ return findings.some((f) => rank[f.severity] >= rank[failOn]) ? 1 : 0;
3099
+ }
3100
+
3101
+ /**
3102
+ * Default scan set when no paths are given: the repo root README, every published
3103
+ * package README under packages/@dzhechkov, and every feature's 08_qe_report.md. Each
3104
+ * entry is guarded by existsSync; a missing packages/ or features/ dir is skipped, never fatal.
3105
+ */
3106
+ function defaultClaimScanSet(root: string): string[] {
3107
+ const set: string[] = [];
3108
+ const rootReadme = join(root, 'README.md');
3109
+ if (existsSync(rootReadme)) set.push(rootReadme);
3110
+ try {
3111
+ for (const p of discoverPackages(root)) {
3112
+ const readme = join(p.dir, 'README.md');
3113
+ if (existsSync(readme)) set.push(readme);
3114
+ }
3115
+ } catch { /* no packages/@dzhechkov dir — skip */ }
3116
+ try {
3117
+ const featuresDir = join(root, 'features');
3118
+ if (existsSync(featuresDir)) {
3119
+ for (const e of readdirSync(featuresDir, { withFileTypes: true })) {
3120
+ if (!e.isDirectory()) continue;
3121
+ const qe = join(featuresDir, e.name, '08_qe_report.md');
3122
+ if (existsSync(qe)) set.push(qe);
3123
+ }
3124
+ }
3125
+ } catch { /* no features dir — skip */ }
3126
+ return set;
3127
+ }
3128
+
3129
+ /** Cheap binary sniff: a NUL byte in the first 512 chars ⇒ skip (never scan binaries). */
3130
+ function looksBinaryText(text: string): boolean {
3131
+ const n = Math.min(text.length, 512);
3132
+ for (let i = 0; i < n; i += 1) if (text.charCodeAt(i) === 0) return true;
3133
+ return false;
3134
+ }
3135
+
3136
+ /**
3137
+ * `dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>]`
3138
+ *
3139
+ * I/O adapter over the pure `claimCheck` engine: resolves the scan set, reads each file
3140
+ * never-throw (unreadable/binary/missing files are skipped and reported in `scanned`), merges
3141
+ * per-file findings (each enriched with its `file`), and applies the exit-code contract.
3142
+ * `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
3143
+ */
3144
+ function cmdClaimCheck(
3145
+ options: Map<string, string>,
3146
+ _optionLists: Map<string, string[]>,
3147
+ flags: Set<string>,
3148
+ cwd: string,
3149
+ write: Write,
3150
+ ): number {
3151
+ // Reject (never silently coerce) an invalid --fail-on.
3152
+ const failOnRaw = options.get('fail-on') ?? 'high';
3153
+ if (!['high', 'medium', 'none'].includes(failOnRaw)) {
3154
+ write(`dz claim-check: invalid --fail-on '${failOnRaw}' (expected high|medium|none)`);
3155
+ return 1;
3156
+ }
3157
+ const failOn = failOnRaw as 'high' | 'medium' | 'none';
3158
+ const root = resolve(cwd, options.get('project') ?? '.');
3159
+
3160
+ // `--json <path>` is captured by parseArgs as the OPTION `json=<path>` (the boolean flag ate the
3161
+ // next token — the same gotcha cmdMcpScan recovers). Recover both: mark json, adopt the eaten
3162
+ // token as the first path. `--json` alone (at end / before another --flag) lands as a bare flag.
3163
+ let json = flags.has('json');
3164
+ const paths: string[] = [];
3165
+ const jsonConsumed = options.get('json');
3166
+ if (jsonConsumed !== undefined) {
3167
+ json = true;
3168
+ if (jsonConsumed !== 'true') paths.push(jsonConsumed);
3169
+ }
3170
+ // Variadic positional paths land as _positional_0, _positional_1, … (see parseArgs).
3171
+ for (let i = 0; ; i += 1) {
3172
+ const p = options.get(`_positional_${i}`);
3173
+ if (p === undefined) break;
3174
+ paths.push(p);
3175
+ }
3176
+
3177
+ const scanSet = paths.length > 0 ? paths.map((p) => resolve(root, p)) : defaultClaimScanSet(root);
3178
+
3179
+ const findings: (ClaimFinding & { file: string })[] = [];
3180
+ const scanned: { path: string; status: 'scanned' | 'skipped'; findings?: number; reason?: string }[] = [];
3181
+
3182
+ for (const abs of scanSet) {
3183
+ // Show a repo-relative path for in-tree files; fall back to the absolute path for
3184
+ // anything outside root (avoids an ugly ../../.. chain for an explicit external path).
3185
+ const relRaw = relative(root, abs);
3186
+ const rel = relRaw && !relRaw.startsWith('..') ? relRaw : abs;
3187
+ let text: string;
3188
+ try {
3189
+ text = readFileSync(abs, 'utf-8');
3190
+ } catch (err) {
3191
+ scanned.push({ path: rel, status: 'skipped', reason: err instanceof Error ? err.message : 'not found' });
3192
+ continue;
3193
+ }
3194
+ if (looksBinaryText(text)) {
3195
+ scanned.push({ path: rel, status: 'skipped', reason: 'binary' });
3196
+ continue;
3197
+ }
3198
+ const result = claimCheck(text);
3199
+ for (const f of result.findings) findings.push({ ...f, file: rel });
3200
+ scanned.push({ path: rel, status: 'scanned', findings: result.findings.length });
3201
+ }
3202
+
3203
+ const ok = findings.length === 0;
3204
+
3205
+ if (json) {
3206
+ write(JSON.stringify({ ok, findings, scanned })); // ALWAYS valid JSON, pass or fail
3207
+ return computeClaimExit(findings, failOn);
3208
+ }
3209
+
3210
+ // Human output.
3211
+ write(summarize({ ok, findings }));
3212
+ for (const f of findings) {
3213
+ write(` [${f.severity}] ${f.file}:${f.line} — ${f.reason}`);
3214
+ write(` ${f.excerpt}`);
3215
+ write(` ↳ ${f.suggestion}`);
3216
+ }
3217
+ const skipped = scanned.filter((s) => s.status === 'skipped');
3218
+ write(`\n ${scanned.length} file(s) in scan set, ${skipped.length} skipped.`);
3219
+ for (const s of skipped) write(` skipped ${s.path} (${s.reason})`);
3220
+ return computeClaimExit(findings, failOn);
3221
+ }
3222
+
3072
3223
  function cmdMcpScan(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
3073
3224
  // The arg parser captures `--boolFlag <next>` as an OPTION value, so a path
3074
3225
  // typed AFTER a boolean flag (e.g. `dz mcp-scan --reconcile .`) lands as that
@@ -3707,6 +3858,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
3707
3858
  return cmdStatusline(options, flags, cwd, write, readStdin);
3708
3859
  case 'usage':
3709
3860
  return cmdUsage(options, optionLists, flags, cwd, write);
3861
+ case 'claim-check':
3862
+ return cmdClaimCheck(options, optionLists, flags, cwd, write);
3710
3863
  case 'setup':
3711
3864
  return await cmdSetup(options, flags, cwd, write);
3712
3865
  case 'pretrain':