@dzhechkov/harness-cli 0.8.5 → 0.8.6

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.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 83 commands, 14 presets, 10 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,23 +41,23 @@
41
41
  "sbom.json"
42
42
  ],
43
43
  "dependencies": {
44
- "@dzhechkov/harness-core": "^0.8.5",
45
- "@dzhechkov/harness-presets": "^0.5.17",
46
- "@dzhechkov/scout": "^0.8.10",
47
- "@dzhechkov/skills-devops": "^0.3.16",
48
- "@dzhechkov/skills-mcp": "^0.3.9",
49
- "@dzhechkov/skills-web3": "^0.2.8",
50
- "@dzhechkov/skills-qe": "^0.3.13",
51
- "@dzhechkov/skills-meta": "^0.9.54",
52
- "@dzhechkov/skills-academic": "^0.3.6",
53
- "@dzhechkov/skills-ecc": "^0.1.11",
54
- "@dzhechkov/skills-news": "^0.1.15",
55
- "@dzhechkov/skills-pm": "^0.1.4",
56
- "@dzhechkov/skills-taste": "^0.1.4",
57
- "@dzhechkov/skills-idea2prd": "^0.1.14",
58
- "@dzhechkov/skills-reverse-engineering": "^0.3.3",
59
- "@dzhechkov/skills-presentation-storyteller": "^0.1.13",
60
- "@dzhechkov/skills-website-cloner": "^0.1.4"
44
+ "@dzhechkov/harness-core": "^0.8.6",
45
+ "@dzhechkov/scout": "^0.8.11",
46
+ "@dzhechkov/skills-mcp": "^0.3.10",
47
+ "@dzhechkov/harness-presets": "^0.5.18",
48
+ "@dzhechkov/skills-devops": "^0.3.17",
49
+ "@dzhechkov/skills-web3": "^0.2.9",
50
+ "@dzhechkov/skills-qe": "^0.3.14",
51
+ "@dzhechkov/skills-meta": "^0.9.55",
52
+ "@dzhechkov/skills-academic": "^0.3.7",
53
+ "@dzhechkov/skills-ecc": "^0.1.12",
54
+ "@dzhechkov/skills-news": "^0.1.16",
55
+ "@dzhechkov/skills-idea2prd": "^0.1.15",
56
+ "@dzhechkov/skills-pm": "^0.1.5",
57
+ "@dzhechkov/skills-reverse-engineering": "^0.3.4",
58
+ "@dzhechkov/skills-presentation-storyteller": "^0.1.14",
59
+ "@dzhechkov/skills-taste": "^0.1.5",
60
+ "@dzhechkov/skills-website-cloner": "^0.1.5"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^25.6.0",
@@ -72,10 +72,13 @@
72
72
  },
73
73
  "repository": {
74
74
  "type": "git",
75
- "url": "https://github.com/djd1m/dz-harness-hub.git",
75
+ "url": "git+https://github.com/djd1m/dz-harness.git",
76
76
  "directory": "packages/@dzhechkov/harness-cli"
77
77
  },
78
- "homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme",
78
+ "homepage": "https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/harness-cli#readme",
79
+ "bugs": {
80
+ "url": "https://github.com/djd1m/dz-harness/issues"
81
+ },
79
82
  "scripts": {
80
83
  "build": "tsc -p tsconfig.json",
81
84
  "test": "vitest run",
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "e74440eef6812dbeaa550bdbbd80483e4cf4f38c22857260c534726f14ebb680"
28
+ "content": "4a54061280f2c8a3173a07e43492a73a927fdb98dccf6c1acd933735e5b9aca5"
29
29
  }
30
30
  ]
31
31
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "a5d8d7579320ddbb30016d48c6d74a8ecf5628d90efe3925ed8ecb06cde7477c"
128
+ "content": "d02abab0b734f19ac7e75d7a28b25ae43cbfc025911128c5848f8e81ba2bf01f"
129
129
  }
130
130
  ]
131
131
  },
