@plumpslabs/kuma 2.4.6 → 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;
@@ -1329,25 +1356,31 @@ var init_sessionMemory = __esm({
1329
1356
  const parsed = JSON.parse(raw);
1330
1357
  const storedBranch = parsed.currentBranch;
1331
1358
  const branchSwitched = Boolean(storedBranch && currentGitBranch && storedBranch !== currentGitBranch);
1359
+ const lastActive = parsed.lastActiveAt || parsed.startTime || 0;
1360
+ const isIdle = lastActive > 0 && Date.now() - lastActive > 2 * 3600 * 1e3;
1361
+ const startTime = isIdle ? Date.now() : parsed.startTime || config.startTime;
1362
+ const metrics = isIdle ? { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 } : parsed.metrics || { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 };
1363
+ const toolCalls = isIdle ? [] : parsed.toolCalls || [];
1332
1364
  this.state = {
1333
1365
  projectRoot: parsed.projectRoot || config.projectRoot,
1334
- startTime: parsed.startTime || config.startTime,
1366
+ startTime,
1367
+ lastActiveAt: Date.now(),
1335
1368
  currentGoal: parsed.currentGoal || "",
1336
1369
  currentBranch: currentGitBranch || storedBranch || void 0,
1337
1370
  previousBranch: branchSwitched ? storedBranch : parsed.previousBranch,
1338
1371
  branchSwitched,
1339
- completedSteps: parsed.completedSteps || [],
1372
+ completedSteps: isIdle ? [] : parsed.completedSteps || [],
1340
1373
  modifiedFiles: new Map(parsed.modifiedFiles || []),
1341
1374
  failedFiles: new Map(parsed.failedFiles || []),
1342
1375
  searchResults: new Map(parsed.searchResults || []),
1343
1376
  dependencyGraph: new Map(parsed.dependencyGraph || []),
1344
- toolCalls: parsed.toolCalls || [],
1377
+ toolCalls,
1345
1378
  conventions: parsed.conventions,
1346
1379
  recordings: parsed.recordings || { archFlows: 0, gotchas: 0, decisions: 0, researchSaves: 0, features: 0, total: 0 },
1347
- metrics: parsed.metrics || { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 }
1380
+ metrics
1348
1381
  };
1349
1382
  this.initialized = true;
1350
- console.error(`[SessionMemory] Loaded persistent session memory.json`);
1383
+ console.error(`[SessionMemory] Loaded persistent session memory.json${isIdle ? " (idle reset applied)" : ""}`);
1351
1384
  return;
1352
1385
  } catch (err) {
1353
1386
  console.error(`[SessionMemory] Failed to load persistent session: ${err}. Re-initializing.`);
@@ -1508,6 +1541,7 @@ No unresolved issues.`;
1508
1541
  const serialized = {
1509
1542
  projectRoot: this.state.projectRoot,
1510
1543
  startTime: this.state.startTime,
1544
+ lastActiveAt: this.state.lastActiveAt || Date.now(),
1511
1545
  currentGoal: this.state.currentGoal,
1512
1546
  currentBranch: this.state.currentBranch,
1513
1547
  previousBranch: this.state.previousBranch,
@@ -1908,6 +1942,32 @@ No unresolved issues.`;
1908
1942
  }
1909
1943
  getModifiedFiles() {
1910
1944
  this.ensureInit();
1945
+ try {
1946
+ const root = this.state.projectRoot;
1947
+ const gitOut = execSync("git status --porcelain", {
1948
+ cwd: root,
1949
+ encoding: "utf-8",
1950
+ timeout: 3e3,
1951
+ stdio: ["pipe", "pipe", "pipe"]
1952
+ }).trim();
1953
+ if (gitOut) {
1954
+ for (const line of gitOut.split("\n")) {
1955
+ const trimmed = line.trim();
1956
+ if (!trimmed) continue;
1957
+ const rawPath = trimmed.slice(2).trim();
1958
+ const cleanPath = rawPath.replace(/^["']|["']$/g, "").split(" -> ").pop();
1959
+ if (cleanPath.startsWith(".kuma/") || cleanPath.startsWith(".git/")) continue;
1960
+ if (!this.state.modifiedFiles.has(cleanPath)) {
1961
+ this.state.modifiedFiles.set(cleanPath, {
1962
+ filePath: cleanPath,
1963
+ modifiedAt: Date.now(),
1964
+ status: trimmed.startsWith("??") || trimmed.startsWith("A") ? "created" : "modified"
1965
+ });
1966
+ }
1967
+ }
1968
+ }
1969
+ } catch {
1970
+ }
1911
1971
  return Array.from(this.state.modifiedFiles.values());
1912
1972
  }
1913
1973
  getFailedFiles() {
@@ -2130,6 +2190,14 @@ No unresolved issues.`;
2130
2190
  projectRoot: process.cwd(),
2131
2191
  startTime: Date.now()
2132
2192
  });
2193
+ } else if (this.state) {
2194
+ const lastActive = this.state.lastActiveAt || this.state.startTime || 0;
2195
+ if (lastActive > 0 && Date.now() - lastActive > 2 * 3600 * 1e3) {
2196
+ this.state.startTime = Date.now();
2197
+ this.state.metrics = { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 };
2198
+ this.state.toolCalls = [];
2199
+ }
2200
+ this.state.lastActiveAt = Date.now();
2133
2201
  }
2134
2202
  }
2135
2203
  };
@@ -2515,6 +2583,8 @@ var init_languageSupport = __esm({
2515
2583
  // src/engine/kumaDriftDetector.ts
2516
2584
  var kumaDriftDetector_exports = {};
2517
2585
  __export(kumaDriftDetector_exports, {
2586
+ computeProjectHash: () => computeProjectHash,
2587
+ computeScopeHash: () => computeScopeHash,
2518
2588
  detectDrift: () => detectDrift,
2519
2589
  flagStaleRecords: () => flagStaleRecords,
2520
2590
  formatDriftReport: () => formatDriftReport,
@@ -2524,6 +2594,7 @@ __export(kumaDriftDetector_exports, {
2524
2594
  import fs5 from "fs";
2525
2595
  import path6 from "path";
2526
2596
  import crypto2 from "crypto";
2597
+ import { execSync as execSync3 } from "child_process";
2527
2598
  function hashFile(filePath) {
2528
2599
  try {
2529
2600
  const root = getProjectRoot();
@@ -2603,34 +2674,70 @@ async function detectDrift() {
2603
2674
  }
2604
2675
  function computeScopeHash(scope) {
2605
2676
  try {
2606
- const hash = crypto2.createHash("sha256");
2607
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");
2608
2696
  hash.update(scope);
2609
2697
  let found = false;
2610
- try {
2611
- const files = fs5.readdirSync(root);
2612
- for (const file of files) {
2613
- 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)) {
2614
2713
  try {
2615
- if (fs5.statSync(path6.join(root, file)).isFile()) {
2616
- const content = fs5.readFileSync(path6.join(root, file), "utf-8");
2617
- hash.update(file);
2618
- hash.update(content.substring(0, 1e4));
2619
- 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
+ }
2620
2725
  }
2621
2726
  } catch {
2622
2727
  }
2623
2728
  }
2624
2729
  }
2625
- } catch {
2626
2730
  }
2627
2731
  const researchDir = path6.join(root, ".kuma", "research");
2628
2732
  if (fs5.existsSync(researchDir)) {
2629
2733
  const scopeFile = path6.join(researchDir, `${scope}.json`);
2630
2734
  if (fs5.existsSync(scopeFile)) {
2631
- const content = fs5.readFileSync(scopeFile, "utf-8");
2632
- hash.update(content);
2633
- found = true;
2735
+ try {
2736
+ const content = fs5.readFileSync(scopeFile, "utf-8");
2737
+ hash.update(content);
2738
+ found = true;
2739
+ } catch {
2740
+ }
2634
2741
  }
2635
2742
  }
2636
2743
  if (!found) return null;
@@ -2703,11 +2810,13 @@ async function getDriftSummary() {
2703
2810
  if (staleCount === 0) return "\u2705 All records fresh";
2704
2811
  return `\u26A0\uFE0F ${staleCount} stale record(s) detected. Use kuma_memory({ action: 'heal' }) to repair.`;
2705
2812
  }
2813
+ var computeProjectHash;
2706
2814
  var init_kumaDriftDetector = __esm({
2707
2815
  "src/engine/kumaDriftDetector.ts"() {
2708
2816
  "use strict";
2709
2817
  init_kumaDb();
2710
2818
  init_pathValidator();
2819
+ computeProjectHash = (scope) => computeScopeHash(scope) || Date.now().toString(16);
2711
2820
  }
2712
2821
  });
2713
2822
 
@@ -3481,7 +3590,8 @@ async function recordDomainFlow(params) {
3481
3590
  type: "feature_domain",
3482
3591
  name: params.domain,
3483
3592
  metadata: {
3484
- hops: params.hops.length,
3593
+ hops: params.hops,
3594
+ hopsCount: params.hops.length,
3485
3595
  gotchas: params.gotchas?.length || 0,
3486
3596
  decisions: params.decisions?.length || 0,
3487
3597
  filePaths: params.filePaths || [],
@@ -4300,9 +4410,9 @@ async function analyzeImpact(target) {
4300
4410
  }
4301
4411
  async function codebaseImpactFallback(target) {
4302
4412
  try {
4303
- const { execSync: execSync6 } = await import("child_process");
4413
+ const { execSync: execSync7 } = await import("child_process");
4304
4414
  const root = process.cwd();
4305
- const grepResult = execSync6(
4415
+ const grepResult = execSync7(
4306
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`,
4307
4417
  { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
4308
4418
  ).trim();
@@ -4429,7 +4539,7 @@ var init_kumaGraph = __esm({
4429
4539
  });
4430
4540
 
4431
4541
  // src/utils/kumaShared.ts
4432
- import { execSync as execSync3 } from "child_process";
4542
+ import { execSync as execSync4 } from "child_process";
4433
4543
  function isEditTool(toolName) {
4434
4544
  const n = (toolName || "").toLowerCase();
4435
4545
  return n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply") || n.includes("replace");
@@ -4467,7 +4577,7 @@ function getSessionStats(inputGoal) {
4467
4577
  function getGitDiffStat(timeout = 3e3) {
4468
4578
  try {
4469
4579
  const root = getProjectRoot();
4470
- return execSync3("git diff --stat", {
4580
+ return execSync4("git diff --stat", {
4471
4581
  cwd: root,
4472
4582
  encoding: "utf-8",
4473
4583
  timeout
@@ -4554,7 +4664,7 @@ function scoreMemoryRelevance(context, limit = 5) {
4554
4664
  const results = [];
4555
4665
  const kumaDir = path9.join(getProjectRoot(), MEMORY_DIR);
4556
4666
  if (!fs8.existsSync(kumaDir)) return [];
4557
- const terms = context.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
4667
+ const terms = Array.from(new Set(context.toLowerCase().split(/[\s/._-]+/).filter((w) => w.length > 2)));
4558
4668
  if (terms.length === 0) return [];
4559
4669
  try {
4560
4670
  const files = fs8.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
@@ -4781,21 +4891,21 @@ function getActiveGotchas() {
4781
4891
  let currentSeverity = "";
4782
4892
  let currentStatus = "active";
4783
4893
  for (const line of lines) {
4784
- const fileMatch = line.match(/^###\s+(.+?)\s*[—–-]+\s*(.+)/);
4894
+ const fileMatch = line.match(/^###\s+\[?(.+?)\]?\s*(?:[—–-]+\s*(.+))?$/);
4785
4895
  if (fileMatch) {
4786
4896
  if (currentFilePath && currentStatus !== "resolved" && currentStatus !== "deprecated") {
4787
4897
  results.push({ filePath: currentFilePath, description: currentDesc, severity: currentSeverity, content: currentSection });
4788
4898
  }
4789
4899
  currentFilePath = fileMatch[1].trim();
4790
- currentDesc = fileMatch[2].trim();
4900
+ currentDesc = (fileMatch[2] || fileMatch[1]).trim();
4791
4901
  currentSeverity = "medium";
4792
4902
  currentStatus = "active";
4793
4903
  currentSection = line + "\n";
4794
4904
  continue;
4795
4905
  }
4796
- const sevMatch = line.match(/- \*\*Severity\*\*:\s*(\w+)/);
4906
+ const sevMatch = line.match(/[-*]\s*(?:⚡\s*)?\*\*Severity\*\*:\s*(\w+)/i);
4797
4907
  if (sevMatch) currentSeverity = sevMatch[1].toLowerCase();
4798
- const statusMatch = line.match(/- \*\*Status\*\*:\s*(\w+)/);
4908
+ const statusMatch = line.match(/[-*]\s*(?:🏷️\s*)?\*\*Status\*\*:\s*(\w+)/i);
4799
4909
  if (statusMatch) currentStatus = statusMatch[1].toLowerCase();
4800
4910
  currentSection += line + "\n";
4801
4911
  }
@@ -4809,8 +4919,10 @@ function getActiveGotchas() {
4809
4919
  function checkFileGotchas(filePath) {
4810
4920
  const gotchas = getActiveGotchas();
4811
4921
  const warnings = [];
4922
+ const base = path10.basename(filePath);
4812
4923
  for (const g of gotchas) {
4813
- if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
4924
+ const gBase = path10.basename(g.filePath);
4925
+ if (filePath.includes(g.filePath) || g.filePath.includes(filePath) || base && gBase && base === gBase) {
4814
4926
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
4815
4927
  warnings.push(`${icon} **Gotcha**: ${g.description} (${g.severity})`);
4816
4928
  }
@@ -4919,8 +5031,9 @@ function isGotchaFresh(storedHash, currentHash) {
4919
5031
  if (!currentHash) return true;
4920
5032
  return storedHash === currentHash;
4921
5033
  }
4922
- async function getFreshGotchasForFile(filePath, limit = 5) {
5034
+ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false) {
4923
5035
  const root = getProjectRoot();
5036
+ const base = path11.basename(filePath);
4924
5037
  try {
4925
5038
  const dbFile = path11.join(root, ".kuma", "kuma.db");
4926
5039
  if (fs10.existsSync(dbFile)) {
@@ -4930,44 +5043,58 @@ async function getFreshGotchasForFile(filePath, limit = 5) {
4930
5043
  SELECT id, file_path, description, severity, workaround, content_hash
4931
5044
  FROM known_gotchas
4932
5045
  WHERE status IN ('active', 'verified')
4933
- AND (file_path = ? OR ? LIKE ('%' || file_path || '%'))
5046
+ AND (
5047
+ file_path = ?
5048
+ OR file_path LIKE ?
5049
+ OR ? LIKE ('%' || file_path || '%')
5050
+ OR file_path LIKE ?
5051
+ )
4934
5052
  ORDER BY
4935
- CASE WHEN file_path = ? THEN 0 ELSE 1 END, -- exact match first
5053
+ CASE WHEN file_path = ? THEN 0 WHEN file_path LIKE ? THEN 1 ELSE 2 END,
4936
5054
  CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
4937
5055
  updated_at DESC
4938
5056
  LIMIT ?
4939
5057
  `);
4940
- stmt.bind([filePath, filePath, filePath, limit]);
5058
+ stmt.bind([filePath, `%${filePath}%`, filePath, `%${base}%`, filePath, `%${filePath}%`, limit * 2]);
4941
5059
  const rows = [];
4942
5060
  while (stmt.step()) rows.push(stmt.getAsObject());
4943
5061
  stmt.free();
4944
- const currentHash = hashFileContent(filePath);
4945
- return rows.map((r) => ({
4946
- id: r.id,
4947
- filePath: r.file_path,
4948
- description: r.description,
4949
- severity: r.severity,
4950
- workaround: r.workaround ?? null,
4951
- stale: !isGotchaFresh(r.content_hash ?? null, currentHash)
4952
- })).filter((g) => !g.stale).slice(0, limit);
5062
+ if (rows.length > 0) {
5063
+ const currentHash = hashFileContent(filePath);
5064
+ const mapped = rows.map((r) => ({
5065
+ id: r.id,
5066
+ filePath: r.file_path,
5067
+ description: r.description,
5068
+ severity: r.severity,
5069
+ workaround: r.workaround ?? null,
5070
+ stale: !isGotchaFresh(r.content_hash ?? null, currentHash)
5071
+ }));
5072
+ const filtered = includeStale ? mapped : mapped.filter((g) => !g.stale);
5073
+ return filtered.slice(0, limit);
5074
+ }
4953
5075
  }
4954
5076
  } catch {
4955
5077
  }
4956
5078
  try {
4957
5079
  if (!fs10.existsSync(path11.join(root, ".kuma"))) return [];
4958
- const { checkFileGotchas: checkFileGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
4959
- const warnings = checkFileGotchas2(filePath);
4960
- if (warnings.length === 0) return [];
4961
- return warnings.slice(0, limit).map((w) => ({
4962
- filePath,
4963
- description: w,
4964
- severity: "medium",
4965
- workaround: null,
4966
- stale: false
4967
- }));
5080
+ const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
5081
+ const allGotchas = getActiveGotchas2();
5082
+ const matched = allGotchas.filter(
5083
+ (g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path11.basename(g.filePath) === base
5084
+ );
5085
+ if (matched.length > 0) {
5086
+ return matched.slice(0, limit).map((g) => ({
5087
+ filePath: g.filePath,
5088
+ description: g.description,
5089
+ severity: g.severity,
5090
+ workaround: null,
5091
+ stale: false
5092
+ }));
5093
+ }
4968
5094
  } catch {
4969
5095
  return [];
4970
5096
  }
5097
+ return [];
4971
5098
  }
4972
5099
  async function getFileTrace(filePath, limit = 8) {
4973
5100
  try {
@@ -5015,14 +5142,81 @@ function formatFileTrace(entries, filePath) {
5015
5142
  return lines.join("\n");
5016
5143
  }
5017
5144
  async function getDecisionsForFile(filePath, limit = 3) {
5145
+ const decisions = [];
5146
+ const root = getProjectRoot();
5147
+ const base = path11.basename(filePath);
5148
+ const baseWithoutExt = base.replace(/\.[^.]+$/, "");
5149
+ try {
5150
+ const decisionsMdPath = path11.join(root, ".kuma", "memories", "decisions.md");
5151
+ if (fs10.existsSync(decisionsMdPath)) {
5152
+ const content = fs10.readFileSync(decisionsMdPath, "utf-8");
5153
+ const sections = content.split(/^##\s+/m);
5154
+ for (const section of sections) {
5155
+ if (!section.trim() || section.startsWith("# ")) continue;
5156
+ const sectionLines = section.split("\n");
5157
+ const title = sectionLines[0].trim();
5158
+ const sectionText = section.toLowerCase();
5159
+ const matches = sectionText.includes(filePath.toLowerCase()) || sectionText.includes(base.toLowerCase()) || baseWithoutExt.length > 3 && sectionText.includes(baseWithoutExt.toLowerCase());
5160
+ if (matches) {
5161
+ let rationale = "";
5162
+ for (const line of sectionLines) {
5163
+ const rMatch = line.match(/^[-*]\s*\*\*Rationale:\*\*\s*(.+)$/i);
5164
+ if (rMatch) {
5165
+ rationale = rMatch[1].trim();
5166
+ break;
5167
+ }
5168
+ const oMatch = line.match(/^[-*]\s*\*\*Outcome:\*\*\s*(.+)$/i);
5169
+ if (!rationale && oMatch) {
5170
+ rationale = oMatch[1].trim();
5171
+ }
5172
+ }
5173
+ decisions.push(`\u{1F4CC} ${title}${rationale ? ` \u2014 *${rationale}*` : ""}`);
5174
+ if (decisions.length >= limit) return decisions;
5175
+ }
5176
+ }
5177
+ }
5178
+ } catch {
5179
+ }
5180
+ try {
5181
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
5182
+ const db = await getDb2();
5183
+ const stmt = db.prepare(`
5184
+ SELECT name, metadata FROM nodes
5185
+ WHERE type = 'decision'
5186
+ AND (name LIKE ? OR metadata LIKE ? OR name LIKE ? OR metadata LIKE ?)
5187
+ LIMIT ?
5188
+ `);
5189
+ stmt.bind([`%${base}%`, `%${base}%`, `%${baseWithoutExt}%`, `%${baseWithoutExt}%`, limit]);
5190
+ while (stmt.step()) {
5191
+ const row = stmt.getAsObject();
5192
+ try {
5193
+ const meta = JSON.parse(row.metadata || "{}");
5194
+ const title = row.name.replace(/^ADR:\s*/, "");
5195
+ const reason = meta.rationale || meta.context || meta.outcome || "";
5196
+ const formatted = `\u{1F4CC} ${title}${reason ? ` \u2014 *${reason.substring(0, 100)}*` : ""}`;
5197
+ if (!decisions.includes(formatted)) {
5198
+ decisions.push(formatted);
5199
+ }
5200
+ } catch {
5201
+ }
5202
+ }
5203
+ stmt.free();
5204
+ if (decisions.length >= limit) return decisions.slice(0, limit);
5205
+ } catch {
5206
+ }
5018
5207
  try {
5019
5208
  const { scoreMemoryRelevance: scoreMemoryRelevance2 } = await Promise.resolve().then(() => (init_kumaMemory(), kumaMemory_exports));
5020
- const base = path11.basename(filePath);
5021
- const scored = scoreMemoryRelevance2(`${filePath} ${base}`, limit);
5022
- return scored.map((m) => `\u{1F4CC} ${m.topic} \u2014 ${m.score}% match (${m.reason})`);
5209
+ const searchTerms = `${filePath} ${base} ${baseWithoutExt}`.replace(/[\/._-]/g, " ");
5210
+ const scored = scoreMemoryRelevance2(searchTerms, limit);
5211
+ for (const m of scored) {
5212
+ const formatted = `\u{1F4CC} ${m.topic} \u2014 ${m.content ? m.content.substring(0, 80) : `${m.score}% match`}`;
5213
+ if (!decisions.includes(formatted)) {
5214
+ decisions.push(formatted);
5215
+ }
5216
+ }
5023
5217
  } catch {
5024
- return [];
5025
5218
  }
5219
+ return decisions.slice(0, limit);
5026
5220
  }
5027
5221
  async function getRelevantContext(filePath, goal) {
5028
5222
  const parts = [];
@@ -5253,6 +5447,7 @@ __export(kumaGotchas_exports, {
5253
5447
  resolveGotcha: () => resolveGotcha,
5254
5448
  resolveGotchasForScope: () => resolveGotchasForScope,
5255
5449
  setGotchaStatus: () => setGotchaStatus,
5450
+ syncGotchaStatusToMarkdown: () => syncGotchaStatusToMarkdown,
5256
5451
  syncGotchasGraph: () => syncGotchasGraph,
5257
5452
  syncGotchasToDb: () => syncGotchasToDb
5258
5453
  });
@@ -5529,34 +5724,8 @@ async function syncGotchasGraph() {
5529
5724
  return { created: 0 };
5530
5725
  }
5531
5726
  }
5532
- async function resolveGotchasForScope(scope) {
5533
- try {
5534
- await ensureGotchasSchema();
5535
- const db = await getDb();
5536
- const stmt = db.prepare(
5537
- `SELECT id, file_path, content_hash FROM known_gotchas
5538
- WHERE status = 'active' AND file_path LIKE ? AND content_hash IS NOT NULL`
5539
- );
5540
- stmt.bind([`%${scope}%`]);
5541
- const rows = [];
5542
- while (stmt.step()) rows.push(stmt.getAsObject());
5543
- stmt.free();
5544
- let resolved = 0;
5545
- for (const r of rows) {
5546
- const current = hashFileContent(r.file_path);
5547
- if (current && current !== r.content_hash) {
5548
- db.run(
5549
- `UPDATE known_gotchas SET status = 'resolved', last_verified_at = strftime('%s','now') WHERE id = ?`,
5550
- [r.id]
5551
- );
5552
- resolved++;
5553
- }
5554
- }
5555
- if (resolved > 0) saveDb();
5556
- return { resolved };
5557
- } catch {
5558
- return { resolved: 0 };
5559
- }
5727
+ async function resolveGotchasForScope(_scope) {
5728
+ return { resolved: 0 };
5560
5729
  }
5561
5730
  async function getActiveGotchasForCommand(command) {
5562
5731
  try {
@@ -5658,14 +5827,73 @@ async function getGotchasByStatus(status, limit = 20) {
5658
5827
  return [];
5659
5828
  }
5660
5829
  }
5830
+ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
5831
+ try {
5832
+ const mdPath = path12.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
5833
+ if (!fs11.existsSync(mdPath)) return false;
5834
+ const content = fs11.readFileSync(mdPath, "utf-8");
5835
+ const lines = content.split("\n");
5836
+ const normalizedTargets = targetPaths.map((p) => p.trim().toLowerCase()).filter(Boolean);
5837
+ if (normalizedTargets.length === 0) return false;
5838
+ let modified = false;
5839
+ const outputLines = [];
5840
+ let inMatchingSection = false;
5841
+ let sectionHasStatus = false;
5842
+ for (let i = 0; i < lines.length; i++) {
5843
+ const line = lines[i];
5844
+ const headingMatch = line.match(/^###\s+\[?(.+?)\]?\s*(?:[—–-]+\s*(.+))?$/);
5845
+ if (headingMatch) {
5846
+ if (inMatchingSection && !sectionHasStatus) {
5847
+ outputLines.push(`- \u{1F3F7}\uFE0F **Status**: ${newStatus}${reason ? ` (${reason})` : ""}`);
5848
+ modified = true;
5849
+ }
5850
+ const headingFile = headingMatch[1].trim().toLowerCase();
5851
+ const headingBase = path12.basename(headingFile);
5852
+ inMatchingSection = normalizedTargets.some(
5853
+ (t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path12.basename(t)
5854
+ );
5855
+ sectionHasStatus = false;
5856
+ outputLines.push(line);
5857
+ continue;
5858
+ }
5859
+ if (inMatchingSection) {
5860
+ const statusMatch = line.match(/^(\s*[-*]\s*(?:🏷️\s*)?\*\*Status\*\*:\s*)(.*)$/i);
5861
+ if (statusMatch) {
5862
+ outputLines.push(`- \u{1F3F7}\uFE0F **Status**: ${newStatus}${reason ? ` (${reason})` : ""}`);
5863
+ sectionHasStatus = true;
5864
+ modified = true;
5865
+ continue;
5866
+ }
5867
+ }
5868
+ outputLines.push(line);
5869
+ }
5870
+ if (inMatchingSection && !sectionHasStatus) {
5871
+ outputLines.push(`- \u{1F3F7}\uFE0F **Status**: ${newStatus}${reason ? ` (${reason})` : ""}`);
5872
+ modified = true;
5873
+ }
5874
+ if (modified) {
5875
+ fs11.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
5876
+ }
5877
+ return modified;
5878
+ } catch {
5879
+ return false;
5880
+ }
5881
+ }
5661
5882
  async function resolveGotcha(idOrScope, resolutionReason, verifiedBy = "agent") {
5662
5883
  try {
5663
5884
  await ensureGotchasSchema();
5664
5885
  const db = await getDb();
5665
5886
  let resolved = 0;
5666
5887
  const isId = typeof idOrScope === "number" || /^\d+$/.test(String(idOrScope).trim());
5888
+ const targetPaths = [];
5667
5889
  if (isId) {
5668
5890
  const id = Number(idOrScope);
5891
+ const findStmt = db.prepare(`SELECT file_path FROM known_gotchas WHERE id = ?`);
5892
+ findStmt.bind([id]);
5893
+ if (findStmt.step()) {
5894
+ targetPaths.push(findStmt.getAsObject().file_path);
5895
+ }
5896
+ findStmt.free();
5669
5897
  db.run(
5670
5898
  `UPDATE known_gotchas SET status = 'resolved', verified_by = ?, last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ? AND status != 'resolved'`,
5671
5899
  [verifiedBy, id]
@@ -5673,12 +5901,22 @@ async function resolveGotcha(idOrScope, resolutionReason, verifiedBy = "agent")
5673
5901
  resolved = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5674
5902
  } else {
5675
5903
  const scope = String(idOrScope).trim();
5904
+ targetPaths.push(scope);
5905
+ const findStmt = db.prepare(`SELECT DISTINCT file_path FROM known_gotchas WHERE (file_path = ? OR file_path LIKE ?)`);
5906
+ findStmt.bind([scope, `%${scope}%`]);
5907
+ while (findStmt.step()) {
5908
+ targetPaths.push(findStmt.getAsObject().file_path);
5909
+ }
5910
+ findStmt.free();
5676
5911
  db.run(
5677
5912
  `UPDATE known_gotchas SET status = 'resolved', verified_by = ?, last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE (file_path = ? OR file_path LIKE ?) AND status != 'resolved'`,
5678
5913
  [verifiedBy, scope, `%${scope}%`]
5679
5914
  );
5680
5915
  resolved = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5681
5916
  }
5917
+ if (targetPaths.length > 0) {
5918
+ syncGotchaStatusToMarkdown(targetPaths, "resolved", resolutionReason);
5919
+ }
5682
5920
  if (resolved > 0) {
5683
5921
  saveDb();
5684
5922
  rebuildFtsIndex();
@@ -5687,6 +5925,13 @@ async function resolveGotcha(idOrScope, resolutionReason, verifiedBy = "agent")
5687
5925
  message: `\u2705 Resolved ${resolved} gotcha(s) for "${idOrScope}"${resolutionReason ? `: ${resolutionReason}` : ""}.`
5688
5926
  };
5689
5927
  }
5928
+ const mdUpdated = syncGotchaStatusToMarkdown(targetPaths.length > 0 ? targetPaths : [String(idOrScope)], "resolved", resolutionReason);
5929
+ if (mdUpdated) {
5930
+ return {
5931
+ resolved: 1,
5932
+ message: `\u2705 Resolved gotcha in KNOWN_GOTCHAS.md for "${idOrScope}"${resolutionReason ? `: ${resolutionReason}` : ""}.`
5933
+ };
5934
+ }
5690
5935
  return { resolved: 0, message: `\u26A0\uFE0F No active gotchas found matching "${idOrScope}".` };
5691
5936
  } catch (err) {
5692
5937
  return { resolved: 0, message: `\u274C Failed to resolve gotcha: ${err}` };
@@ -5698,8 +5943,15 @@ async function deprecateGotcha(idOrScope, reason) {
5698
5943
  const db = await getDb();
5699
5944
  let deprecated = 0;
5700
5945
  const isId = typeof idOrScope === "number" || /^\d+$/.test(String(idOrScope).trim());
5946
+ const targetPaths = [];
5701
5947
  if (isId) {
5702
5948
  const id = Number(idOrScope);
5949
+ const findStmt = db.prepare(`SELECT file_path FROM known_gotchas WHERE id = ?`);
5950
+ findStmt.bind([id]);
5951
+ if (findStmt.step()) {
5952
+ targetPaths.push(findStmt.getAsObject().file_path);
5953
+ }
5954
+ findStmt.free();
5703
5955
  db.run(
5704
5956
  `UPDATE known_gotchas SET status = 'deprecated', verified_by = 'deprecation', last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ? AND status != 'deprecated'`,
5705
5957
  [id]
@@ -5707,12 +5959,22 @@ async function deprecateGotcha(idOrScope, reason) {
5707
5959
  deprecated = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5708
5960
  } else {
5709
5961
  const scope = String(idOrScope).trim();
5962
+ targetPaths.push(scope);
5963
+ const findStmt = db.prepare(`SELECT DISTINCT file_path FROM known_gotchas WHERE (file_path = ? OR file_path LIKE ?)`);
5964
+ findStmt.bind([scope, `%${scope}%`]);
5965
+ while (findStmt.step()) {
5966
+ targetPaths.push(findStmt.getAsObject().file_path);
5967
+ }
5968
+ findStmt.free();
5710
5969
  db.run(
5711
5970
  `UPDATE known_gotchas SET status = 'deprecated', verified_by = 'deprecation', last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE (file_path = ? OR file_path LIKE ?) AND status != 'deprecated'`,
5712
5971
  [scope, `%${scope}%`]
5713
5972
  );
5714
5973
  deprecated = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5715
5974
  }
5975
+ if (targetPaths.length > 0) {
5976
+ syncGotchaStatusToMarkdown(targetPaths, "deprecated", reason);
5977
+ }
5716
5978
  if (deprecated > 0) {
5717
5979
  saveDb();
5718
5980
  rebuildFtsIndex();
@@ -5721,6 +5983,13 @@ async function deprecateGotcha(idOrScope, reason) {
5721
5983
  message: `\u{1F9F9} Deprecated ${deprecated} gotcha(s) for "${idOrScope}"${reason ? `: ${reason}` : ""}.`
5722
5984
  };
5723
5985
  }
5986
+ const mdUpdated = syncGotchaStatusToMarkdown(targetPaths.length > 0 ? targetPaths : [String(idOrScope)], "deprecated", reason);
5987
+ if (mdUpdated) {
5988
+ return {
5989
+ deprecated: 1,
5990
+ message: `\u{1F9F9} Deprecated gotcha in KNOWN_GOTCHAS.md for "${idOrScope}"${reason ? `: ${reason}` : ""}.`
5991
+ };
5992
+ }
5724
5993
  return { deprecated: 0, message: `\u26A0\uFE0F No active gotchas found matching "${idOrScope}".` };
5725
5994
  } catch (err) {
5726
5995
  return { deprecated: 0, message: `\u274C Failed to deprecate gotcha: ${err}` };
@@ -6479,7 +6748,8 @@ var kumaFlowCache_exports = {};
6479
6748
  __export(kumaFlowCache_exports, {
6480
6749
  deriveHopsFromImports: () => deriveHopsFromImports,
6481
6750
  getFlowFreshness: () => getFlowFreshness,
6482
- getFreshDomainFlow: () => getFreshDomainFlow
6751
+ getFreshDomainFlow: () => getFreshDomainFlow,
6752
+ getHopsForDomain: () => getHopsForDomain
6483
6753
  });
6484
6754
  import fs15 from "fs";
6485
6755
  import path16 from "path";
@@ -6570,9 +6840,86 @@ function deriveHopsFromImports(entryFile, maxHops = 6) {
6570
6840
  }
6571
6841
  return hops;
6572
6842
  }
6843
+ function escapeRegex(str) {
6844
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6845
+ }
6846
+ async function getHopsForDomain(domain) {
6847
+ const hops = [];
6848
+ try {
6849
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
6850
+ const db = await getDb2();
6851
+ const stmt = db.prepare(`SELECT metadata FROM nodes WHERE type = 'feature_domain' AND name = ? LIMIT 1`);
6852
+ stmt.bind([domain]);
6853
+ if (stmt.step()) {
6854
+ const meta = JSON.parse(stmt.getAsObject().metadata || "{}");
6855
+ if (Array.isArray(meta.hops) && meta.hops.length > 0 && typeof meta.hops[0] === "object") {
6856
+ stmt.free();
6857
+ return meta.hops;
6858
+ }
6859
+ }
6860
+ stmt.free();
6861
+ const nodeStmt = db.prepare(`
6862
+ SELECT name, metadata FROM nodes
6863
+ WHERE type = 'cross_service_link' AND (metadata LIKE ? OR id LIKE ?)
6864
+ LIMIT 25
6865
+ `);
6866
+ nodeStmt.bind([`%"domain":"${domain}"%`, `%${domain}::%`]);
6867
+ const rawNodes = [];
6868
+ while (nodeStmt.step()) {
6869
+ const row = nodeStmt.getAsObject();
6870
+ try {
6871
+ const meta = JSON.parse(row.metadata || "{}");
6872
+ rawNodes.push({
6873
+ name: row.name,
6874
+ hopIndex: typeof meta.hopIndex === "number" ? meta.hopIndex : 999,
6875
+ relation: meta.relation,
6876
+ description: meta.description
6877
+ });
6878
+ } catch {
6879
+ }
6880
+ }
6881
+ nodeStmt.free();
6882
+ if (rawNodes.length >= 2) {
6883
+ rawNodes.sort((a, b) => a.hopIndex - b.hopIndex);
6884
+ const uniqueSteps = [];
6885
+ for (const n of rawNodes) {
6886
+ if (uniqueSteps.length === 0 || uniqueSteps[uniqueSteps.length - 1] !== n.name) {
6887
+ uniqueSteps.push(n.name);
6888
+ }
6889
+ }
6890
+ for (let i = 0; i < uniqueSteps.length - 1; i++) {
6891
+ hops.push({ from: uniqueSteps[i], to: uniqueSteps[i + 1] });
6892
+ }
6893
+ if (hops.length > 0) return hops;
6894
+ }
6895
+ } catch {
6896
+ }
6897
+ try {
6898
+ const root = getProjectRoot();
6899
+ const archPath = path16.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
6900
+ if (fs15.existsSync(archPath)) {
6901
+ const content = fs15.readFileSync(archPath, "utf-8");
6902
+ const lines = content.split("\n");
6903
+ for (const line of lines) {
6904
+ const domMatch = line.match(new RegExp(`domain:\\s*${escapeRegex(domain)}\\s*\\|\\s*hops:\\s*(.+)`, "i"));
6905
+ if (domMatch) {
6906
+ const hopsPart = domMatch[1].trim();
6907
+ const arrowSteps = hopsPart.split(/→|->/).map((s) => s.trim()).filter(Boolean);
6908
+ for (let i = 0; i < arrowSteps.length - 1; i++) {
6909
+ hops.push({ from: arrowSteps[i], to: arrowSteps[i + 1] });
6910
+ }
6911
+ if (hops.length > 0) return hops;
6912
+ }
6913
+ }
6914
+ }
6915
+ } catch {
6916
+ }
6917
+ return hops;
6918
+ }
6573
6919
  async function getFreshDomainFlow(domain) {
6574
6920
  const freshness = await getFlowFreshness(domain);
6575
- if (!freshness) {
6921
+ const hops = await getHopsForDomain(domain);
6922
+ if (!freshness && hops.length === 0) {
6576
6923
  return `\u2139\uFE0F No arch_flow recorded for domain "${domain}".
6577
6924
  Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} | hops: a.ts \u2192 b.ts \u2192 c.ts' }).`;
6578
6925
  }
@@ -6581,33 +6928,55 @@ Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} |
6581
6928
  `\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`,
6582
6929
  ""
6583
6930
  ];
6584
- if (freshness.fresh) {
6585
- lines.push(`\u2705 Cache FRESH \u2014 served as-is (${freshness.filePaths.length} file(s) tracked).`);
6586
- } else {
6587
- lines.push(`\u{1F501} Cache STALE (${freshness.staleFiles.length} file(s) changed) \u2014 re-deriving from imports...`);
6588
- try {
6589
- const entry = freshness.filePaths[0];
6590
- const hops = deriveHopsFromImports(entry);
6591
- if (hops.length > 0) {
6592
- const { recordDomainFlow: recordDomainFlow2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
6593
- await recordDomainFlow2({
6594
- domain,
6595
- hops,
6596
- filePaths: freshness.filePaths
6597
- });
6598
- lines.push(`\u2705 Re-derived ${hops.length} hop(s) from ${path16.basename(entry)} \u2014 cache refreshed.`);
6599
- } else {
6600
- lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
6931
+ if (freshness) {
6932
+ if (freshness.fresh) {
6933
+ lines.push(`\u2705 Cache FRESH \u2014 served as-is (${freshness.filePaths.length} file(s) tracked).`);
6934
+ } else {
6935
+ lines.push(`\u{1F501} Cache STALE (${freshness.staleFiles.length} file(s) changed) \u2014 re-deriving from imports...`);
6936
+ try {
6937
+ const entry = freshness.filePaths[0];
6938
+ const derivedHops = deriveHopsFromImports(entry);
6939
+ if (derivedHops.length > 0) {
6940
+ const { recordDomainFlow: recordDomainFlow2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
6941
+ await recordDomainFlow2({
6942
+ domain,
6943
+ hops: derivedHops,
6944
+ filePaths: freshness.filePaths
6945
+ });
6946
+ lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path16.basename(entry)} \u2014 cache refreshed.`);
6947
+ hops.splice(0, hops.length, ...derivedHops);
6948
+ } else {
6949
+ lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
6950
+ }
6951
+ } catch (err) {
6952
+ lines.push(`\u26A0\uFE0F Re-derivation failed: ${err}`);
6601
6953
  }
6602
- } catch (err) {
6603
- lines.push(`\u26A0\uFE0F Re-derivation failed: ${err}`);
6604
6954
  }
6955
+ } else {
6956
+ lines.push(`\u2139\uFE0F Serving flow from markdown definition.`);
6605
6957
  }
6606
6958
  lines.push("");
6607
- for (const fp of freshness.filePaths) {
6608
- lines.push(` \u{1F4C4} ${fp}${freshness.staleFiles.includes(fp) ? " \u26A0\uFE0F changed" : ""}`);
6959
+ if (hops.length > 0) {
6960
+ lines.push("\u{1F504} **Flow Sequence (Hops):**");
6961
+ for (let i = 0; i < hops.length; i++) {
6962
+ const h = hops[i];
6963
+ const detail = h.relation && h.relation !== "flows" ? ` (${h.relation})` : "";
6964
+ if (i === 0) {
6965
+ lines.push(` 1. ${h.from} \u2192`);
6966
+ }
6967
+ const isLast = i === hops.length - 1;
6968
+ lines.push(` ${i + 2}. ${h.to}${detail}${isLast ? "" : " \u2192"}`);
6969
+ }
6970
+ lines.push("");
6609
6971
  }
6610
- return lines.join("\n");
6972
+ if (freshness && freshness.filePaths.length > 0) {
6973
+ lines.push("\u{1F4C1} **Tracked Files:**");
6974
+ for (const fp of freshness.filePaths) {
6975
+ lines.push(` \u{1F4C4} ${fp}${freshness.staleFiles.includes(fp) ? " \u26A0\uFE0F changed" : ""}`);
6976
+ }
6977
+ lines.push("");
6978
+ }
6979
+ return lines.join("\n").trimEnd();
6611
6980
  }
6612
6981
  var init_kumaFlowCache = __esm({
6613
6982
  "src/engine/kumaFlowCache.ts"() {
@@ -7170,30 +7539,93 @@ async function createCheckpoint(label, description) {
7170
7539
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
7171
7540
  }
7172
7541
  fs22.mkdirSync(cpDir, { recursive: true });
7173
- const db = await getDb();
7174
- const filesStmt = db.prepare(
7175
- "SELECT DISTINCT file_path FROM change_log ORDER BY id DESC LIMIT 100"
7176
- );
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
+ }
7561
+ const candidatePaths = /* @__PURE__ */ new Set();
7562
+ try {
7563
+ const { execSync: execSync7 } = await import("child_process");
7564
+ const gitOut = execSync7("git status --porcelain", {
7565
+ cwd: root,
7566
+ encoding: "utf-8",
7567
+ timeout: 4e3,
7568
+ stdio: ["pipe", "pipe", "pipe"]
7569
+ }).trim();
7570
+ if (gitOut) {
7571
+ for (const line of gitOut.split("\n")) {
7572
+ const trimmed = line.trim();
7573
+ if (!trimmed) continue;
7574
+ const rawPath = trimmed.slice(2).trim();
7575
+ const cleanPath = rawPath.replace(/^["']|["']$/g, "").split(" -> ").pop();
7576
+ if (cleanPath.startsWith(".kuma/") || cleanPath.startsWith(".git/")) continue;
7577
+ candidatePaths.add(cleanPath);
7578
+ }
7579
+ }
7580
+ } catch {
7581
+ }
7582
+ try {
7583
+ for (const m of sessionMemory.getModifiedFiles()) {
7584
+ if (!m.filePath.startsWith(".kuma/") && !m.filePath.startsWith(".git/")) {
7585
+ candidatePaths.add(m.filePath);
7586
+ }
7587
+ }
7588
+ } catch {
7589
+ }
7590
+ try {
7591
+ const db2 = await getDb();
7592
+ const filesStmt = db2.prepare(
7593
+ "SELECT DISTINCT file_path FROM change_log ORDER BY id DESC LIMIT 100"
7594
+ );
7595
+ while (filesStmt.step()) {
7596
+ const row = filesStmt.getAsObject();
7597
+ const fp = row.file_path;
7598
+ if (fp && !fp.startsWith(".kuma/") && !fp.startsWith(".git/")) {
7599
+ candidatePaths.add(fp);
7600
+ }
7601
+ }
7602
+ filesStmt.free();
7603
+ } catch {
7604
+ }
7177
7605
  const files = [];
7178
- while (filesStmt.step()) {
7179
- const row = filesStmt.getAsObject();
7180
- const fp = row.file_path;
7606
+ for (const fp of candidatePaths) {
7181
7607
  const fullPath = path23.resolve(root, fp);
7182
- if (fs22.existsSync(fullPath)) {
7183
- const content = fs22.readFileSync(fullPath, "utf-8");
7184
- const hash = simpleHash2(content);
7185
- const fileDir = path23.dirname(path23.join(cpDir, "files", fp));
7186
- if (!fs22.existsSync(fileDir)) fs22.mkdirSync(fileDir, { recursive: true });
7187
- fs22.writeFileSync(path23.join(cpDir, "files", fp), content, "utf-8");
7188
- files.push({ path: fp, hash });
7608
+ try {
7609
+ if (fs22.existsSync(fullPath) && fs22.statSync(fullPath).isFile()) {
7610
+ const content = fs22.readFileSync(fullPath, "utf-8");
7611
+ const hash = simpleHash2(content);
7612
+ const snapFile = path23.join(cpDir, "files", `${fp}.kuma-snap`);
7613
+ const fileDir = path23.dirname(snapFile);
7614
+ if (!fs22.existsSync(fileDir)) fs22.mkdirSync(fileDir, { recursive: true });
7615
+ fs22.writeFileSync(snapFile, content, "utf-8");
7616
+ files.push({ path: fp, hash });
7617
+ }
7618
+ } catch {
7189
7619
  }
7190
7620
  }
7191
- filesStmt.free();
7621
+ const db = await getDb();
7192
7622
  const dbData = db.export();
7193
7623
  fs22.writeFileSync(path23.join(cpDir, "kuma.db"), Buffer.from(dbData));
7194
7624
  const manifest = {
7195
7625
  label,
7196
7626
  timestamp: Date.now(),
7627
+ gitHead,
7628
+ isGitClean,
7197
7629
  files,
7198
7630
  dbSnapshot: true,
7199
7631
  description
@@ -7205,13 +7637,15 @@ async function createCheckpoint(label, description) {
7205
7637
  );
7206
7638
  sessionMemory.recordToolCall("kuma_checkpoint_create", {
7207
7639
  label,
7208
- filesCount: files.length
7640
+ filesCount: files.length,
7641
+ gitHead
7209
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`;
7210
7644
  return [
7211
7645
  `\u2705 **Checkpoint Created**: "${label}"`,
7212
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`,
7213
7647
  ``,
7214
- `\u{1F4E6} **${files.length} file(s)** snapshotted`,
7648
+ fileNotice,
7215
7649
  `\u{1F5C4}\uFE0F **Database** snapshotted`,
7216
7650
  `\u{1F4CD} ${cpDir}`,
7217
7651
  ``,
@@ -7238,10 +7672,12 @@ ${listCheckpoints()}`;
7238
7672
  let restored = 0;
7239
7673
  let failed = 0;
7240
7674
  for (const f of manifest.files) {
7241
- const snapshotPath = path23.join(cpDir, "files", f.path);
7242
- 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)) {
7243
7679
  try {
7244
- const content = fs22.readFileSync(snapshotPath, "utf-8");
7680
+ const content = fs22.readFileSync(sourcePath, "utf-8");
7245
7681
  const targetPath = path23.resolve(root, f.path);
7246
7682
  const targetDir = path23.dirname(targetPath);
7247
7683
  if (!fs22.existsSync(targetDir)) fs22.mkdirSync(targetDir, { recursive: true });
@@ -7252,6 +7688,20 @@ ${listCheckpoints()}`;
7252
7688
  }
7253
7689
  }
7254
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
+ }
7255
7705
  const dbSnapshotPath = path23.join(cpDir, "kuma.db");
7256
7706
  if (fs22.existsSync(dbSnapshotPath)) {
7257
7707
  const kumaDir = path23.join(root, ".kuma");
@@ -7620,10 +8070,12 @@ async function runAutoVerification(options = {}) {
7620
8070
  const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
7621
8071
  const denial = checkAllowed(root);
7622
8072
  if (denial) return denial;
7623
- const runawayBlock = checkRunaway();
7624
- if (runawayBlock) {
7625
- releaseFileLock(root);
7626
- return runawayBlock;
8073
+ if (!options.force) {
8074
+ const runawayBlock = checkRunaway();
8075
+ if (runawayBlock) {
8076
+ releaseFileLock(root);
8077
+ return runawayBlock;
8078
+ }
7627
8079
  }
7628
8080
  try {
7629
8081
  if (!options.force) {
@@ -7659,8 +8111,8 @@ async function runAutoVerification(options = {}) {
7659
8111
  let modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
7660
8112
  if (modified.length === 0) {
7661
8113
  try {
7662
- const { execSync: execSync6 } = await import("child_process");
7663
- 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();
7664
8116
  if (gitDiff) {
7665
8117
  modified = gitDiff.split("\n").map((f) => f.trim()).filter(Boolean);
7666
8118
  }
@@ -7800,14 +8252,6 @@ async function runAutoVerification(options = {}) {
7800
8252
  } catch {
7801
8253
  }
7802
8254
  }
7803
- if (passed && !isNoTests && scope !== "session-impact") {
7804
- try {
7805
- const { resolveGotchasForScope: resolveGotchasForScope2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
7806
- const { resolved } = await resolveGotchasForScope2(scope);
7807
- if (resolved > 0) autoGotchaMessage = `\u2705 ${resolved} gotcha(s) resolved automatically (verification passed after file changes).`;
7808
- } catch {
7809
- }
7810
- }
7811
8255
  _localRunning = false;
7812
8256
  _currentProcess = null;
7813
8257
  releaseFileLock(root);
@@ -7872,7 +8316,7 @@ var init_kumaVerifier = __esm({
7872
8316
  STALE_RESULT_MS = 3e5;
7873
8317
  DEFAULT_TIMEOUT_MS = 6e4;
7874
8318
  RUNAWAY_WINDOW_MS = 3e5;
7875
- RUNAWAY_MAX_CALLS = 3;
8319
+ RUNAWAY_MAX_CALLS = 10;
7876
8320
  _verifyCallTimestamps = [];
7877
8321
  }
7878
8322
  });
@@ -10146,10 +10590,10 @@ init_sessionMemory();
10146
10590
  init_kumaDb();
10147
10591
  init_kumaGraph();
10148
10592
  init_kumaMemory();
10593
+ init_kumaDriftDetector();
10149
10594
  init_pathValidator();
10150
10595
  import fs16 from "fs";
10151
10596
  import path17 from "path";
10152
- import crypto3 from "crypto";
10153
10597
  var CONTEXT_ALIASES = {
10154
10598
  "research": "research",
10155
10599
  "search": "research",
@@ -10480,11 +10924,32 @@ async function handleHistory(params) {
10480
10924
  try {
10481
10925
  const { getFileTrace: getFileTrace2, formatFileTrace: formatFileTrace2, getFreshGotchasForFile: getFreshGotchasForFile2, getDecisionsForFile: getDecisionsForFile2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
10482
10926
  const trace = await getFileTrace2(target, 8);
10483
- lines.push(formatFileTrace2(trace, target) || " (no changes recorded for this file yet)");
10927
+ const traceFormatted = formatFileTrace2(trace, target);
10928
+ if (traceFormatted) {
10929
+ lines.push(traceFormatted);
10930
+ } else {
10931
+ try {
10932
+ const { execSync: execSync7 } = await import("child_process");
10933
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
10934
+ const root = getProjectRoot2();
10935
+ const gitHistory = execSync7(
10936
+ `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
10937
+ { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
10938
+ ).trim();
10939
+ if (gitHistory) {
10940
+ lines.push(`\u{1F570}\uFE0F **Git Commit History (latest):**
10941
+ ${gitHistory}`);
10942
+ } else {
10943
+ lines.push(" (no changes recorded for this file yet)");
10944
+ }
10945
+ } catch {
10946
+ lines.push(" (no changes recorded for this file yet)");
10947
+ }
10948
+ }
10484
10949
  lines.push("");
10485
- const gotchas = await getFreshGotchasForFile2(target, 5);
10950
+ const gotchas = await getFreshGotchasForFile2(target, 5, true);
10486
10951
  if (gotchas.length > 0) {
10487
- lines.push("\u26A0\uFE0F **Active gotchas** (fresh):");
10952
+ lines.push("\u26A0\uFE0F **Active gotchas:**");
10488
10953
  for (const g of gotchas) {
10489
10954
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
10490
10955
  lines.push(` ${icon} [${g.severity}] ${g.description}`);
@@ -10499,11 +10964,34 @@ async function handleHistory(params) {
10499
10964
  try {
10500
10965
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
10501
10966
  const db = await getDb2();
10502
- const stmt = db.prepare(`SELECT description, last_verified_at FROM known_gotchas WHERE status = 'resolved' AND file_path LIKE ? ORDER BY last_verified_at DESC LIMIT 3`);
10503
- stmt.bind([`%${path17.basename(target)}%`]);
10967
+ const base = path17.basename(target);
10968
+ const stmt = db.prepare(`
10969
+ SELECT description, last_verified_at FROM known_gotchas
10970
+ WHERE status = 'resolved' AND (file_path LIKE ? OR file_path LIKE ?)
10971
+ ORDER BY last_verified_at DESC LIMIT 5
10972
+ `);
10973
+ stmt.bind([`%${base}%`, `%${target}%`]);
10504
10974
  const resolved = [];
10505
10975
  while (stmt.step()) resolved.push(stmt.getAsObject());
10506
10976
  stmt.free();
10977
+ if (resolved.length === 0) {
10978
+ try {
10979
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
10980
+ const mdPath = path17.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
10981
+ if (fs16.existsSync(mdPath)) {
10982
+ const mdContent = fs16.readFileSync(mdPath, "utf-8");
10983
+ const sections = mdContent.split(/^###\s+/m);
10984
+ for (const sec of sections) {
10985
+ const secLower = sec.toLowerCase();
10986
+ if ((secLower.includes(base.toLowerCase()) || secLower.includes(target.toLowerCase())) && secLower.includes("status**: resolved")) {
10987
+ const descLine = sec.split("\n")[0].trim();
10988
+ resolved.push({ description: descLine, last_verified_at: null });
10989
+ }
10990
+ }
10991
+ }
10992
+ } catch {
10993
+ }
10994
+ }
10507
10995
  if (resolved.length > 0) {
10508
10996
  lines.push("\u2705 **Resolved gotchas** (fixed):");
10509
10997
  for (const r of resolved) {
@@ -10516,7 +11004,7 @@ async function handleHistory(params) {
10516
11004
  }
10517
11005
  const decisions = await getDecisionsForFile2(target, 3);
10518
11006
  if (decisions.length > 0) {
10519
- lines.push("\u{1F4CC} **Relevant decisions**");
11007
+ lines.push("\u{1F4CC} **Relevant decisions:**");
10520
11008
  for (const d of decisions) lines.push(` ${d}`);
10521
11009
  lines.push("");
10522
11010
  }
@@ -10545,24 +11033,6 @@ async function handleImpact(params) {
10545
11033
  const impact = await analyzeImpact2(target);
10546
11034
  return formatImpact2(impact);
10547
11035
  }
10548
- function computeProjectHash(scope) {
10549
- try {
10550
- const root = getProjectRoot();
10551
- const files = fs16.readdirSync(root).slice(0, 20);
10552
- const hash = crypto3.createHash("md5");
10553
- hash.update(scope);
10554
- for (const f of files) {
10555
- try {
10556
- const stat = fs16.statSync(path17.join(root, f));
10557
- hash.update(`${f}:${stat.mtimeMs}`);
10558
- } catch {
10559
- }
10560
- }
10561
- return hash.digest("hex").substring(0, 12);
10562
- } catch {
10563
- return Date.now().toString(16);
10564
- }
10565
- }
10566
11036
 
10567
11037
  // src/tools/kumaMemoryTool.ts
10568
11038
  init_sessionMemory();
@@ -10864,7 +11334,7 @@ init_sessionMemory();
10864
11334
  init_kumaShared();
10865
11335
  import fs19 from "fs";
10866
11336
  import path20 from "path";
10867
- import { execSync as execSync4 } from "child_process";
11337
+ import { execSync as execSync5 } from "child_process";
10868
11338
  var SCRIPT_PATCH_PATTERNS = [
10869
11339
  "writeFileSync",
10870
11340
  "writeFile",
@@ -10958,7 +11428,7 @@ function scanRecentFiles(projectRoot) {
10958
11428
  function detectGitPatchScripts(projectRoot) {
10959
11429
  const warnings = [];
10960
11430
  try {
10961
- const statusStdout = execSync4("git status --porcelain", {
11431
+ const statusStdout = execSync5("git status --porcelain", {
10962
11432
  cwd: projectRoot,
10963
11433
  encoding: "utf-8",
10964
11434
  timeout: 3e3
@@ -10991,7 +11461,7 @@ function detectGitPatchScripts(projectRoot) {
10991
11461
  }
10992
11462
  }
10993
11463
  }
10994
- const deletedStdout = execSync4("git diff --name-only --diff-filter=D HEAD", {
11464
+ const deletedStdout = execSync5("git diff --name-only --diff-filter=D HEAD", {
10995
11465
  cwd: projectRoot,
10996
11466
  encoding: "utf-8",
10997
11467
  timeout: 3e3
@@ -11063,7 +11533,7 @@ init_sessionMemory();
11063
11533
  init_pathValidator();
11064
11534
  import fs20 from "fs";
11065
11535
  import path21 from "path";
11066
- import { execSync as execSync5 } from "child_process";
11536
+ import { execSync as execSync6 } from "child_process";
11067
11537
  var SNAPSHOT_DIR = "context-snapshots";
11068
11538
  function snapshotDir() {
11069
11539
  return path21.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
@@ -11135,7 +11605,7 @@ function formatSnapshot(snapshot) {
11135
11605
  function getGitDiffStat2() {
11136
11606
  try {
11137
11607
  const root = getProjectRoot();
11138
- const stdout = execSync5("git diff --stat", {
11608
+ const stdout = execSync6("git diff --stat", {
11139
11609
  cwd: root,
11140
11610
  encoding: "utf-8",
11141
11611
  timeout: 3e3
@@ -11735,25 +12205,25 @@ async function main() {
11735
12205
  } catch {
11736
12206
  }
11737
12207
  try {
11738
- const { execSync: execSync6 } = await import("child_process");
12208
+ const { execSync: execSync7 } = await import("child_process");
11739
12209
  try {
11740
- execSync6("pkill -f 'jest' 2>/dev/null || true");
12210
+ execSync7("pkill -f 'jest' 2>/dev/null || true");
11741
12211
  } catch {
11742
12212
  }
11743
12213
  try {
11744
- execSync6("pkill -f 'jest-worker' 2>/dev/null || true");
12214
+ execSync7("pkill -f 'jest-worker' 2>/dev/null || true");
11745
12215
  } catch {
11746
12216
  }
11747
12217
  try {
11748
- execSync6("pkill -f 'pnpm test' 2>/dev/null || true");
12218
+ execSync7("pkill -f 'pnpm test' 2>/dev/null || true");
11749
12219
  } catch {
11750
12220
  }
11751
12221
  try {
11752
- execSync6("pkill -f 'npm test' 2>/dev/null || true");
12222
+ execSync7("pkill -f 'npm test' 2>/dev/null || true");
11753
12223
  } catch {
11754
12224
  }
11755
12225
  try {
11756
- execSync6("pkill -f 'yarn test' 2>/dev/null || true");
12226
+ execSync7("pkill -f 'yarn test' 2>/dev/null || true");
11757
12227
  } catch {
11758
12228
  }
11759
12229
  console.error("\u2705 Killed orphaned test processes");