@dzhechkov/harness-cli 0.3.236 → 0.3.237

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.236",
3
+ "version": "0.3.237",
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",
@@ -57,7 +57,7 @@
57
57
  "@dzhechkov/skills-reverse-engineering": "^0.1.0",
58
58
  "@dzhechkov/skills-presentation-storyteller": "^0.1.0",
59
59
  "@dzhechkov/skills-website-cloner": "^0.1.0",
60
- "@dzhechkov/harness-core": "0.3.127"
60
+ "@dzhechkov/harness-core": "0.3.128"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^25.6.0",
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "1e36d7e9e6022d412c030975ec686b1ab91c61c64c807ef2e9ff3fa75ac8917c"
28
+ "content": "7e8b772a6fa9ca0f36364cafd5ea996abeb62dc894c2f67f50ea7704a7a8824d"
29
29
  }
30
30
  ]
31
31
  },
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "1abb00d07ab57ad413d2747baa6a68cda0876a9e0c6f75db89c205fbf4863530"
38
+ "content": "667bf610bf890a7b93480a4b1045e98dc0c765effd80b78fde4e48538f595b3e"
39
39
  }
40
40
  ]
41
41
  },
@@ -45,7 +45,7 @@
45
45
  "hashes": [
46
46
  {
47
47
  "alg": "SHA-256",
48
- "content": "42d2a98f54c2ec773b49a0a43c959a78d070c8c427724f4affbd5bafef35621d"
48
+ "content": "593705e81f851c0e65ffa9feaa9bbd396e7a3295b9b21076cfd953ee5a05b93a"
49
49
  }
50
50
  ]
51
51
  },
@@ -105,7 +105,7 @@
105
105
  "hashes": [
106
106
  {
107
107
  "alg": "SHA-256",
108
- "content": "8a00bf14c3fdd437b7e482da8c4018366041755714ed1791b1c523f9dee5de6c"
108
+ "content": "cf6a3265890a8856ef31e3e7ac58026c15232c3c118ba0fcd1ce0d7590b84dd8"
109
109
  }
110
110
  ]
111
111
  },
@@ -115,7 +115,7 @@
115
115
  "hashes": [
116
116
  {
117
117
  "alg": "SHA-256",
118
- "content": "dea4446db1109285c1fc5f5283d7263462cb446f12b0d9b1e1e821374d0ca57d"
118
+ "content": "b9af9be1bd31acd60e76275ff1055dd8e9b39b531b0c375467648f142b74d2c4"
119
119
  }
120
120
  ]
121
121
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "a253625c94835ef94bbd141b1dd759578433423bd098314fd977c515f140dafe"
128
+ "content": "6a51963ede51129e43524d62f3d757255a002dfec3df2c1dfa519af6f235a667"
129
129
  }
130
130
  ]
131
131
  },
@@ -185,7 +185,7 @@
185
185
  "hashes": [
186
186
  {
187
187
  "alg": "SHA-256",
188
- "content": "543319797d47d599a646c7d695f82aa31491ffd97579edd691b6a9cb99af76ef"
188
+ "content": "1e7ef1d6e0796144999ad64d3650aeb4a2ccc7dfc66c5c5a8b705d783a9d0801"
189
189
  }
190
190
  ]
191
191
  },
@@ -205,7 +205,7 @@
205
205
  "hashes": [
206
206
  {
207
207
  "alg": "SHA-256",
208
- "content": "c5d4902f9b0c4133318b15e757d07856872177dc92752d5996440524342b7f88"
208
+ "content": "5180330ac6612542c80a74ea8e0983f5cd231e95bb3962eccdd65fbdbf6611a8"
209
209
  }
210
210
  ]
211
211
  },
@@ -225,7 +225,7 @@
225
225
  "hashes": [
226
226
  {
227
227
  "alg": "SHA-256",
228
- "content": "0538f3cfd8eb880305c08fef05a78601263003c54a6f1b44f97cd7ebd7bd37f9"
228
+ "content": "461beed08ad6cfde7bbf559701fcbc6978f622c3951c80463c4e4943c538eff5"
229
229
  }
230
230
  ]
231
231
  },
package/src/cli.ts CHANGED
@@ -179,6 +179,12 @@ import {
179
179
  renderProseDiff,
180
180
  readScenarioIds,
181
181
  DEFAULT_MAX_JUDGE_RUNS,
182
+ collectDeliveryFacts,
183
+ planDeliveryCheck,
184
+ renderDeliveryBrief,
185
+ classifyDelivery,
186
+ isUsablePlaneResult,
187
+ renderDeliveryReview,
182
188
  } from '@dzhechkov/harness-core';
