tribunal-kit 5.8.1 → 5.8.2

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.
@@ -487,50 +487,209 @@ async function callLlmApi(prompt, provider, apiKey) {
487
487
  return null;
488
488
  }
489
489
 
490
+ // ── Commands ──────────────────────────────────────────────────────────────────
491
+ // ── Strategy Filtering ────────────────────────────────────────────────────────
492
+ function applyStrategyFilter(signals, strategy) {
493
+ if (strategy === "repair-only") {
494
+ return signals.filter((s) => s.type === "log_error");
495
+ }
496
+ return signals;
497
+ }
498
+
499
+ // ── Log Reflection Prompt ─────────────────────────────────────────────────────
500
+ function generateLogReflectionPrompt(delta) {
501
+ return `You are analyzing runtime log signals (errors, tracebacks, and warnings) from an AI agent's execution.
502
+ Your job is to identify systemic coding issues and extract evolution assets:
503
+ 1. **Idioms (Genes)**: Coding rules/conventions to prevent the AI from generating this mistake again.
504
+ 2. **Cases (Capsules)**: Rejection precedents (offending code pattern and why it failed) to block reviewers from approving it.
505
+
506
+ Rules:
507
+ - Return ONLY a YAML structure containing 'idioms:' and 'cases:'. No markdown boxes, no conversational prose.
508
+ - Maximum 3 idioms and 3 cases.
509
+ - If an asset category is not applicable, leave it empty (e.g., 'idioms: []').
510
+
511
+ Log Signals:
512
+ \`\`\`
513
+ ${delta.slice(0, 1800)}
514
+ \`\`\`
515
+
516
+ Output format (YAML only):
517
+ idioms:
518
+ - pattern: "<code convention or rule to avoid this error>"
519
+ reason: "<why it is needed based on the error>"
520
+ domain: "<backend|frontend|database|security|performance|general>"
521
+ cases:
522
+ - pattern: "<offending code snippet or error-triggering pattern>"
523
+ reason: "<specific runtime error / traceback reason>"
524
+ domain: "<backend|frontend|database|security|performance|general>"
525
+ verdict: "REJECTED"
526
+ `;
527
+ }
528
+
529
+ // ── Combined YAML Parser ──────────────────────────────────────────────────────
530
+ function parseEvolutionYaml(response) {
531
+ const idioms = [];
532
+ const cases = [];
533
+ let currentSection = null; // 'idioms' | 'cases'
534
+ let current = null;
535
+
536
+ const lines = response.split("\n");
537
+ for (const line of lines) {
538
+ const trimmed = line.trim();
539
+ if (trimmed === "idioms:") {
540
+ if (current) {
541
+ if (currentSection === "idioms") idioms.push(current);
542
+ if (currentSection === "cases") cases.push(current);
543
+ }
544
+ currentSection = "idioms";
545
+ current = null;
546
+ continue;
547
+ }
548
+ if (trimmed === "cases:") {
549
+ if (current) {
550
+ if (currentSection === "idioms") idioms.push(current);
551
+ if (currentSection === "cases") cases.push(current);
552
+ }
553
+ currentSection = "cases";
554
+ current = null;
555
+ continue;
556
+ }
557
+
558
+ if (trimmed.startsWith("- pattern:") || (trimmed.startsWith("- ") && trimmed.includes("pattern:"))) {
559
+ if (current) {
560
+ if (currentSection === "idioms") idioms.push(current);
561
+ if (currentSection === "cases") cases.push(current);
562
+ }
563
+ let pat = "";
564
+ if (trimmed.startsWith("- pattern:")) {
565
+ pat = trimmed.substring("- pattern:".length).trim();
566
+ } else {
567
+ pat = trimmed.split("pattern:", 2)[1].trim();
568
+ }
569
+ current = { pattern: pat.replace(/^['"]|['"]$/g, "") };
570
+ } else if (trimmed.startsWith("reason:") && current) {
571
+ current.reason = trimmed.substring("reason:".length).trim().replace(/^['"]|['"]$/g, "");
572
+ } else if (trimmed.startsWith("domain:") && current) {
573
+ current.domain = trimmed.substring("domain:".length).trim().replace(/^['"]|['"]$/g, "");
574
+ } else if (trimmed.startsWith("verdict:") && current) {
575
+ current.verdict = trimmed.substring("verdict:".length).trim().replace(/^['"]|['"]$/g, "");
576
+ }
577
+ }
578
+
579
+ if (current) {
580
+ if (currentSection === "idioms") idioms.push(current);
581
+ if (currentSection === "cases") cases.push(current);
582
+ }
583
+
584
+ return { idioms, cases };
585
+ }
586
+
490
587
  // ── Commands ──────────────────────────────────────────────────────────────────
491
588
  async function cmdDigest(args) {
492
589
  const dryRun = args.includes("--dry-run");
493
590
  const diffMode = args.includes("--head") ? "head" : "staged";
494
591
 
592
+ const logArg = args.find((a) => a.startsWith("--log="));
593
+ const logFile = logArg ? logArg.split("=").slice(1).join("=") : null;
594
+
595
+ const strategyArg = args.find((a) => a.startsWith("--strategy="));
596
+ const strategy = strategyArg ? strategyArg.split("=").slice(1).join("=") : "balanced";
597
+
495
598
  console.log(
496
599
  `\n${BOLD}${CYAN}━━━ Skill Evolution — Digest Cycle ━━━━━━━━━━━━━━━━${RESET}`,
497
600
  );
498
601
  if (dryRun)
499
602
  console.log(` ${YELLOW}DRY RUN — no files will be written${RESET}\n`);
500
603
 
501
- console.log(` ${DIM}[1/5] Fetching git diff (${diffMode})...${RESET}`);
502
- const rawDiff = getGitDiff(diffMode);
503
- if (!rawDiff.trim()) {
604
+ let rawDiff = "";
605
+ let delta = "";
606
+ let rawTokens = 0;
607
+ let deltaTokens = 0;
608
+ let logSignals = [];
609
+ const isLogMode = !!logFile;
610
+
611
+ if (isLogMode) {
612
+ console.log(` ${DIM}[1/5] Reading log file: ${logFile}...${RESET}`);
613
+ if (!fs.existsSync(logFile)) {
614
+ console.log(` ${RED}✖ Log file not found: ${logFile}${RESET}\n`);
615
+ return;
616
+ }
617
+ const logText = fs.readFileSync(logFile, "utf8");
618
+ rawTokens = countTokensEstimate(logText);
619
+
504
620
  console.log(
505
- ` ${YELLOW} No diff found. Commit or stage changes first.${RESET}`,
621
+ ` ${DIM}[2/5] Extracting signals from log (Signal Detector)...${RESET}`,
506
622
  );
623
+ const { detectSignals } = require("./signal_detector");
624
+ const signals = detectSignals(logText);
625
+
626
+ if (signals.length === 0) {
627
+ console.log(
628
+ ` ${GREEN}✔ No errors or performance signals found in logs.${RESET}`,
629
+ );
630
+ console.log(` ${DIM} No self-evolution reflection needed.${RESET}\n`);
631
+ return;
632
+ }
633
+
634
+ logSignals = applyStrategyFilter(signals, strategy);
635
+ if (logSignals.length === 0) {
636
+ console.log(
637
+ ` ${YELLOW}⚠ Strategy filter [${strategy}] filtered out all signals.${RESET}\n`,
638
+ );
639
+ return;
640
+ }
641
+
507
642
  console.log(
508
- ` ${DIM}Tip: Use --head to diff against the last commit.${RESET}\n`,
643
+ ` ${GREEN} Filtered to ${logSignals.length} signal(s) using [${strategy}] strategy.${RESET}`,
509
644
  );
510
- return;
511
- }
512
645
 
513
- const rawTokens = countTokensEstimate(rawDiff);
514
- console.log(
515
- ` ${DIM} Raw diff: ~${rawTokens} tokens (${rawDiff.length} chars)${RESET}`,
516
- );
646
+ delta = logSignals
647
+ .map((s) => {
648
+ let chunk = `--- SIGNAL: ${s.type} ---\n`;
649
+ if (s.file) chunk += `File: ${s.file}${s.line ? `:${s.line}` : ""}\n`;
650
+ chunk += `Message: ${s.message}\n`;
651
+ chunk += `Context:\n${s.context}\n`;
652
+ return chunk;
653
+ })
654
+ .join("\n\n");
655
+
656
+ deltaTokens = countTokensEstimate(delta);
657
+ } else {
658
+ console.log(` ${DIM}[1/5] Fetching git diff (${diffMode})...${RESET}`);
659
+ rawDiff = getGitDiff(diffMode);
660
+ if (!rawDiff.trim()) {
661
+ console.log(
662
+ ` ${YELLOW}⚠ No diff found. Commit or stage changes first.${RESET}`,
663
+ );
664
+ console.log(
665
+ ` ${DIM}Tip: Use --head to diff against the last commit.${RESET}\n`,
666
+ );
667
+ return;
668
+ }
517
669
 
518
- console.log(
519
- ` ${DIM}[2/5] Extracting architectural delta (Semantic Filter)...${RESET}`,
520
- );
521
- const delta = semanticDelta(rawDiff, 2);
522
- if (!delta.trim()) {
670
+ rawTokens = countTokensEstimate(rawDiff);
523
671
  console.log(
524
- ` ${GREEN} Delta is 100% trivial (whitespace/comments/imports only).${RESET}`,
672
+ ` ${DIM} Raw diff: ~${rawTokens} tokens (${rawDiff.length} chars)${RESET}`,
525
673
  );
674
+
526
675
  console.log(
527
- ` ${DIM} No LLM call needed. Zero tokens consumed.${RESET}\n`,
676
+ ` ${DIM}[2/5] Extracting architectural delta (Semantic Filter)...${RESET}`,
528
677
  );
529
- return;
678
+ delta = semanticDelta(rawDiff, 2);
679
+ if (!delta.trim()) {
680
+ console.log(
681
+ ` ${GREEN}✔ Delta is 100% trivial (whitespace/comments/imports only).${RESET}`,
682
+ );
683
+ console.log(
684
+ ` ${DIM} No LLM call needed. Zero tokens consumed.${RESET}\n`,
685
+ );
686
+ return;
687
+ }
688
+
689
+ deltaTokens = countTokensEstimate(delta);
530
690
  }
531
691
 
532
- const deltaTokens = countTokensEstimate(delta);
533
- const savedTokens = rawTokens - deltaTokens;
692
+ const savedTokens = Math.max(0, rawTokens - deltaTokens);
534
693
  const savedPct = Math.floor((savedTokens / Math.max(rawTokens, 1)) * 100);
535
694
  console.log(
536
695
  ` ${GREEN}✔ Filtered to ~${deltaTokens} tokens (${savedPct}% reduction, saved ~${savedTokens} tokens)${RESET}`,
@@ -546,7 +705,7 @@ async function cmdDigest(args) {
546
705
  }
547
706
  if (delta.split("\n").length > 20)
548
707
  console.log(
549
- ` ${DIM}... (${delta.split("\n").length - 20} more lines)${RESET}`,
708
+ ` ... (${delta.split("\n").length - 20} more lines)`,
550
709
  );
551
710
 
552
711
  if (dryRun) {
@@ -559,8 +718,10 @@ async function cmdDigest(args) {
559
718
  return;
560
719
  }
561
720
 
562
- // GENERATE: Auto-LLM call. Tries API first, falls back to manual paste if no key.
563
- const reflectionPrompt = generateReflectionPrompt(delta);
721
+ // GENERATE: Auto-LLM call.
722
+ const reflectionPrompt = isLogMode
723
+ ? generateLogReflectionPrompt(delta)
724
+ : generateReflectionPrompt(delta);
564
725
  let llmResponse = "";
565
726
 
566
727
  let llmCreds = detectLlmProvider();
@@ -582,12 +743,12 @@ async function cmdDigest(args) {
582
743
  console.log(
583
744
  ` ${YELLOW}⚠ API call failed — falling back to manual mode${RESET}`,
584
745
  );
585
- llmCreds = null; // triggers manual fallback below
746
+ llmCreds = null;
586
747
  }
587
748
  }
588
749
 
589
750
  if (!llmCreds || !llmResponse) {
590
- // Manual fallback: copy-paste mode (no API key configured)
751
+ // Manual fallback: copy-paste mode
591
752
  console.log(
592
753
  `\n ${DIM}[3/5] LLM Reflection — copy the prompt below and paste the response${RESET}`,
593
754
  );
@@ -619,21 +780,42 @@ async function cmdDigest(args) {
619
780
  llmResponse = responseLines.join("\n");
620
781
  }
621
782
 
622
- console.log(`\n ${DIM}[4/5] Parsing idioms...${RESET}`);
623
- const newIdioms = parseLlmYamlResponse(llmResponse);
624
- if (!newIdioms.length) {
625
- console.log(` ${YELLOW}⚠ No idioms extracted from LLM response.${RESET}`);
783
+ console.log(`\n ${DIM}[4/5] Parsing idioms/cases...${RESET}`);
784
+ let newIdioms = [];
785
+ let newCases = [];
786
+
787
+ if (isLogMode) {
788
+ const parsed = parseEvolutionYaml(llmResponse);
789
+ newIdioms = parsed.idioms;
790
+ newCases = parsed.cases;
791
+ } else {
792
+ newIdioms = parseLlmYamlResponse(llmResponse);
793
+ }
794
+
795
+ if (!newIdioms.length && !newCases.length) {
626
796
  console.log(
627
- ` ${DIM} The LLM may have returned idioms: [] no architectural pattern detected.${RESET}\n`,
797
+ ` ${YELLOW} No idioms or cases extracted from LLM response.${RESET}`,
628
798
  );
799
+ console.log(`\n`);
629
800
  return;
630
801
  }
631
802
 
632
- console.log(` ${GREEN}✔ Extracted ${newIdioms.length} idiom(s)${RESET}`);
633
- for (const idiom of newIdioms) {
634
- console.log(
635
- ` ${CYAN}• ${idiom.pattern || "?"}${RESET} — ${idiom.reason || ""}`,
636
- );
803
+ if (newIdioms.length > 0) {
804
+ console.log(` ${GREEN}✔ Extracted ${newIdioms.length} idiom(s)${RESET}`);
805
+ for (const idiom of newIdioms) {
806
+ console.log(
807
+ ` ${CYAN}• ${idiom.pattern || "?"}${RESET} — ${idiom.reason || ""}`,
808
+ );
809
+ }
810
+ }
811
+
812
+ if (newCases.length > 0) {
813
+ console.log(` ${GREEN}✔ Extracted ${newCases.length} case precedent(s)${RESET}`);
814
+ for (const c of newCases) {
815
+ console.log(
816
+ ` ${RED}• ${c.pattern || "?"}${RESET} — ${c.reason || ""}`,
817
+ );
818
+ }
637
819
  }
638
820
 
639
821
  console.log(
@@ -647,10 +829,8 @@ async function cmdDigest(args) {
647
829
  let added = 0;
648
830
 
649
831
  for (const idiom of newIdioms) {
650
- // FIX: Use Levenshtein normalised similarity (threshold 0.80) instead of
651
- // substring .includes() which was over-aggressive and blocked valid idioms.
652
832
  if (isDuplicateIdiom(idiom.pattern || "", existing)) {
653
- console.log(` ${DIM} Skipped near-duplicate: ${idiom.pattern}${RESET}`);
833
+ console.log(` ${DIM} Skipped near-duplicate idiom: ${idiom.pattern}${RESET}`);
654
834
  continue;
655
835
  }
656
836
  merged.push({
@@ -664,17 +844,86 @@ async function cmdDigest(args) {
664
844
  added++;
665
845
  }
666
846
 
667
- if (added === 0) {
668
- console.log(
669
- ` ${YELLOW}⚠ All extracted idioms were duplicates. SKILL.md unchanged.${RESET}\n`,
670
- );
671
- return;
847
+ if (added > 0) {
848
+ log.total_idioms = merged.length;
849
+ const skillMd = renderSkillMd(merged, (log.cycles || []).length + 1);
850
+ fs.mkdirSync(SKILL_DIR, { recursive: true });
851
+ fs.writeFileSync(SKILL_FILE, skillMd, "utf8");
852
+ console.log(` ${GREEN}✔ ${added} new idiom(s) added to SKILL.md${RESET}`);
853
+ } else {
854
+ console.log(` ${DIM} No new unique idioms added to SKILL.md.${RESET}`);
672
855
  }
673
856
 
674
- log.total_idioms = merged.length;
675
- const skillMd = renderSkillMd(merged, (log.cycles || []).length + 1);
676
- fs.mkdirSync(SKILL_DIR, { recursive: true });
677
- fs.writeFileSync(SKILL_FILE, skillMd, "utf8");
857
+ // Record Case Precedents if present
858
+ if (newCases && newCases.length > 0) {
859
+ console.log(`\n ${DIM}Merging into Case Law precedents...${RESET}`);
860
+ const caseLawManager = require("./case_law_manager");
861
+ const clIndex = caseLawManager.loadIndex();
862
+ let clNextId = clIndex.next_id;
863
+ let clAdded = 0;
864
+
865
+ for (const c of newCases) {
866
+ const diffText = c.pattern || "";
867
+ const reason = c.reason || "";
868
+ const domain = c.domain || "general";
869
+ const verdict = c.verdict || "REJECTED";
870
+ const fingerprint = caseLawManager.contentHash(diffText);
871
+
872
+ let isDup = false;
873
+ for (const existing of clIndex.cases) {
874
+ if (existing.fingerprint === fingerprint) {
875
+ isDup = true;
876
+ break;
877
+ }
878
+ }
879
+ if (isDup) {
880
+ console.log(
881
+ ` ${DIM} Skipped duplicate case: ${reason.slice(0, 50)}...${RESET}`,
882
+ );
883
+ continue;
884
+ }
885
+
886
+ const deltaCase = caseLawManager.semanticDelta(diffText);
887
+ const tags = caseLawManager.extractTags(diffText + " " + reason);
888
+ const caseRecord = {
889
+ id: clNextId,
890
+ fingerprint,
891
+ timestamp: new Date().toISOString().slice(0, 19),
892
+ domain,
893
+ verdict,
894
+ reason: reason.trim(),
895
+ pr_ref: "auto-evolution",
896
+ reviewer: "skill-evolution",
897
+ tags,
898
+ stack_version: null,
899
+ diff_raw: diffText.trim(),
900
+ diff_delta: deltaCase,
901
+ auto_recorded: true,
902
+ };
903
+
904
+ caseLawManager.saveCase(caseRecord);
905
+ clIndex.cases.push({
906
+ id: clNextId,
907
+ fingerprint,
908
+ domain,
909
+ verdict,
910
+ tags,
911
+ timestamp: caseRecord.timestamp,
912
+ reason_summary: reason.trim().slice(0, 120),
913
+ stack_version: null,
914
+ });
915
+ clNextId++;
916
+ clAdded++;
917
+ }
918
+
919
+ if (clAdded > 0) {
920
+ clIndex.next_id = clNextId;
921
+ caseLawManager.saveIndex(clIndex);
922
+ console.log(` ${GREEN}✔ ${clAdded} new Case Law precedent(s) recorded.${RESET}`);
923
+ } else {
924
+ console.log(` ${DIM} No new unique Case Law precedents recorded.${RESET}`);
925
+ }
926
+ }
678
927
 
679
928
  log.cycles = log.cycles || [];
680
929
  log.cycles.push({
@@ -687,15 +936,11 @@ async function cmdDigest(args) {
687
936
  log.total_tokens_saved = (log.total_tokens_saved || 0) + savedTokens;
688
937
  saveLog(log);
689
938
 
690
- console.log(`\n ${GREEN}✔ ${added} new idiom(s) added to SKILL.md${RESET}`);
691
- console.log(` ${DIM} File: ${SKILL_FILE}${RESET}`);
939
+ console.log(`\n ${GREEN}✔ Learn cycle complete.${RESET}`);
692
940
  console.log(` ${DIM} Total idioms: ${merged.length}${RESET}`);
693
941
  console.log(
694
942
  ` ${DIM} Lifetime tokens saved: ${log.total_tokens_saved}${RESET}\n`,
695
943
  );
696
- console.log(
697
- ` ${CYAN}Commit SKILL.md to share your Engineering Culture with the team.${RESET}\n`,
698
- );
699
944
  }
700
945
 
701
946
  function cmdShow() {
@@ -0,0 +1,134 @@
1
+ # Contributing to Tribunal Kit
2
+
3
+ Thank you for your interest in contributing to Tribunal Kit! This guide will help you get started.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Clone the repository
9
+ git clone https://github.com/Harmitx7/tribunal-kit.git
10
+ cd tribunal-kit
11
+
12
+ # Install dependencies
13
+ npm install
14
+
15
+ # Run tests
16
+ npm run test:unit
17
+
18
+ # Run the CLI locally
19
+ node bin/wrapper.js --help
20
+ ```
21
+
22
+ ## Development Setup
23
+
24
+ ### Prerequisites
25
+
26
+ - **Node.js** >= 18.0.0 (LTS recommended)
27
+ - **npm** >= 9
28
+ - **Rust** (optional) — Only needed if modifying the Rust core engine in `crates/`
29
+
30
+ ### Project Structure
31
+
32
+ ```
33
+ tribunal-kit/
34
+ ├── bin/ # CLI entry points (JS monolith + MCP server)
35
+ │ ├── wrapper.js # Main entry: routes to Rust binary or JS fallback
36
+ │ ├── tribunal-kit.js # Legacy JS CLI (1,500+ lines)
37
+ │ └── mcp-server.js # MCP server over JSON-RPC 2.0 / stdio
38
+ ├── dist/ # Modular CLI (lazy-loaded commands)
39
+ │ ├── cli.js # CLI core with command routing
40
+ │ ├── commands/ # Individual command modules
41
+ │ ├── utils/ # Logger, helpers, version checker, hasher
42
+ │ └── index.d.ts # TypeScript declarations
43
+ ├── crates/core/ # Rust core engine (Tokio-based)
44
+ ├── .agent/ # The intelligence payload (agents, skills, workflows)
45
+ │ ├── agents/ # 43 specialist agent definitions
46
+ │ ├── skills/ # Reusable skill packs
47
+ │ ├── workflows/ # 34 workflow definitions
48
+ │ └── scripts/ # Automation scripts
49
+ ├── test/
50
+ │ ├── unit/ # Unit tests (Jest)
51
+ │ └── integration/ # Integration tests
52
+ └── scripts/ # Build & release scripts
53
+ ```
54
+
55
+ ### Running Tests
56
+
57
+ ```bash
58
+ # Unit tests with coverage
59
+ npm run test:unit
60
+
61
+ # All tests
62
+ npm test
63
+
64
+ # Run a specific test file
65
+ npx jest test/unit/memory.test.js
66
+ ```
67
+
68
+ ### Building the Rust Core (Optional)
69
+
70
+ ```bash
71
+ # Build in release mode
72
+ cargo build --release
73
+
74
+ # Run benchmarks comparing Rust vs JS
75
+ npm run benchmark:rust
76
+ ```
77
+
78
+ ## How to Contribute
79
+
80
+ ### Reporting Bugs
81
+
82
+ 1. Check [existing issues](https://github.com/Harmitx7/tribunal-kit/issues) first
83
+ 2. Use the bug report template
84
+ 3. Include: Node.js version, OS, steps to reproduce, expected vs actual behavior
85
+
86
+ ### Suggesting Features
87
+
88
+ 1. Open a [GitHub Discussion](https://github.com/Harmitx7/tribunal-kit/discussions) first
89
+ 2. Describe the use case, not just the solution
90
+ 3. Be specific about which command or workflow this affects
91
+
92
+ ### Submitting Pull Requests
93
+
94
+ 1. **Fork** the repository and create a branch from `main`
95
+ 2. **Write tests** for any new functionality
96
+ 3. **Run the test suite** — all tests must pass: `npm run test:unit`
97
+ 4. **Follow existing code style** — the project uses ESLint
98
+ 5. **Write clear commit messages** following [Conventional Commits](https://www.conventionalcommits.org/):
99
+ ```
100
+ feat: add --json flag to status command
101
+ fix: handle symlinks in init --force
102
+ docs: update CLI reference for memory command
103
+ test: add coverage for marathon harness
104
+ ```
105
+ 6. **Open a PR** with a clear description of what changed and why
106
+
107
+ ### Contributing Agents or Skills
108
+
109
+ Tribunal Kit's value comes from its agent and skill library. To contribute:
110
+
111
+ 1. **Agents** go in `.agent/agents/` as markdown files
112
+ 2. **Skills** go in `.agent/skills/<skill-name>/SKILL.md`
113
+ 3. Follow the existing format — check any existing agent/skill for the structure
114
+ 4. Include the YAML frontmatter (`name`, `description`)
115
+ 5. Add practical, non-obvious guidance — not generic advice
116
+
117
+ ### Code Style
118
+
119
+ - **JavaScript**: CommonJS (`require`/`module.exports`), no transpilation needed
120
+ - **Naming**: `camelCase` for functions/variables, `UPPER_SNAKE` for constants
121
+ - **Error handling**: Always handle errors in async functions
122
+ - **Comments**: Explain *why*, not *what*
123
+ - **No new dependencies**: Zero production dependencies is a feature, not a limitation
124
+
125
+ ## Review Process
126
+
127
+ 1. All PRs are reviewed by a maintainer
128
+ 2. CI must pass (tests + lint)
129
+ 3. Changes to `.agent/` content are reviewed for accuracy and usefulness
130
+ 4. Breaking changes require a discussion before implementation
131
+
132
+ ## License
133
+
134
+ By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,52 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 5.x | ✅ Active support |
8
+ | 4.x | ⚠️ Critical fixes only |
9
+ | < 4.0 | ❌ End of life |
10
+
11
+ ## Reporting a Vulnerability
12
+
13
+ **Do not open a public GitHub issue for security vulnerabilities.**
14
+
15
+ Instead, please report vulnerabilities privately using one of these methods:
16
+
17
+ 1. **GitHub Security Advisory** (preferred): Use [GitHub's private vulnerability reporting](https://github.com/Harmitx7/tribunal-kit/security/advisories/new) to submit a confidential report.
18
+
19
+ 2. **Email**: Send details to the maintainer via the email listed in the npm package.
20
+
21
+ ### What to include
22
+
23
+ - Description of the vulnerability
24
+ - Steps to reproduce
25
+ - Affected versions
26
+ - Potential impact
27
+ - Suggested fix (if any)
28
+
29
+ ### Response timeline
30
+
31
+ | Action | Timeline |
32
+ | --- | --- |
33
+ | Acknowledgment | Within 48 hours |
34
+ | Initial assessment | Within 5 business days |
35
+ | Fix release (critical) | Within 7 days |
36
+ | Fix release (moderate) | Within 30 days |
37
+
38
+ ## Security Design
39
+
40
+ Tribunal Kit follows these security principles:
41
+
42
+ - **No network requests at runtime** — The CLI operates entirely offline. The only network call is an optional npm registry version check during `init`/`update`, which can be skipped with `--skip-update-check`.
43
+ - **No code execution from user input** — CLI arguments are parsed without `eval()` or shell interpolation. All subprocess spawning uses array-based arguments (never string concatenation).
44
+ - **No secrets stored** — Tribunal Kit does not store, read, or transmit API keys, tokens, or credentials.
45
+ - **Minimal dependencies** — Zero production dependencies. Only `jest`, `eslint`, and `typescript` as devDependencies.
46
+ - **Platform binaries are optional** — The Rust core binary is distributed as optional dependencies. The CLI falls back to pure JavaScript if binaries are unavailable.
47
+
48
+ ## Supply Chain
49
+
50
+ - All releases are published from CI via GitHub Actions
51
+ - Platform binaries are built in GitHub-hosted runners with pinned action versions
52
+ - The package uses `npm provenance` for verifiable supply chain attestation