@dzhechkov/harness-cli 0.3.226 → 0.3.227
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 +27 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +161 -3
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +130 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.227",
|
|
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",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@dzhechkov/skills-reverse-engineering": "^0.1.0",
|
|
56
56
|
"@dzhechkov/skills-presentation-storyteller": "^0.1.0",
|
|
57
57
|
"@dzhechkov/skills-website-cloner": "^0.1.0",
|
|
58
|
-
"@dzhechkov/harness-core": "0.3.
|
|
58
|
+
"@dzhechkov/harness-core": "0.3.120"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^25.6.0",
|
package/src/cli.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmdirSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
|
-
import { homedir } from 'node:os';
|
|
11
|
+
import { homedir, tmpdir } from 'node:os';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
|
|
14
14
|
import {
|
|
@@ -142,6 +142,8 @@ import {
|
|
|
142
142
|
readExistingForScaffold,
|
|
143
143
|
assembleChallengeContext,
|
|
144
144
|
buildChallengeBrief,
|
|
145
|
+
planDiscriminationCheck,
|
|
146
|
+
classifyDiscrimination,
|
|
145
147
|
pickAdversaryModel,
|
|
146
148
|
CHALLENGE_QUESTIONS,
|
|
147
149
|
loadOutcomes,
|
|
@@ -4573,6 +4575,130 @@ function cmdChallenge(options: Map<string, string>, flags: Set<string>, cwd: str
|
|
|
4573
4575
|
return 0;
|
|
4574
4576
|
}
|
|
4575
4577
|
|
|
4578
|
+
/**
|
|
4579
|
+
* `dz discrimination-check` — the §42 test-discrimination gate (feature learned from cve-bench/evaluate.mjs).
|
|
4580
|
+
* feature-adr Step-8 already asserts the ADR safety property HAS a test; this asserts that test DISCRIMINATES:
|
|
4581
|
+
* run the property test(s) in an isolated git worktree at the pre-feature base (default HEAD, since the feature
|
|
4582
|
+
* diff is uncommitted mid-pipeline). They MUST go red without the feature diff — a green is a false green.
|
|
4583
|
+
*
|
|
4584
|
+
* --test <a.test.ts[,b.test.ts]> the property test file(s) mapped from the ADR Confirmation (comma list)
|
|
4585
|
+
* --base <ref> the base ref to fail against (default HEAD)
|
|
4586
|
+
* --name '<filter>' optional -t test-name filter applied to every target
|
|
4587
|
+
* --runner '<cmd>' test runner (default `npx vitest run`)
|
|
4588
|
+
* --json machine-readable {plan, results, verdict, finding}
|
|
4589
|
+
*
|
|
4590
|
+
* NEVER auto-aborts: a non-discriminating (false-green) test is reported as a HIGH finding for the owner to
|
|
4591
|
+
* decide (dz's rule — a false gate kills trust). Exit code is 0 on a clean run regardless of verdict; 2 only on
|
|
4592
|
+
* a usage/setup error, so a caller distinguishes "gate ran" from "gate could not run".
|
|
4593
|
+
*/
|
|
4594
|
+
function cmdDiscriminationCheck(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
4595
|
+
let repoRoot = cwd;
|
|
4596
|
+
try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd; } catch { /* not git */ }
|
|
4597
|
+
|
|
4598
|
+
const testArg = options.get('test');
|
|
4599
|
+
if (testArg === undefined || testArg.trim() === '') {
|
|
4600
|
+
write('dz discrimination-check: pass --test <property-test.ts[,...]> (the test(s) the ADR Confirmation names).');
|
|
4601
|
+
return 2;
|
|
4602
|
+
}
|
|
4603
|
+
const nameFilter = options.get('name');
|
|
4604
|
+
const propertyTests = testArg.split(',').map((s) => s.trim()).filter(Boolean).map((file) =>
|
|
4605
|
+
nameFilter !== undefined && nameFilter.trim() !== '' ? { file, name: nameFilter.trim() } : { file });
|
|
4606
|
+
const baseRef = options.get('base') ?? 'HEAD';
|
|
4607
|
+
const runnerOpt = options.get('runner');
|
|
4608
|
+
|
|
4609
|
+
const plan = planDiscriminationCheck(runnerOpt !== undefined ? { baseRef, propertyTests, runner: runnerOpt } : { baseRef, propertyTests });
|
|
4610
|
+
|
|
4611
|
+
if (!plan.runnable) {
|
|
4612
|
+
// No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
|
|
4613
|
+
const result = classifyDiscrimination({ propertyTests: [], results: [] });
|
|
4614
|
+
if (flags.has('json')) { write(JSON.stringify({ plan, results: [], ...result }, null, 2)); return 0; }
|
|
4615
|
+
write(`discrimination-check: NOT RUN (${plan.reason ?? 'unknown'})`);
|
|
4616
|
+
if (plan.rejected.length) write(' rejected: ' + plan.rejected.map((r) => `${r.file} (${r.reason})`).join(', '));
|
|
4617
|
+
write(` → ${result.finding?.detail ?? 'no property test to check'}`);
|
|
4618
|
+
return 0;
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
// Execute the plan in a temp worktree WE own; substitute {{WORKTREE}} and always clean up.
|
|
4622
|
+
// `git worktree add` must CREATE the path, so compute a fresh non-existent one (do NOT mkdtemp it).
|
|
4623
|
+
const worktree = join(mkdtempSync(join(tmpdir(), 'dz-disc-')), 'wt');
|
|
4624
|
+
const results: { file: string; name?: string; outcome: 'pass' | 'fail' | 'error' }[] = [];
|
|
4625
|
+
try {
|
|
4626
|
+
// 1) add the detached worktree at base (git creates `worktree`; its parent already exists).
|
|
4627
|
+
const addCmd = plan.commands[0]!.replace(/\{\{WORKTREE\}\}/g, worktree);
|
|
4628
|
+
execSync(addCmd, { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
4629
|
+
|
|
4630
|
+
// 1b) a fresh worktree has NO node_modules — without this, every test fails to load (runner + deps
|
|
4631
|
+
// unresolvable) and the gate collapses to always-VIA_ERROR, blind to false greens. Absolute-path
|
|
4632
|
+
// symlinks point back at the main checkout's already-installed trees, robust across pnpm's layout.
|
|
4633
|
+
const linkNodeModules = (relDir: string): void => {
|
|
4634
|
+
const srcNm = join(repoRoot, relDir, 'node_modules');
|
|
4635
|
+
if (!existsSync(srcNm)) return;
|
|
4636
|
+
const dstNm = join(worktree, relDir, 'node_modules');
|
|
4637
|
+
if (existsSync(dstNm)) return;
|
|
4638
|
+
try { mkdirSync(dirname(dstNm), { recursive: true }); symlinkSync(srcNm, dstNm, 'dir'); } catch { /* best effort */ }
|
|
4639
|
+
};
|
|
4640
|
+
linkNodeModules('.'); // root (hoisted deps + .bin)
|
|
4641
|
+
const pkgDirs = new Set<string>();
|
|
4642
|
+
for (const t of plan.targets) {
|
|
4643
|
+
let d = dirname(t.file);
|
|
4644
|
+
while (d && d !== '.' && d !== sep) {
|
|
4645
|
+
if (existsSync(join(repoRoot, d, 'package.json'))) { pkgDirs.add(d); break; }
|
|
4646
|
+
d = dirname(d);
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
for (const d of pkgDirs) linkNodeModules(d);
|
|
4650
|
+
|
|
4651
|
+
// 2) copy each property test into the base worktree, then 3) run it and record pass/fail/error per target.
|
|
4652
|
+
for (const t of plan.targets) {
|
|
4653
|
+
try {
|
|
4654
|
+
const src = resolve(repoRoot, t.file);
|
|
4655
|
+
// containment guard (defense in depth beyond planDiscriminationCheck's path sanitation).
|
|
4656
|
+
if (!resolve(src).startsWith(resolve(repoRoot) + sep)) { results.push(nameFor(t, 'error')); continue; }
|
|
4657
|
+
const dst = join(worktree, t.file);
|
|
4658
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
4659
|
+
writeFileSync(dst, readFileSync(src));
|
|
4660
|
+
} catch { results.push(nameFor(t, 'error')); continue; }
|
|
4661
|
+
|
|
4662
|
+
const runner = (runnerOpt !== undefined && plan.commands.some((c) => c.includes(runnerOpt))) ? runnerOpt : 'npx vitest run';
|
|
4663
|
+
// t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
|
|
4664
|
+
// still quote + `--` so a path can never be read as a runner option or split a word.
|
|
4665
|
+
const nameArg = t.name ? ` -t '${t.name}'` : '';
|
|
4666
|
+
try {
|
|
4667
|
+
execSync(`${runner}${nameArg} -- '${t.file}'`, { cwd: worktree, stdio: 'pipe', encoding: 'utf-8' });
|
|
4668
|
+
results.push(nameFor(t, 'pass')); // exit 0 → test PASSED at base → false green
|
|
4669
|
+
} catch (e) {
|
|
4670
|
+
// vitest exits non-zero on failure AND on load/compile error. Distinguish: a load error usually names
|
|
4671
|
+
// "Cannot find module"/"Failed to load"/"No test files"; otherwise treat as an assertion failure (red).
|
|
4672
|
+
const out = String((e as { stdout?: string; stderr?: string }).stdout ?? '') + String((e as { stderr?: string }).stderr ?? '');
|
|
4673
|
+
const isLoadError = /cannot find module|failed to load|no test (files )?found|error: cannot|transform failed|esbuild/i.test(out);
|
|
4674
|
+
results.push(nameFor(t, isLoadError ? 'error' : 'fail'));
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
} catch (e) {
|
|
4678
|
+
if (flags.has('json')) { write(JSON.stringify({ plan, error: 'worktree-setup-failed', detail: String((e as Error).message).slice(0, 300) }, null, 2)); }
|
|
4679
|
+
else write(`discrimination-check: could not create worktree at ${baseRef}: ${String((e as Error).message).slice(0, 200)}`);
|
|
4680
|
+
return 2;
|
|
4681
|
+
} finally {
|
|
4682
|
+
try { execSync(`git worktree remove --force ${worktree}`, { cwd: repoRoot, stdio: 'pipe' }); } catch { /* fall through to rm */ }
|
|
4683
|
+
// remove the whole mkdtemp parent (worktree is `<mkdtemp>/wt`), so nothing leaks under tmp even on error.
|
|
4684
|
+
try { rmSync(dirname(worktree), { recursive: true, force: true }); } catch { /* best effort */ }
|
|
4685
|
+
try { execSync('git worktree prune', { cwd: repoRoot, stdio: 'pipe' }); } catch { /* best effort */ }
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
const result = classifyDiscrimination({ propertyTests, results });
|
|
4689
|
+
if (flags.has('json')) { write(JSON.stringify({ plan, results, ...result }, null, 2)); return 0; }
|
|
4690
|
+
|
|
4691
|
+
write(`discrimination-check @ ${baseRef} — verdict: ${result.aggregate}`);
|
|
4692
|
+
for (const p of result.perTest) write(` ${p.verdict === 'NON_DISCRIMINATING' ? '✗' : '✓'} ${p.file}${p.name ? ` (${p.name})` : ''}: ${p.verdict}`);
|
|
4693
|
+
if (result.finding) write(`\n [${result.finding.severity}] ${result.finding.title}\n ${result.finding.detail}`);
|
|
4694
|
+
return 0;
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
/** small helper: build a result row, omitting `name` when absent (exactOptionalPropertyTypes). */
|
|
4698
|
+
function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' | 'error'): { file: string; name?: string; outcome: 'pass' | 'fail' | 'error' } {
|
|
4699
|
+
return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4576
4702
|
/**
|
|
4577
4703
|
* `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
|
|
4578
4704
|
* what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
|
|
@@ -4987,6 +5113,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
4987
5113
|
return cmdFeatureAdrSetup(options, flags, cwd, write);
|
|
4988
5114
|
case 'challenge':
|
|
4989
5115
|
return cmdChallenge(options, flags, cwd, write);
|
|
5116
|
+
case 'discrimination-check':
|
|
5117
|
+
return cmdDiscriminationCheck(options, flags, cwd, write);
|
|
4990
5118
|
case 'routing':
|
|
4991
5119
|
return cmdRouting(options, flags, cwd, write);
|
|
4992
5120
|
case 'bto-optimize':
|