@dzhechkov/harness-cli 0.3.223 → 0.3.225

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.223",
3
+ "version": "0.3.225",
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.117"
58
+ "@dzhechkov/harness-core": "0.3.119"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^25.6.0",
package/src/cli.ts CHANGED
@@ -144,7 +144,22 @@ import {
144
144
  buildChallengeBrief,
145
145
  pickAdversaryModel,
146
146
  CHALLENGE_QUESTIONS,
147
+ loadOutcomes,
148
+ renderOutcomes,
149
+ statsForKey,
150
+ selectAutoCost,
151
+ recordProvisional,
152
+ finalizeOutcome,
153
+ COST_LADDER,
154
+ splitScenarios,
155
+ budgetPlan,
156
+ selectWinner,
157
+ proseScopeOk,
158
+ renderProseDiff,
159
+ readScenarioIds,
160
+ DEFAULT_MAX_JUDGE_RUNS,
147
161
  } from '@dzhechkov/harness-core';
162
+ import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
148
163
  import type { SetupSpec } from '@dzhechkov/harness-core';
149
164
  import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow } from '@dzhechkov/harness-core';
150
165
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
@@ -4558,6 +4573,144 @@ function cmdChallenge(options: Map<string, string>, flags: Set<string>, cwd: str
4558
4573
  return 0;
4559
4574
  }
4560
4575
 
