@dzhechkov/harness-cli 0.3.237 → 0.3.239

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/.dz-manifest.json CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "path": "README.md",
12
- "sha256": "7e8b772a6fa9ca0f36364cafd5ea996abeb62dc894c2f67f50ea7704a7a8824d"
12
+ "sha256": "6cdf43e972df9144b6eb93beed515bb216c1545bdd69b696b773a3dbe2d5b679"
13
13
  },
14
14
  {
15
15
  "path": "coverage/coverage-final.json",
@@ -41,15 +41,15 @@
41
41
  },
42
42
  {
43
43
  "path": "dist/cli.d.ts.map",
44
- "sha256": "cf6a3265890a8856ef31e3e7ac58026c15232c3c118ba0fcd1ce0d7590b84dd8"
44
+ "sha256": "dfb260e21d46dfc8020eef0bf5ba570f9c335964181cf90446c1c5d761626415"
45
45
  },
46
46
  {
47
47
  "path": "dist/cli.js",
48
- "sha256": "b9af9be1bd31acd60e76275ff1055dd8e9b39b531b0c375467648f142b74d2c4"
48
+ "sha256": "a5b7d2fc4f582c507dfab54f1cc4754ca6700f7e686a3c72871afa807cceac25"
49
49
  },
50
50
  {
51
51
  "path": "dist/cli.js.map",
52
- "sha256": "6a51963ede51129e43524d62f3d757255a002dfec3df2c1dfa519af6f235a667"
52
+ "sha256": "8fb0c46e092ac7240af9d254f272da649d911b3307b39dc60ac1ef43d349834f"
53
53
  },
54
54
  {
55
55
  "path": "dist/index.d.ts",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  {
75
75
  "path": "package.json",
76
- "sha256": "1e7ef1d6e0796144999ad64d3650aeb4a2ccc7dfc66c5c5a8b705d783a9d0801"
76
+ "sha256": "82a481991b03522d4da62fe49c032c03445928a46a226e8cbb05498eb9321950"
77
77
  },
78
78
  {
79
79
  "path": "src/bin.ts",
@@ -81,7 +81,7 @@
81
81
  },
82
82
  {
83
83
  "path": "src/cli.ts",
84
- "sha256": "5180330ac6612542c80a74ea8e0983f5cd231e95bb3962eccdd65fbdbf6611a8"
84
+ "sha256": "ccb1af3966a258b17eed99ce8b830639166ad8a8a5075ca027d8f30077025d19"
85
85
  },
86
86
  {
87
87
  "path": "src/index.ts",
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "path": "test/cli.test.ts",
92
- "sha256": "461beed08ad6cfde7bbf559701fcbc6978f622c3951c80463c4e4943c538eff5"
92
+ "sha256": "3deb99917f3d8b4a5c9a3b0315bd65a3099ce2b508121cfa2bdeed19b7554d48"
93
93
  },
94
94
  {
95
95
  "path": "tsconfig.json",
@@ -101,5 +101,5 @@
101
101
  }
102
102
  ]
103
103
  },
104
- "signature": "06gdM99WpJhmarOtUFoeMAlGAfAvCboh/QGbCEIBhMAqgLn5rnSCXAb2023QdO+hZsuxrijyGzgBh62l/L+GBQ=="
104
+ "signature": "mJYQHwfoPEK5x7cn8B0AbYnqRpUO9iy3qHhWrIHQ/4xqgsTOb/DMSDScPDY2GxfvZsk2pUVuPQS8xjWMKXH9BA=="
105
105
  }
package/README.md CHANGED
@@ -372,6 +372,41 @@ the release job gets a perfectly attested malicious package.
372
372
  A ready-to-install workflow is at `features/publish-provenance/07_code_changes/publish.yml`; copy it to
373
373
  `.github/workflows/` and add an `NPM_TOKEN` secret.
374
374
 
375
+ ### Lesson quarantine — a fresh lesson is a hypothesis, not knowledge
376
+
377
+ Self-learning has a poisoning problem: the moment `dz teach` stores a lesson, it ranks alongside
378
+ patterns proven over months and can ride the auto-inject hook into your next task's context — even
379
+ if it is wrong, one-off, or junk. Quarantine (opt-in) closes the gap between COLLECT and RANK:
380
+
381
+ ```jsonc
382
+ // .dz/config.json
383
+ { "memory": { "learning": {
384
+ "quarantine": true, // fresh lessons start as quarantined hypotheses
385
+ "quarantineDamp": 0.5, // rank multiplier for ⚠q hits in recall (0..1]
386
+ "quarantineExpireDays": 30 // unconfirmed after N days ⇒ expiry CANDIDATE (report only)
387
+ } } }
388
+ ```
389
+
390
+ Three surfaces, three strictness levels — and promotion is EARNED, never automatic:
391
+
392
+ ```bash
393
+ dz teach "..." # → "⚠ quarantined: excluded from auto-inject, damped in recall"
394
+ dz recall "topic" # ⚠q hits are VISIBLE but marked + rank-damped (never hidden)
395
+ # the UserPromptSubmit auto-inject hook EXCLUDES ⚠q lessons entirely (logged, never silent)
396
+
397
+ dz teach --reinforce "<exact text>" # confirming a lesson IS its promotion
398
+ dz recall --promote <dzId> --apply # or promote explicitly (dry-run by default)
399
+
400
+ dz consolidate --prune-quarantine # report expired unconfirmed lessons (dry-run)
401
+ dz consolidate --prune-quarantine --apply # remove them (snapshots first) — a SEPARATE gate,
402
+ # never coupled to --prune-noise (unproven ≠ garbage)
403
+ ```
404
+
405
+ Absent config = zero behavior change. Existing lessons are grandfathered as promoted; a corrupt
406
+ quarantine marker reads as promoted (a metadata glitch must never isolate proven knowledge).
407
+ **When to use:** any project where subagents teach lessons unattended — the quarantine is the gate
408
+ between "an agent wrote this down" and "agents now act on it".
409
+
375
410
  ### Target parity — `dz parity` (the honest feature × target map)
376
411
 
377
412
  The harness runs on 10 targets, but not every feature runs everywhere: hooks, MCP (Model Context
@@ -399,6 +434,57 @@ dz parity — codex (capabilities: shell, skills, mcp)
399
434
  **When to use:** before promising a workflow to a teammate on Cursor/Codex/Hermes; when choosing
400
435
  a target for a project; as the requirements input for porting a feature to more targets.
401
436
 
437
+ ### Do your skills actually register? — `dz skills-verify`
438
+
439
+ Shipping a skill pack is not the same as a skill **registering**. A layout test that asserts
440
+ `SKILL.md` exists on disk verifies a **proxy**; the property that matters is whether Claude Code
441
+ loads it. `@dzhechkov/health-advisor` 1.2.0 shipped to npm with a green layout test and **zero**
442
+ skills registering — the gap was only found by hand, after publish. `dz skills-verify` closes it.
443
+
444
+ Two layers:
445
+
446
+ | layer | needs a session? | what it proves |
447
+ |---|---|---|
448
+ | `--static` | no — instant, CI-safe | which dirs *can* register, and flags the three shapes that never can |
449
+ | default | yes (~3 s) | the **authoritative** listing, read from the session's `system/init` event — no model prose |
450
+
451
+ ```bash
452
+ dz skills-verify --static # CI gate: layout only, exits 1 on a problem
453
+ dz skills-verify # full check in the current project
454
+ dz skills-verify --dir ../my-app --expect my-skill,my-other-skill
455
+ dz skills-verify --json --strict # machine-readable; --strict makes inconclusive exit 1
456
+ ```
457
+
458
+ A healthy project, and one with the exact defect that shipped:
459
+
460
+ ```
461
+ $ dz skills-verify
462
+ dz skills-verify: PASS — all 72 expected skill(s) are registered
463
+ layout: 72 registrable skill dir(s) under /path/.claude/skills
464
+ session: 106 skill(s) registered · client 2.1.220 · 5 plugin(s) loaded
465
+
466
+ $ dz skills-verify --dir /tmp/broken-install
467
+ dz skills-verify: FAIL — nothing can register: 24 layout problem(s) and no registrable skill directory
468
+ layout problems (these can never register):
469
+ [no-skill-md] no SKILL.md at health-advisor/SKILL.md — this directory cannot register
470
+ [plugin-manifest-trap] health-advisor/.claude-plugin/plugin.json does not auto-register — plugins load from the marketplace, not from .claude/skills
471
+ [buried-skill-md] health-advisor/skills/clinical-decision-support/SKILL.md is 2+ levels deep …
472
+ ```
473
+
474
+ A `.claude-plugin/plugin.json` under `.claude/skills/` is reported as an **advisory**, not a failure:
475
+ that layout may load under workspace trust, so the verdict asks the session whether the plugin actually
476
+ loaded (`init.plugins`) instead of judging the layout. A container with skills inside and no matching
477
+ loaded plugin fails, and says exactly that.
478
+
479
+ **Exit codes: `0` pass · `1` fail · `2` inconclusive.** The third is the point — a missing `claude`
480
+ binary, a login prompt, a timeout, or a session that read a *different* project all yield
481
+ `inconclusive`, **never** a pass. (A session that read another project cannot testify that a skill is
482
+ missing, so its listing is refused rather than believed.) A layout so broken that nothing is
483
+ registrable fails too, instead of passing vacuously on an empty expectation.
484
+
485
+ **Use it when:** publishing a package that installs skills, after `dz setup` in a new project, or in
486
+ CI (`--static` needs no Claude session at all).
487
+
402
488
  ### Portable delivery gate — `dz delivery-check` (Step-10, on every shell target)
403
489
 
404
490
  The feature-adr Step-10 Delivery Gate reviews a landed feature across four orthogonal planes and
@@ -549,7 +635,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
549
635
 
550
636
  > **A skill and its npx toolkit are not duplicates — they're a graduation.** Several skills (e.g. `feature-adr`, `design-thinking`) exist BOTH as a skill inside a `dz` preset AND as a standalone `npx` package. The preset's SKILL.md is **fully functional on its own** (the whole methodology — modules + references — travels with it, and it auto-activates by description), and it's the only way to compile that capability to the **non-Claude platforms** (Codex/OpenCode/Hermes/OpenClaude) via `dz`. The npx package adds **project-level runtime governance** around the same skill: a slash command, governance rules, a context shard, and (for feature-adr) reward-learning + `/harvest`. So: pick the **skill/preset** for a working capability across platforms; pick the **npx toolkit** when you want it as a governed, command-driven fixture of one project.
551
637
 
552
- ## All Commands (57)
638
+ ## All Commands (58)
553
639
 
554
640
  ```
555
641
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
@@ -605,6 +691,7 @@ dz routing [--stage <s>] [--json] # inspect the learned cost-optimal routing s
605
691
  dz bto-optimize --split|--plan|--select|--scope-check|--diff [--json] # deterministic engine behind /bto-optimize: hold-out split + hard-capped budget + no-regress-on-holdout winner selection (defeats judge-gaming); prose-only, diff-confirmed, never auto-writes
606
692
  dz discrimination-check --test <f[,f]> [--base <ref>] [--name <filter>] [--runner <cmd>] [--json] # §42 test-discrimination gate for feature-adr Step-8: run the ADR's property test in an isolated git worktree at pre-feature base — it MUST go red without the fix; a green is a false green (HIGH finding, advisory, never auto-aborts)
607
693
  dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] # portable Step-10 Delivery Gate: the `manual` form that travels to every shell target — prints the 4-plane review brief (regressions ‖ security ‖ code-quality ‖ product-honesty) + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off (only cross-validated BLOCKER/HIGH count) and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked
694
+ dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--timeout <s>] [--json] # does .claude/skills/ actually REGISTER? --static = instant layout scan (CI-safe, no session): flags dirs that can never register; default also starts a real session and reads the authoritative system/init listing. exit 0 pass / 1 fail / 2 inconclusive — an unobservable registration is NEVER a pass
608
695
  dz sign --init --out <path> | --pack <dir> --key <path> # --init: generate the Ed25519 keypair (private OUTSIDE the repo, prints the public key for keys/dz.pub); else sign a pack's manifest + CycloneDX SBOM
609
696
  dz sbom --pack <dir> [--out <file>] # emit the CycloneDX 1.5 SBOM for a pack standalone (file-level bill of materials); print to stdout or write to a file
610
697
  dz guard check --op <publish|teach|consolidate|reindex> [--text <s>] [--json] [--force <reason>] # declarative constraint layer before self-mutating ops: HARD violation → block (exit 1), SOFT → warn; zero-config defaults, .dz/guard.json to customise; dz guard --init | dz guard log (append-only audit). dz publish runs it automatically (--no-guard "<reason>" = logged escape hatch)
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuQH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACvD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAo9K9E,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkJ5E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoRH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACvD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAiuL9E,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAoJ5E"}
package/dist/cli.js CHANGED
@@ -6,10 +6,10 @@
6
6
  import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
7
7
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { execSync } from 'node:child_process';
9
+ import { execSync, spawn } from 'node:child_process';
10
10
  import { homedir, tmpdir } from 'node:os';
11
11
  import { createRequire } from 'node:module';
12
- import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, } from '@dzhechkov/harness-core';
12
+ import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, parseInitFacts, verifyRegistration, registrationExitCode, renderRegistrationReport, } from '@dzhechkov/harness-core';
13
13
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
14
14
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
15
15
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -35,11 +35,12 @@ Usage:
35
35
  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)
36
36
  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)
37
37
  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)
38
+ dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--json] (does .claude/skills/ actually REGISTER? --static = instant layout scan for CI; default reads the authoritative system/init listing from a real session. exit 0 pass / 1 fail / 2 inconclusive — never a false pass)
38
39
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
39
40
  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)
40
41
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
41
- dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] (prune-noise: DRY-RUN by default; --apply snapshots then deletes)
42
- dz recall "<query>" [--limit <N>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] (forget: dry-run default; snapshots before removing)
42
+ dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] [--prune-quarantine [--apply]] (both prunes: DRY-RUN by default; --apply snapshots then deletes; prune-quarantine = expired unproven lessons ONLY, never coupled to noise)
43
+ dz recall "<query>" [--limit <N>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] | dz recall --promote <dzId>[,<dzId>] [--apply] (forget/promote: dry-run default; forget snapshots before removing; promote lifts lesson-quarantine)
43
44
  dz vector status [--project <dir>] [--json] (semantic tier: engine, mirrored vs lexical counts, pending queue)
44
45
  dz vector reindex [--project <dir>] [--json] (snapshot, re-embed learned-pattern vectors, stamp current model)
45
46
  dz vector export <path> [--project <dir>] (portable VECTOR form (.rvf, opt-in RVF engine); patterns ship via recall --all --json)
@@ -1199,6 +1200,18 @@ async function cmdTeach(options, flags, cwd, write) {
1199
1200
  if (receipt.mirrored > 0)
1200
1201
  write(` ↳ mirrored to vector tier (${receipt.engine ?? 'vector'})`);
1201
1202
  };
1203
+ // lesson-quarantine FR-8: the fresh-teach mirror carries the qStatus marker so the hook daemon
1204
+ // (which reads only the mirror's metadata) can exclude unproven lessons from auto-inject.
1205
+ const emitMirrorQ = async (root, records, source, quarantined) => {
1206
+ if (flags.has('no-mirror') || records.length === 0 || !vectorMirrorEnabled(root))
1207
+ return;
1208
+ const entries = records
1209
+ .map((r) => patternVectorEntry(r, source, quarantined ? { quarantined: true } : {}))
1210
+ .filter((e) => e !== undefined);
1211
+ const receipt = await mirrorEntriesToVector(root, entries);
1212
+ if (receipt.mirrored > 0)
1213
+ write(` ↳ mirrored to vector tier (${receipt.engine ?? 'vector'})${quarantined ? ' [quarantined]' : ''}`);
1214
+ };
1202
1215
  // `dz teach --harmonize` — documented ALIAS of `dz vector harmonize`: SEMANTIC dedup of the
1203
1216
  // learned store. ONE implementation (harmonizeVectorStore), two entry points (QR-6). Routed
1204
1217
  // BEFORE the --from-json / single-teach paths. Dry-run by default; --apply after a backup.
@@ -1278,6 +1291,10 @@ async function cmdTeach(options, flags, cwd, write) {
1278
1291
  const trained = await backend.train();
1279
1292
  if (trained.flushed > 0) {
1280
1293
  write(`↳ reinforced ${reinforce}`);
1294
+ // lesson-quarantine: reinforcement IS promotion — keep the hook daemon's mirror in step.
1295
+ const clearedQ = clearAgentdbQuarantine(projectRoot, [reinforce]);
1296
+ if (clearedQ.cleared > 0)
1297
+ write(` ↳ promoted out of quarantine (mirror updated)`);
1281
1298
  return 0;
1282
1299
  }
1283
1300
  // HIGH-fix: a no-match must NOT auto-teach the raw argument — callers pass dzIds or truncated
@@ -1309,6 +1326,9 @@ async function cmdTeach(options, flags, cwd, write) {
1309
1326
  // teach below so the lesson is NEVER silently discarded (the exact silent-drop the ADR forbids).
1310
1327
  if (trained.flushed > 0) {
1311
1328
  write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
1329
+ const clearedQ = clearAgentdbQuarantine(projectRoot, [verdict.dzId]);
1330
+ if (clearedQ.cleared > 0)
1331
+ write(' ↳ promoted out of quarantine (mirror updated)');
1312
1332
  return 0;
1313
1333
  }
1314
1334
  write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
@@ -1335,18 +1355,45 @@ async function cmdTeach(options, flags, cwd, write) {
1335
1355
  // Tier-2 (ADR-005): persist through the unified @dzhechkov/memory store. recordPattern
1336
1356
  // folds any legacy .dz/patterns.jsonl into the backend (idempotent) and returns the
1337
1357
  // total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
1338
- const count = await recordPattern(projectRoot, entry);
1358
+ // lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
1359
+ const quarantineOn = readMemoryLearningConfig(projectRoot).quarantine;
1360
+ const count = await recordPattern(projectRoot, entry, quarantineOn ? { quarantine: true } : {});
1339
1361
  write(`Learned: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
1340
1362
  write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
1341
1363
  write(` Total patterns: ${count}`);
1364
+ if (quarantineOn) {
1365
+ write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
1366
+ }
1342
1367
  // The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
1343
- await emitMirror(projectRoot, [entry], 'dz-teach');
1368
+ await emitMirrorQ(projectRoot, [entry], 'dz-teach', quarantineOn);
1344
1369
  return 0;
1345
1370
  }
