@dzhechkov/harness-cli 0.7.7 → 0.7.9

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.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 67 commands, 14 presets, 10 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,22 +41,22 @@
41
41
  "sbom.json"
42
42
  ],
43
43
  "dependencies": {
44
- "@dzhechkov/harness-core": "^0.7.8",
44
+ "@dzhechkov/harness-core": "^0.7.12",
45
45
  "@dzhechkov/harness-presets": "^0.5.14",
46
46
  "@dzhechkov/scout": "^0.8.7",
47
47
  "@dzhechkov/skills-devops": "^0.3.13",
48
48
  "@dzhechkov/skills-mcp": "^0.3.6",
49
49
  "@dzhechkov/skills-web3": "^0.2.5",
50
50
  "@dzhechkov/skills-qe": "^0.3.10",
51
- "@dzhechkov/skills-academic": "^0.3.3",
52
51
  "@dzhechkov/skills-meta": "^0.9.51",
52
+ "@dzhechkov/skills-academic": "^0.3.3",
53
53
  "@dzhechkov/skills-ecc": "^0.1.8",
54
- "@dzhechkov/skills-pm": "^0.1.1",
54
+ "@dzhechkov/skills-news": "^0.1.12",
55
55
  "@dzhechkov/skills-taste": "^0.1.1",
56
+ "@dzhechkov/skills-pm": "^0.1.1",
56
57
  "@dzhechkov/skills-idea2prd": "^0.1.11",
57
- "@dzhechkov/skills-reverse-engineering": "^0.1.9",
58
58
  "@dzhechkov/skills-presentation-storyteller": "^0.1.10",
59
- "@dzhechkov/skills-news": "^0.1.12",
59
+ "@dzhechkov/skills-reverse-engineering": "^0.1.9",
60
60
  "@dzhechkov/skills-website-cloner": "^0.1.1"
61
61
  },
62
62
  "devDependencies": {
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "3d621db3f7811b174ee46b19fa462316ef13679786a910ac64c65fb05bfd5ef7"
28
+ "content": "07e1f395043324fe72c1170406340df3ebdb1dbabde64d80dc8124269a0cb5a6"
29
29
  }
30
30
  ]
31
31
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "c7856b79fdf6683fe855268191805ed07b1c68f2ff7ac544d46ac3d93e4c94ef"
128
+ "content": "947a13984fe9e992098c4d7b63303b6048f30d7ff0f509b666022e1c8c67cd54"
129
129
  }
130
130
  ]
131
131
  },
@@ -135,7 +135,7 @@
135
135
  "hashes": [
136
136
  {
137
137
  "alg": "SHA-256",
138
- "content": "27741d68ac996a060df7161b49e372f68e31b990a406f0792c79c474ff17eeca"
138
+ "content": "d539a0084dced536df3edadd79bcce94d1cb7205d1e534922fd8a1ca37c7fc7d"
139
139
  }
140
140
  ]
141
141
  },
@@ -145,7 +145,7 @@
145
145
  "hashes": [
146
146
  {
147
147
  "alg": "SHA-256",
148
- "content": "bea844c256e8d947464a4690d2224e4f28d61f433d6e50e5fe26bed3b415ae26"
148
+ "content": "b3b37c4d27ea28e00da2f6d1bc438e22ebb48da8dec365e9f4128071647c3dd0"
149
149
  }
150
150
  ]
151
151
  },
@@ -335,7 +335,7 @@
335
335
  "hashes": [
336
336
  {
337
337
  "alg": "SHA-256",
338
- "content": "b7c47af8811ca86ee8dd5d4d98cab76c4cceddaafbe09676141bacae92b69b9d"
338
+ "content": "98583e52265eb3951c2596f652f5046cc2044e5a4c12353ea1a380a037819778"
339
339
  }
340
340
  ]
341
341
  },
@@ -365,7 +365,7 @@
365
365
  "hashes": [
366
366
  {
367
367
  "alg": "SHA-256",
368
- "content": "d8288240b5420ddca2cabdb7b358c7c18ba59724defda951d0dbc61439b6fc4c"
368
+ "content": "e13c80580e477748ab072078f3b90e9301ad60b01106e228501f16bc753f994a"
369
369
  }
370
370
  ]
371
371
  },
