karajan-code 1.17.0 → 1.18.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/package.json +1 -1
- package/src/config.js +19 -0
- package/src/guards/intent-guard.js +123 -0
- package/src/guards/output-guard.js +158 -0
- package/src/guards/perf-guard.js +126 -0
- package/src/orchestrator.js +85 -1
- package/templates/kj.config.yml +33 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -140,6 +140,25 @@ const DEFAULTS = {
|
|
|
140
140
|
max_backoff_ms: 30000,
|
|
141
141
|
backoff_multiplier: 2,
|
|
142
142
|
jitter_factor: 0.1
|
|
143
|
+
},
|
|
144
|
+
guards: {
|
|
145
|
+
output: {
|
|
146
|
+
enabled: true,
|
|
147
|
+
patterns: [],
|
|
148
|
+
protected_files: [],
|
|
149
|
+
on_violation: "block"
|
|
150
|
+
},
|
|
151
|
+
perf: {
|
|
152
|
+
enabled: true,
|
|
153
|
+
patterns: [],
|
|
154
|
+
block_on_warning: false,
|
|
155
|
+
frontend_extensions: []
|
|
156
|
+
},
|
|
157
|
+
intent: {
|
|
158
|
+
enabled: false,
|
|
159
|
+
patterns: [],
|
|
160
|
+
confidence_threshold: 0.85
|
|
161
|
+
}
|
|
143
162
|
}
|
|
144
163
|
};
|
|
145
164
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { VALID_TASK_TYPES } from "./policy-resolver.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in intent patterns for deterministic pre-triage classification.
|
|
5
|
+
* Each pattern maps keywords/regex to a taskType + complexity level.
|
|
6
|
+
* Evaluated top-down; first match with confidence >= threshold wins.
|
|
7
|
+
*/
|
|
8
|
+
const INTENT_PATTERNS = [
|
|
9
|
+
// Documentation-only tasks
|
|
10
|
+
{
|
|
11
|
+
id: "doc-readme",
|
|
12
|
+
keywords: ["readme", "docs", "documentation", "jsdoc", "typedoc", "changelog"],
|
|
13
|
+
taskType: "doc",
|
|
14
|
+
level: "trivial",
|
|
15
|
+
confidence: 0.95,
|
|
16
|
+
message: "Documentation-only task detected",
|
|
17
|
+
},
|
|
18
|
+
// Test-only tasks
|
|
19
|
+
{
|
|
20
|
+
id: "add-tests",
|
|
21
|
+
keywords: ["add test", "write test", "missing test", "test coverage", "add spec", "write spec", "unit test", "integration test"],
|
|
22
|
+
taskType: "add-tests",
|
|
23
|
+
level: "simple",
|
|
24
|
+
confidence: 0.9,
|
|
25
|
+
message: "Test-addition task detected",
|
|
26
|
+
},
|
|
27
|
+
// Refactoring tasks
|
|
28
|
+
{
|
|
29
|
+
id: "refactor",
|
|
30
|
+
keywords: ["refactor", "rename", "extract method", "extract function", "clean up", "cleanup", "reorganize", "restructure", "simplify"],
|
|
31
|
+
taskType: "refactor",
|
|
32
|
+
level: "simple",
|
|
33
|
+
confidence: 0.85,
|
|
34
|
+
message: "Refactoring task detected",
|
|
35
|
+
},
|
|
36
|
+
// Infrastructure / DevOps tasks
|
|
37
|
+
{
|
|
38
|
+
id: "infra-devops",
|
|
39
|
+
keywords: ["ci/cd", "pipeline", "dockerfile", "docker-compose", "kubernetes", "k8s", "terraform", "deploy", "nginx", "github actions", "gitlab ci"],
|
|
40
|
+
taskType: "infra",
|
|
41
|
+
level: "simple",
|
|
42
|
+
confidence: 0.85,
|
|
43
|
+
message: "Infrastructure/DevOps task detected",
|
|
44
|
+
},
|
|
45
|
+
// Trivial fixes (typos, comments, formatting)
|
|
46
|
+
{
|
|
47
|
+
id: "trivial-fix",
|
|
48
|
+
keywords: ["typo", "fix typo", "spelling", "comment", "fix comment", "formatting", "lint", "fix lint", "whitespace"],
|
|
49
|
+
taskType: "sw",
|
|
50
|
+
level: "trivial",
|
|
51
|
+
confidence: 0.9,
|
|
52
|
+
message: "Trivial fix detected",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compile custom intent patterns from config.guards.intent.patterns
|
|
58
|
+
* Custom patterns are evaluated BEFORE built-in ones.
|
|
59
|
+
*/
|
|
60
|
+
export function compileIntentPatterns(configGuards) {
|
|
61
|
+
const custom = Array.isArray(configGuards?.intent?.patterns)
|
|
62
|
+
? configGuards.intent.patterns.map(p => ({
|
|
63
|
+
id: p.id || "custom-intent",
|
|
64
|
+
keywords: Array.isArray(p.keywords) ? p.keywords : [],
|
|
65
|
+
taskType: VALID_TASK_TYPES.has(p.taskType) ? p.taskType : "sw",
|
|
66
|
+
level: p.level || "simple",
|
|
67
|
+
confidence: typeof p.confidence === "number" ? p.confidence : 0.85,
|
|
68
|
+
message: p.message || "Custom intent pattern matched",
|
|
69
|
+
}))
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
return [...custom, ...INTENT_PATTERNS];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a task description matches any of the keywords.
|
|
77
|
+
* Returns true if at least one keyword (case-insensitive substring) appears in the task.
|
|
78
|
+
*/
|
|
79
|
+
function matchesKeywords(task, keywords) {
|
|
80
|
+
const lower = task.toLowerCase();
|
|
81
|
+
for (const kw of keywords) {
|
|
82
|
+
if (lower.includes(kw.toLowerCase())) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Classify a task description using deterministic keyword patterns.
|
|
91
|
+
*
|
|
92
|
+
* Returns:
|
|
93
|
+
* { classified: true, taskType, level, confidence, patternId, message }
|
|
94
|
+
* or { classified: false } if no pattern matches above threshold
|
|
95
|
+
*/
|
|
96
|
+
export function classifyIntent(task, config = {}) {
|
|
97
|
+
if (!task || typeof task !== "string") {
|
|
98
|
+
return { classified: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const configGuards = config?.guards || {};
|
|
102
|
+
const threshold = configGuards?.intent?.confidence_threshold ?? 0.85;
|
|
103
|
+
const patterns = compileIntentPatterns(configGuards);
|
|
104
|
+
|
|
105
|
+
for (const pattern of patterns) {
|
|
106
|
+
if (!matchesKeywords(task, pattern.keywords)) continue;
|
|
107
|
+
|
|
108
|
+
if (pattern.confidence >= threshold) {
|
|
109
|
+
return {
|
|
110
|
+
classified: true,
|
|
111
|
+
taskType: pattern.taskType,
|
|
112
|
+
level: pattern.level,
|
|
113
|
+
confidence: pattern.confidence,
|
|
114
|
+
patternId: pattern.id,
|
|
115
|
+
message: pattern.message,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { classified: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { INTENT_PATTERNS };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { runCommand } from "../utils/process.js";
|
|
2
|
+
|
|
3
|
+
// Built-in destructive patterns
|
|
4
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
5
|
+
{ id: "rm-rf", pattern: /rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r/, severity: "critical", message: "Recursive file deletion detected" },
|
|
6
|
+
{ id: "drop-table", pattern: /DROP\s+(TABLE|DATABASE|SCHEMA)/i, severity: "critical", message: "SQL destructive operation detected" },
|
|
7
|
+
{ id: "git-reset-hard", pattern: /git\s+reset\s+--hard/i, severity: "critical", message: "Hard git reset detected" },
|
|
8
|
+
{ id: "git-push-force", pattern: /git\s+push\s+.*--force/i, severity: "critical", message: "Force push detected" },
|
|
9
|
+
{ id: "truncate-table", pattern: /TRUNCATE\s+TABLE/i, severity: "critical", message: "SQL truncate detected" },
|
|
10
|
+
{ id: "format-disk", pattern: /mkfs\.|fdisk|dd\s+if=/, severity: "critical", message: "Disk format operation detected" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Built-in credential patterns
|
|
14
|
+
const CREDENTIAL_PATTERNS = [
|
|
15
|
+
{ id: "aws-key", pattern: /AKIA[0-9A-Z]{16}/, severity: "critical", message: "AWS access key exposed" },
|
|
16
|
+
{ id: "private-key", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, severity: "critical", message: "Private key exposed" },
|
|
17
|
+
{ id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "warning", message: "Potential secret/credential exposed" },
|
|
18
|
+
{ id: "github-token", pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, severity: "critical", message: "GitHub token exposed" },
|
|
19
|
+
{ id: "npm-token", pattern: /npm_[A-Za-z0-9]{36,}/, severity: "critical", message: "npm token exposed" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Default protected files (block if these appear in added/modified lines)
|
|
23
|
+
const DEFAULT_PROTECTED_FILES = [
|
|
24
|
+
".env",
|
|
25
|
+
".env.local",
|
|
26
|
+
".env.production",
|
|
27
|
+
"serviceAccountKey.json",
|
|
28
|
+
"credentials.json",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function compilePatterns(configGuards) {
|
|
32
|
+
const customPatterns = Array.isArray(configGuards?.output?.patterns)
|
|
33
|
+
? configGuards.output.patterns.map(p => ({
|
|
34
|
+
id: p.id || "custom",
|
|
35
|
+
pattern: typeof p.pattern === "string" ? new RegExp(p.pattern, p.flags || "") : p.pattern,
|
|
36
|
+
severity: p.severity || "warning",
|
|
37
|
+
message: p.message || "Custom pattern matched",
|
|
38
|
+
}))
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
return [...DESTRUCTIVE_PATTERNS, ...CREDENTIAL_PATTERNS, ...customPatterns];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function compileProtectedFiles(configGuards) {
|
|
45
|
+
const custom = Array.isArray(configGuards?.output?.protected_files)
|
|
46
|
+
? configGuards.output.protected_files
|
|
47
|
+
: [];
|
|
48
|
+
return [...new Set([...DEFAULT_PROTECTED_FILES, ...custom])];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a unified diff to extract only added lines (lines starting with +, not ++)
|
|
53
|
+
*/
|
|
54
|
+
export function extractAddedLines(diff) {
|
|
55
|
+
if (!diff) return [];
|
|
56
|
+
const results = [];
|
|
57
|
+
let currentFile = null;
|
|
58
|
+
let lineNum = 0;
|
|
59
|
+
|
|
60
|
+
for (const line of diff.split("\n")) {
|
|
61
|
+
if (line.startsWith("+++ b/")) {
|
|
62
|
+
currentFile = line.slice(6);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (line.startsWith("@@ ")) {
|
|
66
|
+
const match = /@@ -\d+(?:,\d+)? \+(\d+)/.exec(line);
|
|
67
|
+
lineNum = match ? Number.parseInt(match[1], 10) - 1 : 0;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
71
|
+
lineNum += 1;
|
|
72
|
+
results.push({ file: currentFile, line: lineNum, content: line.slice(1) });
|
|
73
|
+
} else if (!line.startsWith("-")) {
|
|
74
|
+
lineNum += 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if any modified files are in the protected list
|
|
82
|
+
*/
|
|
83
|
+
export function checkProtectedFiles(diff, protectedFiles) {
|
|
84
|
+
const violations = [];
|
|
85
|
+
const modifiedFiles = [];
|
|
86
|
+
|
|
87
|
+
for (const line of diff.split("\n")) {
|
|
88
|
+
if (line.startsWith("+++ b/")) {
|
|
89
|
+
modifiedFiles.push(line.slice(6));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const file of modifiedFiles) {
|
|
94
|
+
const basename = file.split("/").pop();
|
|
95
|
+
if (protectedFiles.some(pf => file === pf || file.endsWith(`/${pf}`) || basename === pf)) {
|
|
96
|
+
violations.push({
|
|
97
|
+
id: "protected-file",
|
|
98
|
+
severity: "critical",
|
|
99
|
+
file,
|
|
100
|
+
line: 0,
|
|
101
|
+
message: `Protected file modified: ${file}`,
|
|
102
|
+
matchedContent: "",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return violations;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Scan a diff for pattern violations.
|
|
112
|
+
* Returns { pass: boolean, violations: Array<{id, severity, file, line, message, matchedContent}> }
|
|
113
|
+
*/
|
|
114
|
+
export function scanDiff(diff, config = {}) {
|
|
115
|
+
if (!diff || typeof diff !== "string") {
|
|
116
|
+
return { pass: true, violations: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const configGuards = config?.guards || {};
|
|
120
|
+
const patterns = compilePatterns(configGuards);
|
|
121
|
+
const protectedFiles = compileProtectedFiles(configGuards);
|
|
122
|
+
const addedLines = extractAddedLines(diff);
|
|
123
|
+
const violations = [];
|
|
124
|
+
|
|
125
|
+
// Check patterns against added lines
|
|
126
|
+
for (const { file, line, content } of addedLines) {
|
|
127
|
+
for (const { id, pattern, severity, message } of patterns) {
|
|
128
|
+
if (pattern.test(content)) {
|
|
129
|
+
violations.push({ id, severity, file, line, message, matchedContent: content.trim().slice(0, 200) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check protected files
|
|
135
|
+
violations.push(...checkProtectedFiles(diff, protectedFiles));
|
|
136
|
+
|
|
137
|
+
const hasCritical = violations.some(v => v.severity === "critical");
|
|
138
|
+
return { pass: !hasCritical, violations };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run output guard on the current git diff.
|
|
143
|
+
* This is the main entry point for the pipeline integration.
|
|
144
|
+
*/
|
|
145
|
+
export async function runOutputGuard(config = {}, baseBranch = "main") {
|
|
146
|
+
const diffResult = await runCommand("git", ["diff", `origin/${baseBranch}...HEAD`]);
|
|
147
|
+
if (diffResult.exitCode !== 0) {
|
|
148
|
+
// Fallback: diff against HEAD~1
|
|
149
|
+
const fallback = await runCommand("git", ["diff", "HEAD~1"]);
|
|
150
|
+
if (fallback.exitCode !== 0) {
|
|
151
|
+
return { pass: true, violations: [], error: "Could not generate diff" };
|
|
152
|
+
}
|
|
153
|
+
return scanDiff(fallback.stdout, config);
|
|
154
|
+
}
|
|
155
|
+
return scanDiff(diffResult.stdout, config);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export { DESTRUCTIVE_PATTERNS, CREDENTIAL_PATTERNS, DEFAULT_PROTECTED_FILES };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const FRONTEND_EXTENSIONS = new Set([".html", ".htm", ".css", ".jsx", ".tsx", ".astro", ".vue", ".svelte"]);
|
|
2
|
+
|
|
3
|
+
// Built-in perf anti-patterns (applied to added lines in frontend files)
|
|
4
|
+
const PERF_PATTERNS = [
|
|
5
|
+
{ id: "img-no-dimensions", pattern: /<img\b(?![^>]*\bwidth\b)(?![^>]*\bheight\b)[^>]*>/i, severity: "warning", message: "Image without width/height attributes (causes CLS)" },
|
|
6
|
+
{ id: "img-no-lazy", pattern: /<img\b(?![^>]*\bloading\s*=)(?![^>]*\bfetchpriority\s*=)[^>]*>/i, severity: "info", message: "Image without loading=\"lazy\" or fetchpriority (consider lazy loading)" },
|
|
7
|
+
{ id: "script-no-defer", pattern: /<script\b(?![^>]*\b(?:defer|async)\b)(?![^>]*type\s*=\s*["']module["'])[^>]*src\s*=/i, severity: "warning", message: "External script without defer/async (render-blocking)" },
|
|
8
|
+
{ id: "font-no-display", pattern: /@font-face\s*\{(?![^}]*font-display)/i, severity: "warning", message: "@font-face without font-display (causes FOIT)" },
|
|
9
|
+
{ id: "css-import", pattern: /@import\s+(?:url\()?["'](?!.*\.module\.)/i, severity: "info", message: "CSS @import (causes sequential loading, prefer <link>)" },
|
|
10
|
+
{ id: "inline-style-large", pattern: /style\s*=\s*["'][^"']{200,}["']/i, severity: "warning", message: "Large inline style (>200 chars, consider external CSS)" },
|
|
11
|
+
{ id: "document-write", pattern: /document\.write\s*\(/, severity: "warning", message: "document.write() blocks parsing and degrades performance" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Patterns for package.json changes (heavy dependencies added)
|
|
15
|
+
const HEAVY_DEPS = [
|
|
16
|
+
{ id: "heavy-moment", pattern: /"moment"/, severity: "info", message: "moment.js added (consider dayjs or date-fns for smaller bundle)" },
|
|
17
|
+
{ id: "heavy-lodash", pattern: /"lodash"(?!\/)/, severity: "info", message: "Full lodash added (consider lodash-es or individual imports)" },
|
|
18
|
+
{ id: "heavy-jquery", pattern: /"jquery"/, severity: "info", message: "jQuery added (consider native DOM APIs)" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function getExtension(filePath) {
|
|
22
|
+
if (!filePath) return "";
|
|
23
|
+
const dot = filePath.lastIndexOf(".");
|
|
24
|
+
return dot >= 0 ? filePath.slice(dot).toLowerCase() : "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if any modified file in the diff is a frontend file
|
|
29
|
+
*/
|
|
30
|
+
export function hasFrontendFiles(diff) {
|
|
31
|
+
if (!diff) return false;
|
|
32
|
+
for (const line of diff.split("\n")) {
|
|
33
|
+
if (line.startsWith("+++ b/")) {
|
|
34
|
+
const ext = getExtension(line.slice(6));
|
|
35
|
+
if (FRONTEND_EXTENSIONS.has(ext)) return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract added lines grouped by file from a unified diff
|
|
43
|
+
*/
|
|
44
|
+
function extractAddedLinesByFile(diff) {
|
|
45
|
+
const results = [];
|
|
46
|
+
let currentFile = null;
|
|
47
|
+
let lineNum = 0;
|
|
48
|
+
|
|
49
|
+
for (const line of diff.split("\n")) {
|
|
50
|
+
if (line.startsWith("+++ b/")) {
|
|
51
|
+
currentFile = line.slice(6);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (line.startsWith("@@ ")) {
|
|
55
|
+
const match = /@@ -\d+(?:,\d+)? \+(\d+)/.exec(line);
|
|
56
|
+
lineNum = match ? Number.parseInt(match[1], 10) - 1 : 0;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
60
|
+
lineNum += 1;
|
|
61
|
+
results.push({ file: currentFile, line: lineNum, content: line.slice(1) });
|
|
62
|
+
} else if (!line.startsWith("-")) {
|
|
63
|
+
lineNum += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scan diff for frontend performance anti-patterns.
|
|
71
|
+
* Returns { pass: boolean, violations: [...], skipped: boolean }
|
|
72
|
+
*/
|
|
73
|
+
export function scanPerfDiff(diff, config = {}) {
|
|
74
|
+
if (!diff || typeof diff !== "string") {
|
|
75
|
+
return { pass: true, violations: [], skipped: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!hasFrontendFiles(diff)) {
|
|
79
|
+
return { pass: true, violations: [], skipped: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const customPatterns = Array.isArray(config?.guards?.perf?.patterns)
|
|
83
|
+
? config.guards.perf.patterns.map(p => ({
|
|
84
|
+
id: p.id || "custom-perf",
|
|
85
|
+
pattern: typeof p.pattern === "string" ? new RegExp(p.pattern, p.flags || "i") : p.pattern,
|
|
86
|
+
severity: p.severity || "warning",
|
|
87
|
+
message: p.message || "Custom perf pattern matched",
|
|
88
|
+
}))
|
|
89
|
+
: [];
|
|
90
|
+
|
|
91
|
+
const allPatterns = [...PERF_PATTERNS, ...customPatterns];
|
|
92
|
+
const addedLines = extractAddedLinesByFile(diff);
|
|
93
|
+
const violations = [];
|
|
94
|
+
|
|
95
|
+
for (const { file, line, content } of addedLines) {
|
|
96
|
+
const ext = getExtension(file);
|
|
97
|
+
const isFrontend = FRONTEND_EXTENSIONS.has(ext);
|
|
98
|
+
const isPackageJson = file?.endsWith("package.json");
|
|
99
|
+
|
|
100
|
+
if (isFrontend) {
|
|
101
|
+
for (const { id, pattern, severity, message } of allPatterns) {
|
|
102
|
+
if (pattern.test(content)) {
|
|
103
|
+
violations.push({ id, severity, file, line, message, matchedContent: content.trim().slice(0, 200) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isPackageJson) {
|
|
109
|
+
for (const { id, pattern, severity, message } of HEAVY_DEPS) {
|
|
110
|
+
if (pattern.test(content)) {
|
|
111
|
+
violations.push({ id, severity, file, line, message, matchedContent: content.trim().slice(0, 200) });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// perf-guard is advisory by default — only blocks on critical (none built-in are critical)
|
|
118
|
+
const blockOnWarning = Boolean(config?.guards?.perf?.block_on_warning);
|
|
119
|
+
const hasCritical = violations.some(v => v.severity === "critical");
|
|
120
|
+
const hasWarning = violations.some(v => v.severity === "warning");
|
|
121
|
+
const pass = !hasCritical && !(blockOnWarning && hasWarning);
|
|
122
|
+
|
|
123
|
+
return { pass, violations, skipped: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { PERF_PATTERNS, HEAVY_DEPS, FRONTEND_EXTENSIONS };
|
package/src/orchestrator.js
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
} from "./git/automation.js";
|
|
24
24
|
import { resolveRoleMdPath, loadFirstExisting } from "./roles/base-role.js";
|
|
25
25
|
import { applyPolicies } from "./guards/policy-resolver.js";
|
|
26
|
+
import { scanDiff } from "./guards/output-guard.js";
|
|
27
|
+
import { scanPerfDiff } from "./guards/perf-guard.js";
|
|
28
|
+
import { classifyIntent } from "./guards/intent-guard.js";
|
|
26
29
|
import { resolveReviewProfile } from "./review/profiles.js";
|
|
27
30
|
import { CoderRole } from "./roles/coder-role.js";
|
|
28
31
|
import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
@@ -272,7 +275,7 @@ function applyFlagOverrides(pipelineFlags, flags) {
|
|
|
272
275
|
|
|
273
276
|
function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
|
|
274
277
|
const resolvedPolicies = applyPolicies({
|
|
275
|
-
taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || null,
|
|
278
|
+
taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
|
|
276
279
|
policies: config.policies,
|
|
277
280
|
});
|
|
278
281
|
session.resolved_policies = resolvedPolicies;
|
|
@@ -764,6 +767,18 @@ async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBa
|
|
|
764
767
|
|
|
765
768
|
|
|
766
769
|
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults }) {
|
|
770
|
+
// --- Intent classifier (deterministic pre-triage, opt-in) ---
|
|
771
|
+
if (config.guards?.intent?.enabled) {
|
|
772
|
+
const intentResult = classifyIntent(task, config);
|
|
773
|
+
stageResults.intent = intentResult;
|
|
774
|
+
if (intentResult.classified) {
|
|
775
|
+
emitProgress(emitter, makeEvent("intent:classified", { ...eventBase, stage: "intent" }, {
|
|
776
|
+
message: `Intent classified: ${intentResult.taskType} (${intentResult.level}) — ${intentResult.message}`,
|
|
777
|
+
detail: intentResult
|
|
778
|
+
}));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
767
782
|
// --- Discover (pre-triage, opt-in) ---
|
|
768
783
|
if (flags.enableDiscover !== undefined) pipelineFlags.discoverEnabled = Boolean(flags.enableDiscover);
|
|
769
784
|
if (pipelineFlags.discoverEnabled) {
|
|
@@ -811,6 +826,72 @@ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refac
|
|
|
811
826
|
return { action: "ok" };
|
|
812
827
|
}
|
|
813
828
|
|
|
829
|
+
async function runGuardStages({ config, logger, emitter, eventBase, session, iteration }) {
|
|
830
|
+
const outputEnabled = config.guards?.output?.enabled !== false;
|
|
831
|
+
const perfEnabled = config.guards?.perf?.enabled !== false;
|
|
832
|
+
|
|
833
|
+
if (!outputEnabled && !perfEnabled) return { action: "ok" };
|
|
834
|
+
|
|
835
|
+
const baseBranch = config.base_branch || "main";
|
|
836
|
+
let diff;
|
|
837
|
+
try {
|
|
838
|
+
const { generateDiff: genDiff, computeBaseRef: compBase } = await import("./review/diff-generator.js");
|
|
839
|
+
const baseRef = await compBase({ baseBranch });
|
|
840
|
+
diff = await genDiff({ baseRef });
|
|
841
|
+
} catch {
|
|
842
|
+
logger.warn("Guards: could not generate diff, skipping");
|
|
843
|
+
return { action: "ok" };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (!diff) return { action: "ok" };
|
|
847
|
+
|
|
848
|
+
if (outputEnabled) {
|
|
849
|
+
const outputResult = scanDiff(diff, config);
|
|
850
|
+
if (outputResult.violations.length > 0) {
|
|
851
|
+
const critical = outputResult.violations.filter(v => v.severity === "critical");
|
|
852
|
+
const warnings = outputResult.violations.filter(v => v.severity === "warning");
|
|
853
|
+
emitProgress(emitter, makeEvent("guard:output", { ...eventBase, stage: "guard" }, {
|
|
854
|
+
message: `Output guard: ${critical.length} critical, ${warnings.length} warnings`,
|
|
855
|
+
detail: { violations: outputResult.violations }
|
|
856
|
+
}));
|
|
857
|
+
logger.info(`Output guard: ${outputResult.violations.length} violation(s) found`);
|
|
858
|
+
for (const v of outputResult.violations) {
|
|
859
|
+
logger.info(` [${v.severity}] ${v.file}:${v.line} — ${v.message}`);
|
|
860
|
+
}
|
|
861
|
+
await addCheckpoint(session, { stage: "guard-output", iteration, pass: outputResult.pass, violations: outputResult.violations.length });
|
|
862
|
+
|
|
863
|
+
if (!outputResult.pass && config.guards.output.on_violation === "block") {
|
|
864
|
+
await markSessionStatus(session, "failed");
|
|
865
|
+
emitProgress(emitter, makeEvent("guard:blocked", { ...eventBase, stage: "guard" }, {
|
|
866
|
+
message: "Output guard blocked: critical violations detected",
|
|
867
|
+
detail: { violations: critical }
|
|
868
|
+
}));
|
|
869
|
+
return {
|
|
870
|
+
action: "return",
|
|
871
|
+
result: { approved: false, sessionId: session.id, reason: "guard_blocked", violations: critical }
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (perfEnabled) {
|
|
878
|
+
const perfResult = scanPerfDiff(diff, config);
|
|
879
|
+
if (!perfResult.skipped && perfResult.violations.length > 0) {
|
|
880
|
+
emitProgress(emitter, makeEvent("guard:perf", { ...eventBase, stage: "guard" }, {
|
|
881
|
+
message: `Perf guard: ${perfResult.violations.length} issue(s)`,
|
|
882
|
+
detail: { violations: perfResult.violations }
|
|
883
|
+
}));
|
|
884
|
+
logger.info(`Perf guard: ${perfResult.violations.length} issue(s) found`);
|
|
885
|
+
for (const v of perfResult.violations) {
|
|
886
|
+
logger.info(` [${v.severity}] ${v.file}:${v.line} — ${v.message}`);
|
|
887
|
+
}
|
|
888
|
+
await addCheckpoint(session, { stage: "guard-perf", iteration, pass: perfResult.pass, violations: perfResult.violations.length });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return { action: "ok" };
|
|
893
|
+
}
|
|
894
|
+
|
|
814
895
|
async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults }) {
|
|
815
896
|
const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion });
|
|
816
897
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
@@ -947,6 +1028,9 @@ async function runSingleIteration(ctx) {
|
|
|
947
1028
|
const crResult = await runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i });
|
|
948
1029
|
if (crResult.action === "return" || crResult.action === "retry") return crResult;
|
|
949
1030
|
|
|
1031
|
+
const guardResult = await runGuardStages({ config, logger, emitter, eventBase, session, iteration: i });
|
|
1032
|
+
if (guardResult.action === "return") return guardResult;
|
|
1033
|
+
|
|
950
1034
|
const qgResult = await runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults });
|
|
951
1035
|
if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
|
|
952
1036
|
|
package/templates/kj.config.yml
CHANGED
|
@@ -61,6 +61,39 @@ sonarqube:
|
|
|
61
61
|
- "javascript:S1116"
|
|
62
62
|
- "javascript:S3776"
|
|
63
63
|
|
|
64
|
+
# Deterministic Guards
|
|
65
|
+
guards:
|
|
66
|
+
output:
|
|
67
|
+
enabled: true
|
|
68
|
+
# Custom patterns (added to built-in destructive + credential patterns)
|
|
69
|
+
# patterns:
|
|
70
|
+
# - id: custom-pattern
|
|
71
|
+
# pattern: "DANGEROUS_FUNCTION"
|
|
72
|
+
# severity: critical
|
|
73
|
+
# message: "Custom dangerous pattern detected"
|
|
74
|
+
# Protected files (added to built-in: .env, serviceAccountKey.json, etc.)
|
|
75
|
+
# protected_files:
|
|
76
|
+
# - secrets.yml
|
|
77
|
+
# - .env.production
|
|
78
|
+
on_violation: block # block | warn | skip
|
|
79
|
+
perf:
|
|
80
|
+
enabled: true
|
|
81
|
+
block_on_warning: false # true to block on perf warnings (default: advisory only)
|
|
82
|
+
# Custom perf patterns (added to built-in frontend anti-patterns)
|
|
83
|
+
# patterns:
|
|
84
|
+
# - id: custom-perf
|
|
85
|
+
# pattern: "eval\\("
|
|
86
|
+
# severity: warning
|
|
87
|
+
# message: "eval() detected"
|
|
88
|
+
intent:
|
|
89
|
+
enabled: false # enable for deterministic pre-triage classification
|
|
90
|
+
confidence_threshold: 0.85
|
|
91
|
+
# Custom intent patterns (evaluated before built-in)
|
|
92
|
+
# patterns:
|
|
93
|
+
# - keywords: ["readme", "docs", "documentation"]
|
|
94
|
+
# taskType: doc
|
|
95
|
+
# confidence: 0.9
|
|
96
|
+
|
|
64
97
|
# Git (post-approval)
|
|
65
98
|
git:
|
|
66
99
|
auto_commit: false
|