@plumpslabs/kuma 2.4.12 → 2.4.13
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/dist/index.js +329 -46
- package/package.json +1 -1
- package/plugin.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1735,6 +1735,19 @@ No unresolved issues.`;
|
|
|
1735
1735
|
this.autoTrackToDb(toolName, params, timestamp).catch(() => {
|
|
1736
1736
|
});
|
|
1737
1737
|
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Issue #32 P1 — obedience enforcement helper (non-blocking).
|
|
1740
|
+
* Returns true when a tool call with the given prefix was recorded
|
|
1741
|
+
* in this session (checks in-memory toolCalls, last 100).
|
|
1742
|
+
*/
|
|
1743
|
+
hasToolCall(prefix) {
|
|
1744
|
+
try {
|
|
1745
|
+
this.ensureInit();
|
|
1746
|
+
return this.state.toolCalls.some((t) => t.toolName.startsWith(prefix));
|
|
1747
|
+
} catch {
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1738
1751
|
/**
|
|
1739
1752
|
* Auto-track tool calls to the database sessions + tool_calls + experiences tables.
|
|
1740
1753
|
* This is the passive observability pipeline — every tool call gets recorded
|
|
@@ -7529,7 +7542,9 @@ async function getFileSummary2(filePath) {
|
|
|
7529
7542
|
function parseHookInput(raw) {
|
|
7530
7543
|
try {
|
|
7531
7544
|
const data = JSON.parse(raw);
|
|
7532
|
-
const
|
|
7545
|
+
const root = data && typeof data === "object" ? data : {};
|
|
7546
|
+
const ti = root.tool_input && typeof root.tool_input === "object" ? root.tool_input : {};
|
|
7547
|
+
const args = root.args && typeof root.args === "object" ? root.args : {};
|
|
7533
7548
|
const paths = /* @__PURE__ */ new Set();
|
|
7534
7549
|
const add = (p) => {
|
|
7535
7550
|
if (typeof p === "string" && p.trim()) paths.add(p.trim());
|
|
@@ -7538,10 +7553,22 @@ function parseHookInput(raw) {
|
|
|
7538
7553
|
add(ti.new_file_path);
|
|
7539
7554
|
add(ti.notebook_path);
|
|
7540
7555
|
if (Array.isArray(ti.file_paths)) ti.file_paths.forEach(add);
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7556
|
+
add(root.file_path);
|
|
7557
|
+
add(root.file);
|
|
7558
|
+
add(root.path);
|
|
7559
|
+
add(root.targetFile);
|
|
7560
|
+
if (Array.isArray(root.file_paths)) root.file_paths.forEach(add);
|
|
7561
|
+
if (Array.isArray(root.files)) root.files.forEach(add);
|
|
7562
|
+
add(args.filePath);
|
|
7563
|
+
add(args.targetFile);
|
|
7564
|
+
add(args.path);
|
|
7565
|
+
add(args.file);
|
|
7566
|
+
const cmd = (typeof ti.command === "string" ? ti.command : void 0) ?? (typeof root.command === "string" ? root.command : void 0) ?? (typeof root.commandline === "string" ? root.commandline : void 0) ?? (typeof args.command === "string" ? args.command : void 0);
|
|
7567
|
+
const goal = (typeof ti.goal === "string" ? ti.goal : void 0) ?? (typeof root.goal === "string" ? root.goal : void 0);
|
|
7568
|
+
const out = { filePaths: [...paths] };
|
|
7569
|
+
if (goal) out.goal = goal;
|
|
7570
|
+
if (cmd) out.command = cmd;
|
|
7571
|
+
return out;
|
|
7545
7572
|
} catch {
|
|
7546
7573
|
return { filePaths: [] };
|
|
7547
7574
|
}
|
|
@@ -7845,7 +7872,7 @@ async function listGotchas(params) {
|
|
|
7845
7872
|
} else if (!params.status) {
|
|
7846
7873
|
sql += " AND status IN ('active', 'verified')";
|
|
7847
7874
|
}
|
|
7848
|
-
sql += " ORDER BY severity
|
|
7875
|
+
sql += " ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END, updated_at DESC LIMIT 51";
|
|
7849
7876
|
const stmt = db.prepare(sql);
|
|
7850
7877
|
stmt.bind(bind);
|
|
7851
7878
|
const results = [];
|
|
@@ -7854,14 +7881,44 @@ async function listGotchas(params) {
|
|
|
7854
7881
|
if (results.length === 0) {
|
|
7855
7882
|
return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
|
|
7856
7883
|
}
|
|
7884
|
+
let total = results.length;
|
|
7885
|
+
try {
|
|
7886
|
+
let countSql = "SELECT COUNT(*) as cnt FROM known_gotchas WHERE 1=1";
|
|
7887
|
+
const countBind = [];
|
|
7888
|
+
if (params.filePath) {
|
|
7889
|
+
countSql += " AND file_path LIKE ?";
|
|
7890
|
+
countBind.push(`%${params.filePath}%`);
|
|
7891
|
+
}
|
|
7892
|
+
if (params.severity) {
|
|
7893
|
+
countSql += " AND severity = ?";
|
|
7894
|
+
countBind.push(params.severity);
|
|
7895
|
+
}
|
|
7896
|
+
if (params.status && params.status !== "all") {
|
|
7897
|
+
countSql += " AND status = ?";
|
|
7898
|
+
countBind.push(params.status);
|
|
7899
|
+
} else if (!params.status) {
|
|
7900
|
+
countSql += " AND status IN ('active', 'verified')";
|
|
7901
|
+
}
|
|
7902
|
+
const cs = db.prepare(countSql);
|
|
7903
|
+
cs.bind(countBind);
|
|
7904
|
+
if (cs.step()) total = cs.getAsObject().cnt || results.length;
|
|
7905
|
+
cs.free();
|
|
7906
|
+
} catch {
|
|
7907
|
+
}
|
|
7908
|
+
const truncated = total > results.length;
|
|
7909
|
+
const shown = truncated ? results.slice(0, 50) : results;
|
|
7910
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
7857
7911
|
const lines = [
|
|
7858
|
-
`\u26A0\uFE0F **Known Gotchas** \u2014 ${
|
|
7912
|
+
`\u26A0\uFE0F **Known Gotchas** \u2014 showing ${shown.length} of ${total}${truncated ? " (truncated: true \u2014 narrow with filePath/severity)" : ""}`,
|
|
7859
7913
|
"\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",
|
|
7860
7914
|
""
|
|
7861
7915
|
];
|
|
7862
|
-
for (const g of
|
|
7916
|
+
for (const g of shown) {
|
|
7863
7917
|
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
7864
|
-
|
|
7918
|
+
const ageSec = nowSec - (Number(g.created_at) || nowSec);
|
|
7919
|
+
const isNew = ageSec < 24 * 3600 ? " \u{1F195}" : "";
|
|
7920
|
+
const updated = g.updated_at ? ` \xB7 lastSeen ${new Date(Number(g.updated_at) * 1e3).toISOString().split("T")[0]}` : "";
|
|
7921
|
+
lines.push(`${icon} [${g.severity}]${isNew} ${g.file_path}${updated}`);
|
|
7865
7922
|
lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
|
|
7866
7923
|
if (g.trigger_command) lines.push(` \u2328\uFE0F when running: \`${g.trigger_command.substring(0, 80)}\``);
|
|
7867
7924
|
if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
|
|
@@ -9188,7 +9245,9 @@ var init_architectureGuard = __esm({
|
|
|
9188
9245
|
var kumaCheckpoint_exports = {};
|
|
9189
9246
|
__export(kumaCheckpoint_exports, {
|
|
9190
9247
|
createCheckpoint: () => createCheckpoint,
|
|
9248
|
+
isRiskyEditPath: () => isRiskyEditPath,
|
|
9191
9249
|
listCheckpoints: () => listCheckpoints,
|
|
9250
|
+
maybeAutoCheckpoint: () => maybeAutoCheckpoint,
|
|
9192
9251
|
pruneCheckpoints: () => pruneCheckpoints,
|
|
9193
9252
|
rollbackToCheckpoint: () => rollbackToCheckpoint
|
|
9194
9253
|
});
|
|
@@ -9463,7 +9522,61 @@ function simpleHash2(str) {
|
|
|
9463
9522
|
}
|
|
9464
9523
|
return Math.abs(hash).toString(36);
|
|
9465
9524
|
}
|
|
9466
|
-
|
|
9525
|
+
function isRiskyEditPath(filePath) {
|
|
9526
|
+
return RISKY_EDIT_PATTERN.test(filePath);
|
|
9527
|
+
}
|
|
9528
|
+
function loadAutoCpDone(root) {
|
|
9529
|
+
try {
|
|
9530
|
+
const fp = path27.join(root, AUTO_CP_STATE);
|
|
9531
|
+
if (fs24.existsSync(fp)) {
|
|
9532
|
+
const parsed = JSON.parse(fs24.readFileSync(fp, "utf-8"));
|
|
9533
|
+
return parsed?.autoCheckpoints ?? {};
|
|
9534
|
+
}
|
|
9535
|
+
} catch {
|
|
9536
|
+
}
|
|
9537
|
+
return {};
|
|
9538
|
+
}
|
|
9539
|
+
function saveAutoCpDone(root, done) {
|
|
9540
|
+
try {
|
|
9541
|
+
const fp = path27.join(root, AUTO_CP_STATE);
|
|
9542
|
+
const parsed = fs24.existsSync(fp) ? JSON.parse(fs24.readFileSync(fp, "utf-8")) : {};
|
|
9543
|
+
parsed.autoCheckpoints = done;
|
|
9544
|
+
fs24.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
|
|
9545
|
+
} catch {
|
|
9546
|
+
}
|
|
9547
|
+
}
|
|
9548
|
+
function maybeAutoCheckpoint(filePath) {
|
|
9549
|
+
try {
|
|
9550
|
+
if (!isRiskyEditPath(filePath)) return null;
|
|
9551
|
+
const root = getProjectRoot();
|
|
9552
|
+
const done = loadAutoCpDone(root);
|
|
9553
|
+
if (done[filePath]) return null;
|
|
9554
|
+
const abs = path27.isAbsolute(filePath) ? filePath : path27.join(root, filePath);
|
|
9555
|
+
if (!fs24.existsSync(abs) || !fs24.statSync(abs).isFile()) return null;
|
|
9556
|
+
const label = `auto-pre-${sanitizeLabel(path27.basename(filePath))}`;
|
|
9557
|
+
const cpDir = path27.join(root, CHECKPOINT_DIR, label);
|
|
9558
|
+
if (!fs24.existsSync(cpDir)) fs24.mkdirSync(cpDir, { recursive: true });
|
|
9559
|
+
fs24.copyFileSync(abs, path27.join(cpDir, path27.basename(abs)));
|
|
9560
|
+
fs24.writeFileSync(
|
|
9561
|
+
path27.join(cpDir, "manifest.json"),
|
|
9562
|
+
JSON.stringify({
|
|
9563
|
+
label,
|
|
9564
|
+
timestamp: Date.now(),
|
|
9565
|
+
files: [{ path: filePath, hash: "" }],
|
|
9566
|
+
dbSnapshot: false,
|
|
9567
|
+
description: `Auto-checkpoint before risky edit of ${filePath} (issue #32)`,
|
|
9568
|
+
auto: true
|
|
9569
|
+
}),
|
|
9570
|
+
"utf-8"
|
|
9571
|
+
);
|
|
9572
|
+
done[filePath] = true;
|
|
9573
|
+
saveAutoCpDone(root, done);
|
|
9574
|
+
return label;
|
|
9575
|
+
} catch {
|
|
9576
|
+
return null;
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9579
|
+
var CHECKPOINT_DIR, RISKY_EDIT_PATTERN, AUTO_CP_STATE;
|
|
9467
9580
|
var init_kumaCheckpoint = __esm({
|
|
9468
9581
|
"src/engine/kumaCheckpoint.ts"() {
|
|
9469
9582
|
"use strict";
|
|
@@ -9471,6 +9584,8 @@ var init_kumaCheckpoint = __esm({
|
|
|
9471
9584
|
init_pathValidator();
|
|
9472
9585
|
init_sessionMemory();
|
|
9473
9586
|
CHECKPOINT_DIR = ".kuma/checkpoints";
|
|
9587
|
+
RISKY_EDIT_PATTERN = /(migrations?|drizzle|schema|prisma|\.sql$|\.db$)/i;
|
|
9588
|
+
AUTO_CP_STATE = ".kuma/hook-state.json";
|
|
9474
9589
|
}
|
|
9475
9590
|
});
|
|
9476
9591
|
|
|
@@ -9765,9 +9880,12 @@ function geminiTemplate() {
|
|
|
9765
9880
|
function opencodePluginTemplate() {
|
|
9766
9881
|
return [
|
|
9767
9882
|
"/**",
|
|
9768
|
-
" * \u{1F43B} Kuma MCP \u2014 OpenCode Plugin",
|
|
9769
|
-
" * Auto-injects gotcha warnings and safety guidance before file edits and commands in OpenCode.",
|
|
9883
|
+
" * \u{1F43B} Kuma MCP \u2014 OpenCode Plugin (risk-sensitive, token-efficient)",
|
|
9770
9884
|
" * OpenCode loads all plugins from .opencode/plugins/*.js automatically.",
|
|
9885
|
+
" *",
|
|
9886
|
+
" * - tool.execute.before: throws ONLY on critical/high gotchas for the",
|
|
9887
|
+
" * file being edited (thrown message IS shown to the agent).",
|
|
9888
|
+
" * Medium/low gotchas stay silent (0 tokens) \u2014 use MCP tools for those.",
|
|
9771
9889
|
" */",
|
|
9772
9890
|
'import fs from "node:fs";',
|
|
9773
9891
|
'import path from "node:path";',
|
|
@@ -9783,24 +9901,26 @@ function opencodePluginTemplate() {
|
|
|
9783
9901
|
" return process.cwd();",
|
|
9784
9902
|
"}",
|
|
9785
9903
|
"",
|
|
9786
|
-
"function
|
|
9904
|
+
"function getBlockingGotchasForFile(filePath) {",
|
|
9787
9905
|
" try {",
|
|
9788
9906
|
" const root = getProjectRoot();",
|
|
9789
9907
|
' const gotchasPath = path.join(root, ".kuma", "KNOWN_GOTCHAS.md");',
|
|
9790
9908
|
" if (!fs.existsSync(gotchasPath)) return [];",
|
|
9791
9909
|
' const content = fs.readFileSync(gotchasPath, "utf-8");',
|
|
9792
|
-
" const
|
|
9793
|
-
' const normalizedTarget = filePath.replace(/^[./]+/, "");',
|
|
9910
|
+
" const blocking = [];",
|
|
9911
|
+
' const normalizedTarget = String(filePath).replace(/^[./]+/, "");',
|
|
9794
9912
|
" const sections = content.split(/###\\s+/);",
|
|
9795
9913
|
" for (const section of sections) {",
|
|
9796
9914
|
" if (!section.trim()) continue;",
|
|
9797
9915
|
' const lines = section.split("\\n");',
|
|
9798
9916
|
' const title = lines[0] || "";',
|
|
9799
|
-
" if (title.includes(normalizedTarget)
|
|
9800
|
-
"
|
|
9801
|
-
|
|
9917
|
+
" if (!title.includes(normalizedTarget) && !section.includes(normalizedTarget)) continue;",
|
|
9918
|
+
" const sevMatch = section.match(/\\*\\*Severity\\*\\*:\\s*(critical|high|medium|low)/i);",
|
|
9919
|
+
' const severity = (sevMatch ? sevMatch[1] : "medium").toLowerCase();',
|
|
9920
|
+
' if (severity !== "critical" && severity !== "high") continue;',
|
|
9921
|
+
" blocking.push(`[${severity}] ${title.trim()}`);",
|
|
9802
9922
|
" }",
|
|
9803
|
-
" return
|
|
9923
|
+
" return blocking.slice(0, 3);",
|
|
9804
9924
|
" } catch {",
|
|
9805
9925
|
" return [];",
|
|
9806
9926
|
" }",
|
|
@@ -9809,20 +9929,25 @@ function opencodePluginTemplate() {
|
|
|
9809
9929
|
"export const KumaPlugin = async () => {",
|
|
9810
9930
|
" return {",
|
|
9811
9931
|
' "tool.execute.before": async (input, output) => {',
|
|
9812
|
-
' const tool = (input.tool || "").toLowerCase();',
|
|
9932
|
+
' const tool = String(input.tool || "").toLowerCase();',
|
|
9813
9933
|
" const args = output.args || {};",
|
|
9814
9934
|
' const targetPath = args.filePath || args.targetFile || args.path || args.file || "";',
|
|
9815
|
-
|
|
9816
|
-
"
|
|
9817
|
-
"
|
|
9818
|
-
"
|
|
9819
|
-
"
|
|
9820
|
-
|
|
9821
|
-
'
|
|
9822
|
-
"
|
|
9823
|
-
"
|
|
9935
|
+
" if (!targetPath) return;",
|
|
9936
|
+
' if (!(tool.includes("edit") || tool.includes("write") || tool.includes("patch"))) return;',
|
|
9937
|
+
" const blocking = getBlockingGotchasForFile(String(targetPath));",
|
|
9938
|
+
" if (blocking.length > 0) {",
|
|
9939
|
+
" throw new Error(",
|
|
9940
|
+
" `\u{1F43B} [Kuma] ${blocking.length} critical/high gotcha(s) for ${targetPath}:\\n` +",
|
|
9941
|
+
' blocking.map((g) => ` \u{1F534} ${g}`).join("\\n") +',
|
|
9942
|
+
' `\\nRead .kuma/KNOWN_GOTCHAS.md for workarounds, or run kuma_memory({ action: "gotcha" }) to list.`',
|
|
9943
|
+
" );",
|
|
9824
9944
|
" }",
|
|
9825
9945
|
" },",
|
|
9946
|
+
' "file.edited": async (input) => {',
|
|
9947
|
+
" // Post-edit verify nudge (log channel only \u2014 never blocks).",
|
|
9948
|
+
' const fp = input.filePath || input.path || (input.file && input.file.path) || "";',
|
|
9949
|
+
' if (fp) console.warn(`\u{1F43B} [Kuma] ${fp} edited \u2014 consider kuma_safety({ action: "verify", scope: "${fp}" })`);',
|
|
9950
|
+
" },",
|
|
9826
9951
|
" };",
|
|
9827
9952
|
"};",
|
|
9828
9953
|
""
|
|
@@ -10773,6 +10898,107 @@ function handleClaudeSecondary(root, results) {
|
|
|
10773
10898
|
}
|
|
10774
10899
|
}
|
|
10775
10900
|
}
|
|
10901
|
+
function writeHooksFile(root, relPath, type, results, build) {
|
|
10902
|
+
const fullPath = path28.resolve(root, relPath);
|
|
10903
|
+
try {
|
|
10904
|
+
let existing = {};
|
|
10905
|
+
let existed = false;
|
|
10906
|
+
if (fs25.existsSync(fullPath)) {
|
|
10907
|
+
existed = true;
|
|
10908
|
+
try {
|
|
10909
|
+
existing = JSON.parse(fs25.readFileSync(fullPath, "utf-8"));
|
|
10910
|
+
} catch {
|
|
10911
|
+
existing = {};
|
|
10912
|
+
}
|
|
10913
|
+
} else {
|
|
10914
|
+
const dir = path28.dirname(fullPath);
|
|
10915
|
+
if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
|
|
10916
|
+
}
|
|
10917
|
+
const { doc, changed } = build(existing);
|
|
10918
|
+
if (!changed) {
|
|
10919
|
+
results.push({ type, filePath: relPath, action: "skipped" });
|
|
10920
|
+
return;
|
|
10921
|
+
}
|
|
10922
|
+
fs25.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
10923
|
+
results.push({ type, filePath: relPath, action: existed ? "appended" : "created" });
|
|
10924
|
+
} catch (err) {
|
|
10925
|
+
results.push({ type, filePath: relPath, action: "error", error: err instanceof Error ? err.message : String(err) });
|
|
10926
|
+
}
|
|
10927
|
+
}
|
|
10928
|
+
function dedupeCommands(list, entry) {
|
|
10929
|
+
const cmd = String(entry.command || "");
|
|
10930
|
+
if (list.some((e) => String(e.command || "") === cmd)) return false;
|
|
10931
|
+
list.push(entry);
|
|
10932
|
+
return true;
|
|
10933
|
+
}
|
|
10934
|
+
function handleCursorHooks(root, results) {
|
|
10935
|
+
writeHooksFile(root, ".cursor/hooks.json", "cursor", results, (existing) => {
|
|
10936
|
+
const doc = { version: 1, ...existing };
|
|
10937
|
+
const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
|
|
10938
|
+
let changed = false;
|
|
10939
|
+
const after = Array.isArray(hooks.afterFileEdit) ? [...hooks.afterFileEdit] : [];
|
|
10940
|
+
changed = dedupeCommands(after, { command: "kuma hook pre-edit" }) || changed;
|
|
10941
|
+
const before = Array.isArray(hooks.beforeShellExecution) ? [...hooks.beforeShellExecution] : [];
|
|
10942
|
+
changed = dedupeCommands(before, { command: "kuma hook pre-bash" }) || changed;
|
|
10943
|
+
hooks.afterFileEdit = after;
|
|
10944
|
+
hooks.beforeShellExecution = before;
|
|
10945
|
+
doc.version = 1;
|
|
10946
|
+
doc.hooks = hooks;
|
|
10947
|
+
return { doc, changed };
|
|
10948
|
+
});
|
|
10949
|
+
}
|
|
10950
|
+
function handleWindsurfHooks(root, results) {
|
|
10951
|
+
writeHooksFile(root, ".windsurf/hooks.json", "windsurf", results, (existing) => {
|
|
10952
|
+
const doc = { ...existing };
|
|
10953
|
+
const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
|
|
10954
|
+
let changed = false;
|
|
10955
|
+
const preWrite = Array.isArray(hooks.pre_write_code) ? [...hooks.pre_write_code] : [];
|
|
10956
|
+
changed = dedupeCommands(preWrite, { command: "kuma hook pre-edit", show_output: false }) || changed;
|
|
10957
|
+
const preRun = Array.isArray(hooks.pre_run_command) ? [...hooks.pre_run_command] : [];
|
|
10958
|
+
changed = dedupeCommands(preRun, { command: "kuma hook pre-bash", show_output: false }) || changed;
|
|
10959
|
+
hooks.pre_write_code = preWrite;
|
|
10960
|
+
hooks.pre_run_command = preRun;
|
|
10961
|
+
doc.hooks = hooks;
|
|
10962
|
+
return { doc, changed };
|
|
10963
|
+
});
|
|
10964
|
+
}
|
|
10965
|
+
function handleCopilotHooks(root, results) {
|
|
10966
|
+
writeHooksFile(root, ".vscode/settings.json", "copilot", results, (existing) => {
|
|
10967
|
+
const doc = { ...existing };
|
|
10968
|
+
const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
|
|
10969
|
+
let changed = false;
|
|
10970
|
+
const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
10971
|
+
const hasEdit = pre.some((e) => String(e.command || "") === "kuma hook pre-edit");
|
|
10972
|
+
if (!hasEdit) {
|
|
10973
|
+
pre.push({ matcher: "Edit|Write|MultiEdit", type: "command", command: "kuma hook pre-edit", timeout: 15 });
|
|
10974
|
+
changed = true;
|
|
10975
|
+
}
|
|
10976
|
+
const hasBash = pre.some((e) => String(e.command || "") === "kuma hook pre-bash");
|
|
10977
|
+
if (!hasBash) {
|
|
10978
|
+
pre.push({ matcher: "Bash", type: "command", command: "kuma hook pre-bash", timeout: 15 });
|
|
10979
|
+
changed = true;
|
|
10980
|
+
}
|
|
10981
|
+
hooks.PreToolUse = pre;
|
|
10982
|
+
doc.hooks = hooks;
|
|
10983
|
+
return { doc, changed };
|
|
10984
|
+
});
|
|
10985
|
+
}
|
|
10986
|
+
function handleCodexHooks(root, results) {
|
|
10987
|
+
writeHooksFile(root, ".codex/hooks.json", "codex", results, (existing) => {
|
|
10988
|
+
const doc = { ...existing };
|
|
10989
|
+
const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
|
|
10990
|
+
let changed = false;
|
|
10991
|
+
const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
10992
|
+
const hasEdit = pre.some((e) => String(e.command || "") === "kuma hook pre-edit");
|
|
10993
|
+
if (!hasEdit) {
|
|
10994
|
+
pre.push({ matcher: "Edit|Write", type: "command", command: "kuma hook pre-edit" });
|
|
10995
|
+
changed = true;
|
|
10996
|
+
}
|
|
10997
|
+
hooks.PreToolUse = pre;
|
|
10998
|
+
doc.hooks = hooks;
|
|
10999
|
+
return { doc, changed };
|
|
11000
|
+
});
|
|
11001
|
+
}
|
|
10776
11002
|
function handleCursorGotchaRules(root, results) {
|
|
10777
11003
|
const rulesDir2 = path28.resolve(root, ".cursor", "rules", "kuma-gotchas");
|
|
10778
11004
|
try {
|
|
@@ -10936,6 +11162,16 @@ function runInit(options) {
|
|
|
10936
11162
|
}
|
|
10937
11163
|
if (selectedSet.has("cursor")) {
|
|
10938
11164
|
handleCursorGotchaRules(root, results);
|
|
11165
|
+
handleCursorHooks(root, results);
|
|
11166
|
+
}
|
|
11167
|
+
if (selectedSet.has("windsurf")) {
|
|
11168
|
+
handleWindsurfHooks(root, results);
|
|
11169
|
+
}
|
|
11170
|
+
if (selectedSet.has("copilot")) {
|
|
11171
|
+
handleCopilotHooks(root, results);
|
|
11172
|
+
}
|
|
11173
|
+
if (selectedSet.has("codex")) {
|
|
11174
|
+
handleCodexHooks(root, results);
|
|
10939
11175
|
}
|
|
10940
11176
|
return results;
|
|
10941
11177
|
}
|
|
@@ -11803,25 +12039,31 @@ async function handleContext(params) {
|
|
|
11803
12039
|
const rawAction = params.action || "init";
|
|
11804
12040
|
const resolvedAction = CONTEXT_ALIASES[rawAction.toLowerCase()] || rawAction;
|
|
11805
12041
|
const action = resolvedAction;
|
|
12042
|
+
let obediencePrefix = "";
|
|
12043
|
+
if (action !== "init" && !sessionMemory.hasToolCall("kuma_context_init")) {
|
|
12044
|
+
obediencePrefix = `\u26A0\uFE0F **Obedience nudge**: \`kuma_context({ action: "init" })\` was not called yet this session. Run it first so project brief + fresh gotchas are loaded. Continuing anyway\u2026
|
|
12045
|
+
|
|
12046
|
+
`;
|
|
12047
|
+
}
|
|
11806
12048
|
switch (action) {
|
|
11807
12049
|
case "init":
|
|
11808
12050
|
return handleInit(params);
|
|
11809
12051
|
case "research":
|
|
11810
|
-
return handleResearch(params);
|
|
12052
|
+
return obediencePrefix + await handleResearch(params);
|
|
11811
12053
|
case "history":
|
|
11812
|
-
return handleHistory(params);
|
|
12054
|
+
return obediencePrefix + await handleHistory(params);
|
|
11813
12055
|
case "flow":
|
|
11814
|
-
return handleFlow(params);
|
|
12056
|
+
return obediencePrefix + await handleFlow(params);
|
|
11815
12057
|
case "map":
|
|
11816
|
-
return handleMap(params);
|
|
12058
|
+
return obediencePrefix + await handleMap(params);
|
|
11817
12059
|
case "impact":
|
|
11818
|
-
return handleImpact(params);
|
|
12060
|
+
return obediencePrefix + await handleImpact(params);
|
|
11819
12061
|
case "cluster":
|
|
11820
|
-
return handleCluster(params);
|
|
12062
|
+
return obediencePrefix + await handleCluster(params);
|
|
11821
12063
|
case "skeleton":
|
|
11822
|
-
return handleSkeleton(params);
|
|
12064
|
+
return obediencePrefix + await handleSkeleton(params);
|
|
11823
12065
|
case "reuse":
|
|
11824
|
-
return handleReuse(params);
|
|
12066
|
+
return obediencePrefix + await handleReuse(params);
|
|
11825
12067
|
default:
|
|
11826
12068
|
return `Unknown action "${action}". Use: init, research, history, flow, map, impact, cluster, skeleton, reuse`;
|
|
11827
12069
|
}
|
|
@@ -13004,24 +13246,41 @@ async function handleKumaGuard(params) {
|
|
|
13004
13246
|
async function handleSafety(params) {
|
|
13005
13247
|
const { action } = params;
|
|
13006
13248
|
sessionMemory.recordToolCall("kuma_safety", { action });
|
|
13249
|
+
let prefix = "";
|
|
13250
|
+
if (!sessionMemory.hasToolCall("kuma_context_init")) {
|
|
13251
|
+
prefix = `\u26A0\uFE0F **Obedience nudge**: \`kuma_context({ action: "init" })\` was not called yet this session. Run it first so project brief + fresh gotchas are loaded. Continuing anyway\u2026
|
|
13252
|
+
|
|
13253
|
+
`;
|
|
13254
|
+
}
|
|
13007
13255
|
switch (action) {
|
|
13008
13256
|
case "guard":
|
|
13009
|
-
return handleGuard(params);
|
|
13257
|
+
return prefix + await handleGuard(params);
|
|
13010
13258
|
case "verify":
|
|
13011
|
-
return handleVerify(params);
|
|
13259
|
+
return prefix + await handleVerify(params);
|
|
13012
13260
|
case "checkpoint":
|
|
13013
|
-
return handleCheckpoint(params);
|
|
13261
|
+
return prefix + await handleCheckpoint(params);
|
|
13014
13262
|
case "rollback_label":
|
|
13015
|
-
return handleRollbackLabel(params);
|
|
13263
|
+
return prefix + await handleRollbackLabel(params);
|
|
13016
13264
|
default:
|
|
13017
13265
|
return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label`;
|
|
13018
13266
|
}
|
|
13019
13267
|
}
|
|
13020
13268
|
async function handleGuard(params) {
|
|
13021
|
-
|
|
13269
|
+
const base = await handleKumaGuard({
|
|
13022
13270
|
goal: params.guardGoal,
|
|
13023
13271
|
check: params.guardCheck || "all"
|
|
13024
13272
|
});
|
|
13273
|
+
try {
|
|
13274
|
+
const summary = sessionMemory.getSummary();
|
|
13275
|
+
const modified = summary.modifiedFiles?.length || 0;
|
|
13276
|
+
if (modified > 0 && !sessionMemory.hasToolCall("kuma_safety_verify")) {
|
|
13277
|
+
return base + `
|
|
13278
|
+
|
|
13279
|
+
\u26A0\uFE0F **Post-edit nudge**: ${modified} file(s) modified but \`kuma_safety({ action: "verify" })\` not called yet. Run verify to close the edit\u2192verify loop.`;
|
|
13280
|
+
}
|
|
13281
|
+
} catch {
|
|
13282
|
+
}
|
|
13283
|
+
return base;
|
|
13025
13284
|
}
|
|
13026
13285
|
async function handleCheckpoint(params) {
|
|
13027
13286
|
sessionMemory.recordToolCall("kuma_safety_checkpoint", { label: params.label });
|
|
@@ -13042,6 +13301,17 @@ async function handleVerify(params) {
|
|
|
13042
13301
|
const { analyzeImpact: analyzeImpact2, formatImpact: formatImpact2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
13043
13302
|
const impact = await analyzeImpact2(target);
|
|
13044
13303
|
const formattedImpact = formatImpact2(impact);
|
|
13304
|
+
let gotchaScan = "";
|
|
13305
|
+
try {
|
|
13306
|
+
const { getFreshGotchasForFile: getFreshGotchasForFile2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
|
|
13307
|
+
const gotchas = await getFreshGotchasForFile2(target, 5, true);
|
|
13308
|
+
if (gotchas.length > 0) {
|
|
13309
|
+
gotchaScan = "\n\n\u26A0\uFE0F **Gotcha-scan** \u2014 this scope touches file(s) with known gotchas:\n" + gotchas.map((g) => ` ${g.stale ? "\u26AA(stale)" : "\u{1F7E1}"} [${g.severity}] ${g.filePath} \u2014 ${g.description.substring(0, 120)}`).join("\n") + "\n \u2192 Confirm the fix does not reintroduce them.";
|
|
13310
|
+
} else {
|
|
13311
|
+
gotchaScan = "\n\n\u2705 **Gotcha-scan**: no known gotchas for this scope.";
|
|
13312
|
+
}
|
|
13313
|
+
} catch {
|
|
13314
|
+
}
|
|
13045
13315
|
let recommendedCmd = "";
|
|
13046
13316
|
try {
|
|
13047
13317
|
const { getWorkspaceInfo: getWorkspaceInfo2, findPackageForFile: findPackageForFile2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
|
|
@@ -13077,7 +13347,8 @@ async function handleVerify(params) {
|
|
|
13077
13347
|
formattedImpact,
|
|
13078
13348
|
"",
|
|
13079
13349
|
`\u{1F4CB} **Recommended Scoped Test Command:**`,
|
|
13080
|
-
` \`${recommendedCmd}
|
|
13350
|
+
` \`${recommendedCmd}\``,
|
|
13351
|
+
gotchaScan
|
|
13081
13352
|
].join("\n");
|
|
13082
13353
|
}
|
|
13083
13354
|
return [
|
|
@@ -13505,6 +13776,15 @@ async function main() {
|
|
|
13505
13776
|
}
|
|
13506
13777
|
const target = hook.filePaths[0];
|
|
13507
13778
|
await trackFileEditLoop2(target);
|
|
13779
|
+
try {
|
|
13780
|
+
const { maybeAutoCheckpoint: maybeAutoCheckpoint2 } = await Promise.resolve().then(() => (init_kumaCheckpoint(), kumaCheckpoint_exports));
|
|
13781
|
+
const autoCp = maybeAutoCheckpoint2(target);
|
|
13782
|
+
if (autoCp) {
|
|
13783
|
+
const { recordInjection: recordInjection2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
|
|
13784
|
+
await recordInjection2({ filePath: target, kind: "edit" });
|
|
13785
|
+
}
|
|
13786
|
+
} catch {
|
|
13787
|
+
}
|
|
13508
13788
|
if (!checkInjectDedupe2(target)) {
|
|
13509
13789
|
process.stdout.write("{}");
|
|
13510
13790
|
process.exit(0);
|
|
@@ -13526,9 +13806,12 @@ async function main() {
|
|
|
13526
13806
|
if (args[0] === "hook" && args[1] === "pre-bash") {
|
|
13527
13807
|
try {
|
|
13528
13808
|
const stdin = await readStdin();
|
|
13529
|
-
const { getCommandContext: getCommandContext2, buildHookResponse: buildHookResponse2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
|
|
13530
|
-
|
|
13531
|
-
|
|
13809
|
+
const { getCommandContext: getCommandContext2, buildHookResponse: buildHookResponse2, parseHookInput: parseHookInput2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
|
|
13810
|
+
let command = "";
|
|
13811
|
+
try {
|
|
13812
|
+
command = parseHookInput2(stdin).command || "";
|
|
13813
|
+
} catch {
|
|
13814
|
+
}
|
|
13532
13815
|
if (!command.trim()) {
|
|
13533
13816
|
process.stdout.write("{}");
|
|
13534
13817
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.13",
|
|
4
4
|
"description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "kuma",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.13",
|
|
5
5
|
"description": "Safety-first context & orchestration engine for AI coding agents. Shadow memory, gotcha lifecycle, blast radius impact analysis, and test verification.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "plumpslabs",
|