@aikdna/kdna-cli 0.16.10 → 0.17.0

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
@@ -1,10 +1,16 @@
1
1
  # @aikdna/kdna-cli
2
2
 
3
- **KDNA CLI is the runtime control plane for loading, validating, composing, testing, and governing domain judgment for AI agents.**
3
+ **KDNA CLI is the official open-source reference implementation for KDNA validation, loading, packaging, comparison, registry access, and agent-facing runtime workflows.**
4
4
 
5
- KDNA CLI AI Agent 加载、验证、组合、测试和治理领域判断的运行控制平面。
5
+ It is the runtime control plane for loading, validating, composing, testing, and governing domain judgment for AI agents.
6
6
 
7
- Part of the [KDNA](https://github.com/aikdna/KDNA) ecosystem.
7
+ KDNA CLI KDNA 验证、加载、打包、比较、注册表访问和 Agent 运行时工作流的官方开源参考实现,也是 AI Agent 加载、验证、组合、测试和治理领域判断的运行控制平面。
8
+
9
+ The CLI is how a KDNA cognitive kernel becomes usable by agents. It installs KDNA domains, verifies their structure and trust metadata, loads them into agent-readable form, compares judgment paths with and without KDNA, and records traces for audit.
10
+
11
+ KDNA CLI 让一个认知内核真正被 Agent 使用。它负责安装 KDNA、验证结构与信任信息、把 KDNA 转换成 Agent 可加载的形式、对比加载前后的判断路径,并记录可审计的使用痕迹。
12
+
13
+ Part of the [KDNA](https://github.com/aikdna/kdna) ecosystem.
8
14
 
9
15
  ## Install
10
16
 
@@ -28,99 +34,157 @@ kdna doctor --agents
28
34
 
29
35
  ### Domain Authoring
30
36
 
31
- | Command | Description |
32
- |---------|-------------|
33
- | `kdna init <name>` | Scaffold a new domain from template |
34
- | `kdna validate <path>` | Validate domain structure |
35
- | `kdna validate --schema <path>` | Schema-only validation |
36
- | `kdna pack <path>` | Pack into .kdna container |
37
- | `kdna pack <path> --encrypt --license <file>` | Pack encrypted .kdnae container |
38
- | `kdna unpack <file>` | Unpack .kdna or .kdnae container |
39
- | `kdna inspect <path>` | Inspect domain or .kdna file |
40
- | `kdna publish <path>` | Pack + sign + publish to registry |
41
- | `kdna publish --check <path>` | Quality gate check only |
42
- | `kdna version bump <level> [path]` | Bump domain version |
37
+ | Command | Status | Description |
38
+ |---------|--------|-------------|
39
+ | `kdna init <name>` | Beta | Scaffold a new domain from template |
40
+ | `kdna validate <path>` | Stable | Validate domain structure |
41
+ | `kdna validate --schema <path>` | Stable | Schema-only validation |
42
+ | `kdna pack <path>` | Beta | Pack into .kdna container |
43
+ | `kdna pack <path> --encrypt --license <file>` | Beta | Pack encrypted .kdnae container |
44
+ | `kdna unpack <file>` | Beta | Unpack .kdna or .kdnae container |
45
+ | `kdna inspect <path>` | Beta | Inspect domain or .kdna file |
46
+ | `kdna publish <path>` | Experimental | Pack + sign + publish to registry |
47
+ | `kdna publish --check <path>` | Experimental | Quality gate check only |
48
+ | `kdna version bump <level> [path]` | Beta | Bump domain version |
43
49
 
44
50
  ### Agent Runtime
45
51
 
46
- | Command | Description |
47
- |---------|-------------|
48
- | `kdna available [--json]` | List installed domains with v2.1 fields |
49
- | `kdna match "<task>" [--json]` | Signal matching — find relevant domains |
50
- | `kdna load <name> [--as=prompt\|json\|raw]` | Emit domain in agent-ready format |
51
- | `kdna postvalidate <name> --output <file>` | Post-generation judgment check |
52
+ | Command | Status | Description |
53
+ |---------|--------|-------------|
54
+ | `kdna available [--json]` | Beta | List installed domains with v2.1 fields |
55
+ | `kdna match "<task>" [--json]` | Beta | Signal matching — find relevant domains |
56
+ | `kdna load <name> [--as=prompt\|json\|raw]` | Beta | Emit domain in agent-ready format |
57
+ | `kdna postvalidate <name> --output <file>` | Beta | Post-generation judgment check |
52
58
 
53
59
  ### Testing & Verification
54
60
 
55
- | Command | Description |
56
- |---------|-------------|
57
- | `kdna verify <name>` | 3-layer: structure + trust + judgment |
58
- | `kdna compare <name> --input "..."` | With/without KDNA reasoning diff |
59
- | `kdna compare <name> --input "..." --report-md` | Markdown report with scoring |
60
- | `kdna compare <name> --input "..." --report-json` | JSON report with scoring |
61
- | `kdna diff <name>@<v1> <name>@<v2>` | Judgment-level diff between versions |
61
+ | Command | Status | Description |
62
+ |---------|--------|-------------|
63
+ | `kdna verify <name>` | Beta | 3-layer: structure + trust + judgment |
64
+ | `kdna compare <name> --input "..."` | Beta | With/without KDNA reasoning diff |
65
+ | `kdna compare <name> --input "..." --report-md` | Beta | Markdown report with scoring |
66
+ | `kdna compare <name> --input "..." --report-json` | Beta | JSON report with scoring |
67
+ | `kdna diff <name>@<v1> <name>@<v2>` | Beta | Judgment-level diff between versions |
62
68
 
63
69
  ### Diagnostics & Trace
64
70
 
65
- | Command | Description |
66
- |---------|-------------|
67
- | `kdna doctor` | System health check |
68
- | `kdna doctor --agents` | Agent integration check (Codex/Claude/OpenCode/Cursor/Gemini) |
69
- | `kdna doctor --json` | Machine-readable health report |
70
- | `kdna trace` | View recent load/postvalidate traces |
71
- | `kdna trace --json` | Machine-readable trace output |
72
- | `kdna trace --export <file>` | Export traces for audit |
73
- | `kdna trace --since 7d\|30d\|90d` | Filter by time range |
74
- | `kdna history` | Recent domain usage (last 20) |
75
- | `kdna history --stats` | Aggregate by domain and agent |
76
- | `kdna history --domain <name>` | Filter by domain |
71
+ | Command | Status | Description |
72
+ |---------|--------|-------------|
73
+ | `kdna doctor` | Beta | System health check |
74
+ | `kdna doctor --agents` | Beta | Agent integration check (Codex/Claude/OpenCode/Cursor/Gemini) |
75
+ | `kdna doctor --json` | Beta | Machine-readable health report |
76
+ | `kdna trace` | Experimental | View recent load/postvalidate traces |
77
+ | `kdna trace --json` | Experimental | Machine-readable trace output |
78
+ | `kdna trace --export <file>` | Experimental | Export traces for audit |
79
+ | `kdna trace --since 7d\|30d\|90d` | Experimental | Filter by time range |
80
+ | `kdna history` | Experimental | Recent domain usage (last 20) |
81
+ | `kdna history --stats` | Experimental | Aggregate by domain and agent |
82
+ | `kdna history --domain <name>` | Experimental | Filter by domain |
77
83
 
78
84
  ### License & Authorization
79
85
 
80
- | Command | Description |
81
- |---------|-------------|
82
- | `kdna license generate <domain> --to <email>` | Generate signed license |
83
- | `kdna license install <license.json>` | Register license for auto-decrypt |
84
- | `kdna license verify <license.json>` | Verify license signature and validity |
85
- | `kdna license bind <license.json>` | Bind license to this machine |
86
- | `kdna license show <license.json>` | Display license details |
86
+ | Command | Status | Description |
87
+ |---------|--------|-------------|
88
+ | `kdna license generate <domain> --to <email>` | Experimental | Generate signed license |
89
+ | `kdna license install <license.json>` | Experimental | Register license for auto-decrypt |
90
+ | `kdna license verify <license.json>` | Experimental | Verify license signature and validity |
91
+ | `kdna license bind <license.json>` | Experimental | Bind license to this machine |
92
+ | `kdna license show <license.json>` | Experimental | Display license details |
87
93
 
88
94
  ### Cluster Composition
89
95
 
90
- | Command | Description |
91
- |---------|-------------|
92
- | `kdna cluster lint <path>` | Validate cluster manifest |
96
+ | Command | Status | Description |
97
+ |---------|--------|-------------|
98
+ | `kdna cluster lint <path>` | Planned | Validate cluster manifest |
93
99
 
94
100
  ### Registry & Distribution
95
101
 
96
- | Command | Description |
97
- |---------|-------------|
98
- | `kdna install <name>` | Install domain from registry |
99
- | `kdna install ./file.kdna` | Install from local .kdna file |
100
- | `kdna install ./file.kdnae` | Install from encrypted .kdnae (auto-decrypt with license) |
101
- | `kdna remove <name>` | Uninstall a domain |
102
- | `kdna update <name>` | Update installed domain |
103
- | `kdna info <name>` | Show domain metadata and trust status |
104
- | `kdna list [--available]` | List installed or available domains |
105
- | `kdna search <keyword>` | Search registry |
106
- | `kdna registry refresh` | Refresh registry cache |
102
+ | Command | Status | Description |
103
+ |---------|--------|-------------|
104
+ | `kdna install <name>` | Beta | Install domain from registry |
105
+ | `kdna install ./file.kdna` | Beta | Install from local .kdna file |
106
+ | `kdna install ./file.kdnae` | Beta | Install from encrypted .kdnae (auto-decrypt with license) |
107
+ | `kdna remove <name>` | Beta | Uninstall a domain |
108
+ | `kdna update <name>` | Beta | Update installed domain |
109
+ | `kdna info <name>` | Beta | Show domain metadata and trust status |
110
+ | `kdna list [--available]` | Beta | List installed or available domains |
111
+ | `kdna search <keyword>` | Beta | Search registry |
112
+ | `kdna registry refresh` | Beta | Refresh registry cache |
107
113
 
108
114
  ### Identity & Signing
109
115
 
110
- | Command | Description |
111
- |---------|-------------|
112
- | `kdna identity init` | Generate Ed25519 signing key |
113
- | `kdna identity show` | Display public key and buyer ID |
114
- | `kdna identity export [--out]` | Backup private key (encrypted) |
115
- | `kdna identity import <file>` | Restore identity from backup |
116
+ | Command | Status | Description |
117
+ |---------|--------|-------------|
118
+ | `kdna identity init` | Experimental | Generate Ed25519 signing key |
119
+ | `kdna identity show` | Experimental | Display public key and buyer ID |
120
+ | `kdna identity export [--out]` | Experimental | Backup private key (encrypted) |
121
+ | `kdna identity import <file>` | Experimental | Restore identity from backup |
116
122
 
117
123
  ### Setup
118
124
 
119
- | Command | Description |
120
- |---------|-------------|
121
- | `kdna setup` | One-command setup: CLI + skill + data root |
125
+ | Command | Status | Description |
126
+ |---------|--------|-------------|
127
+ | `kdna setup` | Beta | One-command setup: CLI + skill + data root |
128
+
129
+ ---
130
+
131
+ ## SPEC Compatibility
132
+
133
+ KDNA CLI follows the canonical KDNA domain structure defined in [`aikdna/kdna`](https://github.com/aikdna/kdna).
134
+
135
+ A valid KDNA domain is a lowercase `snake_case` folder. A complete domain may include up to six files:
136
+
137
+ - `KDNA_Core.json`
138
+ - `KDNA_Patterns.json`
139
+ - `KDNA_Scenarios.json`
140
+ - `KDNA_Cases.json`
141
+ - `KDNA_Reasoning.json`
142
+ - `KDNA_Evolution.json`
143
+
144
+ The minimum valid domain requires:
145
+
146
+ - `KDNA_Core.json`
147
+ - `KDNA_Patterns.json`
148
+
149
+ Each file must include `meta.version`, `meta.domain`, `meta.created`, `meta.purpose`, and `meta.load_condition`.
150
+
151
+ ---
152
+
153
+ ## Default Registry
154
+
155
+ By default, KDNA CLI uses the official KDNA registry. Users may override it with `KDNA_REGISTRY_URL`.
156
+
157
+ ```bash
158
+ # Use the official registry (default)
159
+ kdna install @aikdna/writing
160
+
161
+ # Use a custom registry
162
+ export KDNA_REGISTRY_URL="https://my-registry.example.com/domains.json"
163
+ kdna install @myorg/internal
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Open Source and Commercial Boundary
169
+
170
+ KDNA keeps the protocol, schemas, validator, core CLI, benchmark tools, and reference examples open source.
171
+
172
+ Commercial or hosted layers may include:
173
+
174
+ - Managed registry services
175
+ - Quality badge review workflows
176
+ - Hosted runtime guard
177
+ - Enterprise private registry
178
+ - Team collaboration in KDNA Studio
179
+ - Licensed/private judgment asset distribution
180
+
181
+ KDNA supports both open judgment assets and licensed/private judgment assets. Open domains remain the default path for community adoption, while encrypted containers and licenses support professional and enterprise distribution.
182
+
183
+ KDNA 同时支持开放判断资产和授权/私有判断资产。开放 domain 是社区采用的默认路径;加密容器和 license 用于专业资产与企业分发。
184
+
185
+ ---
122
186
 
123
- ### Environment Variables
187
+ ## Environment Variables
124
188
 
125
189
  | Variable | Purpose |
126
190
  |----------|---------|
@@ -165,7 +229,7 @@ kdna license verify --json <file>
165
229
  | Authoring | KDNA Studio | Human-led judgment production |
166
230
  | Consumption | KDNAChat | Load, use, compare |
167
231
  | Governance | KDNA Governance Console | Approve, release, audit |
168
- | Distribution | Registry | Discover, install, trade |
232
+ | Distribution | Registry | Discover, install, license, distribute |
169
233
 
170
234
  ## Development
171
235
 
@@ -178,9 +242,9 @@ npm test
178
242
 
179
243
  ## Related
180
244
 
181
- - [@aikdna/kdna-core](https://github.com/aikdna/KDNA/tree/main/packages/kdna-core) — Pure logic library
245
+ - [@aikdna/kdna-core](https://github.com/aikdna/kdna/tree/main/packages/kdna-core) — Pure logic library
182
246
  - [KDNA Registry](https://github.com/aikdna/kdna-registry) — Domain catalog
183
- - [KDNA SPEC](https://github.com/aikdna/KDNA) — Protocol specification
247
+ - [KDNA SPEC](https://github.com/aikdna/kdna) — Protocol specification
184
248
  - [aikdna.com](https://aikdna.com) — Website
185
249
 
186
250
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-cli",
3
- "version": "0.16.10",
3
+ "version": "0.17.0",
4
4
  "description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -31,7 +31,6 @@
31
31
  "kdna-cli",
32
32
  "ai-agent",
33
33
  "domain-cognition",
34
- "knowledge-dna",
35
34
  "cli"
36
35
  ],
37
36
  "license": "Apache-2.0",
@@ -47,7 +46,7 @@
47
46
  "node": ">=18"
48
47
  },
49
48
  "dependencies": {
50
- "@aikdna/kdna-core": "^0.3.0"
49
+ "@aikdna/kdna-core": "^0.4.0"
51
50
  },
52
51
  "optionalDependencies": {
53
52
  "ajv": "^8.17.1",
package/src/agent.js CHANGED
@@ -365,12 +365,44 @@ function cmdLoad(input, args = []) {
365
365
  if (manifest.replaced_by) console.error(`Try: ${manifest.replaced_by}`);
366
366
  process.exit(2);
367
367
  }
368
+
369
+ // ═══ Trust check before loading ═══
370
+ const loadWarnings = [];
371
+ const signature = manifest.signature;
372
+ const isPlaceholder = !signature || signature === '' || signature.includes('placeholder');
373
+ if (isPlaceholder) {
374
+ loadWarnings.push('⚠ Domain is unsigned — no cryptographic proof of authorship. Trust depends on source.');
375
+ }
376
+ if (manifest.status === 'deprecated') {
377
+ loadWarnings.push(`⚠ Domain is deprecated${manifest.replaced_by ? ', replaced by ' + manifest.replaced_by : ''}.`);
378
+ }
379
+ const riskLevel = manifest.risk_level || 'R1';
380
+ if (riskLevel === 'R3' || riskLevel === 'R4') {
381
+ loadWarnings.push(`⚠ High risk domain (${riskLevel}) — may influence agent behavior in safety-critical ways.`);
382
+ if (manifest.quality_badge === 'untested' || !manifest.quality_badge) {
383
+ loadWarnings.push('⚠ High risk + untested — load only if you trust the source and understand the risks.');
384
+ }
385
+ }
386
+ if (loadWarnings.length > 0) {
387
+ console.error(loadWarnings.join('\n'));
388
+ }
368
389
  const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
369
390
  const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
370
391
 
371
392
  // JSON format
372
393
  if (format === 'json') {
373
- process.stdout.write(JSON.stringify({ manifest, core, patterns: pat }, null, 2) + '\n');
394
+ process.stdout.write(JSON.stringify({
395
+ manifest,
396
+ core,
397
+ patterns: pat,
398
+ trust: {
399
+ signature: isPlaceholder ? 'unsigned' : 'present',
400
+ risk_level: riskLevel,
401
+ deprecated: manifest.status === 'deprecated',
402
+ yanked: false,
403
+ warnings: loadWarnings,
404
+ },
405
+ }, null, 2) + '\n');
374
406
  recordTrace({
375
407
  timestamp: new Date().toISOString(),
376
408
  agent: detectAgent(),
@@ -910,4 +942,346 @@ function cmdPostvalidate(args = []) {
910
942
  process.exit(results.violations.length ? 1 : 0);
911
943
  }
912
944
 
913
- module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate };
945
+ // ─── kdna route ─────────────────────────────────────────────────────────
946
+
947
+ function cmdRoute(taskText, args = []) {
948
+ const wantJson = args.includes('--json');
949
+
950
+ if (!taskText) {
951
+ const err = { error: 'Usage: kdna route "<task description>" [--json] [--discover]' };
952
+ if (wantJson) { console.log(JSON.stringify(err)); process.exit(2); }
953
+ console.error(err.error);
954
+ process.exit(2);
955
+ }
956
+
957
+ const traceId = `route_${require('crypto').randomUUID()}`;
958
+ const taskTokens = tokenize(taskText);
959
+ const installed = listInstalled();
960
+ const result = {
961
+ status: 'SKIP_NO_JUDGMENT_NEEDED',
962
+ action: 'skip',
963
+ needs_kdna: false,
964
+ selected_domain: null,
965
+ reason: '',
966
+ confidence: 0,
967
+ candidates: [],
968
+ rejected_domains: [],
969
+ trust: null,
970
+ ambiguity: null,
971
+ registry_suggestions: [],
972
+ auto_install: false,
973
+ trace_id: traceId,
974
+ created_at: new Date().toISOString(),
975
+ };
976
+
977
+ // ═══ Gate 1: Intent — does this task need domain judgment? ═══
978
+ const judgmentKeywords = [
979
+ 'review', 'diagnose', 'critique', 'evaluate', 'assess', 'judge',
980
+ 'should i', 'is this good', 'is this correct', 'how would you rate',
981
+ '分析', '诊断', '评估', '判断', '审查', '该怎么', '好不好',
982
+ ];
983
+ const mechanicalKeywords = [
984
+ 'format', 'translate', 'convert', 'list', 'find', 'lookup', 'search',
985
+ 'run', 'execute', 'compile', 'build', 'fix syntax', 'fix the bug',
986
+ '格式化', '翻译', '转换', '列出', '查找', '搜索', '运行', '执行', '编译', '修复语法',
987
+ ];
988
+
989
+ const taskLower = taskText.toLowerCase();
990
+ const hasJudgmentSignal = judgmentKeywords.some(k => taskLower.includes(k));
991
+ const hasMechanicalSignal = mechanicalKeywords.some(k => taskLower.includes(k));
992
+
993
+ result.needs_kdna = hasJudgmentSignal && !hasMechanicalSignal;
994
+
995
+ if (!result.needs_kdna) {
996
+ result.status = 'SKIP_NO_JUDGMENT_NEEDED';
997
+ result.action = 'skip';
998
+ result.reason = hasMechanicalSignal
999
+ ? 'task is mechanical — no domain judgment required'
1000
+ : 'task does not appear to need domain judgment';
1001
+ if (wantJson) { console.log(JSON.stringify(result, null, 2)); return; }
1002
+ console.log('SKIP (no judgment needed)');
1003
+ return;
1004
+ }
1005
+
1006
+ if (!installed.length) {
1007
+ result.status = 'SKIP_NO_LOCAL_DOMAIN';
1008
+ result.action = 'skip';
1009
+ result.reason = 'task may benefit from judgment, but no KDNA domains are installed';
1010
+ if (wantJson) { console.log(JSON.stringify(result, null, 2)); return; }
1011
+ console.log('SKIP (no domains installed)');
1012
+ return;
1013
+ }
1014
+
1015
+ // ═══ Gate 2: Negative Match First — check does_not_apply_when ═══
1016
+ // ═══ Gate 3: Domain Fit — evaluate applies_when + relevance ═══
1017
+ const candidates = [];
1018
+
1019
+ for (const e of installed) {
1020
+ const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
1021
+ if (manifest.yanked === true) {
1022
+ result.rejected_domains.push({
1023
+ domain: manifest.name || e.full,
1024
+ triggered_rule: 'yanked',
1025
+ reason: 'domain has been yanked',
1026
+ });
1027
+ continue;
1028
+ }
1029
+
1030
+ const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
1031
+
1032
+ // Negative match: does_not_apply_when
1033
+ let disqualified = null;
1034
+ for (const a of core.axioms || []) {
1035
+ for (const d of a.does_not_apply_when || []) {
1036
+ const score = overlapScore(taskTokens, d);
1037
+ if (score.hits >= 2) {
1038
+ disqualified = { axiom: a.id, text: d, hits: score.hits };
1039
+ break;
1040
+ }
1041
+ }
1042
+ if (disqualified) break;
1043
+ }
1044
+
1045
+ if (disqualified) {
1046
+ result.rejected_domains.push({
1047
+ domain: manifest.name || e.full,
1048
+ triggered_rule: `${disqualified.axiom}.does_not_apply_when`,
1049
+ reason: `"${disqualified.text.slice(0, 100)}"`,
1050
+ });
1051
+ continue;
1052
+ }
1053
+
1054
+ // Positive fit: applies_when + domain relevance
1055
+ let fitScore = 0;
1056
+ const fitReasons = [];
1057
+
1058
+ for (const a of core.axioms || []) {
1059
+ for (const ap of a.applies_when || []) {
1060
+ const score = overlapScore(taskTokens, ap);
1061
+ if (score.hits >= 2) {
1062
+ fitScore += score.hits * 3;
1063
+ fitReasons.push({ source: a.id, hits: score.hits, text: ap.slice(0, 120) });
1064
+ }
1065
+ }
1066
+ }
1067
+ fitScore += domainRelevanceScore(taskTokens, manifest);
1068
+
1069
+ // Confidence based on fitScore normalized
1070
+ const confidence = Math.min(0.95, fitScore > 0 ? 0.5 + fitScore * 0.05 : 0.15);
1071
+
1072
+ candidates.push({
1073
+ domain: manifest.name || e.full,
1074
+ version: manifest.version || '?',
1075
+ status: manifest.status || 'experimental',
1076
+ score: fitScore,
1077
+ confidence,
1078
+ reasons: fitReasons.slice(0, 5),
1079
+ description: manifest.description || '',
1080
+ });
1081
+ }
1082
+
1083
+ // Sort by score
1084
+ candidates.sort((a, b) => b.score - a.score);
1085
+
1086
+ // ═══ Gate 4: Decision ═══
1087
+ const strongCandidates = candidates.filter(c => c.score >= 6);
1088
+ const weakCandidates = candidates.filter(c => c.score > 0 && c.score < 6);
1089
+
1090
+ if (strongCandidates.length === 0 && weakCandidates.length === 0) {
1091
+ // No matches at all
1092
+ result.status = 'SKIP_NO_LOCAL_DOMAIN';
1093
+ result.action = 'skip';
1094
+ result.reason = 'no installed domain matches this task';
1095
+ if (result.rejected_domains.length > 0) {
1096
+ result.reason += ` (${result.rejected_domains.length} domains explicitly excluded by does_not_apply_when)`;
1097
+ }
1098
+ result.candidates = candidates.map(c => ({
1099
+ domain: c.domain,
1100
+ decision: 'rejected',
1101
+ reason: 'insufficient match score',
1102
+ confidence: c.confidence,
1103
+ }));
1104
+ } else if (strongCandidates.length > 1) {
1105
+ // Multiple strong matches — ambiguity
1106
+ result.status = 'ASK_AMBIGUOUS_DOMAIN';
1107
+ result.action = 'ask';
1108
+ result.reason = `${strongCandidates.length} domains strongly match this task with different judgment frames`;
1109
+
1110
+ result.ambiguity = {
1111
+ domains: strongCandidates.slice(0, 3).map(c => ({
1112
+ domain: c.domain,
1113
+ description: c.description,
1114
+ judgment_frame: c.reasons.length > 0 ? c.reasons[0].text : c.description,
1115
+ risk_if_wrong: `may misclassify the task as a ${c.domain.split('/').pop()} problem`,
1116
+ })),
1117
+ recommendation: 'Choose the domain whose judgment frame best matches the task intent. Do not blend domains.',
1118
+ };
1119
+
1120
+ result.candidates = strongCandidates.map(c => ({
1121
+ domain: c.domain, decision: 'ambiguous', reason: `score ${c.score}`, confidence: c.confidence,
1122
+ }));
1123
+ } else if (strongCandidates.length === 1) {
1124
+ // One strong match + possible weak matches
1125
+ const selected = strongCandidates[0];
1126
+ result.candidates = [
1127
+ { domain: selected.domain, decision: 'strong_match', reason: `score ${selected.score}`, confidence: selected.confidence },
1128
+ ...weakCandidates.map(c => ({ domain: c.domain, decision: 'weak_match', reason: `score ${c.score}`, confidence: c.confidence })),
1129
+ ];
1130
+
1131
+ // ═══ Trust Gate ═══
1132
+ const trust = checkTrust(selected.domain);
1133
+ result.trust = trust;
1134
+
1135
+ if (!trust.passed) {
1136
+ result.status = 'BLOCK_TRUST_FAILED';
1137
+ result.action = 'block';
1138
+ result.reason = `domain matched but trust check failed: ${trust.failures.join(', ')}`;
1139
+ } else {
1140
+ result.status = 'LOAD_STRONG_FIT';
1141
+ result.action = 'load';
1142
+ result.selected_domain = selected.domain;
1143
+ result.confidence = selected.confidence;
1144
+ result.reason = `match: "${selected.description.slice(0, 100)}"`;
1145
+ }
1146
+ } else {
1147
+ // Only weak matches — skip
1148
+ result.status = 'SKIP_WEAK_FIT';
1149
+ result.action = 'skip';
1150
+ result.reason = weakCandidates.length > 0
1151
+ ? `${weakCandidates.length} domain(s) have weak match only — skipping to avoid contamination`
1152
+ : 'no installed domain matches this task';
1153
+ result.candidates = weakCandidates.map(c => ({
1154
+ domain: c.domain, decision: 'weak_match', reason: `score ${c.score}`, confidence: c.confidence,
1155
+ }));
1156
+ }
1157
+
1158
+ // Add rejected domains to candidates array for full trace
1159
+ for (const r of result.rejected_domains) {
1160
+ result.candidates.push({
1161
+ domain: r.domain,
1162
+ decision: 'rejected',
1163
+ reason: r.reason,
1164
+ confidence: 0,
1165
+ matched_does_not_apply_when: r.triggered_rule,
1166
+ });
1167
+ }
1168
+
1169
+ if (wantJson) {
1170
+ console.log(JSON.stringify(result, null, 2));
1171
+ return;
1172
+ }
1173
+
1174
+ // Human output
1175
+ console.log(`Task: ${taskText.slice(0, 100)}${taskText.length > 100 ? '…' : ''}`);
1176
+ console.log(`Route: ${result.status} → ${result.action}`);
1177
+ if (result.reason) console.log(`Reason: ${result.reason}`);
1178
+ if (result.selected_domain) console.log(`Domain: ${result.selected_domain}`);
1179
+ if (result.rejected_domains.length) {
1180
+ console.log(`Rejected: ${result.rejected_domains.map(r => r.domain).join(', ')}`);
1181
+ }
1182
+ }
1183
+
1184
+ function checkTrust(domainName, options = {}) {
1185
+ const failures = [];
1186
+ const warnings = [];
1187
+ const installed = listInstalled();
1188
+ const entry = installed.find(e => `${e.scope}/${e.ident}` === domainName || e.full === domainName);
1189
+ if (!entry) {
1190
+ failures.push('domain not found in installed directory');
1191
+ return { passed: false, failures, warnings };
1192
+ }
1193
+
1194
+ const manifest = readJson(path.join(entry.dir, 'kdna.json')) || {};
1195
+ const core = readJson(path.join(entry.dir, 'KDNA_Core.json')) || {};
1196
+ const evolution = readJson(path.join(entry.dir, 'KDNA_Evolution.json')) || {};
1197
+
1198
+ // 1. Yank check
1199
+ if (manifest.yanked === true) {
1200
+ failures.push('domain is yanked');
1201
+ }
1202
+
1203
+ // 2. Deprecation check
1204
+ if (manifest.status === 'deprecated') {
1205
+ warnings.push(`domain is deprecated${manifest.replaced_by ? ', replaced by ' + manifest.replaced_by : ''}`);
1206
+ }
1207
+
1208
+ // 3. Signature check
1209
+ const signature = manifest.signature;
1210
+ const isPlaceholder = !signature || signature === '' || signature.includes('placeholder');
1211
+ if (manifest.access === 'licensed' || manifest.access === 'runtime') {
1212
+ if (isPlaceholder) {
1213
+ failures.push('commercial domain has no valid signature');
1214
+ }
1215
+ } else if (isPlaceholder) {
1216
+ warnings.push('domain is unsigned — trust depends on source');
1217
+ }
1218
+
1219
+ // 4. Risk level check
1220
+ const riskLevel = manifest.risk_level || entry.risk_level || 'R1';
1221
+ const riskMap = { R0: 0, R1: 1, R2: 2, R3: 3, R4: 4 };
1222
+ const riskNum = riskMap[riskLevel] || 1;
1223
+ if (riskNum >= 3) {
1224
+ warnings.push(`domain risk level is ${riskLevel} — high-risk judgment may influence agent behavior`);
1225
+ }
1226
+ if (riskNum >= 2 && (manifest.quality_badge === 'untested' || !manifest.quality_badge)) {
1227
+ warnings.push(`risk level ${riskLevel} with quality_badge '${manifest.quality_badge || 'none'}' — consider requiring review`);
1228
+ }
1229
+
1230
+ // 5. SPEC compatibility check
1231
+ const specVersion = manifest.spec_version || manifest.kdna_spec || 'unknown';
1232
+ const supportedSpecs = ['1.0-rc', '1.0', '0.7'];
1233
+ if (!supportedSpecs.includes(specVersion)) {
1234
+ warnings.push(`SPEC version '${specVersion}' may not be fully compatible with current loader`);
1235
+ }
1236
+
1237
+ // 6. License validity (commercial domains)
1238
+ if (manifest.access === 'licensed' || manifest.access === 'runtime') {
1239
+ const licenseStorePath = path.join(process.env.HOME || '.', '.kdna', 'licenses.json');
1240
+ let licenseOk = false;
1241
+ try {
1242
+ if (require('fs').existsSync(licenseStorePath)) {
1243
+ const store = JSON.parse(require('fs').readFileSync(licenseStorePath, 'utf8'));
1244
+ const keys = store.keys || {};
1245
+ for (const [hash, entry] of Object.entries(keys)) {
1246
+ if (entry.active && entry.domain_id === domainName) {
1247
+ if (!entry.expires_at || new Date(entry.expires_at) > new Date()) {
1248
+ licenseOk = true;
1249
+ break;
1250
+ }
1251
+ }
1252
+ }
1253
+ if (!licenseOk) {
1254
+ warnings.push('commercial domain has no active license — install with: kdna install ' + domainName + ' --license <key>');
1255
+ }
1256
+ } else {
1257
+ warnings.push('no license store found — commercial domain may require a license');
1258
+ }
1259
+ } catch { /* license check unavailable */ }
1260
+ }
1261
+
1262
+ // 7. Human Lock check (judgment-class cards)
1263
+ const judgmentCardTypes = ['axiom', 'boundary', 'risk'];
1264
+ const axioms = core.axioms || [];
1265
+ const hasJudgmentCards = axioms.length > 0;
1266
+ if (hasJudgmentCards) {
1267
+ const humanLocks = evolution.human_locks || [];
1268
+ const lockedAxioms = axioms.filter(a => {
1269
+ // Check if axiom has a human_lock field OR if an evolution lock covers it
1270
+ return a.human_lock || humanLocks.some(hl => hl.lock_type === 'accept');
1271
+ }).length;
1272
+ if (lockedAxioms === 0 && humanLocks.length === 0) {
1273
+ warnings.push('domain has no Human Lock records — judgment-class content may not be human-verified');
1274
+ }
1275
+ }
1276
+
1277
+ return {
1278
+ passed: failures.length === 0,
1279
+ failures,
1280
+ warnings,
1281
+ riskLevel,
1282
+ specVersion,
1283
+ signatureValid: !isPlaceholder,
1284
+ };
1285
+ }
1286
+
1287
+ module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate, cmdRoute, checkTrust };
package/src/cli.js CHANGED
@@ -26,6 +26,7 @@ const {
26
26
  cmdSelect,
27
27
  cmdLoad,
28
28
  cmdPostvalidate,
29
+ cmdRoute,
29
30
  } = require('./cmds/quality');
30
31
  const { cmdCluster } = require('./cmds/cluster');
31
32
  const { cmdIdentity } = require('./cmds/identity');
@@ -101,6 +102,7 @@ Studio Integration (Phase 1):
101
102
  studio readiness <project.json> Generate domain readiness card
102
103
 
103
104
  Agent Runtime:
105
+ route "<task>" [--json] [--discover] 5-Gate 7-State routing decision
104
106
  available [--json] List installed domains with v2.1 fields
105
107
  match "<task>" [--json] Signal matching — find relevant domains
106
108
  select --input "..." [--json] Selection policy — decide which domains to load
@@ -294,7 +296,7 @@ switch (cmd) {
294
296
  if (!target) error('Usage: kdna card <path> [--json] [--locale zh-CN]');
295
297
  const localeIdx = args.indexOf('--locale');
296
298
  const locale = localeIdx >= 0 ? args[localeIdx + 1] : null;
297
- cmdCard(target, locale);
299
+ cmdCard(target, args.includes('--json'), locale);
298
300
  break;
299
301
  }
300
302
  case 'verify': {
@@ -340,6 +342,10 @@ switch (cmd) {
340
342
  cmdSelect(args);
341
343
  break;
342
344
  }
345
+ case 'route': {
346
+ cmdRoute(args);
347
+ break;
348
+ }
343
349
  case 'postvalidate': {
344
350
  cmdPostvalidate(args);
345
351
  break;
@@ -513,7 +519,7 @@ switch (cmd) {
513
519
  const idx = args.indexOf('--check');
514
520
  const target = args[idx + 1] || args.filter((a) => !a.startsWith('--'))[1] || '.';
515
521
  if (!target || target.startsWith('--')) error('Usage: kdna publish --check <path>');
516
- cmdPublishCheck(target);
522
+ cmdPublishCheck(target, args);
517
523
  } else {
518
524
  const { cmdPublish } = require('./publish');
519
525
  const target = args.filter((a) => !a.startsWith('--'))[1];
@@ -11,38 +11,31 @@ const {
11
11
  ENCRYPTED_FILES,
12
12
  } = require('./encrypt');
13
13
 
14
- // ─── Validate ────────────────────────────────────────────────────────
15
-
16
- function cmdValidate(dir, schemaOnly, jsonMode = false) {
17
- const abs = path.resolve(dir);
18
- if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
19
- error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
20
- }
21
-
22
- const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
23
-
24
- // Resolve schemas from @aikdna/kdna-core package
25
- const SCHEMA_DIR = path.join(
26
- path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
27
- 'schema',
28
- );
29
-
30
- // Read all KDNA JSON files
31
- const files = fs.readdirSync(abs).filter((f) => f.endsWith('.json') && f !== 'kdna.json');
14
+ const KDNA_DOMAIN_FILES = new Set([
15
+ 'KDNA_Core.json',
16
+ 'KDNA_Patterns.json',
17
+ 'KDNA_Scenarios.json',
18
+ 'KDNA_Cases.json',
19
+ 'KDNA_Reasoning.json',
20
+ 'KDNA_Evolution.json',
21
+ ]);
22
+
23
+ function readKDNAContentFiles(abs) {
32
24
  const dataMap = {};
33
- const schemaMap = {};
34
-
35
- for (const f of files) {
25
+ const parseErrors = [];
26
+ for (const f of fs.readdirSync(abs).filter((name) => KDNA_DOMAIN_FILES.has(name))) {
36
27
  try {
37
28
  dataMap[f] = JSON.parse(fs.readFileSync(path.join(abs, f), 'utf8'));
38
29
  } catch (e) {
39
30
  dataMap[f] = null;
40
- console.error(` JSON parse error in ${f}: ${e.message}`);
31
+ parseErrors.push(`${f}: ${e.message}`);
41
32
  }
42
33
  }
34
+ return { dataMap, parseErrors };
35
+ }
43
36
 
44
- // Schema validation — always load all available schemas
45
- const FILE_TO_SCHEMA = {
37
+ function loadSchemaMap(schemaDir) {
38
+ const fileToSchema = {
46
39
  'KDNA_Core.json': 'KDNA_Core.schema.json',
47
40
  'KDNA_Patterns.json': 'KDNA_Patterns.schema.json',
48
41
  'KDNA_Scenarios.json': 'KDNA_Scenarios.schema.json',
@@ -51,10 +44,11 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
51
44
  'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
52
45
  };
53
46
 
47
+ const schemaMap = {};
54
48
  const loadedSchemas = [];
55
49
  const missingSchemas = [];
56
- for (const [, schemaFile] of Object.entries(FILE_TO_SCHEMA)) {
57
- const schemaPath = path.join(SCHEMA_DIR, schemaFile);
50
+ for (const schemaFile of Object.values(fileToSchema)) {
51
+ const schemaPath = path.join(schemaDir, schemaFile);
58
52
  if (fs.existsSync(schemaPath)) {
59
53
  try {
60
54
  schemaMap[schemaFile] = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
@@ -67,50 +61,132 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
67
61
  }
68
62
  }
69
63
 
70
- if (missingSchemas.length) {
71
- console.log(
72
- ` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
73
- );
74
- console.log(` Schema dir: ${SCHEMA_DIR}`);
75
- }
64
+ return { schemaMap, loadedSchemas, missingSchemas };
65
+ }
76
66
 
