@plumpslabs/kuma 2.3.20 → 2.3.23
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 +18 -0
- package/dist/index.js +10316 -380
- package/package.json +2 -2
- package/packages/ide/studio/dist/index.js +90 -9
- package/packages/ide/studio/public/index.html +528 -233
- package/dist/agentDetector-YOWQVJFR.js +0 -186
- package/dist/chunk-3BRBJZ7P.js +0 -1055
- package/dist/chunk-3OHYYXYN.js +0 -71
- package/dist/chunk-ABKE45T4.js +0 -1264
- package/dist/chunk-E2KFPEBT.js +0 -183
- package/dist/chunk-FKRSI5U5.js +0 -282
- package/dist/chunk-GFLSAXAH.js +0 -155
- package/dist/chunk-L7F67KUP.js +0 -172
- package/dist/chunk-LVKOGXLC.js +0 -658
- package/dist/chunk-PRUTTZBS.js +0 -1113
- package/dist/contextDigest-QB5XHPXE.js +0 -177
- package/dist/domainRules-QLPAQASB.js +0 -20
- package/dist/init-PL4XL662.js +0 -15
- package/dist/kumaAstValidator-CNM7FHYA.js +0 -150
- package/dist/kumaCheckpoint-J2LDQMEO.js +0 -207
- package/dist/kumaCodeScanner-J6B2EHGI.js +0 -563
- package/dist/kumaContractEngine-KX27T4N7.js +0 -305
- package/dist/kumaDb-4XZ5S2LH.js +0 -65
- package/dist/kumaDriftDetector-TOORILSZ.js +0 -237
- package/dist/kumaGotchas-XRGFFBTA.js +0 -151
- package/dist/kumaGraph-UMXZNGYF.js +0 -44
- package/dist/kumaMemory-FBJMV77G.js +0 -16
- package/dist/kumaMiner-XJETL7TL.js +0 -176
- package/dist/kumaPolicyEngine-2QDJDLM7.js +0 -311
- package/dist/kumaProgressiveContext-MWEDRXOH.js +0 -231
- package/dist/kumaSearch-PV4QTKE7.js +0 -321
- package/dist/kumaTrajectory-7NOAVNG2.js +0 -460
- package/dist/kumaVerifier-6YEGC77M.js +0 -265
- package/dist/kumaVisualize-264OEBGJ.js +0 -264
- package/dist/pathValidator-V4DC6U6Z.js +0 -22
- package/dist/safetyAudit-O45SPNTS.js +0 -12
- package/dist/safetyScore-TMMRD2MV.js +0 -333
- package/dist/sessionMemory-YPKVIOMV.js +0 -6
- package/dist/skillGenerator-PWEJKZNX.js +0 -304
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendToLayer,
|
|
3
|
-
checkFileGotchas,
|
|
4
|
-
getActiveGotchas
|
|
5
|
-
} from "./chunk-FKRSI5U5.js";
|
|
6
|
-
import {
|
|
7
|
-
sessionMemory
|
|
8
|
-
} from "./chunk-LVKOGXLC.js";
|
|
9
|
-
import {
|
|
10
|
-
getDb,
|
|
11
|
-
saveDb
|
|
12
|
-
} from "./chunk-3BRBJZ7P.js";
|
|
13
|
-
import "./chunk-E2KFPEBT.js";
|
|
14
|
-
|
|
15
|
-
// src/engine/kumaGotchas.ts
|
|
16
|
-
async function ensureGotchasSchema() {
|
|
17
|
-
const db = await getDb();
|
|
18
|
-
db.exec(`
|
|
19
|
-
CREATE TABLE IF NOT EXISTS known_gotchas (
|
|
20
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
-
file_path TEXT NOT NULL,
|
|
22
|
-
description TEXT NOT NULL,
|
|
23
|
-
severity TEXT NOT NULL DEFAULT 'medium'
|
|
24
|
-
CHECK(severity IN ('low','medium','high','critical')),
|
|
25
|
-
workaround TEXT,
|
|
26
|
-
added_by TEXT DEFAULT 'agent',
|
|
27
|
-
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
28
|
-
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
29
|
-
);
|
|
30
|
-
CREATE INDEX IF NOT EXISTS idx_gotchas_file ON known_gotchas(file_path);
|
|
31
|
-
CREATE INDEX IF NOT EXISTS idx_gotchas_severity ON known_gotchas(severity);
|
|
32
|
-
`);
|
|
33
|
-
saveDb();
|
|
34
|
-
}
|
|
35
|
-
async function addGotcha(entry) {
|
|
36
|
-
try {
|
|
37
|
-
await ensureGotchasSchema();
|
|
38
|
-
const db = await getDb();
|
|
39
|
-
const severity = entry.severity || "medium";
|
|
40
|
-
const formatted = [
|
|
41
|
-
`### ${entry.filePath} \u2014 ${entry.description}`,
|
|
42
|
-
`- **Issue**: ${entry.description}`,
|
|
43
|
-
`- **Severity**: ${severity}`,
|
|
44
|
-
entry.workaround ? `- **Workaround**: ${entry.workaround}` : "",
|
|
45
|
-
`- **Added**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
46
|
-
].filter(Boolean).join("\n");
|
|
47
|
-
db.run(
|
|
48
|
-
`INSERT INTO known_gotchas (file_path, description, severity, workaround) VALUES (?, ?, ?, ?)`,
|
|
49
|
-
[entry.filePath, entry.description, severity, entry.workaround || null]
|
|
50
|
-
);
|
|
51
|
-
const mdResult = appendToLayer("gotcha", formatted);
|
|
52
|
-
saveDb();
|
|
53
|
-
sessionMemory.recordToolCall("kuma_gotcha_add", {
|
|
54
|
-
filePath: entry.filePath,
|
|
55
|
-
severity
|
|
56
|
-
});
|
|
57
|
-
return `\u2705 **Gotcha recorded**: ${entry.filePath} \u2014 ${entry.description}
|
|
58
|
-
${mdResult}`;
|
|
59
|
-
} catch (err) {
|
|
60
|
-
return `\u274C Failed to add gotcha: ${err}`;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function listGotchas(params) {
|
|
64
|
-
try {
|
|
65
|
-
await ensureGotchasSchema();
|
|
66
|
-
const db = await getDb();
|
|
67
|
-
let sql = "SELECT * FROM known_gotchas WHERE 1=1";
|
|
68
|
-
const bind = [];
|
|
69
|
-
if (params.filePath) {
|
|
70
|
-
sql += " AND file_path LIKE ?";
|
|
71
|
-
bind.push(`%${params.filePath}%`);
|
|
72
|
-
}
|
|
73
|
-
if (params.severity) {
|
|
74
|
-
sql += " AND severity = ?";
|
|
75
|
-
bind.push(params.severity);
|
|
76
|
-
}
|
|
77
|
-
sql += " ORDER BY severity DESC, created_at DESC LIMIT 50";
|
|
78
|
-
const stmt = db.prepare(sql);
|
|
79
|
-
stmt.bind(bind);
|
|
80
|
-
const results = [];
|
|
81
|
-
while (stmt.step()) results.push(stmt.getAsObject());
|
|
82
|
-
stmt.free();
|
|
83
|
-
if (results.length === 0) {
|
|
84
|
-
return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
|
|
85
|
-
}
|
|
86
|
-
const lines = [
|
|
87
|
-
`\u26A0\uFE0F **Known Gotchas** \u2014 ${results.length} recorded`,
|
|
88
|
-
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
89
|
-
""
|
|
90
|
-
];
|
|
91
|
-
for (const g of results) {
|
|
92
|
-
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
93
|
-
lines.push(`${icon} [${g.severity}] ${g.file_path}`);
|
|
94
|
-
lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
|
|
95
|
-
if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
|
|
96
|
-
lines.push("");
|
|
97
|
-
}
|
|
98
|
-
return lines.join("\n");
|
|
99
|
-
} catch (err) {
|
|
100
|
-
return `Error: ${err}`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function checkGotchasForFile(filePath) {
|
|
104
|
-
const warnings = [];
|
|
105
|
-
const markdownWarnings = checkFileGotchas(filePath);
|
|
106
|
-
try {
|
|
107
|
-
const gotchas = getActiveGotchas();
|
|
108
|
-
for (const g of gotchas) {
|
|
109
|
-
if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
|
|
110
|
-
if (!markdownWarnings.some((w) => w.includes(g.description))) {
|
|
111
|
-
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
112
|
-
warnings.push(`${icon} **Known Gotcha**: ${g.description} (${g.severity})`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
}
|
|
118
|
-
return [.../* @__PURE__ */ new Set([...markdownWarnings, ...warnings])];
|
|
119
|
-
}
|
|
120
|
-
async function syncGotchasToDb() {
|
|
121
|
-
try {
|
|
122
|
-
await ensureGotchasSchema();
|
|
123
|
-
const db = await getDb();
|
|
124
|
-
const gotchas = getActiveGotchas();
|
|
125
|
-
let synced = 0;
|
|
126
|
-
for (const g of gotchas) {
|
|
127
|
-
const checkStmt = db.prepare(
|
|
128
|
-
`SELECT id FROM known_gotchas WHERE file_path = ? AND description = ?`
|
|
129
|
-
);
|
|
130
|
-
checkStmt.bind([g.filePath, g.description]);
|
|
131
|
-
const exists = checkStmt.step();
|
|
132
|
-
checkStmt.free();
|
|
133
|
-
if (exists) continue;
|
|
134
|
-
db.run(
|
|
135
|
-
`INSERT INTO known_gotchas (file_path, description, severity) VALUES (?, ?, ?)`,
|
|
136
|
-
[g.filePath, g.description, g.severity]
|
|
137
|
-
);
|
|
138
|
-
synced++;
|
|
139
|
-
}
|
|
140
|
-
saveDb();
|
|
141
|
-
return { synced };
|
|
142
|
-
} catch {
|
|
143
|
-
return { synced: 0 };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
export {
|
|
147
|
-
addGotcha,
|
|
148
|
-
checkGotchasForFile,
|
|
149
|
-
listGotchas,
|
|
150
|
-
syncGotchasToDb
|
|
151
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addEdge,
|
|
3
|
-
analyzeImpact,
|
|
4
|
-
buildFromSessionMemory,
|
|
5
|
-
formatFlow,
|
|
6
|
-
formatImpact,
|
|
7
|
-
getGraphStats,
|
|
8
|
-
listFederatedReferences,
|
|
9
|
-
nodeId,
|
|
10
|
-
queryGraph,
|
|
11
|
-
recordApiRoute,
|
|
12
|
-
recordDomainFlow,
|
|
13
|
-
recordFileDefinition,
|
|
14
|
-
recordFunctionCall,
|
|
15
|
-
recordImport,
|
|
16
|
-
recordTestRelation,
|
|
17
|
-
resolveFederatedNode,
|
|
18
|
-
searchGraph,
|
|
19
|
-
traceFlow,
|
|
20
|
-
upsertNode
|
|
21
|
-
} from "./chunk-PRUTTZBS.js";
|
|
22
|
-
import "./chunk-3BRBJZ7P.js";
|
|
23
|
-
import "./chunk-E2KFPEBT.js";
|
|
24
|
-
export {
|
|
25
|
-
addEdge,
|
|
26
|
-
analyzeImpact,
|
|
27
|
-
buildFromSessionMemory,
|
|
28
|
-
formatFlow,
|
|
29
|
-
formatImpact,
|
|
30
|
-
getGraphStats,
|
|
31
|
-
listFederatedReferences,
|
|
32
|
-
nodeId,
|
|
33
|
-
queryGraph,
|
|
34
|
-
recordApiRoute,
|
|
35
|
-
recordDomainFlow,
|
|
36
|
-
recordFileDefinition,
|
|
37
|
-
recordFunctionCall,
|
|
38
|
-
recordImport,
|
|
39
|
-
recordTestRelation,
|
|
40
|
-
resolveFederatedNode,
|
|
41
|
-
searchGraph,
|
|
42
|
-
traceFlow,
|
|
43
|
-
upsertNode
|
|
44
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
formatDecisionTemplate,
|
|
3
|
-
getProactiveMemories,
|
|
4
|
-
recordDecision,
|
|
5
|
-
scoreMemoryRelevance,
|
|
6
|
-
shouldRecordDecision
|
|
7
|
-
} from "./chunk-L7F67KUP.js";
|
|
8
|
-
import "./chunk-LVKOGXLC.js";
|
|
9
|
-
import "./chunk-E2KFPEBT.js";
|
|
10
|
-
export {
|
|
11
|
-
formatDecisionTemplate,
|
|
12
|
-
getProactiveMemories,
|
|
13
|
-
recordDecision,
|
|
14
|
-
scoreMemoryRelevance,
|
|
15
|
-
shouldRecordDecision
|
|
16
|
-
};
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
recordDecision
|
|
3
|
-
} from "./chunk-L7F67KUP.js";
|
|
4
|
-
import "./chunk-LVKOGXLC.js";
|
|
5
|
-
import {
|
|
6
|
-
recordDecisionLog
|
|
7
|
-
} from "./chunk-3BRBJZ7P.js";
|
|
8
|
-
import "./chunk-E2KFPEBT.js";
|
|
9
|
-
|
|
10
|
-
// src/engine/kumaMiner.ts
|
|
11
|
-
import fs from "fs";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
|
-
import fastGlob from "fast-glob";
|
|
15
|
-
async function mineHistoricalDecisions(options = {}) {
|
|
16
|
-
const root = process.cwd();
|
|
17
|
-
const limit = options.limit || 15;
|
|
18
|
-
const candidates = [];
|
|
19
|
-
try {
|
|
20
|
-
const sinceFlag = options.since ? `--since="${options.since}"` : `--since="1 year"`;
|
|
21
|
-
const gitCmd = `git log ${sinceFlag} -n 100 --pretty=format:"%h|%an|%ad|%s" --date=short`;
|
|
22
|
-
const gitOutput = execSync(gitCmd, { cwd: root, encoding: "utf-8", timeout: 15e3 });
|
|
23
|
-
const keywords = ["fix", "revert", "hack", "workaround", "urgent", "hotfix", "don't touch", "temporary", "deprecated", "workaround"];
|
|
24
|
-
const lines = gitOutput.split("\n").filter(Boolean);
|
|
25
|
-
const matchingHashes = [];
|
|
26
|
-
for (const line of lines) {
|
|
27
|
-
const parts = line.split("|");
|
|
28
|
-
if (parts.length < 4) continue;
|
|
29
|
-
const [hash, author, date, subject] = parts;
|
|
30
|
-
const lowerSubj = subject.toLowerCase();
|
|
31
|
-
const matchedKeyword = keywords.find((kw) => lowerSubj.includes(kw));
|
|
32
|
-
if (matchedKeyword) {
|
|
33
|
-
matchingHashes.push({ hash, author, date, subject, keyword: matchedKeyword });
|
|
34
|
-
if (matchingHashes.length >= limit) break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
let changedFilesMap = /* @__PURE__ */ new Map();
|
|
38
|
-
if (matchingHashes.length > 0) {
|
|
39
|
-
try {
|
|
40
|
-
const hashesStr = matchingHashes.map((h) => h.hash).join(" ");
|
|
41
|
-
const batchOutput = execSync(
|
|
42
|
-
`git show --name-only --oneline ${hashesStr} 2>/dev/null | head -200`,
|
|
43
|
-
{ cwd: root, encoding: "utf-8", timeout: 1e4, maxBuffer: 256 * 1024 }
|
|
44
|
-
);
|
|
45
|
-
let currentHash = "";
|
|
46
|
-
const files = [];
|
|
47
|
-
for (const line of batchOutput.split("\n")) {
|
|
48
|
-
if (matchingHashes.find((h) => line.startsWith(h.hash))) {
|
|
49
|
-
if (currentHash && files.length > 0) {
|
|
50
|
-
changedFilesMap.set(currentHash, files.slice(0, 3).join(", "));
|
|
51
|
-
}
|
|
52
|
-
currentHash = line.split(" ")[0];
|
|
53
|
-
files.length = 0;
|
|
54
|
-
} else if (line.trim() && !line.startsWith("diff")) {
|
|
55
|
-
files.push(line.trim());
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (currentHash && files.length > 0) {
|
|
59
|
-
changedFilesMap.set(currentHash, files.slice(0, 3).join(", "));
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const { hash, author, date, subject, keyword: matchedKeyword } of matchingHashes) {
|
|
65
|
-
let changedFiles = changedFilesMap.get(hash) || "";
|
|
66
|
-
if (!changedFiles) {
|
|
67
|
-
try {
|
|
68
|
-
changedFiles = execSync(`git show --name-only --oneline ${hash}`, { cwd: root, encoding: "utf-8", timeout: 5e3 }).split("\n").slice(1).filter(Boolean).slice(0, 3).join(", ");
|
|
69
|
-
} catch {
|
|
70
|
-
}
|
|
71
|
-
candidates.push({
|
|
72
|
-
id: `git-${hash}`,
|
|
73
|
-
type: "git_commit",
|
|
74
|
-
title: `[Git Commit] ${subject}`,
|
|
75
|
-
context: `Commit ${hash} by ${author} on ${date}.${changedFiles ? ` Files touched: ${changedFiles}` : ""}`,
|
|
76
|
-
rationale: `Historical signal pattern "${matchedKeyword}" found in commit message.`,
|
|
77
|
-
commitHash: hash,
|
|
78
|
-
pattern: matchedKeyword
|
|
79
|
-
});
|
|
80
|
-
if (candidates.length >= limit) break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
}
|
|
85
|
-
if (candidates.length < limit) {
|
|
86
|
-
try {
|
|
87
|
-
const scopePattern = options.scope ? `**/*${options.scope}*.*` : "**/*.{ts,js,py,go,rs,java,md}";
|
|
88
|
-
const files = await fastGlob(scopePattern, {
|
|
89
|
-
cwd: root,
|
|
90
|
-
ignore: ["node_modules/**", "dist/**", ".kuma/**", ".git/**", "coverage/**"],
|
|
91
|
-
onlyFiles: true
|
|
92
|
-
});
|
|
93
|
-
const commentPatterns = [
|
|
94
|
-
{ tag: "HACK", regex: /\/\/\s*HACK:?\s*(.*)|#\s*HACK:?\s*(.*)/i },
|
|
95
|
-
{ tag: "FIXME", regex: /\/\/\s*FIXME:?\s*(.*)|#\s*FIXME:?\s*(.*)/i },
|
|
96
|
-
{ tag: "TODO", regex: /\/\/\s*TODO:?\s*(.*)|#\s*TODO:?\s*(.*)/i },
|
|
97
|
-
{ tag: "XXX", regex: /\/\/\s*XXX:?\s*(.*)|#\s*XXX:?\s*(.*)/i },
|
|
98
|
-
{ tag: "WARNING", regex: /\/\/\s*WARNING:?\s*(.*)|#\s*WARNING:?\s*(.*)/i }
|
|
99
|
-
];
|
|
100
|
-
for (const file of files.slice(0, 50)) {
|
|
101
|
-
if (candidates.length >= limit) break;
|
|
102
|
-
const fullPath = path.join(root, file);
|
|
103
|
-
if (!fs.existsSync(fullPath)) continue;
|
|
104
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
105
|
-
const lines = content.split("\n");
|
|
106
|
-
for (let i = 0; i < lines.length; i++) {
|
|
107
|
-
const l = lines[i];
|
|
108
|
-
for (const cp of commentPatterns) {
|
|
109
|
-
const match = cp.regex.exec(l);
|
|
110
|
-
if (match) {
|
|
111
|
-
const text = (match[1] || match[2] || l).trim();
|
|
112
|
-
candidates.push({
|
|
113
|
-
id: `comment-${file}-${i + 1}`,
|
|
114
|
-
type: "inline_comment",
|
|
115
|
-
title: `[${cp.tag}] ${file}:${i + 1}`,
|
|
116
|
-
context: `Inline comment marker "${cp.tag}" found in file "${file}" at line ${i + 1}.`,
|
|
117
|
-
rationale: text || l.trim(),
|
|
118
|
-
filePath: file,
|
|
119
|
-
lineNumber: i + 1,
|
|
120
|
-
pattern: cp.tag
|
|
121
|
-
});
|
|
122
|
-
if (candidates.length >= limit) break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (candidates.length >= limit) break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (options.confirm && candidates.length > 0) {
|
|
132
|
-
let recordedCount = 0;
|
|
133
|
-
for (const c of candidates) {
|
|
134
|
-
await recordDecisionLog({
|
|
135
|
-
title: c.title,
|
|
136
|
-
context: c.context,
|
|
137
|
-
rationale: c.rationale,
|
|
138
|
-
outcome: `Mined from ${c.type} (${c.pattern})`,
|
|
139
|
-
status: "proposed"
|
|
140
|
-
});
|
|
141
|
-
await recordDecision({
|
|
142
|
-
title: c.title,
|
|
143
|
-
context: c.context,
|
|
144
|
-
options: [],
|
|
145
|
-
rationale: c.rationale,
|
|
146
|
-
outcome: `Mined from ${c.type}`,
|
|
147
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
148
|
-
});
|
|
149
|
-
recordedCount++;
|
|
150
|
-
}
|
|
151
|
-
return `\u26CF\uFE0F **Mined & Recorded ${recordedCount} Decisions** into decision log & graph.
|
|
152
|
-
Use \`kuma_memory({ action: 'decision_log' })\` to view all active & proposed decisions.`;
|
|
153
|
-
}
|
|
154
|
-
if (candidates.length === 0) {
|
|
155
|
-
return "\u26CF\uFE0F **Decision Mining**: No significant historical decision markers (HACK/FIXME/git fix) found for the requested scope.";
|
|
156
|
-
}
|
|
157
|
-
const report = [
|
|
158
|
-
`\u26CF\uFE0F **Decision Mining Candidates** (${candidates.length} candidate(s) found)`,
|
|
159
|
-
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
160
|
-
"Review these draft historical decisions mined from git history & inline comments:",
|
|
161
|
-
""
|
|
162
|
-
];
|
|
163
|
-
candidates.forEach((c, idx) => {
|
|
164
|
-
report.push(`**${idx + 1}. ${c.title}**`);
|
|
165
|
-
report.push(` \u2022 **Type**: \`${c.type}\` | **Signal**: \`${c.pattern}\``);
|
|
166
|
-
report.push(` \u2022 **Context**: ${c.context}`);
|
|
167
|
-
report.push(` \u2022 **Rationale**: ${c.rationale}`);
|
|
168
|
-
report.push("");
|
|
169
|
-
});
|
|
170
|
-
report.push("\u{1F4A1} *To accept and record these mined decisions into knowledge graph & decision.md:*");
|
|
171
|
-
report.push(`Run: \`kuma_memory({ action: 'mine', confirm: true${options.scope ? `, scope: '${options.scope}'` : ""} })\``);
|
|
172
|
-
return report.join("\n");
|
|
173
|
-
}
|
|
174
|
-
export {
|
|
175
|
-
mineHistoricalDecisions
|
|
176
|
-
};
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
sessionMemory
|
|
3
|
-
} from "./chunk-LVKOGXLC.js";
|
|
4
|
-
import {
|
|
5
|
-
getProjectRoot
|
|
6
|
-
} from "./chunk-E2KFPEBT.js";
|
|
7
|
-
|
|
8
|
-
// src/engine/kumaPolicyEngine.ts
|
|
9
|
-
import fs from "fs";
|
|
10
|
-
import path from "path";
|
|
11
|
-
var DEFAULT_POLICY_CONFIG = {
|
|
12
|
-
version: 1,
|
|
13
|
-
name: "Kuma Default Security Policy",
|
|
14
|
-
description: "Default safety policy for AI coding agents",
|
|
15
|
-
rules: [
|
|
16
|
-
{
|
|
17
|
-
id: "block-rm-rf",
|
|
18
|
-
description: "Block recursive force deletion",
|
|
19
|
-
severity: "error",
|
|
20
|
-
patterns: ["rm -rf", "rm -fr", "rm --recursive --force"],
|
|
21
|
-
action: "block",
|
|
22
|
-
requireOverride: true
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: "block-git-force-push",
|
|
26
|
-
description: "Block force push to git",
|
|
27
|
-
severity: "error",
|
|
28
|
-
patterns: ["git push --force", "git push -f"],
|
|
29
|
-
action: "block",
|
|
30
|
-
requireOverride: true
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: "block-npm-publish",
|
|
34
|
-
description: "Block package publishing",
|
|
35
|
-
severity: "error",
|
|
36
|
-
patterns: ["npm publish", "yarn publish", "pnpm publish"],
|
|
37
|
-
action: "block",
|
|
38
|
-
requireOverride: true
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: "block-pipe-to-shell",
|
|
42
|
-
description: "Block curl/wget pipe to shell",
|
|
43
|
-
severity: "error",
|
|
44
|
-
patterns: ["curl | bash", "curl | sh", "wget -O - | bash", "curl | sudo"],
|
|
45
|
-
action: "block",
|
|
46
|
-
requireOverride: true
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: "warn-destructive-db",
|
|
50
|
-
description: "Warn about destructive database operations",
|
|
51
|
-
severity: "warning",
|
|
52
|
-
patterns: ["DROP DATABASE", "DROP TABLE", "TRUNCATE", "DELETE FROM"],
|
|
53
|
-
action: "warn"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: "block-prod-deploy",
|
|
57
|
-
description: "Block production deployments without override",
|
|
58
|
-
severity: "error",
|
|
59
|
-
patterns: ["deploy --production", "deploy --prod", "deploy:production"],
|
|
60
|
-
action: "block",
|
|
61
|
-
requireOverride: true
|
|
62
|
-
}
|
|
63
|
-
],
|
|
64
|
-
block_commands: [
|
|
65
|
-
"rm -rf",
|
|
66
|
-
"rm -fr",
|
|
67
|
-
"git push --force",
|
|
68
|
-
"git push -f",
|
|
69
|
-
"npm publish",
|
|
70
|
-
"yarn publish",
|
|
71
|
-
"pnpm publish",
|
|
72
|
-
"curl | bash",
|
|
73
|
-
"curl | sh"
|
|
74
|
-
],
|
|
75
|
-
never_touch: [
|
|
76
|
-
".env",
|
|
77
|
-
".env.local",
|
|
78
|
-
".env.production",
|
|
79
|
-
".env.development",
|
|
80
|
-
"package-lock.json",
|
|
81
|
-
"yarn.lock",
|
|
82
|
-
"pnpm-lock.yaml",
|
|
83
|
-
"node_modules/**"
|
|
84
|
-
]
|
|
85
|
-
};
|
|
86
|
-
function loadPolicyConfig() {
|
|
87
|
-
try {
|
|
88
|
-
const root = getProjectRoot();
|
|
89
|
-
const policyPath = path.join(root, ".kuma", "POLICY.json");
|
|
90
|
-
if (fs.existsSync(policyPath)) {
|
|
91
|
-
const content = fs.readFileSync(policyPath, "utf-8");
|
|
92
|
-
const config = JSON.parse(content);
|
|
93
|
-
return { ...DEFAULT_POLICY_CONFIG, ...config };
|
|
94
|
-
}
|
|
95
|
-
const ymlPath = path.join(root, ".kuma", "policy.yml");
|
|
96
|
-
if (fs.existsSync(ymlPath)) {
|
|
97
|
-
console.error("[PolicyEngine] Found legacy policy.yml \u2014 consider migrating to POLICY.json");
|
|
98
|
-
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error(`[PolicyEngine] Failed to load policy config: ${err}`);
|
|
101
|
-
}
|
|
102
|
-
return DEFAULT_POLICY_CONFIG;
|
|
103
|
-
}
|
|
104
|
-
function savePolicyConfig(config) {
|
|
105
|
-
try {
|
|
106
|
-
const root = getProjectRoot();
|
|
107
|
-
const policyPath = path.join(root, ".kuma", "POLICY.json");
|
|
108
|
-
const dir = path.dirname(policyPath);
|
|
109
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
110
|
-
fs.writeFileSync(policyPath, JSON.stringify(config, null, 2), "utf-8");
|
|
111
|
-
return `\u2705 Policy config saved to .kuma/POLICY.json (${config.rules.length} rules)`;
|
|
112
|
-
} catch (err) {
|
|
113
|
-
return `\u274C Failed to save policy: ${err}`;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function evaluateCommand(command) {
|
|
117
|
-
const config = loadPolicyConfig();
|
|
118
|
-
const warnings = [];
|
|
119
|
-
const cmdLower = command.toLowerCase();
|
|
120
|
-
for (const rule of config.rules) {
|
|
121
|
-
const matches = rule.patterns.some((p) => cmdLower.includes(p.toLowerCase()));
|
|
122
|
-
if (!matches) continue;
|
|
123
|
-
if (rule.action === "block" || rule.severity === "error") {
|
|
124
|
-
return {
|
|
125
|
-
allowed: false,
|
|
126
|
-
blockedBy: rule,
|
|
127
|
-
warnings,
|
|
128
|
-
requiresOverride: rule.requireOverride ?? false,
|
|
129
|
-
message: `\u26D4 Policy violation: "${rule.description}" (rule: ${rule.id})`
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if (rule.action === "warn") {
|
|
133
|
-
warnings.push(rule);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
for (const cmd of config.block_commands || []) {
|
|
137
|
-
if (cmdLower.includes(cmd.toLowerCase())) {
|
|
138
|
-
return {
|
|
139
|
-
allowed: false,
|
|
140
|
-
blockedBy: {
|
|
141
|
-
id: "block-command",
|
|
142
|
-
description: `Blocked command: ${cmd}`,
|
|
143
|
-
severity: "error",
|
|
144
|
-
patterns: [cmd],
|
|
145
|
-
action: "block",
|
|
146
|
-
requireOverride: true
|
|
147
|
-
},
|
|
148
|
-
warnings,
|
|
149
|
-
requiresOverride: true,
|
|
150
|
-
message: `\u26D4 Command matches blocked pattern: "${cmd}"`
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
allowed: true,
|
|
156
|
-
blockedBy: null,
|
|
157
|
-
warnings,
|
|
158
|
-
requiresOverride: false,
|
|
159
|
-
message: warnings.length > 0 ? `\u26A0\uFE0F Command allowed with ${warnings.length} warning(s)` : "\u2705 Command allowed by policy"
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function evaluateFilePath(filePath) {
|
|
163
|
-
const config = loadPolicyConfig();
|
|
164
|
-
const filesToCheck = config.never_touch || DEFAULT_POLICY_CONFIG.never_touch || [];
|
|
165
|
-
for (const pattern of filesToCheck) {
|
|
166
|
-
if (matchesGlob(pattern, filePath)) {
|
|
167
|
-
return {
|
|
168
|
-
allowed: false,
|
|
169
|
-
blockedBy: {
|
|
170
|
-
id: "never-touch",
|
|
171
|
-
description: `File matches never_touch pattern: ${pattern}`,
|
|
172
|
-
severity: "error",
|
|
173
|
-
patterns: [pattern],
|
|
174
|
-
action: "block",
|
|
175
|
-
requireOverride: true
|
|
176
|
-
},
|
|
177
|
-
warnings: [],
|
|
178
|
-
requiresOverride: true,
|
|
179
|
-
message: `\u26D4 File "${filePath}" is protected by never_touch policy (pattern: ${pattern}). Use kuma_safety({ action: 'override' }) to bypass.`
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
allowed: true,
|
|
185
|
-
blockedBy: null,
|
|
186
|
-
warnings: [],
|
|
187
|
-
requiresOverride: false,
|
|
188
|
-
message: "\u2705 File allowed by policy"
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function evaluateDatabaseAction(action) {
|
|
192
|
-
const cmdLower = action.toLowerCase();
|
|
193
|
-
if ((cmdLower.includes("drop") || cmdLower.includes("truncate")) && (cmdLower.includes("database") || cmdLower.includes("table") || cmdLower.includes("schema"))) {
|
|
194
|
-
return {
|
|
195
|
-
allowed: false,
|
|
196
|
-
blockedBy: {
|
|
197
|
-
id: "block-destructive-db",
|
|
198
|
-
description: "Destructive database operation blocked",
|
|
199
|
-
severity: "error",
|
|
200
|
-
patterns: ["DROP DATABASE", "DROP TABLE", "TRUNCATE", "DELETE FROM"],
|
|
201
|
-
action: "block",
|
|
202
|
-
requireOverride: true
|
|
203
|
-
},
|
|
204
|
-
warnings: [],
|
|
205
|
-
requiresOverride: true,
|
|
206
|
-
message: `\u26D4 Destructive database action blocked: "${action}". Use kuma_safety({ action: 'override' }) with a clear reason.`
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
allowed: true,
|
|
211
|
-
blockedBy: null,
|
|
212
|
-
warnings: [],
|
|
213
|
-
requiresOverride: false,
|
|
214
|
-
message: "\u2705 Database action allowed by policy"
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
async function processOverride(request) {
|
|
218
|
-
const { recordAudit } = await import("./safetyAudit-O45SPNTS.js");
|
|
219
|
-
await recordAudit({
|
|
220
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
221
|
-
toolName: request.toolName,
|
|
222
|
-
action: "policy_override",
|
|
223
|
-
filePath: request.filePath,
|
|
224
|
-
riskLevel: "high",
|
|
225
|
-
policyViolations: 1,
|
|
226
|
-
allowed: true,
|
|
227
|
-
durationMs: 0,
|
|
228
|
-
metadata: {
|
|
229
|
-
override: true,
|
|
230
|
-
reason: request.reason,
|
|
231
|
-
command: request.command
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
sessionMemory.recordToolCall("kuma_policy_override", {
|
|
235
|
-
toolName: request.toolName,
|
|
236
|
-
reason: request.reason,
|
|
237
|
-
filePath: request.filePath,
|
|
238
|
-
command: request.command
|
|
239
|
-
});
|
|
240
|
-
return [
|
|
241
|
-
`\u26A0\uFE0F **Policy Override** \u2014 ${request.toolName}`,
|
|
242
|
-
`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
|
|
243
|
-
"",
|
|
244
|
-
`\u{1F4DD} Reason: ${request.reason}`,
|
|
245
|
-
request.filePath ? `\u{1F4C4} File: ${request.filePath}` : "",
|
|
246
|
-
request.command ? `\u{1F4BB} Command: ${request.command}` : "",
|
|
247
|
-
"",
|
|
248
|
-
"\u{1F534} This override is recorded in the safety audit trail.",
|
|
249
|
-
"\u{1F534} Overrides reduce project safety \u2014 use sparingly."
|
|
250
|
-
].filter(Boolean).join("\n");
|
|
251
|
-
}
|
|
252
|
-
function formatPolicyStatus() {
|
|
253
|
-
const config = loadPolicyConfig();
|
|
254
|
-
const lines = [
|
|
255
|
-
"\u{1F4DC} **Policy-as-Code Engine**",
|
|
256
|
-
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
257
|
-
"",
|
|
258
|
-
`\u{1F4CB} Policy: ${config.name}`,
|
|
259
|
-
`\u{1F522} Version: ${config.version}`,
|
|
260
|
-
`\u{1F4DD} Rules: ${config.rules.length}`,
|
|
261
|
-
"",
|
|
262
|
-
"**Rules:**"
|
|
263
|
-
];
|
|
264
|
-
for (const rule of config.rules) {
|
|
265
|
-
const icon = rule.action === "block" ? "\u{1F534}" : rule.action === "warn" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
266
|
-
const override = rule.requireOverride ? " \u{1F511} override" : "";
|
|
267
|
-
lines.push(` ${icon} [${rule.action}] ${rule.description}${override}`);
|
|
268
|
-
lines.push(` Patterns: ${rule.patterns.slice(0, 3).join(", ")}`);
|
|
269
|
-
}
|
|
270
|
-
const neverTouch = config.never_touch || [];
|
|
271
|
-
if (neverTouch.length > 0) {
|
|
272
|
-
lines.push("", "**Protected Files (never_touch):**");
|
|
273
|
-
for (const p of neverTouch) {
|
|
274
|
-
lines.push(` \u{1F6E1}\uFE0F ${p}`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return lines.join("\n");
|
|
278
|
-
}
|
|
279
|
-
function matchesGlob(pattern, filePath) {
|
|
280
|
-
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
281
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
282
|
-
let regexStr = "^";
|
|
283
|
-
for (let i = 0; i < normalizedPattern.length; i++) {
|
|
284
|
-
const ch = normalizedPattern[i];
|
|
285
|
-
if (ch === "*" && normalizedPattern[i + 1] === "*" && normalizedPattern[i + 2] === "/") {
|
|
286
|
-
regexStr += "(.+/)?";
|
|
287
|
-
i += 2;
|
|
288
|
-
} else if (ch === "*") {
|
|
289
|
-
regexStr += "[^/]*";
|
|
290
|
-
} else if (ch === "?") {
|
|
291
|
-
regexStr += "[^/]";
|
|
292
|
-
} else {
|
|
293
|
-
regexStr += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
regexStr += "$";
|
|
297
|
-
try {
|
|
298
|
-
return new RegExp(regexStr).test(normalizedPath);
|
|
299
|
-
} catch {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
export {
|
|
304
|
-
evaluateCommand,
|
|
305
|
-
evaluateDatabaseAction,
|
|
306
|
-
evaluateFilePath,
|
|
307
|
-
formatPolicyStatus,
|
|
308
|
-
loadPolicyConfig,
|
|
309
|
-
processOverride,
|
|
310
|
-
savePolicyConfig
|
|
311
|
-
};
|