@aikdna/kdna-cli 0.9.0 → 0.12.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,6 +1,10 @@
1
1
  # @aikdna/kdna-cli
2
2
 
3
- KDNA CLI create, validate, install, and manage domain cognition packages for AI agents.
3
+ **KDNA CLI is the runtime control plane for loading, validating, composing, testing, and governing domain judgment for AI agents.**
4
+
5
+ KDNA CLI 是 AI Agent 加载、验证、组合、测试和治理领域判断的运行控制平面。
6
+
7
+ CLI 不是 Studio,不是 Chat,不是 Governance Console。它是这些产品共同依赖的底层协议接口。
4
8
 
5
9
  Part of the [KDNA](https://github.com/knowledge-dna/KDNA) ecosystem.
6
10
 
@@ -13,45 +17,119 @@ npm install -g @aikdna/kdna-cli
13
17
  ## Quick Start
14
18
 
15
19
  ```bash
16
- # Validate a domain
17
- kdna validate ./my-domain
20
+ kdna install @aikdna/writing # Install a domain
21
+ kdna verify @aikdna/writing # 3-layer verification
22
+ kdna available # List installed domains
23
+ kdna match "improve this post" # Find relevant domains
24
+ kdna load @aikdna/writing # Load for agent consumption
25
+ ```
18
26
 
19
- # Verify a domain (structure + trust + judgment)
20
- kdna verify @aikdna/writing
27
+ ## Commands by Role
21
28
 
22
- # Install a domain
23
- kdna install @aikdna/writing
29
+ ### Domain Authoring
24
30
 
25
- # Create a new domain
26
- kdna init my-domain
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 unpack <file>` | Unpack .kdna container |
38
+ | `kdna inspect <path>` | Inspect domain or .kdna file |
39
+ | `kdna publish <path>` | Pack + sign + publish to registry |
40
+ | `kdna publish --check <path>` | Quality gate check only |
41
+ | `kdna version bump <level> [path]` | Bump domain version |
42
+
43
+ ### Agent Runtime
27
44
 
28
- # Search the registry
29
- kdna search writing
45
+ | Command | Description |
46
+ |---------|-------------|
47
+ | `kdna available [--json]` | List installed domains with v2.1 fields |
48
+ | `kdna match "<task>" [--json]` | Signal matching — find relevant domains |
49
+ | `kdna load <name> [--as=prompt\|json\|raw]` | Emit domain in agent-ready format |
30
50
 
31
- # Compare with/without KDNA
32
- kdna compare @aikdna/writing --input "..."
33
- ```
51
+ ### Testing & Verification
34
52
 
35
- ## Commands
53
+ | Command | Description |
54
+ |---------|-------------|
55
+ | `kdna verify <name>` | 3-layer verification: structure + trust + judgment |
56
+ | `kdna compare <name> --input "..."` | With/without KDNA reasoning diff |
57
+ | `kdna diff <name>@<v1> <name>@<v2>` | Judgment-level diff between versions |
58
+ | `kdna doctor` | Check runtime environment health |
59
+
60
+ ### Cluster Composition
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `kdna cluster lint <path>` | Validate cluster manifest |
65
+
66
+ ### Registry & Distribution
36
67
 
37
68
  | Command | Description |
38
69
  |---------|-------------|
39
- | `kdna validate <dir>` | Validate domain structure |
40
- | `kdna verify <name>` | Full 3-layer verification (structure/trust/judgment) |
41
- | `kdna install <name>` | Install a domain from the registry |
42
- | `kdna remove <name>` | Remove an installed domain |
43
- | `kdna info <name>` | Show domain information |
44
- | `kdna inspect <dir\|file>` | Inspect a domain or .kdna file |
45
- | `kdna list` | List installed domains |
46
- | `kdna search <keyword>` | Search the registry |
47
- | `kdna init <name>` | Create a new domain from template |
48
- | `kdna pack <dir>` | Pack a domain into .kdna file |
49
- | `kdna unpack <file>` | Unpack a .kdna file |
50
- | `kdna publish <dir>` | Publish a domain to the registry |
51
- | `kdna compare <name>` | Compare AI output with/without KDNA |
52
- | `kdna diff <name@v1> <name@v2>` | Diff two domain versions |
53
- | `kdna project init` | Initialize .kdna/config.json for a project |
54
- | `kdna identity init` | Create a signing identity |
70
+ | `kdna install <name>` | Install domain from registry |
71
+ | `kdna remove <name>` | Uninstall a domain |
72
+ | `kdna update <name>` | Update installed domain |
73
+ | `kdna info <name>` | Show domain metadata and trust status |
74
+ | `kdna list [--available]` | List installed or available domains |
75
+ | `kdna search <keyword>` | Search registry |
76
+ | `kdna registry refresh` | Refresh registry cache |
77
+
78
+ ### Identity & Signing
79
+
80
+ | Command | Description |
81
+ |---------|-------------|
82
+ | `kdna identity init` | Generate Ed25519 signing key |
83
+ | `kdna identity show` | Display public key and buyer ID |
84
+ | `kdna identity export [--out]` | Backup private key (encrypted) |
85
+ | `kdna identity import <file>` | Restore identity from backup |
86
+
87
+ ### Setup
88
+
89
+ | Command | Description |
90
+ |---------|-------------|
91
+ | `kdna setup` | One-command setup: CLI + skill + data root |
92
+
93
+ ## Exit Codes
94
+
95
+ | Code | Name | Meaning |
96
+ |------|------|---------|
97
+ | 0 | `OK` | Success |
98
+ | 1 | `VALIDATION_FAILED` | Structure or schema validation failed |
99
+ | 2 | `INPUT_ERROR` | Invalid input, missing argument, not found |
100
+ | 3 | `TRUST_FAILED` | Signature or trust verification failed |
101
+ | 4 | `JUDGMENT_QUALITY_FAILED` | Judgment governance fields missing or insufficient |
102
+ | 5 | `REGISTRY_ERROR` | Registry lookup or network error |
103
+ | 6 | `PROVIDER_ERROR` | LLM provider (API key, rate limit) error |
104
+ | 7 | `POLICY_VIOLATION` | Publishing or governance policy violation |
105
+ | 8 | `HUMAN_LOCK_REQUIRED` | Human lock required but not present |
106
+
107
+ ## JSON Output
108
+
109
+ Machine-consumable commands support `--json` for structured output:
110
+
111
+ ```bash
112
+ kdna verify @aikdna/writing --json
113
+ kdna available --json
114
+ kdna match "help me write" --json
115
+ kdna search writing --json
116
+ kdna info @aikdna/writing --json
117
+ kdna doctor --json
118
+ ```
119
+
120
+ ## Product Matrix
121
+
122
+ | Layer | Product | Responsibility |
123
+ |-------|---------|---------------|
124
+ | Protocol | KDNA SPEC | Define judgment asset format |
125
+ | Core Library | @aikdna/kdna-core | load / validate / compose / render |
126
+ | Runtime | @aikdna/kdna-cli | Agent runtime + compile + verify + test + publish |
127
+ | Authoring | KDNA Studio | Human-led judgment production |
128
+ | Consumption | KDNAChat | Load, use, compare |
129
+ | Governance | KDNA Governance Console | Approve, release, audit |
130
+ | Distribution | Registry | Discover, install, trade |
131
+
132
+ CLI 不应该成为一个"命令行 Studio",而是所有 KDNA 产品共同依赖的协议控制平面。
55
133
 
56
134
  ## Development
57
135
 
package/SECURITY.md ADDED
@@ -0,0 +1,41 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 0.11.x | :white_check_mark: |
8
+ | 0.10.x | :white_check_mark: |
9
+ | < 0.10 | :x: |
10
+
11
+ ## Reporting a Vulnerability
12
+
13
+ KDNA CLI is the runtime control plane for domain judgment. The primary
14
+ security surface is signature verification, identity key management,
15
+ and registry trust.
16
+
17
+ If you discover a security vulnerability:
18
+
19
+ 1. **Do not** open a public issue.
20
+ 2. Report by email to security@aikdna.com or via GitHub private vulnerability reporting.
21
+ 3. Include: affected version, steps to reproduce, potential impact.
22
+
23
+ We will acknowledge within 5 business days and provide a timeline for a fix.
24
+
25
+ ## Scope
26
+
27
+ - `kdna verify --trust`: Ed25519 signature verification
28
+ - `kdna identity init/export/import`: key generation and backup encryption
29
+ - `kdna install`: registry trust chain and SHA-256 verification
30
+ - `kdna publish`: signing and key material handling
31
+
32
+ ## Out of Scope
33
+
34
+ - Domain content files (KDNA_*.json) — these are user-authored judgment assets
35
+ - Network-level attacks (man-in-the-middle on registry fetch) — use HTTPS
36
+ - Local filesystem access — CLI runs with user privileges
37
+
38
+ ## Supply Chain
39
+
40
+ KDNA CLI publishes to npm as `@aikdna/kdna-cli`. Builds are reproducible
41
+ from source. Dependencies are pinned in `package-lock.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-cli",
3
- "version": "0.9.0",
3
+ "version": "0.12.0",
4
4
  "description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "templates/",
15
15
  "skills/",
16
16
  "LICENSE",
17
- "NOTICE"
17
+ "NOTICE",
18
+ "SECURITY.md"
18
19
  ],
19
20
  "scripts": {
20
21
  "lint": "eslint src/ validators/ tests/",
package/src/agent.js CHANGED
@@ -32,6 +32,7 @@
32
32
  const fs = require('fs');
33
33
  const path = require('path');
34
34
  const { parseName } = require('./registry');
35
+ const { recordTrace } = require('./cmds/trace');
35
36
 
36
37
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
37
38
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
@@ -327,6 +328,22 @@ function cmdLoad(input, args = []) {
327
328
  format = eq > 0 ? args[formatIdx].slice(eq + 1) : args[formatIdx + 1];
328
329
  }
329
330
 
331
+ // --profile=<name> for load profiles (Phase 2)
332
+ const profileIdx = args.findIndex((a) => a.startsWith('--profile'));
333
+ let profile = null;
334
+ let profileInput = null;
335
+ if (profileIdx >= 0) {
336
+ const eq = args[profileIdx].indexOf('=');
337
+ const raw = eq > 0 ? args[profileIdx].slice(eq + 1) : args[profileIdx + 1];
338
+ profile = raw || 'compact';
339
+ }
340
+
341
+ // --input for scenario profile
342
+ const inputIdx = args.indexOf('--input');
343
+ if (inputIdx >= 0) {
344
+ profileInput = args[inputIdx + 1] || null;
345
+ }
346
+
330
347
  const parsed = parseName(input);
331
348
  if (!parsed) {
332
349
  console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
@@ -347,11 +364,14 @@ function cmdLoad(input, args = []) {
347
364
  const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
348
365
  const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
349
366
 
367
+ // JSON format
350
368
  if (format === 'json') {
351
369
  process.stdout.write(JSON.stringify({ manifest, core, patterns: pat }, null, 2) + '\n');
370
+ recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'json' });
352
371
  return;
353
372
  }
354
373
 
374
+ // Raw format
355
375
  if (format === 'raw') {
356
376
  for (const f of ['KDNA_Core.json', 'KDNA_Patterns.json']) {
357
377
  const p = path.join(dir, f);
@@ -360,11 +380,148 @@ function cmdLoad(input, args = []) {
360
380
  process.stdout.write(fs.readFileSync(p, 'utf8'));
361
381
  }
362
382
  }
383
+ recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'raw' });
384
+ return;
385
+ }
386
+
387
+ // Load profiles
388
+ if (profile) {
389
+ emitProfile(parsed, manifest, core, pat, profile, profileInput);
390
+ recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: `profile:${profile}` });
363
391
  return;
364
392
  }
365
393
 
366
394
  // Default: --as=prompt — compact text optimized for system-prompt injection.
367
- // Goal: minimum token cost while preserving all judgment surface.
395
+ emitCompact(parsed, manifest, core, pat);
396
+ recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'prompt' });
397
+ }
398
+
399
+ // ─── Load profiles ─────────────────────────────────────────────────────
400
+
401
+ function emitProfile(parsed, manifest, core, pat, profile, input) {
402
+ const lines = [];
403
+ lines.push(`# KDNA loaded: ${manifest.name || parsed.full}`);
404
+ if (manifest.judgment_version) lines.push(`# judgment_version: ${manifest.judgment_version}`);
405
+ lines.push('');
406
+
407
+ const axioms = core.axioms || [];
408
+
409
+ switch (profile) {
410
+ case 'index':
411
+ // Minimal: name + axioms list + applies_when only
412
+ if (manifest.core_insight) lines.push(`# insight: ${manifest.core_insight}`);
413
+ lines.push('');
414
+ if (axioms.length) {
415
+ lines.push('## Axiom index');
416
+ for (const a of axioms) {
417
+ lines.push(`- ${a.one_sentence}`);
418
+ if (a.applies_when?.length) lines.push(` APPLIES: ${a.applies_when.join('; ')}`);
419
+ if (a.does_not_apply_when?.length) lines.push(` NOT: ${a.does_not_apply_when.join('; ')}`);
420
+ }
421
+ lines.push('');
422
+ }
423
+ break;
424
+
425
+ case 'scenario':
426
+ // Scenario-aware: include axioms whose applies_when matches the input
427
+ lines.push(`# Scenario input: ${(input || '').slice(0, 200)}`);
428
+ lines.push('');
429
+ if (axioms.length) {
430
+ const taskTokens = tokenize(input || '');
431
+ const relevant = axioms.filter((a) => {
432
+ if (!a.applies_when?.length) return false;
433
+ const combinedText = [...a.applies_when, a.one_sentence || '', a.full_statement || ''].join(' ');
434
+ const score = overlapScore(taskTokens, combinedText);
435
+ return score.hits >= 1 || score.coverage >= 0.1;
436
+ });
437
+ const selected = relevant.length > 0 ? relevant : axioms;
438
+ lines.push(`## Axioms (${selected.length}/${axioms.length} relevant)`);
439
+ for (const a of selected) {
440
+ lines.push(`- ${a.one_sentence}`);
441
+ if (a.applies_when?.length) {
442
+ lines.push(` APPLIES WHEN: ${a.applies_when.join('; ')}`);
443
+ }
444
+ if (a.failure_risk) lines.push(` RISK IF MISAPPLIED: ${a.failure_risk}`);
445
+ }
446
+ lines.push('');
447
+ }
448
+ break;
449
+
450
+ case 'full':
451
+ // Full: all axiom details including full_statement + why
452
+ if (axioms.length) {
453
+ lines.push('## Axioms (full)');
454
+ for (const a of axioms) {
455
+ lines.push(`### ${a.one_sentence}`);
456
+ if (a.full_statement) lines.push(`${a.full_statement}`);
457
+ if (a.why) lines.push(`Why: ${a.why}`);
458
+ if (a.applies_when?.length) {
459
+ lines.push(`Applies when: ${a.applies_when.join('; ')}`);
460
+ }
461
+ if (a.does_not_apply_when?.length) {
462
+ lines.push(`Does not apply when: ${a.does_not_apply_when.join('; ')}`);
463
+ }
464
+ if (a.failure_risk) lines.push(`Failure risk: ${a.failure_risk}`);
465
+ lines.push('');
466
+ }
467
+ }
468
+ break;
469
+
470
+ case 'compact':
471
+ default:
472
+ emitCompact(parsed, manifest, core, pat);
473
+ return;
474
+ }
475
+
476
+ // Add stances, misunderstandings, self-checks for all non-index profiles
477
+ if (profile !== 'index') {
478
+ if (core.stances?.length) {
479
+ lines.push('## Stances');
480
+ for (const s of core.stances) {
481
+ const text = typeof s === 'string' ? s : s.stance;
482
+ if (text) lines.push(`- ${text}`);
483
+ }
484
+ lines.push('');
485
+ }
486
+
487
+ if (pat.terminology?.banned_terms?.length) {
488
+ lines.push('## Banned terms');
489
+ for (const t of pat.terminology.banned_terms) {
490
+ const term = typeof t === 'string' ? t : t.term;
491
+ const replace = typeof t === 'object' ? t.replace_with : null;
492
+ lines.push(`- "${term}"${replace ? ` → use: ${replace}` : ''}`);
493
+ }
494
+ lines.push('');
495
+ }
496
+
497
+ if (pat.misunderstandings?.length) {
498
+ lines.push('## Misunderstandings to avoid');
499
+ for (const m of pat.misunderstandings) {
500
+ lines.push(`- WRONG: ${m.wrong}`);
501
+ lines.push(` CORRECT: ${m.correct}`);
502
+ if (m.failure_risk) lines.push(` RISK: ${m.failure_risk}`);
503
+ }
504
+ lines.push('');
505
+ }
506
+
507
+ if (pat.self_check?.length) {
508
+ lines.push('## Self-checks');
509
+ for (const q of pat.self_check) {
510
+ const text = typeof q === 'string' ? q : q.question;
511
+ if (text) lines.push(`- ${text}`);
512
+ }
513
+ lines.push('');
514
+ }
515
+ }
516
+
517
+ lines.push('---');
518
+ lines.push('Apply silently. Do not quote KDNA to the user.');
519
+ lines.push('User intent + evidence always override KDNA axioms.');
520
+
521
+ process.stdout.write(lines.join('\n') + '\n');
522
+ }
523
+
524
+ function emitCompact(parsed, manifest, core, pat) {
368
525
  const lines = [];
369
526
  lines.push(`# KDNA loaded: ${manifest.name || parsed.full}`);
370
527
  if (manifest.judgment_version) lines.push(`# judgment_version: ${manifest.judgment_version}`);
@@ -431,4 +588,275 @@ function cmdLoad(input, args = []) {
431
588
  process.stdout.write(lines.join('\n') + '\n');
432
589
  }
433
590
 
434
- module.exports = { cmdAvailable, cmdMatch, cmdLoad };
591
+ // ─── kdna select ───────────────────────────────────────────────────────
592
+
593
+ function cmdSelect(args = []) {
594
+ const wantJson = args.includes('--json');
595
+ const inputIdx = args.indexOf('--input');
596
+ const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
597
+ const maxIdx = args.indexOf('--max-domains');
598
+ const maxDomains = maxIdx >= 0 ? parseInt(args[maxIdx + 1], 10) || 3 : 3;
599
+
600
+ if (!input) {
601
+ if (wantJson) {
602
+ console.log(JSON.stringify({ error: 'Usage: kdna select --input "<task>" [--max-domains=N] [--json]' }));
603
+ process.exit(2);
604
+ }
605
+ console.error('Usage: kdna select --input "<task>" [--max-domains=N] [--json]');
606
+ process.exit(2);
607
+ }
608
+
609
+ const taskTokens = tokenize(input);
610
+ const installed = listInstalled();
611
+ const scores = [];
612
+
613
+ for (const e of installed) {
614
+ const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
615
+ if (manifest.yanked === true) continue;
616
+ const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
617
+
618
+ // Check does_not_apply_when hard exclusion
619
+ let excluded = false;
620
+ for (const a of core.axioms || []) {
621
+ for (const d of a.does_not_apply_when || []) {
622
+ if (overlapScore(taskTokens, d).hits >= 2) {
623
+ excluded = true;
624
+ break;
625
+ }
626
+ }
627
+ if (excluded) break;
628
+ }
629
+ if (excluded) continue;
630
+
631
+ // Score: applies_when matches + domain relevance
632
+ let score = 0;
633
+ const reasons = [];
634
+
635
+ for (const a of core.axioms || []) {
636
+ for (const ap of a.applies_when || []) {
637
+ const s = overlapScore(taskTokens, ap);
638
+ if (s.hits >= 2) {
639
+ score += s.hits * 3;
640
+ reasons.push({ source: `${a.id}.applies_when`, hits: s.hits, text: ap.slice(0, 120) });
641
+ }
642
+ }
643
+ }
644
+
645
+ score += domainRelevanceScore(taskTokens, manifest);
646
+
647
+ if (score > 0) {
648
+ scores.push({
649
+ domain: manifest.name || e.full,
650
+ version: manifest.version || null,
651
+ status: manifest.status || 'experimental',
652
+ score,
653
+ reasons: reasons.slice(0, 5),
654
+ });
655
+ }
656
+ }
657
+
658
+ // Sort descending, take top N
659
+ scores.sort((a, b) => b.score - a.score);
660
+ const selected = scores.slice(0, maxDomains);
661
+
662
+ if (wantJson) {
663
+ console.log(JSON.stringify({
664
+ input: input.slice(0, 200),
665
+ selected,
666
+ max_domains: maxDomains,
667
+ total_candidates: scores.length,
668
+ }, null, 2));
669
+ return;
670
+ }
671
+
672
+ if (!selected.length) {
673
+ console.log(`No domains selected for input: "${input.slice(0, 100)}"`);
674
+ console.log('Run: kdna match "<task>" for candidate hints');
675
+ console.log('Run: kdna list --available to see all registered domains');
676
+ return;
677
+ }
678
+
679
+ console.log(`Selected ${selected.length} domain(s) for: "${input.slice(0, 100)}"`);
680
+ console.log('');
681
+ for (const s of selected) {
682
+ console.log(` ${s.domain.padEnd(36)} score:${s.score} v${s.version || '?'} [${s.status}]`);
683
+ for (const r of s.reasons) {
684
+ console.log(` ↳ ${r.source} (${r.hits} hits): ${r.text}`);
685
+ }
686
+ }
687
+ }
688
+
689
+ // ─── kdna postvalidate ─────────────────────────────────────────────────
690
+
691
+ function cmdPostvalidate(args = []) {
692
+ const wantJson = args.includes('--json');
693
+ const positional = args.filter((a) => !a.startsWith('--'));
694
+ const input = positional[1];
695
+ const outputIdx = args.indexOf('--output');
696
+ const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : null;
697
+
698
+ if (!input) {
699
+ console.error('Usage: kdna postvalidate <domain> --output <response-file> [--json]');
700
+ process.exit(2);
701
+ }
702
+
703
+ const parsed = parseName(input);
704
+ if (!parsed) {
705
+ console.error(`Invalid name "${input}".`);
706
+ process.exit(2);
707
+ }
708
+ const dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
709
+ if (!fs.existsSync(dir)) {
710
+ console.error(`${parsed.full} is not installed.`);
711
+ process.exit(2);
712
+ }
713
+
714
+ const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
715
+ const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
716
+
717
+ // Read agent output
718
+ let agentOutput = '';
719
+ if (outputFile) {
720
+ try {
721
+ agentOutput = fs.readFileSync(outputFile, 'utf8');
722
+ } catch {
723
+ console.error(`Cannot read output file: ${outputFile}`);
724
+ process.exit(2);
725
+ }
726
+ } else {
727
+ // Read from stdin
728
+ try {
729
+ agentOutput = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
730
+ } catch {
731
+ // ignore
732
+ }
733
+ }
734
+
735
+ const results = {
736
+ violations: [],
737
+ warnings: [],
738
+ passed: [],
739
+ };
740
+
741
+ // Check banned terms
742
+ const bannedTerms = (pat.terminology?.banned_terms || []).map((t) =>
743
+ typeof t === 'string' ? t : t.term,
744
+ );
745
+ for (const term of bannedTerms) {
746
+ const regex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
747
+ if (regex.test(agentOutput)) {
748
+ results.violations.push(`banned term used: "${term}"`);
749
+ } else {
750
+ results.passed.push(`banned term avoided: "${term}"`);
751
+ }
752
+ }
753
+
754
+ // Check misunderstandings (wrong patterns)
755
+ const misunderstandings = pat.misunderstandings || [];
756
+ for (const ms of misunderstandings) {
757
+ const wrongTokens = tokenize(ms.wrong || '');
758
+ const agentTokens = tokenize(agentOutput);
759
+ const overlap = wrongTokens.filter((t) => agentTokens.includes(t));
760
+ if (overlap.length >= 3) {
761
+ results.warnings.push(`possible misunderstanding: "${ms.wrong?.slice(0, 80)}"`);
762
+ } else {
763
+ results.passed.push(`misunderstanding avoided: "${(ms.wrong || '').slice(0, 60)}"`);
764
+ }
765
+ }
766
+
767
+ // Check self-checks absence (can't verify answers, but flag missing checks)
768
+ const selfChecks = pat.self_check || [];
769
+ if (selfChecks.length > 0) {
770
+ let foundChecks = 0;
771
+ for (const sc of selfChecks) {
772
+ const text = typeof sc === 'string' ? sc : sc.question;
773
+ if (text) {
774
+ const keywords = tokenize(text).filter((t) => t.length > 3).slice(0, 3);
775
+ const found = keywords.some((k) => agentOutput.toLowerCase().includes(k));
776
+ if (found) foundChecks++;
777
+ }
778
+ }
779
+ if (foundChecks === 0) {
780
+ results.warnings.push('no self-check traces found in output');
781
+ } else {
782
+ results.passed.push(`self-check traces: ${foundChecks}/${selfChecks.length}`);
783
+ }
784
+ }
785
+
786
+ // Check boundary violations
787
+ const boundaries = core.axioms || [];
788
+ let boundaryViolations = 0;
789
+ for (const ax of boundaries) {
790
+ for (const notApply of ax.does_not_apply_when || []) {
791
+ if (overlapScore(tokenize(agentOutput), notApply).hits >= 2) {
792
+ boundaryViolations++;
793
+ results.violations.push(`boundary violation: ${ax.id} (should not apply when "${notApply.slice(0, 80)}")`);
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ if (boundaryViolations === 0) {
799
+ results.passed.push('no boundary violations detected');
800
+ }
801
+
802
+ // Risk flags
803
+ for (const ax of core.axioms || []) {
804
+ if (ax.failure_risk) {
805
+ const riskTokens = tokenize(ax.failure_risk);
806
+ const match = riskTokens.filter((t) => tokenize(agentOutput).includes(t)).length;
807
+ if (match >= riskTokens.length * 0.5) {
808
+ results.warnings.push(`failure risk matched: ${ax.id} — "${ax.failure_risk.slice(0, 80)}"`);
809
+ }
810
+ }
811
+ }
812
+
813
+ if (wantJson) {
814
+ const result = {
815
+ domain: parsed.full,
816
+ violations: results.violations.length,
817
+ warnings: results.warnings.length,
818
+ passed: results.passed.length,
819
+ details: results,
820
+ };
821
+ console.log(JSON.stringify(result, null, 2));
822
+ recordTrace({
823
+ timestamp: new Date().toISOString(),
824
+ agent: 'cli',
825
+ domain: parsed.full,
826
+ type: 'postvalidate',
827
+ postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
828
+ });
829
+ process.exit(results.violations.length ? 1 : 0);
830
+ }
831
+
832
+ console.log(`Post-validation: ${parsed.full}`);
833
+ console.log('');
834
+ console.log(` Violations: ${results.violations.length} Warnings: ${results.warnings.length} Passed: ${results.passed.length}`);
835
+ console.log('');
836
+
837
+ if (results.violations.length) {
838
+ console.log('Violations:');
839
+ results.violations.forEach((v) => console.log(` ✗ ${v}`));
840
+ console.log('');
841
+ }
842
+ if (results.warnings.length) {
843
+ console.log('Warnings:');
844
+ results.warnings.forEach((w) => console.log(` ⚠ ${w}`));
845
+ console.log('');
846
+ }
847
+ if (results.passed.length) {
848
+ console.log('Passed:');
849
+ results.passed.forEach((p) => console.log(` ✓ ${p}`));
850
+ }
851
+
852
+ recordTrace({
853
+ timestamp: new Date().toISOString(),
854
+ agent: 'cli',
855
+ domain: parsed.full,
856
+ type: 'postvalidate',
857
+ postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
858
+ });
859
+ process.exit(results.violations.length ? 1 : 0);
860
+ }
861
+
862
+ module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate };