@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 +41 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +154 -7
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +160 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.201",
|
|
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.
|
|
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(
|
|
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(
|
|
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':
|