package/src/cli.ts CHANGED
@@ -302,6 +302,9 @@ import {
302
302
  renderContentProbe,
303
303
  findNonRegistrableSkillDirs,
304
304
  assembleCompoundingReport,
305
+ banditStats,
306
+ narrowBanditReport,
307
+ renderBanditHealth,
305
308
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
306
309
  replayableInstances,
307
310
  buildWorkOrder,
@@ -518,6 +521,11 @@ Usage:
518
521
  dz tg-post --draft <file.html> [--manifest <sources.json>] [--channel <@name|id>] [--send --yes] [--night] [--preview] [--json] (the sender for an APPROVED channel post, per the accepted genai-tweets-channel ADRs: HTML mode only — never MarkdownV2; link preview OFF by default (x.com previews in Telegram are broken); the 00:00-06:00 MSK quiet window refuses without an explicit --night. DEFAULT IS A DRY-RUN: it validates the draft (tag balance, allowed tags, bare &/<, the 4096 visible-character limit with the overshoot counted) and runs the provenance gate over --manifest IN-PROCESS — a draft with no manifest is refused as unchecked, and anything but ALLOWED refuses. A real send needs --send --yes, stating ADR-004's manual-publishing decision out loud each time. The token comes from TELEGRAM_BOT_TOKEN or telegram.tokenFile in .dz/config.json and is never printed. exit 0 sent or clean dry-run / 1 refused or Telegram error / 2 usage)
519
522
  dz name-check [--command <n>] [--module <basename>] [--export <a,b>] [--project <dir>] [--json] (is this name free, BEFORE a line of code? Scans workspace SOURCE — never dist, because a stale build answers 'free' confidently. Checks a dz command name against the dispatcher AND the help block, a module basename against every package's src/, and exported identifiers against every declaration in the workspace. exit 0 all free / 1 at least one taken, naming where / 2 nothing asked or the scan did not run — an empty sweep is never a clean bill. Honest limit, printed on the passing path: it reads declarations, so a re-export under a different name stays the build's job)
520
523
  dz provenance-check --manifest <sources.json> [--project <dir>] [--json] (nothing goes out citing a source that may not leave this machine. Checks PROVENANCE, not words: every claim names its source, and only a KNOWN kind that resolves safely is cleared. Repo paths go through 'git -C <root> check-ignore' over the RESOLVED path — a symlink into an ignored directory is REFUSED (git classifies the string and never dereferences, MEASURED), and the verdict does not change with your working directory. Store records must be named in the git-TRACKED provenance-public.json, so declaring one public is a reviewable commit rather than a field inside an ignored store. An undeclared kind is refused, never inferred from the path's shape. exit 0 allowed / 1 blocked / 3 NOT ESTABLISHED — an empty manifest, an unreadable one, or an oracle that did not run is never a pass. It proves what was CITED: it cannot see a paraphrase with no citation, nor confidential text pasted by hand into an allowed file)
524
+ dz project-skills [--project <dir>] [--json] [--stages-json] (polymorphic feature-adr: resolve architecture/project-skills.json — fixed roles product-vision/critic/brand/impl-bar plus an open extra[] — into per-stage guidance. READ-ONLY. --project names the root explicitly, so it works from any cwd; without it the manifest is read from the current repo. No manifest ⇒ a byte-identical generic run)
525
+ dz discrimination-check --slug <slug> [--base <ref>] [--json] (does the ADR's named test actually DISCRIMINATE? Re-runs it on a worktree at the pre-feature commit, where it MUST go red. A test that passes with the feature removed proves nothing; dz amendment-check proves the test exists, this proves it bites)
526
+ dz guard [check|promote|init] [--json] [--force] (HARD/SOFT repo rules — readme-first, lockfile-in-sync, claim tagging — run automatically as a pre-flight inside dz publish. HARD blocks, SOFT warns)
527
+ dz architecture [--check --slug <s> --desc <text>] [--project <dir>] [--revise] (the live product map + vision: --check is the soft Step-0 сверка of a new feature against them, reporting {signal,confidence} rather than blocking)
528
+ dz sbom [--pack <name>] [--out <file>] (CycloneDX software bill of materials for the workspace, or for one pack with --pack)
521
529
  dz amendment-check --slug <slug> | --feature-dir <dir> | --all [--json] (the deterministic Step-8 amendment gate: every AM-N row must resolve to a test found INSIDE the file the row names; the PLAN is authoritative when it carries rows, and an ideation amendment the plan drops is a failure. exit 0 pass/skip, 1 fail, 3 NOT-ESTABLISHED — a section that parsed ZERO rows is never a pass. --all is a CENSUS and always exits 0. Does NOT prove non-vacuity — that is dz discrimination-check)
522
530
  dz feature-adr-record --kind ledger|training-pair --stage <s> [--slug <s>] [--row|--pair <json>] [--mark <n>] [--once] [--json] (the witnessed writer for the run-cost ledger and training pairs: the payload arrives as an ARGUMENT, never as shell; a malformed or wrong-kind payload is REFUSED before any write; the timestamp is stamped before serialising; the append is verified by re-reading the tail. exit 0 written|duplicate|skipped, 2 refused, 3 not-verified — a record failure is never blocking)
523
531
  dz feature-adr-checkpoint (--slug <feature> | --feature-dir <abs>) --stage <s> --input-hash <h> --result <json> [--artifact a,b] [--json] (record a pipeline stage ONLY after measuring its artifacts on disk; refuses a null result, an absent artifact, or a stage that declares none — the subagent runs a COMMAND instead of hand-writing durable state)
@@ -3572,8 +3580,8 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
3572
3580
  // second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
3573
3581
  // own output is written after it returns.
3574
3582
  const result = asJson
3575
- ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode }))
3576
- : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode });
3583
+ ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3584
+ : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3577
3585
 