1346
1371
  async function cmdConsolidate(options, flags, cwd, write) {
1347
1372
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1348
1373
  const sessionsDirOpt = options.get('sessions-dir');
1349
1374
  const pruneNoise = flags.has('prune-noise');
1375
+ // lesson-quarantine FR-7: expiry is a SEPARATE, explicit surface — never coupled to prune-noise
1376
+ // (the recalled decay-vs-noise lesson: valid-but-unproven is not garbage). Dry-run by default.
1377
+ if (flags.has('prune-quarantine')) {
1378
+ const cfg = readMemoryLearningConfig(projectRoot);
1379
+ const res = pruneQuarantinePatterns(projectRoot, { dryRun: !flags.has('apply'), expireDays: cfg.quarantineExpireDays });
1380
+ if (!flags.has('apply')) {
1381
+ write(`dz consolidate --prune-quarantine: DRY RUN — ${res.candidates.length} expired quarantined lesson(s) (> ${cfg.quarantineExpireDays}d, never reinforced)`);
1382
+ for (const c of res.candidates)
1383
+ write(` [${c.ageDays < 0 ? '?' : c.ageDays}d] ${c.dzId} ${c.text.slice(0, 70)}`);
1384
+ if (res.candidates.length > 0)
1385
+ write(' → re-run with --apply to remove (snapshots first); or promote keepers: dz recall --promote <dzId> --apply');
1386
+ return 0;
1387
+ }
1388
+ if (res.error !== undefined) {
1389
+ write(`dz consolidate --prune-quarantine: ${res.error}`);
1390
+ return 1;
1391
+ }
1392
+ write(`dz consolidate --prune-quarantine: removed ${res.removed} expired quarantined lesson(s)`);
1393
+ if (res.snapshot !== undefined)
1394
+ write(` snapshot: ${res.snapshot}`);
1395
+ return 0;
1396
+ }
1350
1397
  // --prune-noise: RETRO-PRUNE legacy noise (tool telemetry + system-wrapper "responses") from
1351
1398
  // the lexical store AND the agentdb vector mirror BEFORE harvesting, so this run's watermark
1352
1399
  // never re-learns from junk. Best-effort — a prune error is reported, never fatal.
@@ -1572,6 +1619,44 @@ async function cmdRecallForget(options, flags, projectRoot, write) {
1572
1619
  write(' the vector mirror still holds them — run `dz vector reindex` to resync');
1573
1620
  return 0;
1574
1621
  }
1622
+ /**
1623
+ * `dz recall --promote <dzId>[,<dzId>…] [--apply]` — lift quarantine from NAMED records
1624
+ * (lesson-quarantine FR-6b). Dry-run by default, the --forget symmetry. Also clears the
1625
+ * agentdb mirror's qStatus (best-effort) so the hook daemon stops excluding promoted lessons.
1626
+ */
1627
+ async function cmdRecallPromote(options, flags, projectRoot, write) {
1628
+ const ids = (options.get('promote') ?? '').split(',').map((s) => s.trim()).filter((s) => s.length > 0);
1629
+ if (ids.length === 0) {
1630
+ write('dz recall --promote: no ids given (comma-separated dzIds; quarantined ones are marked ⚠q in recall)');
1631
+ return 1;
1632
+ }
1633
+ if (!flags.has('apply')) {
1634
+ const records = loadStoreRecords(projectRoot);
1635
+ const found = ids.filter((id) => records.some((r) => r.id === id));
1636
+ write(`dz recall --promote: DRY RUN — ${found.length}/${ids.length} id(s) match the store. Re-run with --apply to promote.`);
1637
+ return 0;
1638
+ }
1639
+ const res = await promotePatterns(projectRoot, ids);
1640
+ // Codex-QE finding 5: even on a mid-batch failure, the ALREADY-promoted records must get their
1641
+ // mirror rows cleared — otherwise the canonical store and the hook-visible mirror split-brain.
1642
+ if (res.promoted.length > 0) {
1643
+ const cleared = clearAgentdbQuarantine(projectRoot, res.promoted);
1644
+ if (cleared.cleared > 0)
1645
+ write(` ↳ mirror updated (${cleared.cleared} row(s) un-quarantined in agentdb)`);
1646
+ else if (cleared.error !== undefined)
1647
+ write(` ⚠ mirror not updated (${cleared.error}) — run dz vector reindex to resync`);
1648
+ }
1649
+ if (!res.ok) {
1650
+ write(`dz recall --promote: failed — ${res.error ?? 'unknown error'} (promoted so far: ${res.promoted.length})`);
1651
+ return 1;
1652
+ }
1653
+ write(`dz recall --promote: promoted ${res.promoted.length} record(s)`);
1654
+ if (res.notQuarantined.length > 0)
1655
+ write(` already promoted (not quarantined): ${res.notQuarantined.join(', ')}`);
1656
+ if (res.notFound.length > 0)
1657
+ write(` not found: ${res.notFound.join(', ')}`);
1658
+ return 0;
1659
+ }
1575
1660
  async function cmdRecall(options, flags, cwd, write) {
1576
1661
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1577
1662
  const asJson = flags.has('json');
@@ -1580,6 +1665,8 @@ async function cmdRecall(options, flags, cwd, write) {
1580
1665
  return cmdRecallUsage(options, flags, projectRoot, write);
1581
1666
  if (options.has('forget'))
1582
1667
  return cmdRecallForget(options, flags, projectRoot, write);
1668
+ if (options.has('promote'))
1669
+ return cmdRecallPromote(options, flags, projectRoot, write);
1583
1670
  // --all: dump the entire learned store (backend-agnostic, via loadStorePatternsSync).
1584
1671
  // With --json this is the portable export the agentdb-memory MCP bridge consumes.
1585
1672
  if (all) {
@@ -1679,9 +1766,17 @@ async function cmdRecall(options, flags, cwd, write) {
1679
1766
  const lexLabel = result.lexicalBackend === 'sqlite' ? 'SQLite FTS5' : 'keyword (JSON)';
1680
1767
  const ranking = vectorOn ? `${lexLabel} + vector (${result.vectorEngine}) ranking` : `${lexLabel} ranking (lexical)`;
1681
1768
  write(`dz recall "${query}" — ${hits.length} hit(s), ${ranking}`);
1769
+ let sawQuarantined = false;
1682
1770
  for (const h of hits) {
1683
1771
  const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
1684
- write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag} ${h.pattern.pattern.slice(0, 80)}`);
1772
+ const qTag = h.quarantined === true ? ' ⚠q' : '';
1773
+ if (h.quarantined === true)
1774
+ sawQuarantined = true;
1775
+ write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag}${qTag} ${h.pattern.pattern.slice(0, 80)}`);
1776
+ }
1777
+ if (sawQuarantined) {
1778
+ // The loop stays VISIBLE (ADR D2): a quarantined hit is shown, marked, and explained.
1779
+ write(' ⚠q = quarantined (unproven hypothesis, rank damped) — confirm with dz teach --reinforce, or dz recall --promote <dzId> --apply');
1685
1780
  }
1686
1781
  if (result.vectorError !== undefined && mode !== 'lexical') {
1687
1782
  // Engine present but the semantic leg failed/timed out — one honest line, exit 0 (05 §2.3).
@@ -5208,6 +5303,176 @@ function nameFor(t, outcome) {
5208
5303
  * --author <model> cosmetic: the reviewer to dispatch, printed alongside the brief
5209
5304
  * --json machine contract { planesChecked, planesSkipped, findings, handoff, artifact }
5210
5305
  */
5306
+ /**
5307
+ * Env vars an INHERITED Claude session leaks into a child. Left in place, the probe can silently
5308
+ * read the parent's project instead of the target — the exact confound that made a hand-rolled
5309
+ * 2026-07-23 probe untrustworthy until controls were added (feature skills-verify, ADR-001).
5310
+ */
5311
+ const PROBE_SCRUB_ENV = [
5312
+ 'CLAUDE_CODE_CHILD_SESSION',
5313
+ 'CLAUDE_CODE_SESSION_ID',
5314
+ 'CLAUDECODE',
5315
+ 'CLAUDE_CODE_ENTRYPOINT',
5316
+ 'CLAUDE_CODE_EXECPATH',
5317
+ 'CLAUDE_PLUGIN_DATA',
5318
+ 'AI_AGENT',
5319
+ ];
5320
+ /**
5321
+ * Start a real Claude session in `projectDir` and capture its stream until the `system/init` event
5322
+ * lands, then kill it — the model never answers, so the probe costs ~no tokens. Never throws:
5323
+ * every failure becomes an `error` string, which the pure classifier turns into `inconclusive`.
5324
+ */
5325
+ function probeInitStream(projectDir, timeoutMs) {
5326
+ return new Promise((resolveProbe) => {
5327
+ const env = { ...process.env };
5328
+ for (const key of PROBE_SCRUB_ENV)
5329
+ delete env[key];
5330
+ env.CLAUDE_PROJECT_DIR = projectDir;
5331
+ let out = '';
5332
+ let err = '';
5333
+ let settled = false;
5334
+ let child;
5335
+ const finish = (error) => {
5336
+ if (settled)
5337
+ return;
5338
+ settled = true;
5339
+ clearTimeout(timer);
5340
+ try {
5341
+ child?.kill('SIGTERM');
5342
+ }
5343
+ catch {
5344
+ /* the child may already be gone */
5345
+ }
5346
+ resolveProbe({ stream: out, error });
5347
+ };
5348
+ const timer = setTimeout(() => finish(`no init event within ${Math.round(timeoutMs / 1000)}s (is \`claude\` logged in?)`), timeoutMs);
5349
+ try {
5350
+ // NOT `--bare`: that mode skips plugin credentials and fails with "Not logged in".
5351
+ child = spawn('claude', ['-p', 'ok', '--output-format', 'stream-json', '--verbose'], {
5352
+ cwd: projectDir,
5353
+ env,
5354
+ stdio: ['ignore', 'pipe', 'pipe'],
5355
+ });
5356
+ }
5357
+ catch (error) {
5358
+ finish(`cannot run \`claude\`: ${error instanceof Error ? error.message : String(error)}`);
5359
+ return;
5360
+ }
5361
+ child.stdout?.on('data', (chunk) => {
5362
+ out += chunk.toString();
5363
+ // Reuse the pure parser so a partially-flushed line simply waits for more data.
5364
+ if (parseInitFacts(out))
5365
+ finish(null);
5366
+ });
5367
+ child.stderr?.on('data', (chunk) => {
5368
+ err += chunk.toString();
5369
+ });
5370
+ child.on('error', (error) => finish(`cannot run \`claude\`: ${error.message}`));
5371
+ child.on('close', (code) => {
5372
+ if (parseInitFacts(out))
5373
+ finish(null);
5374
+ else
5375
+ finish(err.trim() || `\`claude\` exited ${code} without an init event`);
5376
+ });
5377
+ });
5378
+ }
5379
+ /**
5380
+ * `dz skills-verify` — does this project's `.claude/skills/` actually REGISTER? (feature
5381
+ * skills-verify, ADR-001.) L1 static scan is instant and CI-safe; the live layer reads the
5382
+ * authoritative `system/init` listing. Fail-closed: unobservable ⇒ inconclusive, never pass.
5383
+ */
5384
+ async function cmdSkillsVerify(options, flags, cwd, write) {
5385
+ const json = flags.has('json');
5386
+ if (flags.has('help')) {
5387
+ write('dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--timeout <s>] [--json]');
5388
+ write(' Verifies that a project\'s .claude/skills/ actually register in Claude Code.');
5389
+ write(' --static layout scan only (no Claude session, CI-safe): flags dirs that can never register');
5390
+ write(' default also starts a real session and reads the authoritative system/init listing');
5391
+ write(' exit: 0 pass · 1 fail · 2 inconclusive (--strict makes inconclusive exit 1)');
5392
+ return 0;
5393
+ }
5394
+ const allowedFlags = new Set(['json', 'help', 'static', 'strict']);
5395
+ const allowedOptions = new Set(['dir', 'expect', 'timeout']);
5396
+ const usage = ' allowed: --dir <project>, --expect a,b, --timeout <s>, --static, --strict, --json';
5397
+ if (options.has('_positional_0')) {
5398
+ const message = `unexpected argument "${options.get('_positional_0')}"`;
5399
+ write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}\n${usage}`);
5400
+ return 1;
5401
+ }
5402
+ for (const flag of flags) {
5403
+ if (!allowedFlags.has(flag)) {
5404
+ write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz skills-verify: unknown option --${flag}\n${usage}`);
5405
+ return 1;
5406
+ }
5407
+ }
5408
+ for (const key of options.keys()) {
5409
+ if (key.startsWith('_positional_'))
5410
+ continue;
5411
+ if (!allowedOptions.has(key)) {
5412
+ write(json ? JSON.stringify({ error: `unknown option --${key}`, exitCode: 1 }) : `dz skills-verify: unknown option --${key}\n${usage}`);
5413
+ return 1;
5414
+ }
5415
+ }
5416
+ const projectDir = resolve(cwd, options.get('dir') ?? '.');
5417
+ const scan = scanSkillsLayout(projectDir);
5418
+ const expected = options.has('expect')
5419
+ ? (options.get('expect') ?? '')
5420
+ .split(',')
5421
+ .map((s) => s.trim())
5422
+ .filter(Boolean)
5423
+ : scan.registrable;
5424
+ // ── L1 only: deterministic, no session, safe for CI ──
5425
+ if (flags.has('static')) {
5426
+ const exitCode = scan.findings.length > 0 ? 1 : 0;
5427
+ if (json) {
5428
+ write(JSON.stringify({ mode: 'static', ...scan, expected, exitCode }, null, 2));
5429
+ }
5430
+ else {
5431
+ write(`dz skills-verify (static): ${scan.registrable.length} registrable skill dir(s) under ${scan.skillsRoot}`);
5432
+ if (!scan.exists)
5433
+ write(' no .claude/skills/ directory here');
5434
+ for (const f of scan.findings)
5435
+ write(` [${f.kind}] ${f.detail}`);
5436
+ write(scan.findings.length ? ` ${scan.findings.length} layout problem(s) — these can never register` : ' no layout problems found');
5437
+ write(' (static is a PROXY — run without --static to read the real registration listing)');
5438
+ }
5439
+ return exitCode;
5440
+ }
5441
+ // ── L2: the authoritative listing ──
5442
+ const timeoutSec = Number(options.get('timeout') ?? '180');
5443
+ const timeoutMs = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec * 1000 : 180_000;
5444
+ if (!json)
5445
+ write(`dz skills-verify: starting a session in ${projectDir} to read the real registration listing…`);
5446
+ const { stream, error } = await probeInitStream(projectDir, timeoutMs);
5447
+ // `init.skills` carries names, not provenance: a USER-level skill of the same name would satisfy
5448
+ // the expectation while the project's own copy stays broken. Collect the collisions so the
5449
+ // classifier can refuse to attribute registration to this project (Codex QE #2).
5450
+ const userSkillsDir = join(homedir(), '.claude', 'skills');
5451
+ const ambiguous = expected.filter((name) => existsSync(join(userSkillsDir, name, 'SKILL.md')));
5452
+ // realpath so a symlinked project still matches its canonical cwd (Codex QE #6).
5453
+ const canonical = (p) => {
5454
+ try {
5455
+ return realpathSync(p);
5456
+ }
5457
+ catch {
5458
+ return resolve(p);
5459
+ }
5460
+ };
5461
+ const result = verifyRegistration({
5462
+ projectDir,
5463
+ scan,
5464
+ probe: error === null ? { ok: true, stream } : { ok: false, error },
5465
+ // The provenance check RAN (that is what `checked: true` asserts) — see `ambiguous` above.
5466
+ provenance: { checked: true, ambiguous },
5467
+ ...(options.has('expect') ? { expected } : {}),
5468
+ }, { resolvePath: canonical });
5469
+ const exitCode = registrationExitCode(result.verdict, flags.has('strict'));
5470
+ if (json)
5471
+ write(JSON.stringify({ mode: 'live', ...result, skillsRoot: scan.skillsRoot, exitCode }, null, 2));
5472
+ else
5473
+ write(renderRegistrationReport(result, scan));
5474
+ return exitCode;
5475
+ }
5211
5476
  function cmdDeliveryCheck(options, flags, cwd, write) {
5212
5477
  let repoRoot = cwd;
5213
5478
  try {
@@ -5776,6 +6041,8 @@ export async function runCli(argv, io = {}) {
5776
6041
  return cmdDiscriminationCheck(options, flags, cwd, write);
5777
6042
  case 'delivery-check':
5778
6043
  return cmdDeliveryCheck(options, flags, cwd, write);
6044
+ case 'skills-verify':
6045
+ return cmdSkillsVerify(options, flags, cwd, write);
5779
6046
  case 'routing':
5780
6047
  return cmdRouting(options, flags, cwd, write);
5781
6048
  case 'bto-optimize':