@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/LICENSE +21 -0
- package/README.md +92 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +332 -23
- package/dist/cli.js.map +1 -1
- package/package.json +12 -12
- package/src/cli.ts +380 -29
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>]
|
|
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
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
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
|
|
959
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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':
|