3578
3586
  if (mode === 'semantic' && result.vectorEngine === 'none') {
3579
3587
  // --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
@@ -3605,6 +3613,23 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
3605
3613
  // "foreign-domain lessons kept" about the pre-cut list while the printed list was
3606
3614
  // missing one. Count it and say so; the reader can act on it (raise --limit).
3607
3615
  const displaced = boost !== null ? countDisplacedByCut(result.hits, boost.hits, limit) : 0;
3616
+ // lesson-bandit-rerank FR-8/AC-11: the bandit block must describe the POST-cut list the reader
3617
+ // actually sees. `dz recall` over-fetches under --domain and truncates AGAIN here, so reporting
3618
+ // the core's own (already post-merge-cut) count would still describe a list one cut too early.
3619
+ // ABSENT while disarmed — its presence is what tells a reader the feature ran.
3620
+ const shownDzIds = hits.map((h) => patternRecordId(h.pattern));
3621
+ const banditBlock = result.bandit === undefined
3622
+ ? undefined
3623
+ : narrowBanditReport(result.bandit, shownDzIds);
3624
+ // Exposures were DEFERRED (`deferExposures: true`) precisely so this cut happens first: `dz recall`
3625
+ // over-fetches under --domain and truncates again here, and counting an over-fetched hit as "seen"
3626
+ // both inflates the health metrics and mislabels a candidate the reader never laid eyes on.
3627
+ result.commitExposures?.(shownDzIds);
3628
+ const renderBanditNote = (b: NonNullable<typeof banditBlock>): string =>
3629
+ ` ℹ bandit payoff: ${b.moved} of ${b.armsConsidered} shown hit(s) moved (ctx ${b.contextKey}` +
3630
+ `, ${b.unknownArms} with no measured payoff yet, ${b.quarantinedExcluded} quarantined excluded` +
3631
+ `${b.exploration ? `, exploration ON — ${b.explored} trial impression(s)` : ''}` +
3632
+ `${b.reason !== null ? `, state ${b.reason}` : ''})`;
3608
3633
  if (asJson) {
3609
3634
  // Portable contract UNCHANGED (I-7/AC-6): a plain PatternRecord[] — round-trips through
3610
3635
  // `dz teach --from-json` regardless of which backend ranked each hit.
@@ -3635,6 +3660,10 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
3635
3660
  const cutNoteJson = renderDomainCutNote(displaced, limit);
3636
3661
  if (cutNoteJson !== '') process.stderr.write(`${cutNoteJson}\n`);
3637
3662
  }
3663
+ // STDERR, not stdout: `--json`'s stdout contract is a plain PatternRecord[] that round-trips
3664
+ // through `dz teach --from-json`, and wrapping it in an object to make room for one report
3665
+ // would break every existing consumer. The block travels beside the other honesty notes.
3666
+ if (banditBlock !== undefined) process.stderr.write(`${JSON.stringify({ bandit: banditBlock })}\n`);
3638
3667
  return semanticUnserved ? 1 : 0;
3639
3668
  }
3640
3669
 
@@ -3733,6 +3762,12 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
3733
3762
  // The loop stays VISIBLE (ADR D2): a quarantined hit is shown, marked, and explained.
3734
3763
  write(' ⚠q = quarantined (unproven hypothesis, rank damped) — confirm with dz teach --reinforce, or dz recall --promote <dzId> --apply');
3735
3764
  }