@@ -135,7 +135,7 @@
135
135
  "hashes": [
136
136
  {
137
137
  "alg": "SHA-256",
138
- "content": "bee6b00ec45da2d49c60ff85a675f989100bf703ce05cbaa5e4f730417161ccc"
138
+ "content": "3ac75ba951cac7ae985b2b7407ffb2645ef36d98f44897d9094b00f4db20517f"
139
139
  }
140
140
  ]
141
141
  },
@@ -145,7 +145,7 @@
145
145
  "hashes": [
146
146
  {
147
147
  "alg": "SHA-256",
148
- "content": "e68711d595224a75faf9527aa6ec186b0c37ef551470c9df3690b780efe9cfd5"
148
+ "content": "c7c308fbc3d596905c25760ba8fa337656f42de06d0565f73ad7c230979a514d"
149
149
  }
150
150
  ]
151
151
  },
@@ -339,7 +339,7 @@
339
339
  },
340
340
  {
341
341
  "name": "dz:canonical-json-sha256-v2",
342
- "value": "11df4bc14008b0e123b91bf5dc395589a147c24de3afd1f7beb58093ec753325"
342
+ "value": "2549a7e988b7a04071319dbbad8917d4231cd648720e70485a594ee9e3bd4aa1"
343
343
  }
344
344
  ]
345
345
  },
@@ -369,7 +369,7 @@
369
369
  "hashes": [
370
370
  {
371
371
  "alg": "SHA-256",
372
- "content": "6d402ac972df1c859da5996a65dbd94699f5df5080217cf6fce9af354fe92699"
372
+ "content": "5ff3270b1b82fe28e15d09f37d8099468102abc926512303c7ef76e6c75ecf0d"
373
373
  }
374
374
  ]
375
375
  },
package/src/cli.ts CHANGED
@@ -15,6 +15,7 @@ import { execFile, execFileSync, execSync, spawn, spawnSync, type ChildProcess }
15
15
  import { createHash, randomBytes } from 'node:crypto';
16
16
  import { homedir, tmpdir } from 'node:os';
17
17
  import { createRequire } from 'node:module';
18
+ import { isDeepStrictEqual } from 'node:util';
18
19
 