77
- // Validation layers
78
- const errors = [];
67
+ function validateDomainDirectory(abs, schemaMap, schemaOnly) {
68
+ const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
69
+ const { dataMap, parseErrors } = readKDNAContentFiles(abs);
70
+ const errors = parseErrors.map((msg) => `JSON parse error in ${msg}`);
79
71
  const warnings = [];
80
72
 
81
- // Layer 1: Lint (structural + content checks)
82
73
  if (!schemaOnly) {
83
74
  const lintResult = lintDomain(dataMap);
84
75
  errors.push(...lintResult.errors);
85
76
  warnings.push(...lintResult.warnings);
86
77
  }
87
78
 
88
- // Layer 2: JSON Schema validation against loaded schemas
89
79
  const schemaResult = validateDomainSchema(dataMap, schemaMap);
90
80
  errors.push(...schemaResult.errors);
91
81
  warnings.push(...schemaResult.warnings);
92
82
 
93
- // Layer 3: Cross-file consistency
94
83
  const crossResult = validateCrossFile(dataMap);
95
84
  errors.push(...crossResult.errors);
96
85
  warnings.push(...crossResult.warnings);
97
86
 
98
- const validCount = Object.keys(dataMap).filter((k) => dataMap[k]).length;
87
+ return {
88
+ path: abs,
89
+ valid: errors.length === 0,
90
+ files: Object.keys(dataMap).filter((k) => dataMap[k]).length,
91
+ errors,
92
+ warnings,
93
+ };
94
+ }
95
+
96
+ function isClusterDirectory(abs) {
97
+ const manifest = readJson(path.join(abs, 'kdna.json'));
98
+ return !!(
99
+ manifest?.cluster ||
100
+ fs.existsSync(path.join(abs, 'KDNA_Cluster.json')) ||
101
+ fs.existsSync(path.join(abs, 'cluster_manifest.json'))
102
+ );
103
+ }
104
+
105
+ function validateClusterDirectory(abs, schemaMap, schemaOnly) {
106
+ const manifest = readJson(path.join(abs, 'kdna.json')) || {};
107
+ const clusterManifest = readJson(path.join(abs, 'KDNA_Cluster.json')) || {};
108
+ const fallbackManifest = readJson(path.join(abs, 'cluster_manifest.json')) || {};
109
+ const subDomains = manifest.sub_domains || fallbackManifest.domains || [];
110
+ const errors = [];
111
+ const warnings = [];
112
+
113
+ if (!Array.isArray(subDomains) || subDomains.length === 0) {
114
+ errors.push('Cluster has no sub_domains/domains list to validate');
115
+ }
116
+
117
+ const domains = [];
118
+ for (const name of subDomains) {
119
+ const domainPath = path.join(abs, name);
120
+ if (!fs.existsSync(domainPath) || !fs.statSync(domainPath).isDirectory()) {
121
+ errors.push(`Cluster sub-domain not found: ${name}`);
122
+ continue;
123
+ }
124
+ const result = validateDomainDirectory(domainPath, schemaMap, schemaOnly);
125
+ domains.push({ name, ...result });
126
+ warnings.push(...result.warnings.map((w) => `${name}: ${w}`));
127
+ errors.push(...result.errors.map((e) => `${name}: ${e}`));
128
+ }
129
+
130
+ if (!manifest.cluster && !clusterManifest.name && !fallbackManifest.name) {
131
+ warnings.push('Cluster metadata is minimal: no cluster marker or cluster name found');
132
+ }
133
+
134
+ return {
135
+ path: abs,
136
+ valid: errors.length === 0,
137
+ cluster: true,
138
+ domains,
139
+ files: domains.reduce((sum, d) => sum + d.files, 0),
140
+ errors,
141
+ warnings,
142
+ };
143
+ }
144
+
145
+ // ─── Validate ────────────────────────────────────────────────────────
146
+
147
+ function cmdValidate(dir, schemaOnly, jsonMode = false) {
148
+ const abs = path.resolve(dir);
149
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
150
+ error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
151
+ }
152
+
153
+ // Resolve schemas from @aikdna/kdna-core package
154
+ const SCHEMA_DIR = path.join(
155
+ path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
156
+ 'schema',
157
+ );
158
+
159
+ const { schemaMap, loadedSchemas, missingSchemas } = loadSchemaMap(SCHEMA_DIR);
160
+
161
+ if (missingSchemas.length) {
162
+ console.log(
163
+ ` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
164
+ );
165
+ console.log(` Schema dir: ${SCHEMA_DIR}`);
166
+ }
167
+
168
+ const result = isClusterDirectory(abs)
169
+ ? validateClusterDirectory(abs, schemaMap, schemaOnly)
170
+ : validateDomainDirectory(abs, schemaMap, schemaOnly);
171
+ const { errors, warnings } = result;
172
+ const validCount = result.files;
99
173
  const schemaInfo = schemaOnly
100
174
  ? ` (schema-only mode, ${loadedSchemas.length} schemas loaded)`
101
175
  : '';
102
176
 
103
177
  if (jsonMode) {
104
- const result = {
178
+ const payload = {
105
179
  path: abs,
106
180
  valid: errors.length === 0,
107
181
  files: validCount,
182
+ cluster: !!result.cluster,
183
+ domains: result.domains,
108
184
  schemas_loaded: loadedSchemas.length,
109
185
  schema_only: schemaOnly,
110
186
  errors,
111
187
  warnings,
112
188
  };
113
- console.log(JSON.stringify(result, null, 2));
189
+ console.log(JSON.stringify(payload, null, 2));
114
190
  process.exit(errors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
115
191
  }
116
192
 
@@ -124,7 +200,13 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
124
200
  process.exit(EXIT.VALIDATION_FAILED);
125
201
  }
126
202
 
127
- console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
203
+ if (result.cluster) {
204
+ console.log(
205
+ `✓ KDNA cluster valid: ${abs} (${result.domains.length} domains, ${validCount} KDNA files, schema OK${schemaInfo})`,
206
+ );
207
+ } else {
208
+ console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
209
+ }
128
210
  }
129
211
 
130
212
  // ─── Pack / Unpack (.kdna ZIP container) ──────────────────────────────────
@@ -140,6 +222,19 @@ function cmdPack(dir, outputDir) {
140
222
  if (!core) error('KDNA_Core.json not found or invalid');
141
223
  if (!pat) error('KDNA_Patterns.json not found or invalid');
142
224
 
225
+ // Human Lock Gate — check judgment-class cards before packing
226
+ const { checkHumanLock } = require('../publish');
227
+ const hl = checkHumanLock(abs);
228
+ if (!hl.passed) {
229
+ console.error('Human Lock Gate: BLOCKED');
230
+ for (const issue of hl.issues) {
231
+ console.error(` ✗ ${issue}`);
232
+ }
233
+ console.error('Judgment-class cards must be locked with valid Human Lock before packing.');
234
+ console.error('Use kdna publish --check for details.');
235
+ process.exit(EXIT.HUMAN_LOCK_REQUIRED);
236
+ }
237
+
143
238
  const domainName = core.meta?.domain || path.basename(abs);
144
239
 
145
240
  // Ensure kdna.json manifest exists (generate if missing)
@@ -1025,7 +1120,7 @@ zf.close()
1025
1120
 
1026
1121
  // ─── KDNA Card (locale-aware) ────────────────────────────────────
1027
1122
 
1028
- function cmdCard(dir, locale = null) {
1123
+ function cmdCard(dir, jsonMode = false, locale = null) {
1029
1124
  const abs = path.resolve(dir);
1030
1125
  let card = readJson(path.join(abs, 'KDNA_CARD.json'));
1031
1126
 
@@ -1043,8 +1138,6 @@ function cmdCard(dir, locale = null) {
1043
1138
  );
1044
1139
  }
1045
1140
 
1046
- const jsonMode = process.argv.includes('--json');
1047
-
1048
1141
  if (jsonMode) {
1049
1142
  console.log(JSON.stringify(card, null, 2));
1050
1143
  return;
@@ -100,6 +100,17 @@ function cmdPostvalidate(args) {
100
100
  cmdPostvalidate(args);
101
101
  }
102
102
 
103
+ function cmdRoute(args) {
104
+ const { cmdRoute } = require('../agent');
105
+ const positional = args.filter((a) => !a.startsWith('--'));
106
+ const flags = args.filter((a) => a.startsWith('--'));
107
+ if (!positional[1]) {
108
+ const { error, EXIT } = require('./_common');
109
+ error('Usage: kdna route "<task description>" [--json] [--discover]', EXIT.INPUT_ERROR);
110
+ }
111
+ cmdRoute(positional.slice(1).join(' ').trim(), flags);
112
+ }
113
+
103
114
  module.exports = {
104
115
  cmdCompare,
105
116
  cmdDiff,
@@ -109,4 +120,5 @@ module.exports = {
109
120
  cmdSelect,
110
121
  cmdLoad,
111
122
  cmdPostvalidate,
123
+ cmdRoute,
112
124
  };
package/src/publish.js CHANGED
@@ -141,9 +141,71 @@ function isGenericSelfCheck(question) {
141
141
  return false;
142
142
  }
143
143
 
144
+ // ─── Human Lock Gate ──────────────────────────────────────────────────
145
+
146
+ const JUDGMENT_CARD_TYPES = ['axiom', 'boundary', 'risk', 'aesthetic'];
147
+
148
+ /**
149
+ * Check whether the domain satisfies Human Lock requirements.
150
+ * Returns { passed, issues[] } — publish should be blocked if !passed.
151
+ */
152
+ function checkHumanLock(domainPath) {
153
+ const core = readJson(path.join(domainPath, 'KDNA_Core.json'));
154
+ if (!core) return { passed: false, issues: ['KDNA_Core.json not found'] };
155
+
156
+ const issues = [];
157
+ const cards = [];
158
+
159
+ // Collect judgment-class cards from axioms, boundaries, risks
160
+ if (core.axioms) {
161
+ for (const a of core.axioms) {
162
+ cards.push({ type: 'axiom', id: a.id || '?', status: a.status, human_lock: a.human_lock });
163
+ }
164
+ }
165
+ if (core.boundaries) {
166
+ for (const b of core.boundaries) {
167
+ cards.push({ type: 'boundary', id: b.id || '?', status: b.status, human_lock: b.human_lock });
168
+ }
169
+ }
170
+ if (core.risks || core.risk_model) {
171
+ const risks = core.risks || core.risk_model || [];
172
+ for (const r of risks) {
173
+ cards.push({ type: 'risk', id: r.id || '?', status: r.status, human_lock: r.human_lock });
174
+ }
175
+ }
176
+
177
+ if (cards.length === 0) return { passed: true, issues: [] };
178
+
179
+ for (const card of cards) {
180
+ // Rule 1: Must be locked
181
+ if (!card.status || !['locked', 'tested', 'published'].includes(card.status)) {
182
+ issues.push(`${card.type} "${card.id}" is not locked. Human Lock required before publish.`);
183
+ continue;
184
+ }
185
+ // Rule 2: Must have human_lock record
186
+ if (!card.human_lock || !card.human_lock.by || !card.human_lock.statement) {
187
+ issues.push(`${card.type} "${card.id}" is locked but has no valid Human Lock record.`);
188
+ continue;
189
+ }
190
+ // Rule 3: Lock must confirm judgment fields were reviewed
191
+ const checked = card.human_lock.checked || {};
192
+ if (!checked.applies_when) {
193
+ issues.push(`${card.type} "${card.id}" Human Lock does not confirm applies_when was reviewed.`);
194
+ }
195
+ if (!checked.does_not_apply_when) {
196
+ issues.push(`${card.type} "${card.id}" Human Lock does not confirm does_not_apply_when was reviewed.`);
197
+ }
198
+ if (!checked.failure_risk) {
199
+ issues.push(`${card.type} "${card.id}" Human Lock does not confirm failure_risk was reviewed.`);
200
+ }
201
+ }
202
+
203
+ return { passed: issues.length === 0, issues };
204
+ }
205
+
144
206
  // ─── Main check function ──────────────────────────────────────────────
145
207
 
146
- function cmdPublishCheck(domainPath) {
208
+ function cmdPublishCheck(domainPath, args = []) {
147
209
  const abs = path.resolve(domainPath);
148
210
  if (!fs.existsSync(abs)) error(`Domain not found: ${abs}`);
149
211
 
@@ -152,6 +214,35 @@ function cmdPublishCheck(domainPath) {
152
214
  console.log('═'.repeat(60));
153
215
  console.log('');
154
216
 
217
+ // ─── Human Lock Gate (must pass before any other checks) ──────────
218
+ const hl = checkHumanLock(abs);
219
+ if (!hl.passed) {
220
+ if (args.includes('--force')) {
221
+ console.warn(' ⚠ Human Lock Gate: OVERRIDDEN (--force). Proceeding with checks.');
222
+ console.warn(` ${hl.issues.length} unresolved Human Lock issue(s):`);
223
+ for (const issue of hl.issues) {
224
+ console.warn(` ${issue}`);
225
+ }
226
+ console.warn('');
227
+ } else {
228
+ console.error(' Human Lock Gate: BLOCKED');
229
+ console.error(` ${hl.issues.length} issue(s) found:`);
230
+ for (const issue of hl.issues) {
231
+ console.error(` ✗ ${issue}`);
232
+ }
233
+ console.error('');
234
+ console.error(' Judgment-class cards (axiom, boundary, risk, aesthetic)');
235
+ console.error(' must be locked with a valid Human Lock record before publishing.');
236
+ console.error(' Use kdna-studio or manually add human_lock to each card.');
237
+ console.error(' Use --force for emergency override (audited).');
238
+ console.error('');
239
+ process.exit(EXIT.HUMAN_LOCK_REQUIRED);
240
+ }
241
+ } else {
242
+ console.log(' ✓ Human Lock Gate: passed');
243
+ console.log('');
244
+ }
245
+
155
246
  let errors = 0;
156
247
  let warnings = 0;
157
248
  let passes = 0;
@@ -551,6 +642,26 @@ function cmdPublish(domainPath, args = []) {
551
642
  console.log('═'.repeat(60));
552
643
  console.log(` Publishing ${name}@${manifest.version}`);
553
644
  console.log('═'.repeat(60));
645
+
646
+ // ─── Human Lock Gate ──────────────────────────────────────────────
647
+ const hl = checkHumanLock(abs);
648
+ if (!hl.passed) {
649
+ console.error('');
650
+ console.error(' Human Lock Gate: BLOCKED');
651
+ for (const issue of hl.issues) {
652
+ console.error(` ✗ ${issue}`);
653
+ }
654
+ console.error('');
655
+ console.error(' Use kdna publish --check for details, or --force to override.');
656
+ if (!args.includes('--force')) {
657
+ process.exit(EXIT.HUMAN_LOCK_REQUIRED);
658
+ }
659
+ console.warn(' ⚠ --force override: publishing without Human Lock (emergency only)');
660
+ } else {
661
+ console.log(` ✓ Human Lock Gate: passed`);
662
+ }
663
+ console.log('');
664
+
554
665
  console.log(` Identity fingerprint: ${fingerprint(publicKey)}`);
555
666
  console.log(` Scope trust key: ${scopeKey.slice(0, 28)}…`);
556
667
  console.log('');
@@ -633,4 +744,4 @@ function cmdPublish(domainPath, args = []) {
633
744
  );
634
745
  }
635
746
 
636
- module.exports = { cmdPublishCheck, cmdPublish, canonicalPayload, publicKeyToScopeFormat };
747
+ module.exports = { cmdPublishCheck, cmdPublish, checkHumanLock, canonicalPayload, publicKeyToScopeFormat };
package/src/verify.js CHANGED
@@ -21,6 +21,13 @@ const crypto = require('crypto');
21
21
  const { RegistryResolver, parseName } = require('./registry');
22
22
  const { EXIT } = require('./cmds/_common');
23
23
 
24
+ let validateManifestFn;
25
+ try {
26
+ validateManifestFn = require('@aikdna/kdna-core').validateManifest;
27
+ } catch {
28
+ // kdna-core not available — manifest validation skipped
29
+ }
30
+
24
31
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
25
32
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
26
33
 
@@ -54,6 +61,17 @@ function checkStructure(destDir) {
54
61
  }
55
62
  }
56
63
 
64
+ // Validate kdna.json against canonical manifest schema
65
+ if (validateManifestFn) {
66
+ const manifest = readJson(path.join(destDir, 'kdna.json'));
67
+ if (manifest) {
68
+ const mResult = validateManifestFn(manifest);
69
+ for (const e of mResult.errors) issues.push({ severity: 'error', msg: e });
70
+ for (const w of mResult.warnings) issues.push({ severity: 'warn', msg: w });
71
+ if (mResult.errors.length === 0) passed.push('kdna.json conforms to manifest schema v1.0-rc');
72
+ }
73
+ }
74
+
57
75
  for (const f of optional) {
58
76
  if (fs.existsSync(path.join(destDir, f))) passed.push(`has ${f}`);
59
77
  }
@@ -552,6 +570,33 @@ function checkGovernance(destDir) {
552
570
 
553
571
  function cmdVerify(input, args = []) {
554
572
  const jsonMode = args.includes('--json');
573
+ const trustReport = args.includes('--trust-report');
574
+
575
+ // --trust-report: standalone mode — output full trust report and exit
576
+ if (trustReport) {
577
+ const parsed = parseName(input);
578
+ if (!parsed) {
579
+ console.log(JSON.stringify({ ok: false, error: `Invalid name: ${input}` }));
580
+ process.exit(EXIT.INPUT_ERROR);
581
+ }
582
+ const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
583
+ if (!destDir || !require('fs').existsSync(destDir)) {
584
+ console.log(JSON.stringify({ ok: false, error: `Domain not installed: ${input}` }));
585
+ process.exit(EXIT.INPUT_ERROR);
586
+ }
587
+ const { checkTrust: agentCheckTrust } = require('./agent');
588
+ const trust = agentCheckTrust(parsed.full);
589
+ console.log(JSON.stringify({
590
+ domain: parsed.full,
591
+ passed: trust.passed,
592
+ failures: trust.failures,
593
+ warnings: trust.warnings,
594
+ risk_level: trust.riskLevel,
595
+ spec_version: trust.specVersion,
596
+ signature_valid: trust.signatureValid,
597
+ }, null, 2));
598
+ process.exit(trust.passed ? 0 : EXIT.TRUST_FAILED);
599
+ }
555
600
 
556
601
  const want = {
557
602
  structure: args.includes('--structure'),
@@ -10,6 +10,15 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { lintDomain } = require('@aikdna/kdna-core');
12
12
 
13
+ const KDNA_DOMAIN_FILES = new Set([
14
+ 'KDNA_Core.json',
15
+ 'KDNA_Patterns.json',
16
+ 'KDNA_Scenarios.json',
17
+ 'KDNA_Cases.json',
18
+ 'KDNA_Reasoning.json',
19
+ 'KDNA_Evolution.json',
20
+ ]);
21
+
13
22
  const domainDir = process.argv[2];
14
23
  if (!domainDir || domainDir === '--help' || domainDir === '-h') {
15
24
  console.log(`kdna-lint — Structural and content validation for KDNA domains.
@@ -28,19 +37,44 @@ if (!fs.existsSync(domainDir) || !fs.statSync(domainDir).isDirectory()) {
28
37
  process.exit(2);
29
38
  }
30
39
 
31
- // Read all KDNA JSON files in the domain directory
32
- const files = fs.readdirSync(domainDir).filter((f) => f.endsWith('.json') && f !== 'kdna.json');
40
+ // Read only canonical domain content files. Governance metadata such as
41
+ // KDNA_CARD.json is part of the package, but not one of the 6 KDNA JSON files.
42
+ const files = fs.readdirSync(domainDir).filter((f) => KDNA_DOMAIN_FILES.has(f));
33
43
  const dataMap = {};
34
44
  for (const f of files) {
35
45
  try {
36
46
  dataMap[f] = JSON.parse(fs.readFileSync(path.join(domainDir, f), 'utf8'));
37
- } catch (e) {
47
+ } catch {
38
48
  // lintDomain will report missing required files; skip unparseable here
39
49
  }
40
50
  }
41
51
 
42
52
  const result = lintDomain(dataMap);
43
53
 
54
+ // Also validate kdna.json manifest if present and validateManifest is available
55
+ let manifestPath;
56
+ try {
57
+ manifestPath = path.join(domainDir, 'kdna.json');
58
+ if (fs.existsSync(manifestPath)) {
59
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
60
+ let validateManifestFn;
61
+ try {
62
+ validateManifestFn = require('@aikdna/kdna-core').validateManifest;
63
+ } catch {
64
+ // validateManifest not yet available in installed kdna-core — skip manifest check
65
+ }
66
+ if (validateManifestFn) {
67
+ const mResult = validateManifestFn(manifest);
68
+ for (const e of mResult.errors) result.errors.push(`kdna.json: ${e}`);
69
+ for (const w of mResult.warnings) result.warnings.push(`kdna.json: ${w}`);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ if (e.code !== 'ENOENT') {
74
+ result.errors.push(`kdna.json: failed to parse — ${e.message}`);
75
+ }
76
+ }
77
+
44
78
  if (result.warnings.length) {
45
79
  console.log('Warnings:');
46
80
  result.warnings.forEach((w) => console.log(` - ${w}`));
@@ -42,7 +42,8 @@ const FILE_MAP = {
42
42
  'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
43
43
  };
44
44
 
45
- // Read all KDNA JSON files
45
+ // Read only canonical domain content files. Governance metadata such as
46
+ // KDNA_CARD.json is valid package metadata, but not part of the 6-file domain set.
46
47
  const dataMap = {};
47
48
  for (const [file] of Object.entries(FILE_MAP)) {
48
49
  const filePath = path.join(domainDir, file);
@@ -57,7 +58,7 @@ for (const [file] of Object.entries(FILE_MAP)) {
57
58
 
58
59
  // Read schemas
59
60
  const schemaMap = {};
60
- for (const [file, schemaFile] of Object.entries(FILE_MAP)) {
61
+ for (const schemaFile of Object.values(FILE_MAP)) {
61
62
  const schemaPath = path.join(SCHEMA_DIR, schemaFile);
62
63
  if (fs.existsSync(schemaPath)) {
63
64
  try {