tribunal-kit 5.8.1 → 5.8.3
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/.agent/ARCHITECTURE.md +13 -13
- package/.agent/agents/complexity-reviewer.md +53 -0
- package/.agent/agents/precedence-reviewer.md +12 -12
- package/.agent/agents/swarm-worker-registry.md +3 -3
- package/.agent/history/memory/.memory.idx +457 -1
- package/.agent/history/memory/MEMORY.md +31 -1
- package/.agent/rules/GEMINI.md +31 -28
- package/.agent/scripts/signal_detector.js +173 -0
- package/.agent/scripts/skill_evolution.js +298 -53
- package/.agent/skills/app-builder/SKILL.md +1 -1
- package/.agent/skills/lint-and-validate/SKILL.md +2 -2
- package/.agent/skills/project-idioms/SKILL.md +7 -7
- package/.agent/skills/test-result-analyzer/SKILL.md +1 -1
- package/.agent/workflows/fix.md +2 -2
- package/.agent/workflows/generate.md +1 -1
- package/.agent/workflows/preview.md +5 -5
- package/.agent/workflows/status.md +1 -1
- package/.agent/workflows/tribunal-full.md +3 -3
- package/.agent/workflows/tribunal-speed.md +1 -1
- package/CONTRIBUTING.md +134 -0
- package/SECURITY.md +52 -0
- package/bin/tribunal-kit.js +92 -46
- package/dist/commands/init.js +68 -30
- package/dist/esm/index.mjs +116 -0
- package/dist/index.d.ts +288 -0
- package/dist/utils/helpers.js +21 -9
- package/package.json +20 -8
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* signal_detector.js — Tribunal Kit Signal Detector
|
|
3
|
+
* ====================================================
|
|
4
|
+
* Parses runtime log files, test logs, and build transcripts to extract
|
|
5
|
+
* structured "Improvement Signals" (errors, bottlenecks, gaps).
|
|
6
|
+
*
|
|
7
|
+
* Supported log sources/patterns:
|
|
8
|
+
* - JS/TS Node Stack Traces
|
|
9
|
+
* - Python Stack Traces
|
|
10
|
+
* - Rust Compiler Errors
|
|
11
|
+
* - Jest/Vitest Test Failures
|
|
12
|
+
* - ESLint Warnings/Errors
|
|
13
|
+
* - Performance measurements (e.g. "Slow query", "duration: 1200ms")
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
|
|
21
|
+
const SIGNAL_TYPES = {
|
|
22
|
+
ERROR: "log_error",
|
|
23
|
+
PERF: "perf_bottleneck",
|
|
24
|
+
GAP: "capability_gap",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses a string log and extracts list of typed signals.
|
|
29
|
+
* @param {string} logText
|
|
30
|
+
* @returns {Array<{type: string, file: string|null, line: number|null, message: string, context: string}>}
|
|
31
|
+
*/
|
|
32
|
+
function detectSignals(logText) {
|
|
33
|
+
const signals = [];
|
|
34
|
+
const lines = logText.split(/\r?\n/);
|
|
35
|
+
|
|
36
|
+
// 1. Check for stack traces and syntax/runtime errors
|
|
37
|
+
// JS/TS stack trace line: " at Object.foo (src/utils.js:12:45)" or " at src/utils.js:12:45"
|
|
38
|
+
const jsStackRegex = /\bat\s+(?:async\s+)?(?:[^(]+\()?([^:()\s]+):(\d+):(\d+)\)?/;
|
|
39
|
+
// Python File location: ' File "src/main.py", line 12, in <module>'
|
|
40
|
+
const pyStackRegex = /File\s+"([^"]+)"\s*,\s*line\s*(\d+)/;
|
|
41
|
+
// Rust location: ' --> src/main.rs:12:34'
|
|
42
|
+
const rustRegex = /-->\s*([^:\s]+):(\d+):(\d+)/;
|
|
43
|
+
// ESLint standard line: ' /path/to/file.js:12:34: error: Message' or ' 12:34 error Message'
|
|
44
|
+
// But ESLint often has file heading first. So let's handle inline: 'file.js: line 12, col 34, Error - Message'
|
|
45
|
+
const eslintInlineRegex = /([^:\s]+):\s*line\s*(\d+)\s*,\s*col\s*\d+,\s*(?:Error|Warning)\s*-\s*(.+)/i;
|
|
46
|
+
// Also standard unix format: '/path/to/file.js:12:34: error message'
|
|
47
|
+
const unixErrorRegex = /^([^:\s\(\)]+):(\d+):(?:\d+:)?\s*(error|warning|info):?\s*(.+)/i;
|
|
48
|
+
|
|
49
|
+
// Let's also scan for performance bottlenecks
|
|
50
|
+
const slowQueryRegex = /slow\s+query|duration:\s*(\d+)ms|execution\s+time:\s*(\d+)ms|response\s+time\s*>\s*(\d+)ms/i;
|
|
51
|
+
|
|
52
|
+
let currentFile = null;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i];
|
|
56
|
+
|
|
57
|
+
// Detect Jest/Vitest file heading: "FAIL src/utils.test.js"
|
|
58
|
+
if (line.startsWith("FAIL ")) {
|
|
59
|
+
const match = line.match(/^FAIL\s+(\S+)/);
|
|
60
|
+
if (match) {
|
|
61
|
+
currentFile = match[1];
|
|
62
|
+
signals.push({
|
|
63
|
+
type: SIGNAL_TYPES.ERROR,
|
|
64
|
+
file: currentFile,
|
|
65
|
+
line: null,
|
|
66
|
+
message: `Test suite failed: ${path.basename(currentFile)}`,
|
|
67
|
+
context: lines.slice(Math.max(0, i - 1), Math.min(lines.length, i + 5)).join("\n"),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// JS Stack Trace Match
|
|
74
|
+
const jsMatch = line.match(jsStackRegex);
|
|
75
|
+
if (jsMatch && !line.includes("node_modules")) {
|
|
76
|
+
const filePath = jsMatch[1].replace(/\\/g, "/");
|
|
77
|
+
signals.push({
|
|
78
|
+
type: SIGNAL_TYPES.ERROR,
|
|
79
|
+
file: filePath,
|
|
80
|
+
line: parseInt(jsMatch[2], 10),
|
|
81
|
+
message: `Stack Trace Error: ${line.trim()}`,
|
|
82
|
+
context: lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join("\n"),
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Python Stack Trace Match
|
|
88
|
+
const pyMatch = line.match(pyStackRegex);
|
|
89
|
+
if (pyMatch) {
|
|
90
|
+
const filePath = pyMatch[1].replace(/\\/g, "/");
|
|
91
|
+
signals.push({
|
|
92
|
+
type: SIGNAL_TYPES.ERROR,
|
|
93
|
+
file: filePath,
|
|
94
|
+
line: parseInt(pyMatch[2], 10),
|
|
95
|
+
message: `Python Exception: ${lines[i+1] ? lines[i+1].trim() : line.trim()}`,
|
|
96
|
+
context: lines.slice(Math.max(0, i - 1), Math.min(lines.length, i + 4)).join("\n"),
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Rust Compiler Error Match
|
|
102
|
+
const rustMatch = line.match(rustRegex);
|
|
103
|
+
if (rustMatch) {
|
|
104
|
+
const filePath = rustMatch[1].replace(/\\/g, "/");
|
|
105
|
+
signals.push({
|
|
106
|
+
type: SIGNAL_TYPES.ERROR,
|
|
107
|
+
file: filePath,
|
|
108
|
+
line: parseInt(rustMatch[2], 10),
|
|
109
|
+
message: `Rust compilation failure: ${lines[Math.max(0, i - 1)].trim()}`,
|
|
110
|
+
context: lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 5)).join("\n"),
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ESLint Inline Match
|
|
116
|
+
const eslintMatch = line.match(eslintInlineRegex);
|
|
117
|
+
if (eslintMatch) {
|
|
118
|
+
signals.push({
|
|
119
|
+
type: SIGNAL_TYPES.ERROR,
|
|
120
|
+
file: eslintMatch[1].replace(/\\/g, "/"),
|
|
121
|
+
line: parseInt(eslintMatch[2], 10),
|
|
122
|
+
message: `Linter Violation: ${eslintMatch[3]}`,
|
|
123
|
+
context: line.trim(),
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Unix Error Format Match
|
|
129
|
+
const unixMatch = line.match(unixErrorRegex);
|
|
130
|
+
if (unixMatch && !line.includes("node_modules")) {
|
|
131
|
+
const filePath = unixMatch[1].replace(/\\/g, "/");
|
|
132
|
+
signals.push({
|
|
133
|
+
type: SIGNAL_TYPES.ERROR,
|
|
134
|
+
file: filePath,
|
|
135
|
+
line: parseInt(unixMatch[2], 10),
|
|
136
|
+
message: `${unixMatch[3].toUpperCase()}: ${unixMatch[4]}`,
|
|
137
|
+
context: line.trim(),
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Slow Query/Performance Match
|
|
143
|
+
const slowMatch = line.match(slowQueryRegex);
|
|
144
|
+
if (slowMatch) {
|
|
145
|
+
signals.push({
|
|
146
|
+
type: SIGNAL_TYPES.PERF,
|
|
147
|
+
file: currentFile || null,
|
|
148
|
+
line: null,
|
|
149
|
+
message: `Performance warning: ${line.trim()}`,
|
|
150
|
+
context: lines.slice(Math.max(0, i - 1), Math.min(lines.length, i + 2)).join("\n"),
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Deduplicate near-identical signals (same file, line, and message type)
|
|
157
|
+
const unique = [];
|
|
158
|
+
const keys = new Set();
|
|
159
|
+
for (const s of signals) {
|
|
160
|
+
const key = `${s.type}:${s.file}:${s.line}:${s.message.substring(0, 40)}`;
|
|
161
|
+
if (!keys.has(key)) {
|
|
162
|
+
keys.add(key);
|
|
163
|
+
unique.push(s);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return unique;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
detectSignals,
|
|
172
|
+
SIGNAL_TYPES,
|
|
173
|
+
};
|
|
@@ -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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
643
|
+
` ${GREEN}✔ Filtered to ${logSignals.length} signal(s) using [${strategy}] strategy.${RESET}`,
|
|
509
644
|
);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
645
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
` ${
|
|
672
|
+
` ${DIM} Raw diff: ~${rawTokens} tokens (${rawDiff.length} chars)${RESET}`,
|
|
525
673
|
);
|
|
674
|
+
|
|
526
675
|
console.log(
|
|
527
|
-
` ${DIM}
|
|
676
|
+
` ${DIM}[2/5] Extracting architectural delta (Semantic Filter)...${RESET}`,
|
|
528
677
|
);
|
|
529
|
-
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
563
|
-
const reflectionPrompt =
|
|
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;
|
|
746
|
+
llmCreds = null;
|
|
586
747
|
}
|
|
587
748
|
}
|
|
588
749
|
|
|
589
750
|
if (!llmCreds || !llmResponse) {
|
|
590
|
-
// Manual fallback: copy-paste mode
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
` ${
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
);
|
|
671
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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}✔
|
|
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() {
|
|
@@ -185,8 +185,8 @@ repos:
|
|
|
185
185
|
|
|
186
186
|
| Script | Purpose | Run With |
|
|
187
187
|
| -------------------------- | ----------------------------------------- | ------------------------------------------------ |
|
|
188
|
-
|
|
|
189
|
-
| `scripts/type_coverage.py`
|
|
188
|
+
| `.agent/scripts/lint_runner.js` | Runs project linting and reports findings | `node .agent/scripts/lint_runner.js <project_path>` |
|
|
189
|
+
| `scripts/type_coverage.py` | Measures TypeScript type coverage | `python scripts/type_coverage.py <project_path>` |
|
|
190
190
|
|
|
191
191
|
---
|
|
192
192
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: project-idioms
|
|
3
3
|
description: >
|
|
4
4
|
Auto-evolved skill containing project-specific architectural idioms extracted
|
|
5
|
-
from the developer's own code decisions. Generated by skill_evolution.
|
|
5
|
+
from the developer's own code decisions. Generated by skill_evolution.js.
|
|
6
6
|
Commit this file to share your Engineering Culture across the team.
|
|
7
7
|
Every agent MUST respect these idioms above generic defaults.
|
|
8
8
|
version: auto
|
|
@@ -33,7 +33,7 @@ routing:
|
|
|
33
33
|
## How Idioms Are Born
|
|
34
34
|
|
|
35
35
|
1. Developer commits code that **differs** from what the AI proposed.
|
|
36
|
-
2. `skill_evolution.
|
|
36
|
+
2. `skill_evolution.js digest` extracts the architectural delta (semantic filter).
|
|
37
37
|
3. A minimal LLM prompt (< 500 tokens) identifies the **WHY** behind the change.
|
|
38
38
|
4. The idiom is recorded here with a stable pattern + reason pair.
|
|
39
39
|
5. All future code generations must align with these idioms.
|
|
@@ -54,16 +54,16 @@ Run after committing or staging a meaningful architectural change:
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
# Analyze staged changes (default)
|
|
57
|
-
|
|
57
|
+
node .agent/scripts/skill_evolution.js digest
|
|
58
58
|
|
|
59
59
|
# Preview without writing
|
|
60
|
-
|
|
60
|
+
node .agent/scripts/skill_evolution.js digest --dry-run
|
|
61
61
|
|
|
62
62
|
# Analyze last commit instead of staged
|
|
63
|
-
|
|
63
|
+
node .agent/scripts/skill_evolution.js digest --head
|
|
64
64
|
|
|
65
65
|
# Check current idiom count and token savings
|
|
66
|
-
|
|
66
|
+
node .agent/scripts/skill_evolution.js status
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
---
|
|
@@ -87,7 +87,7 @@ python .agent/scripts/skill_evolution.py status
|
|
|
87
87
|
Last digest: `never`
|
|
88
88
|
Total cycles: `0`
|
|
89
89
|
|
|
90
|
-
Run `
|
|
90
|
+
Run `node .agent/scripts/skill_evolution.js status` to see full statistics.
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|