@plumpslabs/kuma 2.4.7 → 2.4.8

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
@@ -258,9 +258,23 @@ function generateNodeId(type, name) {
258
258
  function resetDbInstance() {
259
259
  dbInstance = null;
260
260
  initPromise = null;
261
+ lastLoadedMtime = 0;
261
262
  }
262
263
  async function getDb() {
263
- if (dbInstance) return dbInstance;
264
+ if (dbInstance) {
265
+ try {
266
+ const kumaDir = getKumaDir();
267
+ const dbPath = path2.join(kumaDir, DB_FILENAME);
268
+ if (fs2.existsSync(dbPath)) {
269
+ const stat = fs2.statSync(dbPath);
270
+ if (stat.mtimeMs > lastLoadedMtime + 50) {
271
+ resetDbInstance();
272
+ }
273
+ }
274
+ } catch {
275
+ }
276
+ if (dbInstance) return dbInstance;
277
+ }
264
278
  if (initPromise) return initPromise;
265
279
  initPromise = initDb();
266
280
  return initPromise;
@@ -274,6 +288,10 @@ async function initDb() {
274
288
  }
275
289
  let db;
276
290
  if (fs2.existsSync(dbPath)) {
291
+ try {
292
+ lastLoadedMtime = fs2.statSync(dbPath).mtimeMs;
293
+ } catch {
294
+ }
277
295
  const buffer = fs2.readFileSync(dbPath);
278
296
  db = new SQL.Database(buffer);
279
297
  } else {
@@ -309,8 +327,16 @@ function flushDb(db) {
309
327
  fs2.writeFileSync(tmpPath, buffer);
310
328
  try {
311
329
  fs2.renameSync(tmpPath, dbPath);
330
+ try {
331
+ lastLoadedMtime = fs2.statSync(dbPath).mtimeMs;
332
+ } catch {
333
+ }
312
334
  } catch {
313
335
  fs2.writeFileSync(dbPath, buffer);
336
+ try {
337
+ lastLoadedMtime = fs2.statSync(dbPath).mtimeMs;
338
+ } catch {
339
+ }
314
340
  try {
315
341
  if (fs2.existsSync(tmpPath)) fs2.unlinkSync(tmpPath);
316
342
  } catch {
@@ -1146,8 +1172,8 @@ async function runDoctor() {
1146
1172
  } catch {
1147
1173
  }
1148
1174
  try {
1149
- const { execSync: execSync6 } = await import("child_process");
1150
- const psOutput = execSync6(
1175
+ const { execSync: execSync7 } = await import("child_process");
1176
+ const psOutput = execSync7(
1151
1177
  `ps -eo pid,ppid,command | grep -E "(jest|pnpm test|npm test|yarn test)" | grep -v grep | head -10`,
1152
1178
  { encoding: "utf-8", timeout: 5e3 }
1153
1179
  ).trim();
@@ -1265,7 +1291,7 @@ function rebuildFtsIndex() {
1265
1291
  } catch {
1266
1292
  }
1267
1293
  }
1268
- var DB_FILENAME, dbInstance, initPromise, _saveTimeout, _pendingDb, SAVE_DEBOUNCE_MS;
1294
+ var DB_FILENAME, dbInstance, initPromise, lastLoadedMtime, _saveTimeout, _pendingDb, SAVE_DEBOUNCE_MS;
1269
1295
  var init_kumaDb = __esm({
1270
1296
  "src/engine/kumaDb.ts"() {
1271
1297
  "use strict";
@@ -1273,6 +1299,7 @@ var init_kumaDb = __esm({
1273
1299
  DB_FILENAME = "kuma.db";
1274
1300
  dbInstance = null;
1275
1301
  initPromise = null;
1302
+ lastLoadedMtime = 0;
1276
1303
  _saveTimeout = null;
1277
1304
  _pendingDb = null;
1278
1305
  SAVE_DEBOUNCE_MS = 100;
@@ -2556,6 +2583,8 @@ var init_languageSupport = __esm({
2556
2583
  // src/engine/kumaDriftDetector.ts
2557
2584
  var kumaDriftDetector_exports = {};
2558
2585
  __export(kumaDriftDetector_exports, {
2586
+ computeProjectHash: () => computeProjectHash,
2587
+ computeScopeHash: () => computeScopeHash,
2559
2588
  detectDrift: () => detectDrift,
2560
2589
  flagStaleRecords: () => flagStaleRecords,
2561
2590
  formatDriftReport: () => formatDriftReport,
@@ -2565,6 +2594,7 @@ __export(kumaDriftDetector_exports, {
2565
2594
  import fs5 from "fs";
2566
2595
  import path6 from "path";
2567
2596
  import crypto2 from "crypto";
2597
+ import { execSync as execSync3 } from "child_process";
2568
2598
  function hashFile(filePath) {
2569
2599
  try {
2570
2600
  const root = getProjectRoot();
@@ -2644,34 +2674,70 @@ async function detectDrift() {
2644
2674
  }
2645
2675
  function computeScopeHash(scope) {
2646
2676
  try {
2647
- const hash = crypto2.createHash("sha256");
2648
2677
  const root = getProjectRoot();
2678
+ let gitHead = "";
2679
+ let gitStatus = "";
2680
+ try {
2681
+ gitHead = execSync3("git rev-parse HEAD", {
2682
+ cwd: root,
2683
+ encoding: "utf-8",
2684
+ timeout: 1500,
2685
+ stdio: ["pipe", "pipe", "pipe"]
2686
+ }).trim();
2687
+ gitStatus = execSync3("git status --porcelain", {
2688
+ cwd: root,
2689
+ encoding: "utf-8",
2690
+ timeout: 2e3,
2691
+ stdio: ["pipe", "pipe", "pipe"]
2692
+ }).trim();
2693
+ } catch {
2694
+ }
2695
+ const hash = crypto2.createHash("sha256");
2649
2696
  hash.update(scope);
2650
2697
  let found = false;
2651
- try {
2652
- const files = fs5.readdirSync(root);
2653
- for (const file of files) {
2654
- if (file.toLowerCase().includes(scope.toLowerCase())) {
2698
+ if (gitHead || gitStatus) {
2699
+ hash.update(gitHead);
2700
+ hash.update(gitStatus);
2701
+ found = true;
2702
+ } else {
2703
+ const checkDirs = [
2704
+ root,
2705
+ path6.join(root, "src"),
2706
+ path6.join(root, "lib"),
2707
+ path6.join(root, "backend"),
2708
+ path6.join(root, "packages"),
2709
+ path6.join(root, "app")
2710
+ ];
2711
+ for (const dir of checkDirs) {
2712
+ if (fs5.existsSync(dir)) {
2655
2713
  try {
2656
- if (fs5.statSync(path6.join(root, file)).isFile()) {
2657
- const content = fs5.readFileSync(path6.join(root, file), "utf-8");
2658
- hash.update(file);
2659
- hash.update(content.substring(0, 1e4));
2660
- found = true;
2714
+ const files = fs5.readdirSync(dir).slice(0, 30);
2715
+ for (const f of files) {
2716
+ try {
2717
+ const fullPath = path6.join(dir, f);
2718
+ const stat = fs5.statSync(fullPath);
2719
+ if (stat.isFile()) {
2720
+ hash.update(`${f}:${stat.mtimeMs}`);
2721
+ found = true;
2722
+ }
2723
+ } catch {
2724
+ }
2661
2725
  }
2662
2726
  } catch {
2663
2727
  }
2664
2728
  }
2665
2729
  }
2666
- } catch {
2667
2730
  }
2668
2731
  const researchDir = path6.join(root, ".kuma", "research");
2669
2732
  if (fs5.existsSync(researchDir)) {
2670
2733
  const scopeFile = path6.join(researchDir, `${scope}.json`);
2671
2734
  if (fs5.existsSync(scopeFile)) {
2672
- const content = fs5.readFileSync(scopeFile, "utf-8");
2673
- hash.update(content);
2674
- found = true;
2735
+ try {
2736
+ const content = fs5.readFileSync(scopeFile, "utf-8");
2737
+ hash.update(content);
2738
+ found = true;
2739
+ } catch {
2740
+ }
2675
2741
  }
2676
2742
  }
2677
2743
  if (!found) return null;
@@ -2744,11 +2810,13 @@ async function getDriftSummary() {
2744
2810
  if (staleCount === 0) return "\u2705 All records fresh";
2745
2811
  return `\u26A0\uFE0F ${staleCount} stale record(s) detected. Use kuma_memory({ action: 'heal' }) to repair.`;
2746
2812
  }
2813
+ var computeProjectHash;
2747
2814
  var init_kumaDriftDetector = __esm({
2748
2815
  "src/engine/kumaDriftDetector.ts"() {
2749
2816
  "use strict";
2750
2817
  init_kumaDb();
2751
2818
  init_pathValidator();
2819
+ computeProjectHash = (scope) => computeScopeHash(scope) || Date.now().toString(16);
2752
2820
  }
2753
2821
  });
2754
2822
 
@@ -4342,9 +4410,9 @@ async function analyzeImpact(target) {
4342
4410
  }
4343
4411
  async function codebaseImpactFallback(target) {
4344
4412
  try {
4345
- const { execSync: execSync6 } = await import("child_process");
4413
+ const { execSync: execSync7 } = await import("child_process");
4346
4414
  const root = process.cwd();
4347
- const grepResult = execSync6(
4415
+ const grepResult = execSync7(
4348
4416
  `grep -rn ${grepIncludeFlags()} --include="*.json" --include="*.md" -l "${target.replace(/"/g, '\\"')}" . 2>/dev/null | grep -v node_modules | grep -v .git | grep -v dist | grep -v .kuma | head -50`,
4349
4417
  { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
4350
4418
  ).trim();
@@ -4471,7 +4539,7 @@ var init_kumaGraph = __esm({
4471
4539
  });
4472
4540
 
4473
4541
  // src/utils/kumaShared.ts
4474
- import { execSync as execSync3 } from "child_process";
4542
+ import { execSync as execSync4 } from "child_process";
4475
4543
  function isEditTool(toolName) {
4476
4544
  const n = (toolName || "").toLowerCase();
4477
4545
  return n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply") || n.includes("replace");
@@ -4509,7 +4577,7 @@ function getSessionStats(inputGoal) {
4509
4577
  function getGitDiffStat(timeout = 3e3) {
4510
4578
  try {
4511
4579
  const root = getProjectRoot();
4512
- return execSync3("git diff --stat", {
4580
+ return execSync4("git diff --stat", {
4513
4581
  cwd: root,
4514
4582
  encoding: "utf-8",
4515
4583
  timeout
@@ -5656,39 +5724,8 @@ async function syncGotchasGraph() {
5656
5724
  return { created: 0 };
5657
5725
  }
5658
5726
  }
5659
- async function resolveGotchasForScope(scope) {
5660
- try {
5661
- await ensureGotchasSchema();
5662
- const db = await getDb();
5663
- const stmt = db.prepare(
5664
- `SELECT id, file_path, content_hash FROM known_gotchas
5665
- WHERE status = 'active' AND (file_path LIKE ? OR file_path = ?) AND content_hash IS NOT NULL`
5666
- );
5667
- stmt.bind([`%${scope}%`, scope]);
5668
- const rows = [];
5669
- while (stmt.step()) rows.push(stmt.getAsObject());
5670
- stmt.free();
5671
- let resolved = 0;
5672
- const resolvedPaths = [];
5673
- for (const r of rows) {
5674
- const current = hashFileContent(r.file_path);
5675
- if (current && current !== r.content_hash) {
5676
- db.run(
5677
- `UPDATE known_gotchas SET status = 'resolved', last_verified_at = strftime('%s','now') WHERE id = ?`,
5678
- [r.id]
5679
- );
5680
- resolved++;
5681
- resolvedPaths.push(r.file_path);
5682
- }
5683
- }
5684
- if (resolved > 0) {
5685
- saveDb();
5686
- syncGotchaStatusToMarkdown([scope, ...resolvedPaths], "resolved", "auto-verified");
5687
- }
5688
- return { resolved };
5689
- } catch {
5690
- return { resolved: 0 };
5691
- }
5727
+ async function resolveGotchasForScope(_scope) {
5728
+ return { resolved: 0 };
5692
5729
  }
5693
5730
  async function getActiveGotchasForCommand(command) {
5694
5731
  try {
@@ -7502,10 +7539,29 @@ async function createCheckpoint(label, description) {
7502
7539
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
7503
7540
  }
7504
7541
  fs22.mkdirSync(cpDir, { recursive: true });
7542
+ let gitHead;
7543
+ let isGitClean = false;
7544
+ try {
7545
+ const { execSync: execSync7 } = await import("child_process");
7546
+ gitHead = execSync7("git rev-parse HEAD", {
7547
+ cwd: root,
7548
+ encoding: "utf-8",
7549
+ timeout: 3e3,
7550
+ stdio: ["pipe", "pipe", "pipe"]
7551
+ }).trim();
7552
+ const statusOut = execSync7("git status --porcelain", {
7553
+ cwd: root,
7554
+ encoding: "utf-8",
7555
+ timeout: 3e3,
7556
+ stdio: ["pipe", "pipe", "pipe"]
7557
+ }).trim();
7558
+ isGitClean = statusOut.length === 0;
7559
+ } catch {
7560
+ }
7505
7561
  const candidatePaths = /* @__PURE__ */ new Set();
7506
7562
  try {
7507
- const { execSync: execSync6 } = await import("child_process");
7508
- const gitOut = execSync6("git status --porcelain", {
7563
+ const { execSync: execSync7 } = await import("child_process");
7564
+ const gitOut = execSync7("git status --porcelain", {
7509
7565
  cwd: root,
7510
7566
  encoding: "utf-8",
7511
7567
  timeout: 4e3,
@@ -7553,9 +7609,10 @@ async function createCheckpoint(label, description) {
7553
7609
  if (fs22.existsSync(fullPath) && fs22.statSync(fullPath).isFile()) {
7554
7610
  const content = fs22.readFileSync(fullPath, "utf-8");
7555
7611
  const hash = simpleHash2(content);
7556
- const fileDir = path23.dirname(path23.join(cpDir, "files", fp));
7612
+ const snapFile = path23.join(cpDir, "files", `${fp}.kuma-snap`);
7613
+ const fileDir = path23.dirname(snapFile);
7557
7614
  if (!fs22.existsSync(fileDir)) fs22.mkdirSync(fileDir, { recursive: true });
7558
- fs22.writeFileSync(path23.join(cpDir, "files", fp), content, "utf-8");
7615
+ fs22.writeFileSync(snapFile, content, "utf-8");
7559
7616
  files.push({ path: fp, hash });
7560
7617
  }
7561
7618
  } catch {
@@ -7567,6 +7624,8 @@ async function createCheckpoint(label, description) {
7567
7624
  const manifest = {
7568
7625
  label,
7569
7626
  timestamp: Date.now(),
7627
+ gitHead,
7628
+ isGitClean,
7570
7629
  files,
7571
7630
  dbSnapshot: true,
7572
7631
  description
@@ -7578,13 +7637,15 @@ async function createCheckpoint(label, description) {
7578
7637
  );
7579
7638
  sessionMemory.recordToolCall("kuma_checkpoint_create", {
7580
7639
  label,
7581
- filesCount: files.length
7640
+ filesCount: files.length,
7641
+ gitHead
7582
7642
  });
7643
+ const fileNotice = files.length > 0 ? `\u{1F4E6} **${files.length} file(s)** snapshotted` : gitHead ? `\u{1F4E6} **Git commit snapshotted** (\`${gitHead.substring(0, 8)}\` \u2014 clean tree)` : `\u{1F4E6} **0 file(s)** snapshotted`;
7583
7644
  return [
7584
7645
  `\u2705 **Checkpoint Created**: "${label}"`,
7585
7646
  `\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`,
7586
7647
  ``,
7587
- `\u{1F4E6} **${files.length} file(s)** snapshotted`,
7648
+ fileNotice,
7588
7649
  `\u{1F5C4}\uFE0F **Database** snapshotted`,
7589
7650
  `\u{1F4CD} ${cpDir}`,
7590
7651
  ``,
@@ -7611,10 +7672,12 @@ ${listCheckpoints()}`;
7611
7672
  let restored = 0;
7612
7673
  let failed = 0;
7613
7674
  for (const f of manifest.files) {
7614
- const snapshotPath = path23.join(cpDir, "files", f.path);
7615
- if (fs22.existsSync(snapshotPath)) {
7675
+ const snapPath = path23.join(cpDir, "files", `${f.path}.kuma-snap`);
7676
+ const legacyPath = path23.join(cpDir, "files", f.path);
7677
+ const sourcePath = fs22.existsSync(snapPath) ? snapPath : legacyPath;
7678
+ if (fs22.existsSync(sourcePath)) {
7616
7679
  try {
7617
- const content = fs22.readFileSync(snapshotPath, "utf-8");
7680
+ const content = fs22.readFileSync(sourcePath, "utf-8");
7618
7681
  const targetPath = path23.resolve(root, f.path);
7619
7682
  const targetDir = path23.dirname(targetPath);
7620
7683
  if (!fs22.existsSync(targetDir)) fs22.mkdirSync(targetDir, { recursive: true });
@@ -7625,6 +7688,20 @@ ${listCheckpoints()}`;
7625
7688
  }
7626
7689
  }
7627
7690
  }
7691
+ if (manifest.files.length === 0 && manifest.gitHead) {
7692
+ try {
7693
+ const { execSync: execSync7 } = await import("child_process");
7694
+ execSync7(`git checkout ${manifest.gitHead} -- .`, {
7695
+ cwd: root,
7696
+ encoding: "utf-8",
7697
+ timeout: 8e3,
7698
+ stdio: ["pipe", "pipe", "pipe"]
7699
+ });
7700
+ restored++;
7701
+ } catch {
7702
+ failed++;
7703
+ }
7704
+ }
7628
7705
  const dbSnapshotPath = path23.join(cpDir, "kuma.db");
7629
7706
  if (fs22.existsSync(dbSnapshotPath)) {
7630
7707
  const kumaDir = path23.join(root, ".kuma");
@@ -7993,10 +8070,12 @@ async function runAutoVerification(options = {}) {
7993
8070
  const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
7994
8071
  const denial = checkAllowed(root);
7995
8072
  if (denial) return denial;
7996
- const runawayBlock = checkRunaway();
7997
- if (runawayBlock) {
7998
- releaseFileLock(root);
7999
- return runawayBlock;
8073
+ if (!options.force) {
8074
+ const runawayBlock = checkRunaway();
8075
+ if (runawayBlock) {
8076
+ releaseFileLock(root);
8077
+ return runawayBlock;
8078
+ }
8000
8079
  }
8001
8080
  try {
8002
8081
  if (!options.force) {
@@ -8032,8 +8111,8 @@ async function runAutoVerification(options = {}) {
8032
8111
  let modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
8033
8112
  if (modified.length === 0) {
8034
8113
  try {
8035
- const { execSync: execSync6 } = await import("child_process");
8036
- const gitDiff = execSync6("git diff --name-only HEAD", { encoding: "utf-8", timeout: 3e3, cwd: root }).trim();
8114
+ const { execSync: execSync7 } = await import("child_process");
8115
+ const gitDiff = execSync7("git diff --name-only HEAD", { encoding: "utf-8", timeout: 3e3, cwd: root }).trim();
8037
8116
  if (gitDiff) {
8038
8117
  modified = gitDiff.split("\n").map((f) => f.trim()).filter(Boolean);
8039
8118
  }
@@ -8173,14 +8252,6 @@ async function runAutoVerification(options = {}) {
8173
8252
  } catch {
8174
8253
  }
8175
8254
  }
8176
- if (passed && !isNoTests && scope !== "session-impact") {
8177
- try {
8178
- const { resolveGotchasForScope: resolveGotchasForScope2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
8179
- const { resolved } = await resolveGotchasForScope2(scope);
8180
- if (resolved > 0) autoGotchaMessage = `\u2705 ${resolved} gotcha(s) resolved automatically (verification passed after file changes).`;
8181
- } catch {
8182
- }
8183
- }
8184
8255
  _localRunning = false;
8185
8256
  _currentProcess = null;
8186
8257
  releaseFileLock(root);
@@ -8245,7 +8316,7 @@ var init_kumaVerifier = __esm({
8245
8316
  STALE_RESULT_MS = 3e5;
8246
8317
  DEFAULT_TIMEOUT_MS = 6e4;
8247
8318
  RUNAWAY_WINDOW_MS = 3e5;
8248
- RUNAWAY_MAX_CALLS = 3;
8319
+ RUNAWAY_MAX_CALLS = 10;
8249
8320
  _verifyCallTimestamps = [];
8250
8321
  }
8251
8322
  });
@@ -10519,10 +10590,10 @@ init_sessionMemory();
10519
10590
  init_kumaDb();
10520
10591
  init_kumaGraph();
10521
10592
  init_kumaMemory();
10593
+ init_kumaDriftDetector();
10522
10594
  init_pathValidator();
10523
10595
  import fs16 from "fs";
10524
10596
  import path17 from "path";
10525
- import crypto3 from "crypto";
10526
10597
  var CONTEXT_ALIASES = {
10527
10598
  "research": "research",
10528
10599
  "search": "research",
@@ -10858,10 +10929,10 @@ async function handleHistory(params) {
10858
10929
  lines.push(traceFormatted);
10859
10930
  } else {
10860
10931
  try {
10861
- const { execSync: execSync6 } = await import("child_process");
10932
+ const { execSync: execSync7 } = await import("child_process");
10862
10933
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
10863
10934
  const root = getProjectRoot2();
10864
- const gitHistory = execSync6(
10935
+ const gitHistory = execSync7(
10865
10936
  `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
10866
10937
  { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
10867
10938
  ).trim();
@@ -10962,24 +11033,6 @@ async function handleImpact(params) {
10962
11033
  const impact = await analyzeImpact2(target);
10963
11034
  return formatImpact2(impact);
10964
11035
  }
10965
- function computeProjectHash(scope) {
10966
- try {
10967
- const root = getProjectRoot();
10968
- const files = fs16.readdirSync(root).slice(0, 20);
10969
- const hash = crypto3.createHash("md5");
10970
- hash.update(scope);
10971
- for (const f of files) {
10972
- try {
10973
- const stat = fs16.statSync(path17.join(root, f));
10974
- hash.update(`${f}:${stat.mtimeMs}`);
10975
- } catch {
10976
- }
10977
- }
10978
- return hash.digest("hex").substring(0, 12);
10979
- } catch {
10980
- return Date.now().toString(16);
10981
- }
10982
- }
10983
11036
 
10984
11037
  // src/tools/kumaMemoryTool.ts
10985
11038
  init_sessionMemory();
@@ -11281,7 +11334,7 @@ init_sessionMemory();
11281
11334
  init_kumaShared();
11282
11335
  import fs19 from "fs";
11283
11336
  import path20 from "path";
11284
- import { execSync as execSync4 } from "child_process";
11337
+ import { execSync as execSync5 } from "child_process";
11285
11338
  var SCRIPT_PATCH_PATTERNS = [
11286
11339
  "writeFileSync",
11287
11340
  "writeFile",
@@ -11375,7 +11428,7 @@ function scanRecentFiles(projectRoot) {
11375
11428
  function detectGitPatchScripts(projectRoot) {
11376
11429
  const warnings = [];
11377
11430
  try {
11378
- const statusStdout = execSync4("git status --porcelain", {
11431
+ const statusStdout = execSync5("git status --porcelain", {
11379
11432
  cwd: projectRoot,
11380
11433
  encoding: "utf-8",
11381
11434
  timeout: 3e3
@@ -11408,7 +11461,7 @@ function detectGitPatchScripts(projectRoot) {
11408
11461
  }
11409
11462
  }
11410
11463
  }
11411
- const deletedStdout = execSync4("git diff --name-only --diff-filter=D HEAD", {
11464
+ const deletedStdout = execSync5("git diff --name-only --diff-filter=D HEAD", {
11412
11465
  cwd: projectRoot,
11413
11466
  encoding: "utf-8",
11414
11467
  timeout: 3e3
@@ -11480,7 +11533,7 @@ init_sessionMemory();
11480
11533
  init_pathValidator();
11481
11534
  import fs20 from "fs";
11482
11535
  import path21 from "path";
11483
- import { execSync as execSync5 } from "child_process";
11536
+ import { execSync as execSync6 } from "child_process";
11484
11537
  var SNAPSHOT_DIR = "context-snapshots";
11485
11538
  function snapshotDir() {
11486
11539
  return path21.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
@@ -11552,7 +11605,7 @@ function formatSnapshot(snapshot) {
11552
11605
  function getGitDiffStat2() {
11553
11606
  try {
11554
11607
  const root = getProjectRoot();
11555
- const stdout = execSync5("git diff --stat", {
11608
+ const stdout = execSync6("git diff --stat", {
11556
11609
  cwd: root,
11557
11610
  encoding: "utf-8",
11558
11611
  timeout: 3e3
@@ -12152,25 +12205,25 @@ async function main() {
12152
12205
  } catch {
12153
12206
  }
12154
12207
  try {
12155
- const { execSync: execSync6 } = await import("child_process");
12208
+ const { execSync: execSync7 } = await import("child_process");
12156
12209
  try {
12157
- execSync6("pkill -f 'jest' 2>/dev/null || true");
12210
+ execSync7("pkill -f 'jest' 2>/dev/null || true");
12158
12211
  } catch {
12159
12212
  }
12160
12213
  try {
12161
- execSync6("pkill -f 'jest-worker' 2>/dev/null || true");
12214
+ execSync7("pkill -f 'jest-worker' 2>/dev/null || true");
12162
12215
  } catch {
12163
12216
  }
12164
12217
  try {
12165
- execSync6("pkill -f 'pnpm test' 2>/dev/null || true");
12218
+ execSync7("pkill -f 'pnpm test' 2>/dev/null || true");
12166
12219
  } catch {
12167
12220
  }
12168
12221
  try {
12169
- execSync6("pkill -f 'npm test' 2>/dev/null || true");
12222
+ execSync7("pkill -f 'npm test' 2>/dev/null || true");
12170
12223
  } catch {
12171
12224
  }
12172
12225
  try {
12173
- execSync6("pkill -f 'yarn test' 2>/dev/null || true");
12226
+ execSync7("pkill -f 'yarn test' 2>/dev/null || true");
12174
12227
  } catch {
12175
12228
  }
12176
12229
  console.error("\u2705 Killed orphaned test processes");
@@ -5,11 +5,18 @@
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
 
8
+ const GENERIC_FILENAMES = new Set([
9
+ "index.ts", "index.js", "index.tsx", "index.jsx",
10
+ "types.ts", "types.d.ts", "constants.ts", "utils.ts",
11
+ "config.ts", "schema.ts", "styles.css", "main.ts", "main.js"
12
+ ]);
13
+
8
14
  function findGotchasForFile(targetPath) {
9
15
  if (!targetPath) return [];
10
16
  const results = [];
11
17
  const normalized = targetPath.replace(/\\/g, "/");
12
18
  const baseName = path.basename(normalized);
19
+ const isGeneric = GENERIC_FILENAMES.has(baseName.toLowerCase());
13
20
 
14
21
  const gotchasMd = path.resolve(process.cwd(), ".kuma", "KNOWN_GOTCHAS.md");
15
22
  if (fs.existsSync(gotchasMd)) {
@@ -17,11 +24,29 @@ function findGotchasForFile(targetPath) {
17
24
  const content = fs.readFileSync(gotchasMd, "utf-8");
18
25
  const sections = content.split(/(?=^###\s+)/m);
19
26
  for (const sec of sections) {
20
- if (sec.includes(baseName) || sec.includes(normalized)) {
21
- const firstLine = sec.split("\n")[0].replace(/^###\s+/, "").trim();
27
+ // Skip resolved or deprecated gotchas
28
+ if (/[-*]\s*(?:🏷️\s*)?\*\*Status\*\*:\s*(?:resolved|deprecated)/i.test(sec)) {
29
+ continue;
30
+ }
31
+
32
+ const headingLine = sec.split("\n")[0] || "";
33
+ const headingMatch = headingLine.match(/^###\s+\[?(.+?)\]?\s*(?:[—–-]+\s*(.+))?$/);
34
+ if (!headingMatch) continue;
35
+
36
+ const secFilePath = headingMatch[1].trim().replace(/\\/g, "/");
37
+ const secBase = path.basename(secFilePath);
38
+
39
+ // Path matching: exact match, suffix match, or non-generic basename match
40
+ const isMatch =
41
+ normalized.endsWith(secFilePath) ||
42
+ secFilePath.endsWith(normalized) ||
43
+ (!isGeneric && secBase.toLowerCase() === baseName.toLowerCase());
44
+
45
+ if (isMatch) {
46
+ const firstLine = headingLine.replace(/^###\s+/, "").trim();
22
47
  const trapMatch = sec.match(/- \S*\s*\*\*Trap\*\*:\s*([^\n]+)/i) || sec.match(/- \*\*Issue\*\*:\s*([^\n]+)/i);
23
48
  const ruleMatch = sec.match(/- \S*\s*\*\*Rule(?:\/Fix)?\*\*:\s*([^\n]+)/i) || sec.match(/- \*\*Workaround\*\*:\s*([^\n]+)/i);
24
- const trap = trapMatch ? trapMatch[1].trim() : firstLine;
49
+ const trap = trapMatch ? trapMatch[1].trim() : (headingMatch[2]?.trim() || firstLine);
25
50
  const rule = ruleMatch ? ruleMatch[1].trim() : "";
26
51
  results.push({ file: baseName, trap, rule });
27
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
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",
@@ -82,6 +82,6 @@
82
82
  "studio:dev": "bun run packages/ide/studio/src/index.ts",
83
83
  "typecheck": "tsc --noEmit",
84
84
  "lint": "echo 'No linting configured'",
85
- "test": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest"
85
+ "test": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --runInBand"
86
86
  }
87
87
  }
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.7",
4
+ "version": "2.4.8",
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",