@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 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 ti = (data && typeof data === "object" ? data.tool_input : {}) || {};
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
- return {
7542
- filePaths: [...paths],
7543
- goal: typeof ti.goal === "string" ? ti.goal : void 0
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 DESC, created_at DESC LIMIT 50";
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 ${results.length} recorded`,
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 results) {
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
- lines.push(`${icon} [${g.severity}] ${g.file_path}`);
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
- var CHECKPOINT_DIR;
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 getActiveGotchasForFile(filePath) {",
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 gotchas = [];",
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) || section.includes(normalizedTarget)) {",
9800
- " gotchas.push(title.trim());",
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 gotchas;",
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
- ' if (targetPath && (tool.includes("edit") || tool.includes("write") || tool.includes("patch"))) {',
9816
- " const gotchas = getActiveGotchasForFile(String(targetPath));",
9817
- " if (gotchas.length > 0) {",
9818
- " console.warn(",
9819
- " `\\n\u{1F43B} [Kuma Alert] Active gotchas found for ${targetPath}:\\n` +",
9820
- ' gotchas.map((g) => ` \u26A0\uFE0F ${g}`).join("\\n") +',
9821
- ' `\\n\u{1F449} Remember to record any new gotchas via kuma_memory({ action: "gotcha" })\\n`',
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
- return await handleKumaGuard({
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
- const data = JSON.parse(stdin || "{}");
13531
- const command = data && typeof data === "object" && data.tool_input && typeof data.tool_input.command === "string" ? data.tool_input.command : "";
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.12",
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.12",
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",