3765
+ if (banditBlock !== undefined) {
3766
+ // Say what the payoff term did — INCLUDING when it did nothing. `moved: 0` over many queries
3767
+ // means the feature is armed and inert, which is precisely the outcome nobody would notice
3768
+ // without this line.
3769
+ write(renderBanditNote(banditBlock));
3770
+ }
3736
3771
  if (boost !== null && shownDomain !== undefined) {
3737
3772
  // Say what the boost did — INCLUDING when it did nothing. A silent reorder would
3738
3773
  // let the reader believe the ranking was domain-aware when it had no match to work with.
@@ -8041,11 +8076,16 @@ async function cmdDownloads(cwd: string, write: Write): Promise<number> {
8041
8076
  * The pure engine lives in harness-core; this is the thin I/O shell (scan disk + git, render).
8042
8077
  */
8043
8078
  function cmdArchitecture(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
8044
- // Resolve repo root: prefer git toplevel, fall back to cwd (matches cmdRoam / cmdStats duality).
8045
- let repoRoot = cwd;
8079
+ // Resolve repo root: an explicit --project wins, then git toplevel, then cwd (matches cmdRoam /
8080
+ // cmdStats duality). The --project leg is the same fix as cmdProjectSkills got for field report
8081
+ // doc-25b, applied to its neighbour before it costs a second report: this command also reads an
8082
+ // `architecture/` tree, and on a feature-adr run against a target checkout the Step-0 product-vision
8083
+ // сверка silently no-ops for exactly that reason.
8084
+ const explicitRoot = (options.get('project') ?? '').trim();
8085
+ let repoRoot = explicitRoot !== '' ? resolve(cwd, explicitRoot) : cwd;
8046
8086
  try {
8047
- repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
8048
- } catch { /* not a git repo — use cwd */ }
8087
+ repoRoot = execSync('git rev-parse --show-toplevel', { cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || repoRoot;
8088
+ } catch { /* not a git repo — use the root as given */ }
8049
8089
 
8050
8090
  const manifest = loadSubsystemManifest(repoRoot);
8051
8091
  if (!manifest) {
@@ -8091,11 +8131,21 @@ function cmdArchitecture(options: Map<string, string>, flags: Set<string>, cwd:
8091
8131
  * --stages-json { hasManifest, design, code, qe, report } — ready to thread into prompts
8092
8132
  * No manifest ⇒ empty guidance strings ⇒ byte-identical run (FR-7).
8093
8133
  */
8094
- function cmdProjectSkills(flags: Set<string>, cwd: string, write: Write): number {
8095
- let repoRoot = cwd;
8134
+ function cmdProjectSkills(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
8135
+ // `--project <dir>` names the root EXPLICITLY. Until 2026-08-25 this command took only `cwd`, so
8136
+ // the manifest was reachable exclusively from the workspace you happened to stand in — and because
8137
+ // the known-flag list is deliberately FLAT, `--project` passed validation on a command that never
8138
+ // read it: exit 0, no warning, no manifest. MEASURED that day: a feature-adr run against a target
8139
+ // repo probed with `cd REPO`, found nothing, and fell open to a generic run — the honest
8140
+ // `polymorphism:null` was recorded and not one project lens reached any stage.
8141
+ const explicitRoot = (options.get('project') ?? '').trim();
8142
+ let repoRoot = explicitRoot !== '' ? resolve(cwd, explicitRoot) : cwd;
8096
8143
  try {
8097
- repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
8098
- } catch { /* not a git repo use cwd */ }
8144
+ // stderr is SWALLOWED: a non-repo directory is an ordinary case here, and letting git print
8145
+ // "fatal: not a git repository" onto the operator's terminal made a working command look broken.
8146
+ // The sibling cmdFeatureAdrSetup already redirects; this one did not.
8147
+ repoRoot = execSync('git rev-parse --show-toplevel', { cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || repoRoot;
8148
+ } catch { /* not a git repo — use the root as given */ }
8099
8149
 
8100
8150
  const plan = planProjectSkills(repoRoot);
8101
8151
  const hasManifest = plan.injections.length > 0 || plan.skipped.length > 0;
@@ -12036,8 +12086,13 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
12036
12086
  }
12037
12087
 
12038
12088
  const report = assembleCompoundingReport({ lessons, usage, guard, nowTs: new Date().toISOString(), evidenceLogs });
12039
- if (json) write(JSON.stringify({ ...report, exitCode: 0 }, null, 2));
12040
- else write(renderCompoundingReport(report));
12089
+ // lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
12090
+ // dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
12091
+ // already asks of the reinforcement loop (is the apply leg alive, or is it a write-only log?).
12092
+ // Read-only, and INSUFFICIENT_DATA on an absent state file — never a fake verdict.
12093
+ const bandit = banditStats(root);
12094
+ if (json) write(JSON.stringify({ ...report, bandit, exitCode: 0 }, null, 2));
12095
+ else write(`${renderCompoundingReport(report)}\n\n${renderBanditHealth(bandit)}`);
12041
12096
  return 0;
12042
12097
  }
12043
12098
 
@@ -13674,7 +13729,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
13674
13729
  case 'architecture':
13675
13730
  return cmdArchitecture(options, flags, cwd, write);
13676
13731
  case 'project-skills':
13677
- return cmdProjectSkills(flags, cwd, write);
13732
+ return cmdProjectSkills(options, flags, cwd, write);
13678
13733
  case 'mr-rakes':
13679
13734
  return await cmdMrRakes(options, flags, cwd, write);
13680
13735
  case 'retro':