@dzhechkov/harness-cli 0.3.228 → 0.3.230

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/README.md CHANGED
@@ -399,7 +399,7 @@ Each pack is an npm package — click through for the **full per-skill documenta
399
399
  | `health` | 8 | Medical AI (diagnostics, drugs, labs, clinical decisions) |
400
400
  | `keysarium` | 9 | Full research toolkit (feature-adr, presentation, reverse-eng) |
401
401
  | `p-replicator` | 10 | AI product development (/replicate, SPARC PRD, pipeline-forge) |
402
- | `feature-adr` | 8 | Feature pipeline (feature-adr, explore, frontend-design, code-critic, code-impl, system-grill) |
402
+ | `feature-adr` | 9 | Feature pipeline (feature-adr, explore, knowledge-extractor, problem-solver-enhanced, frontend-design, code-critic, code-impl, system-grill, code-skills-creator) |
403
403
  | `reasoning` | 4 | Generic reasoning & code-quality (investigate, solid, karpathy-guidelines, agents-md-creator) — stack-neutral, zero coupling |
404
404
  | `devops` | 30 | DevOps skills (terraform, kubernetes, c4-architecture, incident-response, problem-management, risk-assessment, ...) |
405
405
  | `web3` | 12 | Web3/DeFi (quicknode, zerion, symbiosis, bankr, veil, neynar, ...) |
@@ -443,7 +443,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
443
443
 
444
444
  > **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.
445
445
 
446
- ## All Commands (53)
446
+ ## All Commands (54)
447
447
 
