@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 +8 -8
- package/README.md +88 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +274 -7
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +7 -7
- package/src/cli.ts +290 -6
package/.dz-manifest.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "README.md",
|
|
12
|
-
"sha256": "
|
|
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": "
|
|
44
|
+
"sha256": "dfb260e21d46dfc8020eef0bf5ba570f9c335964181cf90446c1c5d761626415"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"path": "dist/cli.js",
|
|
48
|
-
"sha256": "
|
|
48
|
+
"sha256": "a5b7d2fc4f582c507dfab54f1cc4754ca6700f7e686a3c72871afa807cceac25"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "dist/cli.js.map",
|
|
52
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
92
|
+
"sha256": "3deb99917f3d8b4a5c9a3b0315bd65a3099ce2b508121cfa2bdeed19b7554d48"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"path": "tsconfig.json",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
}
|
|
102
102
|
]
|
|
103
103
|
},
|
|
104
|
-
"signature": "
|
|
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 (
|
|
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;
|
|
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]] (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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':
|