4576
+ /**
4577
+ * `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
4578
+ * what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
4579
+ * attempts / successes / rate. Read-only.
4580
+ * --stage <s> filter to one pipeline stage (code, qe, plan, …)
4581
+ * --json raw store JSON
4582
+ */
4583
+ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
4584
+ let repoRoot = cwd;
4585
+ try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd; } catch { /* not git */ }
4586
+
4587
+ const stage = options.get('stage');
4588
+ const tier = options.get('tier');
4589
+ const model = options.get('model');
4590
+
4591
+ // --select: resolve an `auto-cost` stage → the concrete model + escalate chain (the workflow shells out here
4592
+ // because it is sandboxed with no fs). --family restricts to the coder's cross-family (qe guard); --ladder is
4593
+ // the probe-filtered id set (account-specific Codex ids that answered).
4594
+ if (flags.has('select')) {
4595
+ if (!stage || !tier) { write('dz routing --select needs --stage and --tier'); return 1; }
4596
+ const fam = options.get('family');
4597
+ const family: Family | undefined = fam === 'claude' || fam === 'openai' ? fam : undefined;
4598
+ const ladderCsv = options.get('ladder');
4599
+ let ladder: readonly ModelRung[] | undefined;
4600
+ if (ladderCsv !== undefined) {
4601
+ const ids = new Set(ladderCsv.split(',').map((s) => s.trim()).filter(Boolean));
4602
+ ladder = COST_LADDER.filter((r) => ids.has(r.id));
4603
+ }
4604
+ const statsFor = statsForKey(loadOutcomes(repoRoot), stage, tier);
4605
+ const pick = selectAutoCost(stage, tier, statsFor, { ...(family ? { family } : {}), ...(ladder ? { ladder } : {}) });
4606
+ write(JSON.stringify(pick));
4607
+ return 0;
4608
+ }
4609
+
4610
+ // --record-provisional / --finalize: the two-phase outcome label. The workflow calls these at stage end
4611
+ // (provisional) and at the downstream gate (finalize, authoritative).
4612
+ if (flags.has('record-provisional')) {
4613
+ if (!stage || !tier || !model) { write('dz routing --record-provisional needs --stage --tier --model'); return 1; }
4614
+ recordProvisional(repoRoot, stage, tier, model, flags.has('weak'));
4615
+ write(`recorded provisional: ${stage}/${tier}/${model}${flags.has('weak') ? ' (weak-credit)' : ''}`);
4616
+ return 0;
4617
+ }
4618
+ if (flags.has('finalize')) {
4619
+ if (!stage || !tier || !model) { write('dz routing --finalize needs --stage --tier --model --success <true|false>'); return 1; }
4620
+ const success = options.get('success') === 'true';
4621
+ finalizeOutcome(repoRoot, stage, tier, model, success);
4622
+ write(`finalized: ${stage}/${tier}/${model} → ${success ? 'success' : 'FAILURE'}`);
4623
+ return 0;
4624
+ }
4625
+
4626
+ // default / --json: inspect the learned table.
4627
+ const store = loadOutcomes(repoRoot);
4628
+ if (flags.has('json')) { write(JSON.stringify(store, null, 2)); return 0; }
4629
+ write(renderOutcomes(store, stage));
4630
+ return 0;
4631
+ }
4632
+
4633
+ /**
4634
+ * `dz bto-optimize` — the deterministic engine BEHIND the `/bto-optimize` skill (feature bto-optimize-holdout).
4635
+ * Adds dspy-MIPROv2 rigor the current evolutionary loop lacks: a hold-out split, a hard budget cap, and a
4636
+ * no-regress-on-holdout winner selector. NOT a rival command — the skill delegates these steps; candidate-prose
4637
+ * generation + judge scoring stay skill-side. `--json` on every subcommand.
4638
+ * --split --scenarios <csv|@file> [--holdout <r>] deterministic tune/holdout split
4639
+ * --plan --candidates K --rounds R --tune N --holdout M [--max C] budget plan (trims to the cap)
4640
+ * --select --baseline <@json> --candidates <@json> [--tolerance t] accept only on holdout no-regress
4641
+ * --scope-check --original <f> --candidate <f> prose-only guard
4642
+ * --diff --original <f> --candidate <f> the prose diff to confirm
4643
+ */
4644
+ function cmdBtoOptimize(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
4645
+ const json = flags.has('json');
4646
+ const emit = (o: unknown): number => { write(JSON.stringify(o, null, json ? 2 : 0)); return 0; };
4647
+ const readContained = (rel: string): string => {
4648
+ // Containment (QE #traversal): an @file must stay under cwd — `@../../etc/passwd` must not read outside.
4649
+ const rootAbs = resolve(cwd);
4650
+ const abs = resolve(rootAbs, rel);
4651
+ if (abs !== rootAbs && !abs.startsWith(rootAbs + sep)) throw new Error(`path escapes the working directory: ${rel}`);
4652
+ return readFileSync(abs, 'utf-8');
4653
+ };
4654
+ const readJson = (spec: string | undefined): unknown => {
4655
+ if (spec === undefined) return undefined;
4656
+ const raw = spec.startsWith('@') ? readContained(spec.slice(1)) : spec;
4657
+ return JSON.parse(raw);
4658
+ };
4659
+ try {
4660
+ if (flags.has('split')) {
4661
+ const sc = options.get('scenarios');
4662
+ if (sc === undefined) { write('dz bto-optimize --split needs --scenarios <csv|@file>'); return 1; }
4663
+ const ids = sc.startsWith('@') ? readScenarioIds(resolve(cwd, sc.slice(1))) : sc.split(',').map((s) => s.trim()).filter(Boolean);
4664
+ const ratio = Number.parseFloat(options.get('holdout') ?? '');
4665
+ const split = splitScenarios(ids, Number.isFinite(ratio) ? ratio : undefined);
4666
+ if (json) return emit(split);
4667
+ write(`tune (${split.tune.length}): ${split.tune.join(', ')}`);
4668
+ write(`holdout (${split.holdout.length}): ${split.holdout.join(', ')}`);
4669
+ return 0;
4670
+ }
4671
+ if (flags.has('plan')) {
4672
+ const n = (k: string, d: number): number => { const v = Number.parseInt(options.get(k) ?? '', 10); return Number.isFinite(v) ? v : d; };
4673
+ const max = Number.parseInt(options.get('max') ?? '', 10);
4674
+ const plan = budgetPlan({ candidates: n('candidates', 5), rounds: n('rounds', 1), tuneCount: n('tune', 3), holdoutCount: n('holdout', 2) }, Number.isFinite(max) ? max : DEFAULT_MAX_JUDGE_RUNS);
4675
+ if (json) return emit(plan);
4676
+ write(`budget plan: ${plan.candidates} candidates × ${plan.rounds} round(s) → ${plan.tuneRuns} tune + ${plan.holdoutRuns} holdout = ${plan.totalRuns} judge run(s) (cap ${plan.cap}, ${plan.withinCap ? 'within cap' : 'OVER CAP'})`);
4677
+ if (plan.trimmed) write(` trimmed to fit: ${plan.trimmed}`);
4678
+ return 0;
4679
+ }
4680
+ if (flags.has('select')) {
4681
+ const baseline = readJson(options.get('baseline')) as { holdout: DimScores } | undefined;
4682
+ const candidates = readJson(options.get('candidates')) as BtoCandidate[] | undefined;
4683
+ if (!baseline || !Array.isArray(candidates)) { write('dz bto-optimize --select needs --baseline <@json {holdout}> and --candidates <@json [..]>'); return 1; }
4684
+ const tol = Number.parseFloat(options.get('tolerance') ?? '');
4685
+ const result = selectWinner(baseline, candidates, Number.isFinite(tol) ? { tolerance: tol } : {});
4686
+ if (json) return emit(result);
4687
+ write(result.winner ? `✓ winner: ${result.winner} — ${result.reason}` : `✗ no winner — ${result.reason}`);
4688
+ return 0;
4689
+ }
4690
+ if (flags.has('scope-check') || flags.has('diff')) {
4691
+ const o = options.get('original'); const c = options.get('candidate');
4692
+ if (o === undefined || c === undefined) { write('needs --original <file> and --candidate <file>'); return 1; }
4693
+ const origText = readContained(o);
4694
+ const candText = readContained(c);
4695
+ if (flags.has('scope-check')) {
4696
+ const r = proseScopeOk(origText, candText);
4697
+ if (json) return emit(r);
4698
+ write(r.ok ? `✓ prose-only: ${r.reason}` : `✗ out of scope: ${r.reason}`);
4699
+ return r.ok ? 0 : 1;
4700
+ }
4701
+ write(renderProseDiff(origText, candText));
4702
+ return 0;
4703
+ }
4704
+ write('dz bto-optimize: pass --split | --plan | --select | --scope-check | --diff (see docs)');
4705
+ return 1;
4706
+ } catch (e) {
4707
+ const msg = e instanceof Error ? e.message : String(e);
4708
+ if (json) { write(JSON.stringify({ error: msg })); return 1; }
4709
+ write(`dz bto-optimize: ${msg}`);
4710
+ return 1;
4711
+ }
4712
+ }
4713
+
4561
4714
  function cmdStats(cwd: string, write: Write): number {
4562
4715
  const baseDir = join(cwd, 'packages', '@dzhechkov');
4563
4716
  if (!existsSync(baseDir)) {
@@ -4834,6 +4987,10 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
4834
4987
  return cmdFeatureAdrSetup(options, flags, cwd, write);
4835
4988
  case 'challenge':
4836
4989
  return cmdChallenge(options, flags, cwd, write);
4990
+ case 'routing':
4991
+ return cmdRouting(options, flags, cwd, write);
4992
+ case 'bto-optimize':
4993
+ return cmdBtoOptimize(options, flags, cwd, write);
4837
4994
  case 'dashboard':
4838
4995
  return cmdDashboard(cwd, write);
4839
4996
  case 'roam':