448
448
  ```
449
449
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
@@ -500,6 +500,7 @@ dz bto-optimize --split|--plan|--select|--scope-check|--diff [--json] # determ
500
500
  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)
501
501
  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
502
502
  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
503
+ 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)
503
504
  dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] (dry-run by default; pass --yes/--confirm to go live; claim-check gate defaults to warn — surfaces README claim findings, never blocks)
504
505
  dz auto-canonicalize --source <github-url> --pack <skills-pack>
505
506
  dz sync-upstream [--package <dir>] [--list] [--all]
@@ -1595,6 +1596,29 @@ rule: a false gate kills trust) — exit 0 on any verdict, exit 2 only on a usag
1595
1596
  sanitation live in tested CLI code; base ref, paths, name filter, and runner are all injection-checked, and the
1596
1597
  worktree is always removed. Step-8 runs this on the ADR Confirmation's `Required automated check` automatically.
1597
1598
 
1599
+ ### `dz guard` — when a self-mutating operation should be refused, not regretted
1600
+
1601
+ Recurring failures — a raw `workspace:*` shipped to npm, a credential pasted into a lesson, skill drift
1602
+ published — were each caught by hand, after the fact. `dz guard` is one declarative place for those
1603
+ invariants: HARD rules **block** the operation, SOFT rules warn. Zero config needed — built-in defaults
1604
+ cover the known rakes; `.dz/guard.json` (via `dz guard --init`) exists only if you want to tune a severity
1605
+ or disable a rule.
1606
+ ```bash
1607
+ dz guard check --op publish # no-workspace-star · no-skill-drift · no-secrets · readme-consistency
1608
+ dz guard check --op teach --text "the fix: export sk-abc..." # → BLOCK (exit 1): looks like a credential
1609
+ dz guard log # append-only audit: every verdict + every forced override
1610
+ ```
1611
+ ```
1612
+ dz guard (teach): ✗ BLOCK [checked: no-secrets, store-bloat-cap]
1613
+ [BLOCK] no-secrets: lesson: looks like a openai-key — do not teach/publish a credential
1614
+ → blocked. Fix the HARD violation(s), or override with --force "<reason>" (logged).
1615
+ ```
1616
+ `dz publish` runs the guard **automatically** as a pre-flight and refuses on a HARD block; the escape
1617
+ hatch `--no-guard "<reason>"` requires a reason and is logged to `.dz/guard-audit.jsonl` — an override is
1618
+ visible, never silent. Calibrated against false gates: in a pnpm workspace, `workspace:*` in source is
1619
+ *correct* (pnpm rewrites it at publish), so the rule resolves each workspace dep to the version it would
1620
+ ship as and blocks only a dep that would ship raw.
1621
+
1598
1622
  ### Semantic recall (vector tier)
1599
1623
 
1600
1624
  `dz recall` is **hybrid** when the vector tier is available and **exactly the old lexical command** when it is not — enabling it never changes behavior for projects that skip it.
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;AA4OH,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;CACzB;AAovJD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA0I5E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiPH,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;CACzB;AA66JD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA4I5E"}
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { execSync } 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, 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, decideProvenance, isInsideTree, signManifest, verifyManifest, assertKeyOutsideTree, decidePublishGate, 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, } 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, 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, assertKeyOutsideTree, decidePublishGate, 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, } 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
@@ -3107,8 +3107,8 @@ function cmdPublish(options, flags, cwd, write) {
3107
3107
  // Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
3108
3108
  // silently swallowed and flip the command into live-publish mode.
3109
3109
  const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance']);
3110
- const allowedOptions = new Set(['filter', 'claim-check']);
3111
- const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>';
3110
+ const allowedOptions = new Set(['filter', 'claim-check', 'no-guard']);
3111
+ const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>, --no-guard "<reason>" (skip the guard pre-flight; logged)';
3112
3112
  for (const flag of flags) {
3113
3113
  if (!allowedFlags.has(flag)) {
3114
3114
  write(`dz publish: unknown option --${flag}`);
@@ -3125,6 +3125,36 @@ function cmdPublish(options, flags, cwd, write) {
3125
3125
  return 1;
3126
3126
  }
3127
3127
  }
3128
+ // dz guard pre-flight (ADR-002 option A): publish is the most dangerous, least-reversible self-mutation, so
3129
+ // it ALWAYS runs the declarative guard first. A HARD violation refuses the publish; `--no-guard "<reason>"`
3130
+ // is the logged escape hatch (the override lands in .dz/guard-audit.jsonl — visible, never silent).
3131
+ {
3132
+ let guardRoot = cwd;
3133
+ try {
3134
+ guardRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
3135
+ }
3136
+ catch { /* not git */ }
3137
+ const noGuard = options.get('no-guard');
3138
+ if (noGuard !== undefined && noGuard.trim() === '') {
3139
+ write('dz publish: --no-guard requires a reason (it is logged): --no-guard "hotfix, guard re-run after"');
3140
+ return 1;
3141
+ }
3142
+ const guardResult = runGuardEvaluation(guardRoot, 'publish', undefined, noGuard);
3143
+ if (guardResult.verdict === 'block' && noGuard === undefined) {
3144
+ write('dz publish: ✗ BLOCKED by dz guard (HARD invariant violated):');
3145
+ for (const v of guardResult.violations.filter((x) => x.severity === 'hard'))
3146
+ write(` [BLOCK] ${v.rule}: ${v.detail}`);
3147
+ write(' → fix the violation(s), or override with --no-guard "<reason>" (logged to .dz/guard-audit.jsonl).');
3148
+ return 1;
3149
+ }
3150
+ if (guardResult.verdict === 'block')
3151
+ write(`dz publish: ⚠ guard BLOCK overridden via --no-guard: ${noGuard} (logged)`);
3152
+ else if (guardResult.verdict === 'warn')
3153
+ for (const v of guardResult.violations)
3154
+ write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
3155
+ else
3156
+ write('dz publish: ✓ guard pre-flight passed');
3157
+ }
3128
3158
  // ADR-001 (publish-provenance): decide BEFORE any work — flag validation, then a pre-flight that
3129
3159
  // refuses `--provenance` where no OIDC token can be minted. `off` is an escape hatch that names itself.
3130
3160
  if (flags.has('provenance') && flags.has('no-provenance')) {
@@ -3890,6 +3920,210 @@ function cmdDriftCheck(options, flags, cwd, write) {
3890
3920
  const sigFatal = reportPackVerification(root, options.get('pubkey'), flags.has('require-signing'), write);
3891
3921
  return driftExit || sigFatal;
3892
3922
  }
3923
+ const DEFAULT_STORE_CAP = 5000;
3924
+ /** Read the optional `.dz/guard.json` — `{ rules?: [...], storeCap?: number }`. Missing/broken ⇒ defaults. */
3925
+ function loadGuardConfig(root) {
3926
+ const p = join(root, '.dz', 'guard.json');
3927
+ if (!existsSync(p))
3928
+ return {};
3929
+ try {
3930
+ const j = JSON.parse(readFileSync(p, 'utf8'));
3931
+ return j && typeof j === 'object' ? j : {};
3932
+ }
3933
+ catch {
3934
+ return {};
3935
+ }
3936
+ }
3937
+ /** Extract labelled (a,b) count pairs from the READMEs that must agree (the parity invariant, inline). */
3938
+ function gatherReadmeCounts(root) {
3939
+ const read = (rel) => { try {
3940
+ return readFileSync(join(root, rel), 'utf8');
3941
+ }
3942
+ catch {
3943
+ return '';
3944
+ } };
3945
+ const rootMd = read('README.md');
3946
+ const cliMd = read('packages/@dzhechkov/harness-cli/README.md');
3947
+ const num = (s, re) => { const m = s.match(re); return m && m[1] ? Number(m[1]) : null; };
3948
+ const pairs = [];
3949
+ const cjm = num(rootMd, /## User Journey — 6 phases, (\d+) commands/);
3950
+ const cliAll = num(cliMd, /## All Commands \((\d+)\)/);
3951
+ const rootAll = num(rootMd, /## All Commands \((\d+)\)/);
3952
+ if (cjm !== null && cliAll !== null)
3953
+ pairs.push({ label: 'commands (root CJM header vs cli All Commands)', a: cjm, b: cliAll });
3954
+ if (rootAll !== null && cliAll !== null)
3955
+ pairs.push({ label: 'All Commands (root vs cli)', a: rootAll, b: cliAll });
3956
+ return pairs;
3957
+ }
3958
+ /** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
3959
+ function gatherGuardFacts(op, root, text, storeCap) {
3960
+ const facts = { op };
3961
+ if (op === 'publish') {
3962
+ // Read every workspace manifest ONCE: build a name→version map, then resolve each `workspace:*` dep to the
3963
+ // version pnpm WOULD publish it as. In a pnpm workspace (pnpm-workspace.yaml present) `workspace:*` in source
3964
+ // is correct and gets rewritten at publish — so reporting it raw would be a FALSE gate. We mirror the rewrite:
3965
+ // a resolvable workspace dep becomes its real semver (safe → the rule passes); an UNRESOLVABLE one (points at
3966
+ // no workspace package, or not a pnpm workspace) stays `workspace:*` so the rule catches a dep that WOULD ship
3967
+ // raw. That is the genuinely dangerous case the rule exists for.
3968
+ const manifests = [];
3969
+ try {
3970
+ const out = execSync('git ls-files "packages/@dzhechkov/*/package.json"', { cwd: root, encoding: 'utf-8' });
3971
+ for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
3972
+ try {
3973
+ manifests.push(JSON.parse(readFileSync(join(root, rel), 'utf8')));
3974
+ }
3975
+ catch { /* skip unreadable */ }
3976
+ }
3977
+ }
3978
+ catch { /* not a git repo */ }
3979
+ const versionByName = new Map();
3980
+ for (const m of manifests)
3981
+ if (m.name && typeof m.version === 'string')
3982
+ versionByName.set(m.name, m.version);
3983
+ const pnpmWorkspace = existsSync(join(root, 'pnpm-workspace.yaml'));
3984
+ const packages = [];
3985
+ for (const m of manifests) {
3986
+ if (m.private === true)
3987
+ continue; // unpublished packages are exempt
3988
+ const deps = {};
3989
+ for (const [dep, spec] of Object.entries(m.dependencies ?? {})) {
3990
+ deps[dep] = (typeof spec === 'string' && spec.startsWith('workspace:') && pnpmWorkspace && versionByName.has(dep))
3991
+ ? versionByName.get(dep) // pnpm rewrites this to a real semver at publish → safe
3992
+ : spec; // non-pnpm, or an unresolvable workspace dep → keep raw so the rule catches a would-ship-raw dep
3993
+ }
3994
+ packages.push({ name: m.name ?? '(unnamed)', deps });
3995
+ }
3996
+ facts['packages'] = packages;
3997
+ try {
3998
+ facts['drift'] = sweepSkillDrift(root, { scope: 'packages', allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name);
3999
+ }
4000
+ catch { /* skip */ }
4001
+ facts['counts'] = gatherReadmeCounts(root);
4002
+ }
4003
+ if (op === 'consolidate') {
4004
+ try {
4005
+ facts['drift'] = sweepSkillDrift(root, { scope: 'packages', allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name);
4006
+ }
4007
+ catch { /* skip */ }
4008
+ }
4009
+ if (op === 'teach' || op === 'consolidate') {
4010
+ if (op === 'teach' && text)
4011
+ facts['secretTargets'] = [{ label: 'lesson', text }];
4012
+ let count = 0;
4013
+ try {
4014
+ count = loadStorePatternsSync(root).length;
4015
+ }
4016
+ catch { /* skip → cap never trips */
4017
+ count = 0;
4018
+ }
4019
+ facts['store'] = { count, cap: storeCap };
4020
+ }
4021
+ return facts;
4022
+ }
4023
+ /**
4024
+ * Load config → resolve rules → gather facts → evaluate → append the audit record. The ONE evaluation path,
4025
+ * shared by `dz guard check` and the `dz publish` pre-flight (ADR-002 option A) so they can never disagree.
4026
+ * `overrideReason` (when the caller forces through a block) is logged, never silent.
4027
+ */
4028
+ function runGuardEvaluation(root, op, text, overrideReason) {
4029
+ const cfg = loadGuardConfig(root);
4030
+ // Number.isFinite, not just > 0: a config `storeCap: 1e400` parses to Infinity, passes `> 0`, and would
4031
+ // silently DISABLE the cap (count <= Infinity always). Non-finite ⇒ fall back to the default.
4032
+ const storeCap = typeof cfg.storeCap === 'number' && Number.isFinite(cfg.storeCap) && cfg.storeCap > 0 ? cfg.storeCap : DEFAULT_STORE_CAP;
4033
+ const rules = resolveRules(Array.isArray(cfg.rules) ? cfg.rules : undefined);
4034
+ const facts = gatherGuardFacts(op, root, text, storeCap);
4035
+ const result = evaluateGuard(facts, rules);
4036
+ // audit (append-only). ts is real time here (a CLI, not the sandboxed workflow).
4037
+ try {
4038
+ const rec = auditRecord(result, new Date().toISOString(), overrideReason !== undefined ? { reason: overrideReason } : undefined);
4039
+ mkdirSync(join(root, '.dz'), { recursive: true });
4040
+ writeFileSync(join(root, '.dz', 'guard-audit.jsonl'), JSON.stringify(rec) + '\n', { flag: 'a' });
4041
+ }
4042
+ catch { /* audit is best-effort, never blocks the verdict */ }
4043
+ return result;
4044
+ }
4045
+ /**
4046
+ * `dz guard` — the declarative constraint layer that refuses a self-mutating op when a HARD invariant is
4047
+ * violated. Simple outside: `dz guard check --op publish` works with zero config (built-in defaults).
4048
+ * check --op <publish|teach|consolidate|reindex> [--text <s>] [--json] [--force <reason>]
4049
+ * --init scaffold an editable .dz/guard.json (only if you want to customise)
4050
+ * log [--limit N] tail the append-only .dz/guard-audit.jsonl
4051
+ * Exit 1 on a HARD block (0 with --force <reason>, which is logged); 0 on warn/pass.
4052
+ */
4053
+ function cmdGuard(options, flags, cwd, write) {
4054
+ let root = cwd;
4055
+ try {
4056
+ root = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
4057
+ }
4058
+ catch { /* not git */ }
4059
+ const sub = options.get('_positional_0') ?? 'check';
4060
+ if (flags.has('init') || sub === 'init') {
4061
+ const p = join(root, '.dz', 'guard.json');
4062
+ if (existsSync(p) && !flags.has('force')) {
4063
+ write(`dz guard --init: ${p} already exists (pass --force to overwrite)`);
4064
+ return 1;
4065
+ }
4066
+ const scaffold = {
4067
+ storeCap: DEFAULT_STORE_CAP,
4068
+ rules: DEFAULT_RULES.map((r) => ({ id: r.id, severity: r.severity, enabled: true, description: r.description })),
4069
+ };
4070
+ mkdirSync(dirname(p), { recursive: true });
4071
+ writeFileSync(p, JSON.stringify(scaffold, null, 2) + '\n');
4072
+ write(`dz guard --init: wrote ${p} (edit severity/enabled to customise; delete it to return to built-in defaults)`);
4073
+ return 0;
4074
+ }
4075
+ if (sub === 'log') {
4076
+ const p = join(root, '.dz', 'guard-audit.jsonl');
4077
+ if (!existsSync(p)) {
4078
+ write('dz guard log: no audit yet (.dz/guard-audit.jsonl)');
4079
+ return 0;
4080
+ }
4081
+ const limit = Math.max(1, Number(options.get('limit') ?? '20') || 20);
4082
+ // parse-filter BEFORE emitting: a corrupt line must not make the --json output invalid JSON.
4083
+ const rows = readFileSync(p, 'utf8').split('\n').filter(Boolean).slice(-limit)
4084
+ .map((row) => { try {
4085
+ return JSON.parse(row);
4086
+ }
4087
+ catch {
4088
+ return null;
4089
+ } })
4090
+ .filter((r) => r !== null);
4091
+ if (flags.has('json')) {
4092
+ write(JSON.stringify(rows));
4093
+ return 0;
4094
+ }
4095
+ for (const r of rows) {
4096
+ write(`${r.ts} ${String(r.op).padEnd(11)} ${String(r.verdict).toUpperCase()}${r.override ? ` (forced: ${r.override.reason})` : ''}`);
4097
+ }
4098
+ return 0;
4099
+ }
4100
+ if (sub !== 'check') {
4101
+ write(`dz guard: unknown subcommand '${sub}' — use: check --op <op> | --init | log`);
4102
+ return 1;
4103
+ }
4104
+ // check
4105
+ const op = options.get('op');
4106
+ if (op === undefined || !['publish', 'teach', 'consolidate', 'reindex'].includes(op)) {
4107
+ write('dz guard check: --op must be one of publish | teach | consolidate | reindex');
4108
+ return 1;
4109
+ }
4110
+ const force = options.get('force');
4111
+ const forced = force !== undefined;
4112
+ const result = runGuardEvaluation(root, op, options.get('text'), force);
4113
+ if (flags.has('json')) {
4114
+ write(JSON.stringify({ ...result, forced }, null, 2));
4115
+ return guardExitCode(result, forced);
4116
+ }
4117
+ const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
4118
+ write(`dz guard (${op}): ${glyph} ${result.verdict.toUpperCase()} [checked: ${result.checked.join(', ') || 'no rules for this op'}]`);
4119
+ for (const v of result.violations)
4120
+ write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
4121
+ if (result.verdict === 'block' && forced)
4122
+ write(` → forced through: ${force} (logged to .dz/guard-audit.jsonl)`);
4123
+ else if (result.verdict === 'block')
4124
+ write(' → blocked. Fix the HARD violation(s), or override with --force "<reason>" (logged).');
4125
+ return guardExitCode(result, forced);
4126
+ }
3893
4127
  /**
3894
4128
  * `dz sync-canonical <skill>` — the healer. Treats the resolved canonical (`--from` →
3895
4129
  * `skills-meta/<skill>` → `--auto` most-complete copy) as authoritative and overwrites every other
@@ -4913,6 +5147,8 @@ export async function runCli(argv, io = {}) {
4913
5147
  return cmdSign(options, flags, cwd, write);
4914
5148
  case 'sbom':
4915
5149
  return cmdSbom(options, flags, cwd, write);
5150
+ case 'guard':
5151
+ return cmdGuard(options, flags, cwd, write);
4916
5152
  case 'verify-pack':
4917
5153
  return cmdVerifyPack(options, flags, cwd, write);
4918
5154
  case 'setup':