19
20
  import {
20
21
  createSkill,
@@ -283,6 +284,7 @@ import {
283
284
  harvestStageOutcomes,
284
285
  recommendModels,
285
286
  planFeed,
287
+ unfedRuns,
286
288
  GRADE_SUCCESS_FLOOR,
287
289
  COST_LADDER,
288
290
  splitScenarios,
@@ -609,7 +611,7 @@ Usage:
609
611
  dz backlog ship <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (mark work DONE: new|enriched|in-progress → shipped, removing it from the roulette pool — run it after finishing a task; short id prefixes ok, ambiguous = loud error)
610
612
  dz backlog drop <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (retire an idea: new|enriched|in-progress → dropped)
611
613
  dz backlog edit <id> --text "<new>" | --append "<more>" [--dry-run] [--project <dir>] [--json] (rewrite ONE idea's text, preserving every other field; re-embeds the dedup vector, and on a failed re-embed MARKS the record embedStale so dedup refuses to trust it — previous text preserved in .dz/backlog/edits.jsonl)
612
- dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis: n runs, window, the grade-floor rule, skip reasons; qe is FORCED cross-family of code; --apply feeds .dz/routing-outcomes.json idempotently by runId)
614
+ dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis + current/STALE/UNFED store receipt; qe is FORCED cross-family of code; --apply feeds .dz/routing-outcomes.json idempotently by runId)
613
615
  dz backlog reopen <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (back to the pool: shipped|dropped|in-progress → new)
614
616
  dz backlog enrich <id> [--project <dir>] [--json] (stage the idea2prd input scaffold in features/<slug>/ and hand off to the idea2prd-manual skill — the CLI never fabricates a PRD)
615
617
  dz backlog jira <id> [--project <dir>] [--json] (draft a Jira issue via the configurable adapter (backlog.jira.adapter: jira-mcp|copilot-mcp|none); none writes an auditable jira-outbox/<id>.json stub)
@@ -7833,6 +7835,90 @@ function gatherReadmeCounts(root: string): { label: string; a: number; b: number
7833
7835
  function gatherGuardFacts(op: string, root: string, text: string | undefined, storeCap: number): Record<string, unknown> {
7834
7836
  const facts: Record<string, unknown> = { op };
7835
7837
  if (op === 'publish') {
7838
+ // Advisory I/O: unreadable telemetry or fed state is absence of evidence, never a fabricated
7839
+ // stale finding and never a publish blocker.
7840
+ try {
7841
+ const routing = readRoutingTelemetry(root);
7842
+ facts['routingFreshness'] = { unfedRunIds: unfedRuns(routing.harvest.samples, routing.alreadyFed) };
7843
+ } catch { /* fail-open: routing-store-stale is SOFT and needs gathered evidence */ }
7844
+ // marketplace-parity: regenerate the two published showcase manifests from the LIVE registry,
7845
+ // but redirect every write to a unique temp root. Version is operator-owned, so pinning the
7846
+ // published value into generatePlugin makes the comparison composition-only by construction.
7847
+ try {
7848
+ const showcaseDir = join(root, '.claude-plugin');
7849
+ if (!existsSync(showcaseDir)) {
7850
+ facts['marketplaceParity'] = { applicable: false };
7851
+ } else {
7852
+ const pluginPath = join(showcaseDir, 'plugin.json');
7853
+ const marketplacePath = join(showcaseDir, 'marketplace.json');
7854
+ const hasPlugin = existsSync(pluginPath);
7855
+ const hasMarketplace = existsSync(marketplacePath);
7856
+ const manifestFailures: { file: 'plugin.json' | 'marketplace.json'; error: string }[] = [];
7857
+ const readManifest = (path: string, file: 'plugin.json' | 'marketplace.json'): unknown => {
7858
+ try {
7859
+ return JSON.parse(readFileSync(path, 'utf8')) as unknown;
7860
+ } catch (error) {
7861
+ const message = error instanceof Error ? error.message : String(error);
7862
+ manifestFailures.push({ file, error: message.replace(/\s+/g, ' ').slice(0, 240) });
7863
+ return undefined;
7864
+ }
7865
+ };
7866
+ const versionOf = (manifest: unknown): string | undefined => {
7867
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return undefined;
7868
+ const version = (manifest as Record<string, unknown>)['version'];
7869
+ return typeof version === 'string' && version !== '' ? version : undefined;
7870
+ };
7871
+ const publishedPlugin = hasPlugin ? readManifest(pluginPath, 'plugin.json') : undefined;
7872
+ const publishedMarketplace = hasMarketplace ? readManifest(marketplacePath, 'marketplace.json') : undefined;
7873
+ const marketplaceRecord = publishedMarketplace && typeof publishedMarketplace === 'object' && !Array.isArray(publishedMarketplace)
7874
+ ? publishedMarketplace as Record<string, unknown>
7875
+ : undefined;
7876
+ const marketplacePlugins = marketplaceRecord !== undefined && Array.isArray(marketplaceRecord['plugins'])
7877
+ ? marketplaceRecord['plugins']
7878
+ : [];
7879
+ const publishedVersion = versionOf(publishedPlugin)
7880
+ ?? versionOf(publishedMarketplace)
7881
+ ?? versionOf(marketplacePlugins[0]);
7882
+ if (manifestFailures.length > 0) {
7883
+ facts['marketplaceParity'] = {
7884
+ applicable: true,
7885
+ manifestFailures,
7886
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
7887
+ };
7888
+ } else if (hasPlugin !== hasMarketplace) {
7889
+ facts['marketplaceParity'] = {
7890
+ applicable: true,
7891
+ onlyOnePresent: true,
7892
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
7893
+ };
7894
+ } else if (!hasPlugin) {
7895
+ facts['marketplaceParity'] = { applicable: false };
7896
+ } else {
7897
+ let scratch: string | undefined;
7898
+ try {
7899
+ const registry = buildRegistry(root);
7900
+ scratch = mkdtempSync(join(tmpdir(), 'dz-guard-marketplace-'));
7901
+ const generated = generatePlugin(scratch, registry, { version: publishedVersion });
7902
+ const freshPlugin = JSON.parse(readFileSync(generated.pluginJsonPath, 'utf8')) as unknown;
7903
+ const freshMarketplace = JSON.parse(readFileSync(generated.marketplaceJsonPath, 'utf8')) as unknown;
7904
+ facts['marketplaceParity'] = {
7905
+ applicable: true,
7906
+ diverged: !isDeepStrictEqual(publishedPlugin, freshPlugin)
7907
+ || !isDeepStrictEqual(publishedMarketplace, freshMarketplace),
7908
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
7909
+ };
7910
+ } catch {
7911
+ facts['marketplaceParity'] = {
7912
+ applicable: true,
7913
+ regenerateFailed: true,
7914
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
7915
+ };
7916
+ } finally {
7917
+ if (scratch !== undefined) rmSync(scratch, { recursive: true, force: true });
7918
+ }
7919
+ }
7920
+ }
7921
+ } catch { /* fail-open only on unexpected showcase-discovery failure; manifest read/parse errors are structured violations above */ }
7836
7922
  // agents-md-policy-sync: fixed registry, no tree walk. The pure detector
7837
7923
  // recomputes every expected hash from current source text; this gatherer
7838
7924
  // only supplies bytes. Any unexpected gather failure omits the fact, and
@@ -14272,6 +14358,34 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
14272
14358
  return sub === undefined ? 0 : 1;
14273
14359
  }
14274
14360
 
14361
+ /** Read the recommender's existing telemetry planes plus its idempotency receipt. One adapter is
14362
+ * shared by `recommend` and the publish advisory so their freshness definitions cannot drift. */
14363
+ function readRoutingTelemetry(repoRoot: string): { harvest: ReturnType<typeof harvestStageOutcomes>; alreadyFed: string[] } {
14364
+ const records: unknown[] = readHarnessRecords(repoRoot);
14365
+ for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
14366
+ if (!existsSync(base)) continue;
14367
+ let names: string[] = [];
14368
+ try { names = readdirSync(base); } catch { continue; }
14369
+ for (const name of names) {
14370
+ const sidecar = join(base, name, 'run-meta.json');
14371
+ if (!existsSync(sidecar)) continue;
14372
+ try {
14373
+ const meta = JSON.parse(readFileSync(sidecar, 'utf-8')) as { runMeta?: { resolved?: boolean; records?: unknown[] } };
14374
+ if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records)) records.push(...meta.runMeta.records);
14375
+ } catch { /* an unreadable sidecar contributes no asserted record */ }
14376
+ }
14377
+ }
14378
+ let alreadyFed: string[] = [];
14379
+ const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
14380
+ if (existsSync(fedPath)) {
14381
+ const parsed = JSON.parse(readFileSync(join(repoRoot, '.dz', 'routing-fed.json'), 'utf-8')) as unknown;
14382
+ if (!Array.isArray(parsed)) throw new Error('.dz/routing-fed.json must be a JSON array of runIds');
14383
+ if (parsed.some((v) => typeof v !== 'string')) throw new Error('.dz/routing-fed.json contains a non-string runId');
14384
+ alreadyFed = parsed as string[];
14385
+ }
14386
+ return { harvest: harvestStageOutcomes(records), alreadyFed };
14387
+ }
14388
+
14275
14389
  /**
14276
14390
  * `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
14277
14391
  * what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
@@ -14286,28 +14400,20 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
14286
14400
  // ── recommend — harvest real telemetry, print per-stage picks WITH THE BASIS, optionally feed the
14287
14401
  // store (a9c3dd5c fn 3, ADR-001). Sources: live harness records + imported run-meta.json sidecars.
14288
14402
  if (options.get('_positional_0') === 'recommend') {
14289
- const records: unknown[] = readHarnessRecords(repoRoot);
14290
- // imported runs (trace-bundle ADR-001 D4): run-meta sidecars carry runMeta.records of the SAME shape
14291
- for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
14292
- if (!existsSync(base)) continue;
14293
- let names: string[] = [];
14294
- try { names = readdirSync(base); } catch { continue; }
14295
- for (const name of names) {
14296
- const sidecar = join(base, name, 'run-meta.json');
14297
- if (!existsSync(sidecar)) continue;
14298
- try {
14299
- const meta = JSON.parse(readFileSync(sidecar, 'utf-8')) as { runMeta?: { resolved?: boolean; records?: unknown[] } };
14300
- if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records)) records.push(...meta.runMeta.records);
14301
- } catch { /* an unreadable sidecar contributes nothing — counted below as noResult */ }
14302
- }
14403
+ let routing: ReturnType<typeof readRoutingTelemetry>;
14404
+ try { routing = readRoutingTelemetry(repoRoot); }
14405
+ catch (e) {
14406
+ write(`dz routing recommend: cannot read routing freshness state — ${(e as Error).message}`);
14407
+ return 1;
14303
14408
  }