183
189
  import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
184
190
  import type { SetupSpec } from '@dzhechkov/harness-core';
@@ -208,6 +214,7 @@ Usage:
208
214
  dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] [--require-signing] [--provenance|--no-provenance] (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)
209
215
  dz release [--filter <name>] [--tag] [--publish] [--json] [--dry-run] [--no-issue] (VERIFIED release: 4 HARD gates in FRONT of dz publish — full package test suites, audit >=high, node --check of every dist/bin file, bin smoke-boot via "node <bin> --help" — any red gate STOPS the release (exit 1) + best-effort gh issue; all green ⇒ re-sign reminder, then prints the ready dz publish command (or chains with --publish); never duplicates publish's own gates)
210
216
  dz parity [--target <name>] [--json] (the honest feature×target map, COMPUTED from the capability model — which harness feature is full / manual / absent on each of the ${TARGET_NAMES.length} targets, and via which form)
217
+ dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] (portable Step-10 Delivery Gate: prints the 4-plane review brief + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked)
211
218
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
212
219
  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)
213
220
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
@@ -5201,8 +5208,18 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
5201
5208
  return 1;
5202
5209
  }
5203
5210
 
5211
+ // portable-gates (direction b): --gates scaffolds the zero-config architecture/gates/delivery-check.md.
5212
+ // Its "runnable here" list is computed for --target (default agents-md, the AGENTS.md-class target class).
5213
+ const wantGates = flags.has('gates');
5214
+ const targetOpt = options.get('target');
5215
+ if (targetOpt !== undefined && !isTargetName(targetOpt)) {
5216
+ write(`dz feature-adr-setup: --target must be one of: ${TARGET_NAMES.join(', ')}`);
5217
+ return 1;
5218
+ }
5219
+ const gatesTarget: TargetName = isTargetName(targetOpt ?? '') ? (targetOpt as TargetName) : 'agents-md';
5220
+
5204
5221
  const specPath = options.get('from-spec');
5205
- if (specPath === undefined && !wantGuards) {
5222
+ if (specPath === undefined && !wantGuards && !wantGates) {
5206
5223
  // default + --plan: the read-only "which documents, and where?" answer.
5207
5224
  const plan = buildSetupPlan(scanForSetup(repoRoot));
5208
5225
  if (flags.has('json')) { write(JSON.stringify(plan, null, 2)); return 0; }
@@ -5224,8 +5241,9 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
5224
5241
  spec = {}; // --guards standalone: scaffold just the guard set
5225
5242
  }
5226
5243
  if (wantGuards) spec = { ...spec, guards: locCap !== undefined ? { locCap } : true };
5244
+ if (wantGates) spec = { ...spec, gates: true };
5227
5245
 
5228
- const result = scaffoldFromSpec(spec, readExistingForScaffold(repoRoot));
5246
+ const result = scaffoldFromSpec(spec, readExistingForScaffold(repoRoot), gatesTarget);
5229
5247
  if (flags.has('json') && !flags.has('apply')) { write(JSON.stringify(result, null, 2)); return 0; }
5230
5248
  write(renderScaffoldPreview(result));
5231
5249
 
@@ -5428,6 +5446,137 @@ function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' |
5428
5446
  return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
5429
5447
  }
5430
5448
 
