@dzhechkov/harness-cli 0.3.227 → 0.3.229
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 +38 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +388 -23
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +321 -25
package/README.md
CHANGED
|
@@ -289,6 +289,10 @@ Ed25519 over a file manifest, plus a CycloneDX SBOM. Zero dependencies (`node:cr
|
|
|
289
289
|
tamper-evidence, never truthfulness.
|
|
290
290
|
|
|
291
291
|
```bash
|
|
292
|
+
# One-time: generate the Ed25519 keypair. The PRIVATE key is written OUTSIDE the repo (mode 0600);
|
|
293
|
+
# the PUBLIC key is printed — commit it as keys/dz.pub. dz refuses an --out inside the repo tree.
|
|
294
|
+
dz sign --init --out ~/.dz/keys/dz.key
|
|
295
|
+
|
|
292
296
|
# Sign a pack. The private key MUST live outside the repo — dz refuses otherwise.
|
|
293
297
|
dz sign --pack packages/@dzhechkov/skills-qe --key ~/.dz/keys/dz.key
|
|
294
298
|
|
|
@@ -296,11 +300,17 @@ dz sign --pack packages/@dzhechkov/skills-qe --key ~/.dz/keys/dz.key
|
|
|
296
300
|
# whoever replaced the pack would have replaced a key shipped inside it.
|
|
297
301
|
dz verify-pack --pack packages/@dzhechkov/skills-qe # exit 0 = unmodified
|
|
298
302
|
dz verify-pack --pack ./downloaded-pack --pubkey keys/dz.pub # explicit trust root
|
|
303
|
+
|
|
304
|
+
# The SBOM on its own (CycloneDX 1.5, a file-level bill of materials for the pack):
|
|
305
|
+
dz sbom --pack packages/@dzhechkov/skills-qe # print to stdout
|
|
306
|
+
dz sbom --pack packages/@dzhechkov/skills-qe --out sbom.json # write to a file
|
|
299
307
|
```
|
|
300
308
|
|
|
301
309
|
A single flipped byte, a deleted file, or an **added** file all fail verification and the offending
|
|
302
310
|
path is named. Every degenerate input (no manifest, empty file list, empty signature, no public key)
|
|
303
|
-
fails **closed** — absence never reads as success.
|
|
311
|
+
fails **closed** — absence never reads as success. `dz doctor` **and** `dz drift-check` run this check
|
|
312
|
+
over the installed packs: a **tampered** pack is fatal (blocks); an **unsigned** pack or a missing trust
|
|
313
|
+
root is reported, not fatal (transitional — the existing packs are not yet signed).
|
|
304
314
|
|
|
305
315
|
`dz publish` runs the check before publishing anything. With `keys/dz.pub` committed, an unsigned or
|
|
306
316
|
mismatching pack **blocks the release**. Until then, packs publish unsigned and `dz publish` says so on
|
|
@@ -433,7 +443,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
|
|
|
433
443
|
|
|
434
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.
|
|
435
445
|
|
|
436
|
-
## All Commands (
|
|
446
|
+
## All Commands (54)
|
|
437
447
|
|
|
438
448
|
```
|
|
439
449
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
|
|
@@ -488,6 +498,9 @@ dz challenge --plan <plan.md> [--json] [--context-only] [--author <model>] # a
|
|
|
488
498
|
dz routing [--stage <s>] [--json] # inspect the learned cost-optimal routing store: what `args.models.<stage>='auto-cost'` believes per (stage, complexity-tier, model) — gated attempts/successes/rate (feeds feature-adr model selection)
|
|
489
499
|
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
|
|
490
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
|
+
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
|
+
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)
|
|
491
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)
|
|
492
505
|
dz auto-canonicalize --source <github-url> --pack <skills-pack>
|
|
493
506
|
dz sync-upstream [--package <dir>] [--list] [--all]
|
|
@@ -1583,6 +1596,29 @@ rule: a false gate kills trust) — exit 0 on any verdict, exit 2 only on a usag
|
|
|
1583
1596
|
sanitation live in tested CLI code; base ref, paths, name filter, and runner are all injection-checked, and the
|
|
1584
1597
|
worktree is always removed. Step-8 runs this on the ADR Confirmation's `Required automated check` automatically.
|
|
1585
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
|
+
|
|
1586
1622
|
### Semantic recall (vector tier)
|
|
1587
1623
|
|
|
1588
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;
|
|
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
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
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
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, 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
|
|
@@ -2912,10 +2912,86 @@ function reportPackVerification(cwd, explicitPubkey, requireSigning, write) {
|
|
|
2912
2912
|
return fatal > 0 ? 1 : 0;
|
|
2913
2913
|
}
|
|
2914
2914
|
function cmdSign(options, flags, cwd, write) {
|
|
2915
|
+
// `dz sign --init` — one-time keygen. Writes the PRIVATE key OUTSIDE the repo (default ~/.dz/keys/dz.key,
|
|
2916
|
+
// mode 0600) and prints the PUBLIC key for the operator to commit as keys/dz.pub. The private key never
|
|
2917
|
+
// touches the repo or a tarball (the hard invariant — enforced by assertKeyOutsideTree).
|
|
2918
|
+
if (flags.has('init')) {
|
|
2919
|
+
const outPath = resolve(cwd, options.get('out') ?? join(homedir(), '.dz', 'keys', 'dz.key'));
|
|
2920
|
+
// Resolve symlinks BEFORE the containment check: an --out (or a parent dir) that is a symlink pointing
|
|
2921
|
+
// INTO the repo would otherwise slip the private key inside the tree past a string-only guard — including a
|
|
2922
|
+
// DANGLING symlink whose target does not exist yet (existsSync follows the link and reports false, so the
|
|
2923
|
+
// symlink itself must be resolved via lstat/readlink, not existsSync).
|
|
2924
|
+
const realTarget = (() => {
|
|
2925
|
+
// walk up to the deepest path that exists as ANY entry (file, dir, or even a dangling symlink).
|
|
2926
|
+
let anc = outPath;
|
|
2927
|
+
const tail = [];
|
|
2928
|
+
for (;;) {
|
|
2929
|
+
try {
|
|
2930
|
+
lstatSync(anc);
|
|
2931
|
+
break;
|
|
2932
|
+
}
|
|
2933
|
+
catch { /* not present */ }
|
|
2934
|
+
const parent = dirname(anc);
|
|
2935
|
+
if (parent === anc)
|
|
2936
|
+
return outPath; // reached root without an existing entry
|
|
2937
|
+
tail.unshift(basename(anc));
|
|
2938
|
+
anc = parent;
|
|
2939
|
+
}
|
|
2940
|
+
let base;
|
|
2941
|
+
try {
|
|
2942
|
+
base = realpathSync(anc); // resolves dirs/files and any RESOLVABLE symlink chain
|
|
2943
|
+
}
|
|
2944
|
+
catch {
|
|
2945
|
+
// `anc` exists but realpath threw → a dangling symlink: resolve its immediate target by hand.
|
|
2946
|
+
try {
|
|
2947
|
+
base = resolve(dirname(anc), readlinkSync(anc));
|
|
2948
|
+
}
|
|
2949
|
+
catch {
|
|
2950
|
+
return outPath;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
return tail.length ? join(base, ...tail) : base;
|
|
2954
|
+
})();
|
|
2955
|
+
try {
|
|
2956
|
+
assertKeyOutsideTree(realTarget, cwd);
|
|
2957
|
+
}
|
|
2958
|
+
catch (err) {
|
|
2959
|
+
write(`dz sign --init: ${err.message}`);
|
|
2960
|
+
write(' choose an --out path (and parent) OUTSIDE the repository (e.g. ~/.dz/keys/dz.key)');
|
|
2961
|
+
return 1;
|
|
2962
|
+
}
|
|
2963
|
+
if (existsSync(outPath) && !flags.has('force')) {
|
|
2964
|
+
write(`dz sign --init: ${outPath} already exists — refusing to overwrite (pass --force to replace)`);
|
|
2965
|
+
write(' overwriting a signing key orphans every pack signed with the old one.');
|
|
2966
|
+
return 1;
|
|
2967
|
+
}
|
|
2968
|
+
const { privateKey, publicKey } = generateSigningKeypair();
|
|
2969
|
+
try {
|
|
2970
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
2971
|
+
// Unlink an existing file first: writeFileSync's `mode` is ignored when the file already exists, so a
|
|
2972
|
+
// pre-existing 0644 key would stay world-readable after --force. Removing it forces a fresh 0600 create.
|
|
2973
|
+
if (existsSync(outPath))
|
|
2974
|
+
rmSync(outPath, { force: true });
|
|
2975
|
+
writeFileSync(outPath, privateKey, { mode: 0o600 });
|
|
2976
|
+
chmodSync(outPath, 0o600); // belt-and-suspenders: guarantee 0600 regardless of umask/prior state
|
|
2977
|
+
}
|
|
2978
|
+
catch (err) {
|
|
2979
|
+
write(`dz sign --init: could not write the private key to ${outPath}: ${err.message}`);
|
|
2980
|
+
return 1;
|
|
2981
|
+
}
|
|
2982
|
+
write(`dz sign --init: Ed25519 keypair generated.`);
|
|
2983
|
+
write(` private key → ${outPath} (mode 0600, OUTSIDE the repo — never commit it)`);
|
|
2984
|
+
write(` public key → commit the block below as ${TRUST_ROOT_REL}:`);
|
|
2985
|
+
write('');
|
|
2986
|
+
write(publicKey.trimEnd());
|
|
2987
|
+
write('');
|
|
2988
|
+
write(` then: dz sign --pack <dir> --key ${outPath} (sign a pack)`);
|
|
2989
|
+
return 0;
|
|
2990
|
+
}
|
|
2915
2991
|
const pack = options.get('pack');
|
|
2916
2992
|
const key = options.get('key');
|
|
2917
2993
|
if (!pack || !key) {
|
|
2918
|
-
write('dz sign: --pack <dir> and --key <path> are both required');
|
|
2994
|
+
write('dz sign: --pack <dir> and --key <path> are both required (or: dz sign --init to generate a keypair)');
|
|
2919
2995
|
write(' the key path MUST be outside the repository working tree (a leaked signing key is not revertible)');
|
|
2920
2996
|
return 1;
|
|
2921
2997
|
}
|
|
@@ -2984,12 +3060,55 @@ function cmdVerifyPack(options, flags, cwd, write) {
|
|
|
2984
3060
|
write(` ${f.path}: ${f.reason}`);
|
|
2985
3061
|
return 1;
|
|
2986
3062
|
}
|
|
3063
|
+
/**
|
|
3064
|
+
* `dz sbom` — emit a CycloneDX 1.5 SBOM (Software Bill of Materials) for a pack, standalone (no signing).
|
|
3065
|
+
* The same inventory `dz sign` writes as `sbom.json`, available on its own for audit/procurement. Components
|
|
3066
|
+
* are the pack's shipped files with their SHA-256 — a file-level bill of materials for the pack.
|
|
3067
|
+
* --pack <dir> the pack to inventory (required)
|
|
3068
|
+
* --out <file> write here (default: print to stdout)
|
|
3069
|
+
* --json (implied) SPDX/CycloneDX is already JSON
|
|
3070
|
+
*/
|
|
3071
|
+
function cmdSbom(options, flags, cwd, write) {
|
|
3072
|
+
const pack = options.get('pack');
|
|
3073
|
+
if (!pack) {
|
|
3074
|
+
write('dz sbom: --pack <dir> is required');
|
|
3075
|
+
return 1;
|
|
3076
|
+
}
|
|
3077
|
+
const packDir = resolve(cwd, pack);
|
|
3078
|
+
if (!existsSync(packDir)) {
|
|
3079
|
+
write(`dz sbom: no such pack: ${packDir}`);
|
|
3080
|
+
return 1;
|
|
3081
|
+
}
|
|
3082
|
+
const files = packFiles(packDir);
|
|
3083
|
+
if (files.length === 0) {
|
|
3084
|
+
write('dz sbom: the pack contains no files');
|
|
3085
|
+
return 1;
|
|
3086
|
+
}
|
|
3087
|
+
const manifest = buildManifest(packDir, basename(packDir), files);
|
|
3088
|
+
const sbom = buildSbom(manifest);
|
|
3089
|
+
const out = JSON.stringify(sbom, null, 2);
|
|
3090
|
+
const outOpt = options.get('out');
|
|
3091
|
+
if (outOpt !== undefined) {
|
|
3092
|
+
const outPath = resolve(cwd, outOpt);
|
|
3093
|
+
try {
|
|
3094
|
+
writeFileSync(outPath, out + '\n');
|
|
3095
|
+
}
|
|
3096
|
+
catch (err) {
|
|
3097
|
+
write(`dz sbom: could not write ${outPath}: ${err.message}`);
|
|
3098
|
+
return 1;
|
|
3099
|
+
}
|
|
3100
|
+
write(`dz sbom: ${sbom.components.length} component(s) → ${outPath} (CycloneDX ${sbom.specVersion})`);
|
|
3101
|
+
return 0;
|
|
3102
|
+
}
|
|
3103
|
+
write(out);
|
|
3104
|
+
return 0;
|
|
3105
|
+
}
|
|
2987
3106
|
function cmdPublish(options, flags, cwd, write) {
|
|
2988
3107
|
// Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
|
|
2989
3108
|
// silently swallowed and flip the command into live-publish mode.
|
|
2990
3109
|
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance']);
|
|
2991
|
-
const allowedOptions = new Set(['filter', 'claim-check']);
|
|
2992
|
-
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)';
|
|
2993
3112
|
for (const flag of flags) {
|
|
2994
3113
|
if (!allowedFlags.has(flag)) {
|
|
2995
3114
|
write(`dz publish: unknown option --${flag}`);
|
|
@@ -3006,6 +3125,36 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
3006
3125
|
return 1;
|
|
3007
3126
|
}
|
|
3008
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
|
+
}
|
|
3009
3158
|
// ADR-001 (publish-provenance): decide BEFORE any work — flag validation, then a pre-flight that
|
|
3010
3159
|
// refuses `--provenance` where no OIDC token can be minted. `off` is an escape hatch that names itself.
|
|
3011
3160
|
if (flags.has('provenance') && flags.has('no-provenance')) {
|
|
@@ -3739,29 +3888,241 @@ function cmdDriftCheck(options, flags, cwd, write) {
|
|
|
3739
3888
|
write(`allowlisted (accepted drift): ${r.allowlisted.map((d) => d.name).join(', ')}`);
|
|
3740
3889
|
}
|
|
3741
3890
|
write(`DRIFTED (unexpected, byte-differences between copies): ${r.drifted.length}`);
|
|
3891
|
+
let driftExit = 0;
|
|
3742
3892
|
if (r.drifted.length === 0) {
|
|
3743
3893
|
write('✓ no unexpected intra-monorepo skill drift');
|
|
3744
|
-
return 0;
|
|
3745
3894
|
}
|
|
3895
|
+
else {
|
|
3896
|
+
write('');
|
|
3897
|
+
write('skill'.padEnd(34) + 'copies drift/total');
|
|
3898
|
+
for (const d of r.drifted) {
|
|
3899
|
+
write(d.name.padEnd(34) +
|
|
3900
|
+
String(d.copies).padStart(4) +
|
|
3901
|
+
' ' +
|
|
3902
|
+
`${d.driftFiles}/${d.totalFiles}` +
|
|
3903
|
+
(d.missingFiles ? ` (+${d.missingFiles} missing)` : ''));
|
|
3904
|
+
}
|
|
3905
|
+
write('');
|
|
3906
|
+
write('→ fix each: heal drift, then commit. Remediation per skill:');
|
|
3907
|
+
for (const d of r.drifted) {
|
|
3908
|
+
const hasMeta = existsSync(join(root, 'packages', '@dzhechkov', 'skills-meta', d.name));
|
|
3909
|
+
write(hasMeta
|
|
3910
|
+
? ` dz sync-canonical ${d.name}`
|
|
3911
|
+
: ` dz sync-canonical ${d.name} --from <a-known-good-copy> (no skills-meta canonical)`);
|
|
3912
|
+
}
|
|
3913
|
+
write(' (accept a drift intentionally: add its name to .dz/drift-allowlist.json with a reason)');
|
|
3914
|
+
driftExit = 1; // EXIT CODE 1 = the CI gate trips
|
|
3915
|
+
}
|
|
3916
|
+
// Supplementary CI check: installed-pack signatures. A TAMPERED pack trips the gate (fatal); an unsigned
|
|
3917
|
+
// pack or a missing trust root is reported, not fatal — the same warn/block posture as `dz doctor` (the
|
|
3918
|
+
// primary signature gate). drift-check surfaces it so the CI drift view also flags a tampered pack.
|
|
3746
3919
|
write('');
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3920
|
+
const sigFatal = reportPackVerification(root, options.get('pubkey'), flags.has('require-signing'), write);
|
|
3921
|
+
return driftExit || sigFatal;
|
|
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 : {};
|
|
3754
3932
|
}
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
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);
|
|
3765
4126
|
}
|
|
3766
4127
|
/**
|
|
3767
4128
|
* `dz sync-canonical <skill>` — the healer. Treats the resolved canonical (`--from` →
|
|
@@ -4784,6 +5145,10 @@ export async function runCli(argv, io = {}) {
|
|
|
4784
5145
|
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
4785
5146
|
case 'sign':
|
|
4786
5147
|
return cmdSign(options, flags, cwd, write);
|
|
5148
|
+
case 'sbom':
|
|
5149
|
+
return cmdSbom(options, flags, cwd, write);
|
|
5150
|
+
case 'guard':
|
|
5151
|
+
return cmdGuard(options, flags, cwd, write);
|
|
4787
5152
|
case 'verify-pack':
|
|
4788
5153
|
return cmdVerifyPack(options, flags, cwd, write);
|
|
4789
5154
|
case 'setup':
|