14304
- const harvest = harvestStageOutcomes(records);
14305
- const rec = recommendModels(harvest, { ...(options.get('tier') !== undefined ? { tier: options.get('tier')! } : {}) });
14409
+ const harvest = routing.harvest;
14410
+ let rec = recommendModels(harvest, {
14411
+ ...(options.get('tier') !== undefined ? { tier: options.get('tier')! } : {}),
14412
+ alreadyFed: routing.alreadyFed,
14413
+ });
14306
14414
  if (flags.has('apply')) {
14307
14415
  const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
14308
- let alreadyFed: string[] = [];
14309
- try { alreadyFed = JSON.parse(readFileSync(fedPath, 'utf-8')) as string[]; } catch { /* first apply */ }
14310
- const plan = planFeed(harvest.samples, alreadyFed);
14416
+ const plan = planFeed(harvest.samples, routing.alreadyFed);
14311
14417
  for (const sample of plan.toFeed) finalizeOutcome(repoRoot, sample.stage, sample.tier, sample.model, sample.success);
14312
14418
  try {
14313
14419
  mkdirSync(join(repoRoot, '.dz'), { recursive: true });
@@ -14316,6 +14422,10 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
14316
14422
  write(`dz routing recommend: fed ${plan.toFeed.length} sample(s) but could NOT persist the fed-set — a re-apply WILL double-count: ${(e as Error).message}`);
14317
14423
  return 1;
14318
14424
  }
14425
+ rec = recommendModels(harvest, {
14426
+ ...(options.get('tier') !== undefined ? { tier: options.get('tier')! } : {}),
14427
+ alreadyFed: plan.fedAfter,
14428
+ });
14319
14429
  write(`dz routing recommend --apply: fed ${plan.toFeed.length} sample(s) into .dz/routing-outcomes.json${plan.skippedRuns.length > 0 ? `; skipped ${plan.skippedRuns.length} already-fed run(s) (idempotent by runId)` : ''}`);
14320
14430
  }
14321
14431
  if (flags.has('json')) {
@@ -14327,6 +14437,9 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
14327
14437
  write(` ${s.stage.padEnd(14)} ${s.spec.padEnd(20)} ${s.insufficientData ? '[insufficient data — cold-start pick]' : '[quality bar met]'} ${s.pick.evidence}`);
14328
14438
  }
14329
14439
  write(` basis: ${rec.basis.rule}`);
14440
+ if (rec.basis.freshness === 'current') write(' store: current (0 unharvested runs)');
14441
+ else if (rec.basis.freshness === 'stale') write(` store: STALE — ${rec.basis.unfed.count} run(s) harvested but never fed; auto-cost is routing from an older snapshot — run \`dz routing recommend --apply\``);
14442
+ else write(' store: UNFED — auto-cost has no learned rows; picks below are cold-start');
14330
14443
  write(` basis: skipped records — no result ${rec.basis.skipped.noResult}, no modelsUsed ${rec.basis.skipped.noModels} (predates model routing — history, not an error), no grade ${rec.basis.skipped.noGrade}, unknown model ${rec.basis.skipped.unknownModel}`);
14331
14444
  write(` basis: ${rec.basis.crossFamilyNote}`);
14332
14445
  return 0;