5449
+ /**
5450
+ * `dz delivery-check` — the portable Step-10 Delivery Gate (feature portable-gates). The `manual` form that
5451
+ * travels to every `shell` target: the deterministic parts (artifact probes, hand-off arithmetic,
5452
+ * cross-validation bookkeeping) run IN the CLI; the semantic 4-plane review is DISPATCHED to the caller's own
5453
+ * agent runtime — the `dz challenge` cartridge shape. All logic lives in harness-core's pure engine; this is
5454
+ * the executor. Advisory exit codes (0 by default; `--strict` ⇒ 1 on `blocked`).
5455
+ *
5456
+ * --slug <slug> the feature under features/<slug> (required; featureDir derived internally)
5457
+ * --context-only default mode: print artifact probes + the 4-plane review brief (no verdict)
5458
+ * --findings <f.json> classify a fed-back findings array → write 10_delivery_review.md + print ready/blocked
5459
+ * --strict with --findings: exit 1 iff handoff === 'blocked' (CI gate)
5460
+ * --author <model> cosmetic: the reviewer to dispatch, printed alongside the brief
5461
+ * --json machine contract { planesChecked, planesSkipped, findings, handoff, artifact }
5462
+ */
5463
+ function cmdDeliveryCheck(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
5464
+ let repoRoot = cwd;
5465
+ try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; } catch { /* not git */ }
5466
+
5467
+ const json = flags.has('json');
5468
+ const errOut = (error: string): number => {
5469
+ if (json) write(JSON.stringify({ error, planesChecked: [], planesSkipped: [], findings: [], handoff: null, artifact: 'not-written' }, null, 2));
5470
+ else { write(`dz delivery-check: ${error}`); write(' allowed: --slug <slug>, --findings <f.json>, --author <model>, --context-only, --strict, --json'); }
5471
+ return 1;
5472
+ };
5473
+ // Strict allowlist (delivery finding: a typo'd --findings silently downgraded the --strict CI
5474
+ // gate to a vacuous exit-0 pass — a mistyped option must be an ERROR, never a mode change).
5475
+ const allowedFlags = new Set(['context-only', 'strict', 'json', 'help']);
5476
+ const allowedOptions = new Set(['slug', 'findings', 'author']);
5477
+ for (const flag of flags) if (!allowedFlags.has(flag)) return errOut(`unknown option --${flag}`);
5478
+ for (const key of options.keys()) {
5479
+ if (key.startsWith('_positional_')) return errOut(`unexpected argument "${options.get(key)}"`);
5480
+ if (!allowedOptions.has(key)) return errOut(`unknown option --${key}`);
5481
+ }
5482
+
5483
+ const slug = options.get('slug');
5484
+ if (slug === undefined || slug.trim() === '') return errOut('pass --slug <slug> (the feature under features/<slug>)');
5485
+ // Slug convention is a SECURITY boundary here (delivery finding, CONFIRMED traversal reproducer:
5486
+ // --slug '../../outside/evil' wrote 10_delivery_review.md outside the repo). kebab-case Latin,
5487
+ // <=40 chars — the repo's own slug rule as a layer-1 three-line check.
5488
+ const slugTrimmed = slug.trim();
5489
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slugTrimmed) || slugTrimmed.length > 40) {
5490
+ return errOut(`invalid --slug "${slugTrimmed}" — kebab-case Latin, max 40 chars (a path-like slug would redirect the report write)`);
5491
+ }
5492
+ const featureDir = join(repoRoot, 'features', slugTrimmed);
5493
+
5494
+ // --context-only is a real mode flag, not decoration: combining it with --findings contradicts
5495
+ // itself (delivery finding — the flag was accepted and silently ignored).
5496
+ if (flags.has('context-only') && options.get('findings') !== undefined) {
5497
+ return errOut('--context-only and --findings are mutually exclusive (context-only prints the brief; findings classifies the verdict)');
5498
+ }
5499
+
5500
+ // git status --porcelain — CLI layer only (the `no child_process in core` contract); a non-git target
5501
+ // degrades to an empty list, which is honest (AM-10: changed-files is informational, never a required gate).
5502
+ let changedFiles: string[] = [];
5503
+ try {
5504
+ const out = execSync('git status --porcelain', { cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
5505
+ changedFiles = out.split('\n').map((l) => l.slice(3).trim()).filter((s) => s.length > 0);
5506
+ } catch { /* non-git / unavailable → empty (honest) */ }
5507
+
5508
+ const facts = collectDeliveryFacts(featureDir, { changedFiles, repoRoot });
5509
+ const plan = planDeliveryCheck(facts);
5510
+ const author = options.get('author');
5511
+
5512
+ const findingsPath = options.get('findings');
5513
+ if (findingsPath === undefined) {
5514
+ // Default / --context-only: emit the led protocol (probes + brief). No verdict — the CLI has no model.
5515
+ if (json) {
5516
+ write(JSON.stringify({
5517
+ planesChecked: [],
5518
+ planesSkipped: plan.planes.map((p) => p.id),
5519
+ findings: [],
5520
+ handoff: null,
5521
+ artifact: 'not-written',
5522
+ probes: plan.probes,
5523
+ }, null, 2));
5524
+ return 0;
5525
+ }
5526
+ write(`dz delivery-check — ${slug} (portable 4-plane hand-off protocol; you drive the review)`);
5527
+ for (const p of plan.probes) {
5528
+ const mark = p.passed === true ? '✓' : p.passed === false ? '✗' : '○';
5529
+ write(` ${mark} ${p.description}${p.required ? ' [required]' : ' [informational]'}`);
5530
+ }
5531
+ write('');
5532
+ write(renderDeliveryBrief(plan, facts));
5533
+ // The hand-off criterion the verdict will be judged by, shown UP FRONT (this also makes
5534
+ // plan.criterionTemplate a consumed surface, not dead product code — delivery finding).
5535
+ write('\n── hand-off criterion (all must PASS for ready) ──');
5536
+ for (const c of plan.criterionTemplate) write(` ○ ${c.label}`);
5537
+ if (author !== undefined && author.trim() !== '') write(`\n── reviewer to dispatch ──\n${author.trim()}`);
5538
+ write('\n(next: run the four planes, then `dz delivery-check --slug ' + slug + ' --findings findings.json` for the verdict)');
5539
+ return 0;
5540
+ }
5541
+
5542
+ // --findings mode: parse (malformed/missing ⇒ [], a failed-plane input — fail-closed, never a throw).
5543
+ let reviewResults: unknown[] = [];
5544
+ try {
5545
+ const parsed: unknown = JSON.parse(readFileSync(resolve(cwd, findingsPath), 'utf-8'));
5546
+ if (Array.isArray(parsed)) reviewResults = parsed;
5547
+ else if (parsed !== null && typeof parsed === 'object' && Array.isArray((parsed as { planes?: unknown }).planes)) reviewResults = (parsed as { planes: unknown[] }).planes;
5548
+ else reviewResults = [];
5549
+ } catch { reviewResults = []; }
5550
+
5551
+ const verdict = classifyDelivery(plan, reviewResults as ({ findings?: readonly unknown[] } | null | undefined)[]);
5552
+ // The SAME predicate as the fail-closed verdict (core-exported) — a local copy was a drift channel.
5553
+ const planesChecked = plan.planes.filter((_, i) => isUsablePlaneResult(reviewResults[i])).map((p) => p.id);
5554
+ const planesSkipped = plan.planes.filter((_, i) => !isUsablePlaneResult(reviewResults[i])).map((p) => p.id);
5555
+
5556
+ // Write the regenerable report (NOT augment-never-clobber — deliberately overwritten each run).
5557
+ let artifact: 'written' | 'not-written' = 'not-written';
5558
+ try {
5559
+ const reviewPath = join(featureDir, '10_delivery_review.md');
5560
+ mkdirSync(dirname(reviewPath), { recursive: true });
5561
+ writeFileSync(reviewPath, renderDeliveryReview(verdict, facts));
5562
+ artifact = 'written';
5563
+ } catch { /* best-effort — the verdict is still returned */ }
5564
+
5565
+ if (json) {
5566
+ write(JSON.stringify({ planesChecked, planesSkipped, findings: verdict.findings, handoff: verdict.handoff, artifact }, null, 2));
5567
+ } else {
5568
+ write(`dz delivery-check — ${slug}: ${verdict.handoff}`);
5569
+ for (const c of verdict.criterion) {
5570
+ const mark = c.status === 'PASS' ? '✓' : c.status === 'FAIL' ? '✗' : '○';
5571
+ write(` ${mark} ${c.label}: ${c.status}${c.detail ? ` — ${c.detail}` : ''}`);
5572
+ }
5573
+ write(artifact === 'written' ? `\n↳ wrote features/${slug}/10_delivery_review.md` : '\n↳ 10_delivery_review.md NOT written (verdict above is authoritative)');
5574
+ }
5575
+
5576
+ if (flags.has('strict') && verdict.handoff === 'blocked') return 1;
5577
+ return 0;
5578
+ }
5579
+
5431
5580
  /**
5432
5581
  * `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
5433
5582
  * what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
@@ -5852,6 +6001,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
5852
6001
  return cmdChallenge(options, flags, cwd, write);
5853
6002
  case 'discrimination-check':
5854
6003
  return cmdDiscriminationCheck(options, flags, cwd, write);
6004
+ case 'delivery-check':
6005
+ return cmdDeliveryCheck(options, flags, cwd, write);
5855
6006
  case 'routing':
5856
6007
  return cmdRouting(options, flags, cwd, write);
5857
6008
  case 'bto-optimize':