@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/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",
|
|
@@ -37,15 +37,7 @@
|
|
|
37
37
|
"src",
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsc -p tsconfig.json",
|
|
42
|
-
"test": "vitest run",
|
|
43
|
-
"test:watch": "vitest",
|
|
44
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
-
"lint": "tsc -p tsconfig.json --noEmit"
|
|
46
|
-
},
|
|
47
40
|
"dependencies": {
|
|
48
|
-
"@dzhechkov/harness-core": "workspace:*",
|
|
49
41
|
"@dzhechkov/harness-presets": "^0.5.0",
|
|
50
42
|
"@dzhechkov/scout": "^0.8.0",
|
|
51
43
|
"@dzhechkov/skills-devops": "^0.3.0",
|
|
@@ -61,7 +53,8 @@
|
|
|
61
53
|
"@dzhechkov/skills-idea2prd": "^0.1.0",
|
|
62
54
|
"@dzhechkov/skills-reverse-engineering": "^0.1.0",
|
|
63
55
|
"@dzhechkov/skills-presentation-storyteller": "^0.1.0",
|
|
64
|
-
"@dzhechkov/skills-website-cloner": "^0.1.0"
|
|
56
|
+
"@dzhechkov/skills-website-cloner": "^0.1.0",
|
|
57
|
+
"@dzhechkov/harness-core": "0.3.101"
|
|
65
58
|
},
|
|
66
59
|
"devDependencies": {
|
|
67
60
|
"@types/node": "^25.6.0",
|
|
@@ -79,5 +72,12 @@
|
|
|
79
72
|
"url": "https://github.com/djd1m/dz-harness-hub.git",
|
|
80
73
|
"directory": "packages/@dzhechkov/harness-cli"
|
|
81
74
|
},
|
|
82
|
-
"homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme"
|
|
83
|
-
|
|
75
|
+
"homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme",
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsc -p tsconfig.json",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"test:watch": "vitest",
|
|
80
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
81
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -67,7 +67,11 @@ import {
|
|
|
67
67
|
statuslineData,
|
|
68
68
|
writeFeatureAdrState,
|
|
69
69
|
computeUsage,
|
|
70
|
+
deriveUsageCalibration,
|
|
71
|
+
normalizeClaudeUsageModelKey,
|
|
70
72
|
readUsageLimits,
|
|
73
|
+
claimCheck,
|
|
74
|
+
summarize,
|
|
71
75
|
queryBookKnowledge,
|
|
72
76
|
loadStorePatternsSync,
|
|
73
77
|
bundleSkills,
|
|
@@ -84,7 +88,7 @@ import {
|
|
|
84
88
|
importBrainSlice,
|
|
85
89
|
registerKusToBrain,
|
|
86
90
|
} from '@dzhechkov/harness-core';
|
|
87
|
-
import type { PatternRecord, TargetName, BookKU, HarmonizeReport } from '@dzhechkov/harness-core';
|
|
91
|
+
import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding } from '@dzhechkov/harness-core';
|
|
88
92
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
89
93
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
90
94
|
|
|
@@ -104,7 +108,7 @@ Usage:
|
|
|
104
108
|
dz workflow <task> [--dry-run]
|
|
105
109
|
dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
|
|
106
110
|
dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
|
|
107
|
-
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)
|
|
108
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]
|
|
109
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)
|
|
110
114
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
@@ -127,7 +131,8 @@ Usage:
|
|
|
127
131
|
dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
|
|
128
132
|
dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
|
|
129
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)
|
|
130
|
-
dz usage [--json] [--project <dir>]
|
|
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)
|
|
131
136
|
dz pretrain [--project <dir>]
|
|
132
137
|
dz recommend "<task description>"
|
|
133
138
|
dz compose <preset1+preset2+...> [--target <name>]
|
|
@@ -170,12 +175,14 @@ export interface CliIo {
|
|
|
170
175
|
interface ParsedArgs {
|
|
171
176
|
readonly command: string;
|
|
172
177
|
readonly options: Map<string, string>;
|
|
178
|
+
readonly optionLists: Map<string, string[]>;
|
|
173
179
|
readonly flags: Set<string>;
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
/** Parse `<command> [--key value] [--flag]` argv. */
|
|
177
183
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
178
184
|
const options = new Map<string, string>();
|
|
185
|
+
const optionLists = new Map<string, string[]>();
|
|
179
186
|
const flags = new Set<string>();
|
|
180
187
|
const positional: string[] = [];
|
|
181
188
|
|
|
@@ -186,6 +193,9 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
186
193
|
const next = argv[index + 1];
|
|
187
194
|
if (next !== undefined && !next.startsWith('--')) {
|
|
188
195
|
options.set(key, next);
|
|
196
|
+
const list = optionLists.get(key) ?? [];
|
|
197
|
+
list.push(next);
|
|
198
|
+
optionLists.set(key, list);
|
|
189
199
|
index += 1;
|
|
190
200
|
} else {
|
|
191
201
|
flags.add(key);
|
|
@@ -200,7 +210,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
200
210
|
options.set(`_positional_${pi - 1}`, positional[pi] ?? '');
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
return { command: positional[0] ?? '', options, flags };
|
|
213
|
+
return { command: positional[0] ?? '', options, optionLists, flags };
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
type Write = (line: string) => void;
|
|
@@ -1112,18 +1122,187 @@ function cmdStatusline(
|
|
|
1112
1122
|
}
|
|
1113
1123
|
}
|
|
1114
1124
|
|
|
1125
|
+
function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
1126
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function usageConfigPath(projectRoot: string): string {
|
|
1130
|
+
return join(projectRoot, '.dz', 'config.json');
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function readProjectConfigForUsage(projectRoot: string): { config: Record<string, unknown>; warning?: string } {
|
|
1134
|
+
const path = usageConfigPath(projectRoot);
|
|
1135
|
+
try {
|
|
1136
|
+
if (!existsSync(path)) return { config: {} };
|
|
1137
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
|
|
1138
|
+
if (isJsonRecord(parsed)) return { config: parsed };
|
|
1139
|
+
return { config: {}, warning: 'existing config is not a JSON object; writing a minimal config' };
|
|
1140
|
+
} catch {
|
|
1141
|
+
return { config: {}, warning: 'existing config could not be parsed; writing a minimal config' };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function applyUsageCalibrationToConfig(config: Record<string, unknown>, plan: UsageCalibrationPlan): Record<string, unknown> {
|
|
1146
|
+
const next: Record<string, unknown> = { ...config };
|
|
1147
|
+
const memory = isJsonRecord(next['memory']) ? { ...next['memory'] } : {};
|
|
1148
|
+
const usage = isJsonRecord(memory['usage']) ? { ...memory['usage'] } : {};
|
|
1149
|
+
|
|
1150
|
+
for (const change of plan.changes) {
|
|
1151
|
+
if (change.key === 'session') {
|
|
1152
|
+
usage['sessionTokenLimit'] = change.after;
|
|
1153
|
+
} else if (change.key === 'weekly') {
|
|
1154
|
+
usage['weeklyTokenLimit'] = change.after;
|
|
1155
|
+
} else {
|
|
1156
|
+
const model = normalizeClaudeUsageModelKey(change.key);
|
|
1157
|
+
if (model) {
|
|
1158
|
+
const existingByModel = isJsonRecord(usage['weeklyTokenLimitByModel']) ? { ...usage['weeklyTokenLimitByModel'] } : {};
|
|
1159
|
+
existingByModel[model] = change.after;
|
|
1160
|
+
usage['weeklyTokenLimitByModel'] = existingByModel;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (plan.changes.length > 0) {
|
|
1166
|
+
usage['calibratedAt'] = plan.after.calibratedAt;
|
|
1167
|
+
usage['source'] = plan.after.source;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
memory['usage'] = usage;
|
|
1171
|
+
next['memory'] = memory;
|
|
1172
|
+
return next;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function parseUsageModelArgs(modelArgs: readonly string[]): { modelPct: Record<string, unknown>; skipped: string[] } {
|
|
1176
|
+
const modelPct: Record<string, unknown> = {};
|
|
1177
|
+
const skipped: string[] = [];
|
|
1178
|
+
for (const raw of modelArgs) {
|
|
1179
|
+
const eq = raw.indexOf('=');
|
|
1180
|
+
if (eq <= 0 || eq === raw.length - 1) {
|
|
1181
|
+
skipped.push(`model ${raw}: skipped malformed model=pct argument`);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const modelName = raw.slice(0, eq).trim();
|
|
1185
|
+
const model = normalizeClaudeUsageModelKey(modelName);
|
|
1186
|
+
if (!model) {
|
|
1187
|
+
skipped.push(`model ${modelName}: skipped unknown model`);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
modelPct[model] = raw.slice(eq + 1).trim();
|
|
1191
|
+
}
|
|
1192
|
+
return { modelPct, skipped };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function writeUsageCalibrationSummary(opts: {
|
|
1196
|
+
projectRoot: string;
|
|
1197
|
+
plan: UsageCalibrationPlan;
|
|
1198
|
+
preSkipped: readonly string[];
|
|
1199
|
+
configWarning?: string | undefined;
|
|
1200
|
+
wrote: boolean;
|
|
1201
|
+
write: Write;
|
|
1202
|
+
}): void {
|
|
1203
|
+
opts.write('usage calibrate: estimated local transcript counts; claude.ai/settings/usage is authoritative');
|
|
1204
|
+
opts.write(`usage calibrate: project ${opts.projectRoot}`);
|
|
1205
|
+
if (opts.configWarning) opts.write(`usage calibrate: ${opts.configWarning}`);
|
|
1206
|
+
for (const change of opts.plan.changes) {
|
|
1207
|
+
opts.write(
|
|
1208
|
+
`usage calibrate: ${change.key} tokens=${change.tokens} pct=${change.pct}% limit ${change.before ?? 'null'} -> ${change.after}`,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
const skipped = [...opts.preSkipped, ...opts.plan.skipped];
|
|
1212
|
+
for (const item of skipped) opts.write(`usage calibrate: skipped ${item}`);
|
|
1213
|
+
if (opts.wrote) {
|
|
1214
|
+
opts.write('usage calibrate: wrote .dz/config.json with source claude.ai/settings/usage');
|
|
1215
|
+
} else {
|
|
1216
|
+
opts.write('usage calibrate: no config changes written');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function cmdUsageCalibrate(
|
|
1221
|
+
options: Map<string, string>,
|
|
1222
|
+
optionLists: Map<string, string[]>,
|
|
1223
|
+
cwd: string,
|
|
1224
|
+
write: Write,
|
|
1225
|
+
): number {
|
|
1226
|
+
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1227
|
+
const suppliedModels = optionLists.get('model') ?? [];
|
|
1228
|
+
const parsedModels = parseUsageModelArgs(suppliedModels);
|
|
1229
|
+
const modelPct = parsedModels.modelPct;
|
|
1230
|
+
const hasModelPct = Object.keys(modelPct).length > 0;
|
|
1231
|
+
const input = {
|
|
1232
|
+
...(options.has('session') ? { sessionPct: options.get('session') } : {}),
|
|
1233
|
+
...(options.has('weekly') ? { weeklyPct: options.get('weekly') } : {}),
|
|
1234
|
+
...(hasModelPct ? { modelPct } : {}),
|
|
1235
|
+
calibratedAt: new Date().toISOString(),
|
|
1236
|
+
source: 'claude.ai/settings/usage' as const,
|
|
1237
|
+
};
|
|
1238
|
+
const missingInputs: string[] = [];
|
|
1239
|
+
if (!options.has('session') && !options.has('weekly') && !hasModelPct) {
|
|
1240
|
+
missingInputs.push('no calibration percentages supplied');
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
try {
|
|
1244
|
+
const current = computeUsage(projectRoot);
|
|
1245
|
+
const before = readUsageLimits(projectRoot);
|
|
1246
|
+
const plan = deriveUsageCalibration(current, before, input);
|
|
1247
|
+
if (plan.changes.length === 0) {
|
|
1248
|
+
writeUsageCalibrationSummary({
|
|
1249
|
+
projectRoot,
|
|
1250
|
+
plan,
|
|
1251
|
+
preSkipped: [...parsedModels.skipped, ...missingInputs],
|
|
1252
|
+
wrote: false,
|
|
1253
|
+
write,
|
|
1254
|
+
});
|
|
1255
|
+
return 0;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const existing = readProjectConfigForUsage(projectRoot);
|
|
1259
|
+
const nextConfig = applyUsageCalibrationToConfig(existing.config, plan);
|
|
1260
|
+
try {
|
|
1261
|
+
mkdirSync(join(projectRoot, '.dz'), { recursive: true });
|
|
1262
|
+
writeFileSync(usageConfigPath(projectRoot), JSON.stringify(nextConfig, null, 2) + '\n');
|
|
1263
|
+
writeUsageCalibrationSummary({
|
|
1264
|
+
projectRoot,
|
|
1265
|
+
plan,
|
|
1266
|
+
preSkipped: [...parsedModels.skipped, ...missingInputs],
|
|
1267
|
+
configWarning: existing.warning,
|
|
1268
|
+
wrote: true,
|
|
1269
|
+
write,
|
|
1270
|
+
});
|
|
1271
|
+
} catch {
|
|
1272
|
+
writeUsageCalibrationSummary({
|
|
1273
|
+
projectRoot,
|
|
1274
|
+
plan,
|
|
1275
|
+
preSkipped: [...parsedModels.skipped, ...missingInputs, 'write failed'],
|
|
1276
|
+
configWarning: existing.warning,
|
|
1277
|
+
wrote: false,
|
|
1278
|
+
write,
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return 0;
|
|
1282
|
+
} catch {
|
|
1283
|
+
write('usage calibrate: skipped internal error; no config changes written');
|
|
1284
|
+
return 0;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1115
1288
|
/**
|
|
1116
|
-
* `dz usage` — print an ESTIMATE of Claude session
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1289
|
+
* `dz usage` — print an ESTIMATE of Claude session + weekly usage from fixed reset windows,
|
|
1290
|
+
* aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json` emits
|
|
1291
|
+
* the single-line contract the feature-adr usage-probe agent parses; `--calibrate` is the only
|
|
1292
|
+
* write path and records human-transcribed claude.ai percentages in `.dz/config.json`.
|
|
1120
1293
|
*
|
|
1121
1294
|
* **Exit code is 0 ALWAYS** — including on internal error the whole body is guarded and prints the
|
|
1122
1295
|
* all-null JSON, so a probe can NEVER distinguish "usage unknown" from "command failed" via a
|
|
1123
|
-
* non-zero exit
|
|
1124
|
-
*
|
|
1296
|
+
* non-zero exit. `--project <dir>` scopes ONLY the `.dz/config.json` read/write; measurement is
|
|
1297
|
+
* account-wide (all projects).
|
|
1125
1298
|
*/
|
|
1126
|
-
function cmdUsage(
|
|
1299
|
+
function cmdUsage(
|
|
1300
|
+
options: Map<string, string>,
|
|
1301
|
+
optionLists: Map<string, string[]>,
|
|
1302
|
+
flags: Set<string>,
|
|
1303
|
+
cwd: string,
|
|
1304
|
+
write: Write,
|
|
1305
|
+
): number {
|
|
1127
1306
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1128
1307
|
const nullContract = () =>
|
|
1129
1308
|
JSON.stringify({
|
|
@@ -1136,19 +1315,40 @@ function cmdUsage(options: Map<string, string>, flags: Set<string>, cwd: string,
|
|
|
1136
1315
|
estimated: true,
|
|
1137
1316
|
});
|
|
1138
1317
|
try {
|
|
1318
|
+
if (flags.has('calibrate')) return cmdUsageCalibrate(options, optionLists, cwd, write);
|
|
1319
|
+
|
|
1139
1320
|
const u = computeUsage(projectRoot);
|
|
1140
1321
|
const lim = readUsageLimits(projectRoot);
|
|
1322
|
+
const modelLimits = lim.weeklyTokenLimitByModel;
|
|
1323
|
+
const hasModelLimits = modelLimits !== undefined && Object.keys(modelLimits).length > 0;
|
|
1141
1324
|
if (flags.has('json')) {
|
|
1325
|
+
const limitsPayload: {
|
|
1326
|
+
session: number | null;
|
|
1327
|
+
weekly: number | null;
|
|
1328
|
+
weeklyByModel?: Partial<Record<ClaudeUsageModel, number>>;
|
|
1329
|
+
} = { session: lim.sessionTokenLimit ?? null, weekly: lim.weeklyTokenLimit ?? null };
|
|
1330
|
+
if (hasModelLimits) limitsPayload.weeklyByModel = { ...modelLimits };
|
|
1331
|
+
const payload: {
|
|
1332
|
+
sessionPct: number | null;
|
|
1333
|
+
weeklyPct: number | null;
|
|
1334
|
+
sessionTokens: number;
|
|
1335
|
+
weeklyTokens: number;
|
|
1336
|
+
resetsAt: { session: string | null; weekly: string | null };
|
|
1337
|
+
limits: typeof limitsPayload;
|
|
1338
|
+
weeklyByModel?: typeof u.weeklyByModel;
|
|
1339
|
+
estimated: true;
|
|
1340
|
+
} = {
|
|
1341
|
+
sessionPct: u.sessionPct,
|
|
1342
|
+
weeklyPct: u.weeklyPct,
|
|
1343
|
+
sessionTokens: u.sessionTokens,
|
|
1344
|
+
weeklyTokens: u.weeklyTokens,
|
|
1345
|
+
resetsAt: { session: u.sessionResetsAt, weekly: u.weeklyResetsAt },
|
|
1346
|
+
limits: limitsPayload,
|
|
1347
|
+
estimated: true,
|
|
1348
|
+
};
|
|
1349
|
+
if (hasModelLimits && u.weeklyByModel !== undefined) payload.weeklyByModel = u.weeklyByModel;
|
|
1142
1350
|
write(
|
|
1143
|
-
JSON.stringify(
|
|
1144
|
-
sessionPct: u.sessionPct,
|
|
1145
|
-
weeklyPct: u.weeklyPct,
|
|
1146
|
-
sessionTokens: u.sessionTokens,
|
|
1147
|
-
weeklyTokens: u.weeklyTokens,
|
|
1148
|
-
resetsAt: { session: u.sessionResetsAt, weekly: u.weeklyResetsAt },
|
|
1149
|
-
limits: { session: lim.sessionTokenLimit ?? null, weekly: lim.weeklyTokenLimit ?? null },
|
|
1150
|
-
estimated: true,
|
|
1151
|
-
}),
|
|
1351
|
+
JSON.stringify(payload),
|
|
1152
1352
|
);
|
|
1153
1353
|
return 0;
|
|
1154
1354
|
}
|
|
@@ -1168,7 +1368,8 @@ function cmdUsage(options: Map<string, string>, flags: Set<string>, cwd: string,
|
|
|
1168
1368
|
}
|
|
1169
1369
|
};
|
|
1170
1370
|
const s = u.sessionPct === null ? 'n/a' : '~' + u.sessionPct + '%';
|
|
1171
|
-
const
|
|
1371
|
+
const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
|
|
1372
|
+
const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
|
|
1172
1373
|
write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
|
|
1173
1374
|
return 0;
|
|
1174
1375
|
} catch {
|
|
@@ -2628,11 +2829,12 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
2628
2829
|
// Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
|
|
2629
2830
|
// silently swallowed and flip the command into live-publish mode.
|
|
2630
2831
|
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help']);
|
|
2631
|
-
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>';
|
|
2632
2834
|
for (const flag of flags) {
|
|
2633
2835
|
if (!allowedFlags.has(flag)) {
|
|
2634
2836
|
write(`dz publish: unknown option --${flag}`);
|
|
2635
|
-
write(
|
|
2837
|
+
write(allowedHelp);
|
|
2636
2838
|
return 1;
|
|
2637
2839
|
}
|
|
2638
2840
|
}
|
|
@@ -2640,11 +2842,21 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
2640
2842
|
if (key.startsWith('_positional_')) continue;
|
|
2641
2843
|
if (!allowedOptions.has(key)) {
|
|
2642
2844
|
write(`dz publish: unknown option --${key}`);
|
|
2643
|
-
write(
|
|
2845
|
+
write(allowedHelp);
|
|
2644
2846
|
return 1;
|
|
2645
2847
|
}
|
|
2646
2848
|
}
|
|
2647
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
|
+
|
|
2648
2860
|
const bumpOnly = flags.has('bump-only');
|
|
2649
2861
|
const filterStr = options.get('filter');
|
|
2650
2862
|
// SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
|
|
@@ -2690,14 +2902,18 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
2690
2902
|
write(`╚══════════════════════════════════════════════════════════════════════╝`);
|
|
2691
2903
|
}
|
|
2692
2904
|
|
|
2693
|
-
const report = publishPackages(cwd, { dryRun, filter, bumpOnly });
|
|
2905
|
+
const report = publishPackages(cwd, { dryRun, filter, bumpOnly, claimGate: claimCheckOpt });
|
|
2694
2906
|
|
|
2695
|
-
write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}`);
|
|
2907
|
+
write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
|
|
2696
2908
|
write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
|
|
2697
2909
|
for (const pkg of report.packages) {
|
|
2698
2910
|
const icon = pkg.status === 'published' ? '✓' : pkg.status === 'skipped' ? '○' : '✗';
|
|
2699
2911
|
const detail = pkg.error ? ` (${pkg.error.slice(0, 60)})` : '';
|
|
2700
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
|
+
}
|
|
2701
2917
|
}
|
|
2702
2918
|
return report.errors > 0 ? 1 : 0;
|
|
2703
2919
|
}
|
|
@@ -2871,6 +3087,139 @@ function cmdBenchmark(options: Map<string, string>, flags: Set<string>, cwd: str
|
|
|
2871
3087
|
return score.passRate >= 80 ? 0 : 1;
|
|
2872
3088
|
}
|
|
2873
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
|
+
|
|
2874
3223
|
function cmdMcpScan(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
2875
3224
|
// The arg parser captures `--boolFlag <next>` as an OPTION value, so a path
|
|
2876
3225
|
// typed AFTER a boolean flag (e.g. `dz mcp-scan --reconcile .`) lands as that
|
|
@@ -3461,7 +3810,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
3461
3810
|
return '';
|
|
3462
3811
|
}
|
|
3463
3812
|
};
|
|
3464
|
-
const { command, options, flags } = parseArgs(argv);
|
|
3813
|
+
const { command, options, optionLists, flags } = parseArgs(argv);
|
|
3465
3814
|
|
|
3466
3815
|
if (command === '' || command === 'help' || flags.has('help')) {
|
|
3467
3816
|
write(USAGE);
|
|
@@ -3482,7 +3831,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
3482
3831
|
case 'create-skill':
|
|
3483
3832
|
return cmdCreateSkill(options, flags, cwd, write);
|
|
3484
3833
|
case 'info':
|
|
3485
|
-
return cmdInfo(options, { command, options, flags }, cwd, write);
|
|
3834
|
+
return cmdInfo(options, { command, options, optionLists, flags }, cwd, write);
|
|
3486
3835
|
case 'scout':
|
|
3487
3836
|
return await cmdScout(options, flags, cwd, write);
|
|
3488
3837
|
case 'workflow':
|
|
@@ -3508,7 +3857,9 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
3508
3857
|
case 'statusline':
|
|
3509
3858
|
return cmdStatusline(options, flags, cwd, write, readStdin);
|
|
3510
3859
|
case 'usage':
|
|
3511
|
-
return cmdUsage(options, flags, cwd, write);
|
|
3860
|
+
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
3861
|
+
case 'claim-check':
|
|
3862
|
+
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
3512
3863
|
case 'setup':
|
|
3513
3864
|
return await cmdSetup(options, flags, cwd, write);
|
|
3514
3865
|
case 'pretrain':
|