@plumpslabs/kuma 2.4.6 → 2.4.7

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
@@ -1329,25 +1329,31 @@ var init_sessionMemory = __esm({
1329
1329
  const parsed = JSON.parse(raw);
1330
1330
  const storedBranch = parsed.currentBranch;
1331
1331
  const branchSwitched = Boolean(storedBranch && currentGitBranch && storedBranch !== currentGitBranch);
1332
+ const lastActive = parsed.lastActiveAt || parsed.startTime || 0;
1333
+ const isIdle = lastActive > 0 && Date.now() - lastActive > 2 * 3600 * 1e3;
1334
+ const startTime = isIdle ? Date.now() : parsed.startTime || config.startTime;
1335
+ const metrics = isIdle ? { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 } : parsed.metrics || { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 };
1336
+ const toolCalls = isIdle ? [] : parsed.toolCalls || [];
1332
1337
  this.state = {
1333
1338
  projectRoot: parsed.projectRoot || config.projectRoot,
1334
- startTime: parsed.startTime || config.startTime,
1339
+ startTime,
1340
+ lastActiveAt: Date.now(),
1335
1341
  currentGoal: parsed.currentGoal || "",
1336
1342
  currentBranch: currentGitBranch || storedBranch || void 0,
1337
1343
  previousBranch: branchSwitched ? storedBranch : parsed.previousBranch,
1338
1344
  branchSwitched,
1339
- completedSteps: parsed.completedSteps || [],
1345
+ completedSteps: isIdle ? [] : parsed.completedSteps || [],
1340
1346
  modifiedFiles: new Map(parsed.modifiedFiles || []),
1341
1347
  failedFiles: new Map(parsed.failedFiles || []),
1342
1348
  searchResults: new Map(parsed.searchResults || []),
1343
1349
  dependencyGraph: new Map(parsed.dependencyGraph || []),
1344
- toolCalls: parsed.toolCalls || [],
1350
+ toolCalls,
1345
1351
  conventions: parsed.conventions,
1346
1352
  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 }
1353
+ metrics
1348
1354
  };
1349
1355
  this.initialized = true;
1350
- console.error(`[SessionMemory] Loaded persistent session memory.json`);
1356
+ console.error(`[SessionMemory] Loaded persistent session memory.json${isIdle ? " (idle reset applied)" : ""}`);
1351
1357
  return;
1352
1358
  } catch (err) {
1353
1359
  console.error(`[SessionMemory] Failed to load persistent session: ${err}. Re-initializing.`);
@@ -1508,6 +1514,7 @@ No unresolved issues.`;
1508
1514
  const serialized = {
1509
1515
  projectRoot: this.state.projectRoot,
1510
1516
  startTime: this.state.startTime,
1517
+ lastActiveAt: this.state.lastActiveAt || Date.now(),
1511
1518
  currentGoal: this.state.currentGoal,
1512
1519
  currentBranch: this.state.currentBranch,
1513
1520
  previousBranch: this.state.previousBranch,
@@ -1908,6 +1915,32 @@ No unresolved issues.`;
1908
1915
  }
1909
1916
  getModifiedFiles() {
1910
1917
  this.ensureInit();
1918
+ try {
1919
+ const root = this.state.projectRoot;
1920
+ const gitOut = execSync("git status --porcelain", {
1921
+ cwd: root,
1922
+ encoding: "utf-8",
1923
+ timeout: 3e3,
1924
+ stdio: ["pipe", "pipe", "pipe"]
1925
+ }).trim();
1926
+ if (gitOut) {
1927
+ for (const line of gitOut.split("\n")) {
1928
+ const trimmed = line.trim();
1929
+ if (!trimmed) continue;
1930
+ const rawPath = trimmed.slice(2).trim();
1931
+ const cleanPath = rawPath.replace(/^["']|["']$/g, "").split(" -> ").pop();
1932
+ if (cleanPath.startsWith(".kuma/") || cleanPath.startsWith(".git/")) continue;
1933
+ if (!this.state.modifiedFiles.has(cleanPath)) {
1934
+ this.state.modifiedFiles.set(cleanPath, {
1935
+ filePath: cleanPath,
1936
+ modifiedAt: Date.now(),
1937
+ status: trimmed.startsWith("??") || trimmed.startsWith("A") ? "created" : "modified"
1938
+ });
1939
+ }
1940
+ }
1941
+ }
1942
+ } catch {
1943
+ }
1911
1944
  return Array.from(this.state.modifiedFiles.values());
1912
1945
  }
1913
1946
  getFailedFiles() {
@@ -2130,6 +2163,14 @@ No unresolved issues.`;
2130
2163
  projectRoot: process.cwd(),
2131
2164
  startTime: Date.now()
2132
2165
  });
2166
+ } else if (this.state) {
2167
+ const lastActive = this.state.lastActiveAt || this.state.startTime || 0;
2168
+ if (lastActive > 0 && Date.now() - lastActive > 2 * 3600 * 1e3) {
2169
+ this.state.startTime = Date.now();
2170
+ this.state.metrics = { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 };
2171
+ this.state.toolCalls = [];
2172
+ }
2173
+ this.state.lastActiveAt = Date.now();
2133
2174
  }
2134
2175
  }
2135
2176
  };
@@ -3481,7 +3522,8 @@ async function recordDomainFlow(params) {
3481
3522
  type: "feature_domain",
3482
3523
  name: params.domain,
3483
3524
  metadata: {
3484
- hops: params.hops.length,
3525
+ hops: params.hops,
3526
+ hopsCount: params.hops.length,
3485
3527
  gotchas: params.gotchas?.length || 0,
3486
3528
  decisions: params.decisions?.length || 0,
3487
3529
  filePaths: params.filePaths || [],
@@ -4554,7 +4596,7 @@ function scoreMemoryRelevance(context, limit = 5) {
4554
4596
  const results = [];
4555
4597
  const kumaDir = path9.join(getProjectRoot(), MEMORY_DIR);
4556
4598
  if (!fs8.existsSync(kumaDir)) return [];
4557
- const terms = context.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
4599
+ const terms = Array.from(new Set(context.toLowerCase().split(/[\s/._-]+/).filter((w) => w.length > 2)));
4558
4600
  if (terms.length === 0) return [];
4559
4601
  try {
4560
4602
  const files = fs8.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
@@ -4781,21 +4823,21 @@ function getActiveGotchas() {
4781
4823
  let currentSeverity = "";
4782
4824
  let currentStatus = "active";
4783
4825
  for (const line of lines) {
4784
- const fileMatch = line.match(/^###\s+(.+?)\s*[—–-]+\s*(.+)/);
4826
+ const fileMatch = line.match(/^###\s+\[?(.+?)\]?\s*(?:[—–-]+\s*(.+))?$/);
4785
4827
  if (fileMatch) {
4786
4828
  if (currentFilePath && currentStatus !== "resolved" && currentStatus !== "deprecated") {
4787
4829
  results.push({ filePath: currentFilePath, description: currentDesc, severity: currentSeverity, content: currentSection });
4788
4830
  }
4789
4831
  currentFilePath = fileMatch[1].trim();
4790
- currentDesc = fileMatch[2].trim();
4832
+ currentDesc = (fileMatch[2] || fileMatch[1]).trim();
4791
4833
  currentSeverity = "medium";
4792
4834
  currentStatus = "active";
4793
4835
  currentSection = line + "\n";
4794
4836
  continue;
4795
4837
  }
4796
- const sevMatch = line.match(/- \*\*Severity\*\*:\s*(\w+)/);
4838
+ const sevMatch = line.match(/[-*]\s*(?:⚡\s*)?\*\*Severity\*\*:\s*(\w+)/i);
4797
4839
  if (sevMatch) currentSeverity = sevMatch[1].toLowerCase();
4798
- const statusMatch = line.match(/- \*\*Status\*\*:\s*(\w+)/);
4840
+ const statusMatch = line.match(/[-*]\s*(?:🏷️\s*)?\*\*Status\*\*:\s*(\w+)/i);
4799
4841
  if (statusMatch) currentStatus = statusMatch[1].toLowerCase();
4800
4842
  currentSection += line + "\n";
4801
4843
  }
@@ -4809,8 +4851,10 @@ function getActiveGotchas() {
4809
4851
  function checkFileGotchas(filePath) {
4810
4852
  const gotchas = getActiveGotchas();
4811
4853
  const warnings = [];
4854
+ const base = path10.basename(filePath);
4812
4855
  for (const g of gotchas) {
4813
- if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
4856
+ const gBase = path10.basename(g.filePath);
4857
+ if (filePath.includes(g.filePath) || g.filePath.includes(filePath) || base && gBase && base === gBase) {
4814
4858
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
4815
4859
  warnings.push(`${icon} **Gotcha**: ${g.description} (${g.severity})`);
4816
4860
  }
@@ -4919,8 +4963,9 @@ function isGotchaFresh(storedHash, currentHash) {
4919
4963
  if (!currentHash) return true;
4920
4964
  return storedHash === currentHash;
4921
4965
  }
4922
- async function getFreshGotchasForFile(filePath, limit = 5) {
4966
+ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false) {
4923
4967
  const root = getProjectRoot();
4968
+ const base = path11.basename(filePath);
4924
4969
  try {
4925
4970
  const dbFile = path11.join(root, ".kuma", "kuma.db");
4926
4971
  if (fs10.existsSync(dbFile)) {
@@ -4930,44 +4975,58 @@ async function getFreshGotchasForFile(filePath, limit = 5) {
4930
4975
  SELECT id, file_path, description, severity, workaround, content_hash
4931
4976
  FROM known_gotchas
4932
4977
  WHERE status IN ('active', 'verified')
4933
- AND (file_path = ? OR ? LIKE ('%' || file_path || '%'))
4978
+ AND (
4979
+ file_path = ?
4980
+ OR file_path LIKE ?
4981
+ OR ? LIKE ('%' || file_path || '%')
4982
+ OR file_path LIKE ?
4983
+ )
4934
4984
  ORDER BY
4935
- CASE WHEN file_path = ? THEN 0 ELSE 1 END, -- exact match first
4985
+ CASE WHEN file_path = ? THEN 0 WHEN file_path LIKE ? THEN 1 ELSE 2 END,
4936
4986
  CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
4937
4987
  updated_at DESC
4938
4988
  LIMIT ?
4939
4989
  `);
4940
- stmt.bind([filePath, filePath, filePath, limit]);
4990
+ stmt.bind([filePath, `%${filePath}%`, filePath, `%${base}%`, filePath, `%${filePath}%`, limit * 2]);
4941
4991
  const rows = [];
4942
4992
  while (stmt.step()) rows.push(stmt.getAsObject());
4943
4993
  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);
4994
+ if (rows.length > 0) {
4995
+ const currentHash = hashFileContent(filePath);
4996
+ const mapped = rows.map((r) => ({
4997
+ id: r.id,
4998
+ filePath: r.file_path,
4999
+ description: r.description,
5000
+ severity: r.severity,
5001
+ workaround: r.workaround ?? null,
5002
+ stale: !isGotchaFresh(r.content_hash ?? null, currentHash)
5003
+ }));
5004
+ const filtered = includeStale ? mapped : mapped.filter((g) => !g.stale);
5005
+ return filtered.slice(0, limit);
5006
+ }
4953
5007
  }
4954
5008
  } catch {
4955
5009
  }
4956
5010
  try {
4957
5011
  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
- }));
5012
+ const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
5013
+ const allGotchas = getActiveGotchas2();
5014
+ const matched = allGotchas.filter(
5015
+ (g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path11.basename(g.filePath) === base
5016
+ );
5017
+ if (matched.length > 0) {
5018
+ return matched.slice(0, limit).map((g) => ({
5019
+ filePath: g.filePath,
5020
+ description: g.description,
5021
+ severity: g.severity,
5022
+ workaround: null,
5023
+ stale: false
5024
+ }));
5025
+ }
4968
5026
  } catch {
4969
5027
  return [];
4970
5028
  }
5029
+ return [];
4971
5030
  }
4972
5031
  async function getFileTrace(filePath, limit = 8) {
4973
5032
  try {
@@ -5015,14 +5074,81 @@ function formatFileTrace(entries, filePath) {
5015
5074
  return lines.join("\n");
5016
5075
  }
5017
5076
  async function getDecisionsForFile(filePath, limit = 3) {
5077
+ const decisions = [];
5078
+ const root = getProjectRoot();
5079
+ const base = path11.basename(filePath);
5080
+ const baseWithoutExt = base.replace(/\.[^.]+$/, "");
5081
+ try {
5082
+ const decisionsMdPath = path11.join(root, ".kuma", "memories", "decisions.md");
5083
+ if (fs10.existsSync(decisionsMdPath)) {
5084
+ const content = fs10.readFileSync(decisionsMdPath, "utf-8");
5085
+ const sections = content.split(/^##\s+/m);
5086
+ for (const section of sections) {
5087
+ if (!section.trim() || section.startsWith("# ")) continue;
5088
+ const sectionLines = section.split("\n");
5089
+ const title = sectionLines[0].trim();
5090
+ const sectionText = section.toLowerCase();
5091
+ const matches = sectionText.includes(filePath.toLowerCase()) || sectionText.includes(base.toLowerCase()) || baseWithoutExt.length > 3 && sectionText.includes(baseWithoutExt.toLowerCase());
5092
+ if (matches) {
5093
+ let rationale = "";
5094
+ for (const line of sectionLines) {
5095
+ const rMatch = line.match(/^[-*]\s*\*\*Rationale:\*\*\s*(.+)$/i);
5096
+ if (rMatch) {
5097
+ rationale = rMatch[1].trim();
5098
+ break;
5099
+ }
5100
+ const oMatch = line.match(/^[-*]\s*\*\*Outcome:\*\*\s*(.+)$/i);
5101
+ if (!rationale && oMatch) {
5102
+ rationale = oMatch[1].trim();
5103
+ }
5104
+ }
5105
+ decisions.push(`\u{1F4CC} ${title}${rationale ? ` \u2014 *${rationale}*` : ""}`);
5106
+ if (decisions.length >= limit) return decisions;
5107
+ }
5108
+ }
5109
+ }
5110
+ } catch {
5111
+ }
5112
+ try {
5113
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
5114
+ const db = await getDb2();
5115
+ const stmt = db.prepare(`
5116
+ SELECT name, metadata FROM nodes
5117
+ WHERE type = 'decision'
5118
+ AND (name LIKE ? OR metadata LIKE ? OR name LIKE ? OR metadata LIKE ?)
5119
+ LIMIT ?
5120
+ `);
5121
+ stmt.bind([`%${base}%`, `%${base}%`, `%${baseWithoutExt}%`, `%${baseWithoutExt}%`, limit]);
5122
+ while (stmt.step()) {
5123
+ const row = stmt.getAsObject();
5124
+ try {
5125
+ const meta = JSON.parse(row.metadata || "{}");
5126
+ const title = row.name.replace(/^ADR:\s*/, "");
5127
+ const reason = meta.rationale || meta.context || meta.outcome || "";
5128
+ const formatted = `\u{1F4CC} ${title}${reason ? ` \u2014 *${reason.substring(0, 100)}*` : ""}`;
5129
+ if (!decisions.includes(formatted)) {
5130
+ decisions.push(formatted);
5131
+ }
5132
+ } catch {
5133
+ }
5134
+ }
5135
+ stmt.free();
5136
+ if (decisions.length >= limit) return decisions.slice(0, limit);
5137
+ } catch {
5138
+ }
5018
5139
  try {
5019
5140
  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})`);
5141
+ const searchTerms = `${filePath} ${base} ${baseWithoutExt}`.replace(/[\/._-]/g, " ");
5142
+ const scored = scoreMemoryRelevance2(searchTerms, limit);
5143
+ for (const m of scored) {
5144
+ const formatted = `\u{1F4CC} ${m.topic} \u2014 ${m.content ? m.content.substring(0, 80) : `${m.score}% match`}`;
5145
+ if (!decisions.includes(formatted)) {
5146
+ decisions.push(formatted);
5147
+ }
5148
+ }
5023
5149
  } catch {
5024
- return [];
5025
5150
  }
5151
+ return decisions.slice(0, limit);
5026
5152
  }
5027
5153
  async function getRelevantContext(filePath, goal) {
5028
5154
  const parts = [];
@@ -5253,6 +5379,7 @@ __export(kumaGotchas_exports, {
5253
5379
  resolveGotcha: () => resolveGotcha,
5254
5380
  resolveGotchasForScope: () => resolveGotchasForScope,
5255
5381
  setGotchaStatus: () => setGotchaStatus,
5382
+ syncGotchaStatusToMarkdown: () => syncGotchaStatusToMarkdown,
5256
5383
  syncGotchasGraph: () => syncGotchasGraph,
5257
5384
  syncGotchasToDb: () => syncGotchasToDb
5258
5385
  });
@@ -5535,13 +5662,14 @@ async function resolveGotchasForScope(scope) {
5535
5662
  const db = await getDb();
5536
5663
  const stmt = db.prepare(
5537
5664
  `SELECT id, file_path, content_hash FROM known_gotchas
5538
- WHERE status = 'active' AND file_path LIKE ? AND content_hash IS NOT NULL`
5665
+ WHERE status = 'active' AND (file_path LIKE ? OR file_path = ?) AND content_hash IS NOT NULL`
5539
5666
  );
5540
- stmt.bind([`%${scope}%`]);
5667
+ stmt.bind([`%${scope}%`, scope]);
5541
5668
  const rows = [];
5542
5669
  while (stmt.step()) rows.push(stmt.getAsObject());
5543
5670
  stmt.free();
5544
5671
  let resolved = 0;
5672
+ const resolvedPaths = [];
5545
5673
  for (const r of rows) {
5546
5674
  const current = hashFileContent(r.file_path);
5547
5675
  if (current && current !== r.content_hash) {
@@ -5550,9 +5678,13 @@ async function resolveGotchasForScope(scope) {
5550
5678
  [r.id]
5551
5679
  );
5552
5680
  resolved++;
5681
+ resolvedPaths.push(r.file_path);
5553
5682
  }
5554
5683
  }
5555
- if (resolved > 0) saveDb();
5684
+ if (resolved > 0) {
5685
+ saveDb();
5686
+ syncGotchaStatusToMarkdown([scope, ...resolvedPaths], "resolved", "auto-verified");
5687
+ }
5556
5688
  return { resolved };
5557
5689
  } catch {
5558
5690
  return { resolved: 0 };
@@ -5658,14 +5790,73 @@ async function getGotchasByStatus(status, limit = 20) {
5658
5790
  return [];
5659
5791
  }
5660
5792
  }
5793
+ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
5794
+ try {
5795
+ const mdPath = path12.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
5796
+ if (!fs11.existsSync(mdPath)) return false;
5797
+ const content = fs11.readFileSync(mdPath, "utf-8");
5798
+ const lines = content.split("\n");
5799
+ const normalizedTargets = targetPaths.map((p) => p.trim().toLowerCase()).filter(Boolean);
5800
+ if (normalizedTargets.length === 0) return false;
5801
+ let modified = false;
5802
+ const outputLines = [];
5803
+ let inMatchingSection = false;
5804
+ let sectionHasStatus = false;
5805
+ for (let i = 0; i < lines.length; i++) {
5806
+ const line = lines[i];
5807
+ const headingMatch = line.match(/^###\s+\[?(.+?)\]?\s*(?:[—–-]+\s*(.+))?$/);
5808
+ if (headingMatch) {
5809
+ if (inMatchingSection && !sectionHasStatus) {
5810
+ outputLines.push(`- \u{1F3F7}\uFE0F **Status**: ${newStatus}${reason ? ` (${reason})` : ""}`);
5811
+ modified = true;
5812
+ }
5813
+ const headingFile = headingMatch[1].trim().toLowerCase();
5814
+ const headingBase = path12.basename(headingFile);
5815
+ inMatchingSection = normalizedTargets.some(
5816
+ (t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path12.basename(t)
5817
+ );
5818
+ sectionHasStatus = false;
5819
+ outputLines.push(line);
5820
+ continue;
5821
+ }
5822
+ if (inMatchingSection) {
5823
+ const statusMatch = line.match(/^(\s*[-*]\s*(?:🏷️\s*)?\*\*Status\*\*:\s*)(.*)$/i);
5824
+ if (statusMatch) {
5825
+ outputLines.push(`- \u{1F3F7}\uFE0F **Status**: ${newStatus}${reason ? ` (${reason})` : ""}`);
5826
+ sectionHasStatus = true;
5827
+ modified = true;
5828
+ continue;
5829
+ }
5830
+ }
5831
+ outputLines.push(line);
5832
+ }
5833
+ if (inMatchingSection && !sectionHasStatus) {
5834
+ outputLines.push(`- \u{1F3F7}\uFE0F **Status**: ${newStatus}${reason ? ` (${reason})` : ""}`);
5835
+ modified = true;
5836
+ }
5837
+ if (modified) {
5838
+ fs11.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
5839
+ }
5840
+ return modified;
5841
+ } catch {
5842
+ return false;
5843
+ }
5844
+ }
5661
5845
  async function resolveGotcha(idOrScope, resolutionReason, verifiedBy = "agent") {
5662
5846
  try {
5663
5847
  await ensureGotchasSchema();
5664
5848
  const db = await getDb();
5665
5849
  let resolved = 0;
5666
5850
  const isId = typeof idOrScope === "number" || /^\d+$/.test(String(idOrScope).trim());
5851
+ const targetPaths = [];
5667
5852
  if (isId) {
5668
5853
  const id = Number(idOrScope);
5854
+ const findStmt = db.prepare(`SELECT file_path FROM known_gotchas WHERE id = ?`);
5855
+ findStmt.bind([id]);
5856
+ if (findStmt.step()) {
5857
+ targetPaths.push(findStmt.getAsObject().file_path);
5858
+ }
5859
+ findStmt.free();
5669
5860
  db.run(
5670
5861
  `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
5862
  [verifiedBy, id]
@@ -5673,12 +5864,22 @@ async function resolveGotcha(idOrScope, resolutionReason, verifiedBy = "agent")
5673
5864
  resolved = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5674
5865
  } else {
5675
5866
  const scope = String(idOrScope).trim();
5867
+ targetPaths.push(scope);
5868
+ const findStmt = db.prepare(`SELECT DISTINCT file_path FROM known_gotchas WHERE (file_path = ? OR file_path LIKE ?)`);
5869
+ findStmt.bind([scope, `%${scope}%`]);
5870
+ while (findStmt.step()) {
5871
+ targetPaths.push(findStmt.getAsObject().file_path);
5872
+ }
5873
+ findStmt.free();
5676
5874
  db.run(
5677
5875
  `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
5876
  [verifiedBy, scope, `%${scope}%`]
5679
5877
  );
5680
5878
  resolved = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5681
5879
  }
5880
+ if (targetPaths.length > 0) {
5881
+ syncGotchaStatusToMarkdown(targetPaths, "resolved", resolutionReason);
5882
+ }
5682
5883
  if (resolved > 0) {
5683
5884
  saveDb();
5684
5885
  rebuildFtsIndex();
@@ -5687,6 +5888,13 @@ async function resolveGotcha(idOrScope, resolutionReason, verifiedBy = "agent")
5687
5888
  message: `\u2705 Resolved ${resolved} gotcha(s) for "${idOrScope}"${resolutionReason ? `: ${resolutionReason}` : ""}.`
5688
5889
  };
5689
5890
  }
5891
+ const mdUpdated = syncGotchaStatusToMarkdown(targetPaths.length > 0 ? targetPaths : [String(idOrScope)], "resolved", resolutionReason);
5892
+ if (mdUpdated) {
5893
+ return {
5894
+ resolved: 1,
5895
+ message: `\u2705 Resolved gotcha in KNOWN_GOTCHAS.md for "${idOrScope}"${resolutionReason ? `: ${resolutionReason}` : ""}.`
5896
+ };
5897
+ }
5690
5898
  return { resolved: 0, message: `\u26A0\uFE0F No active gotchas found matching "${idOrScope}".` };
5691
5899
  } catch (err) {
5692
5900
  return { resolved: 0, message: `\u274C Failed to resolve gotcha: ${err}` };
@@ -5698,8 +5906,15 @@ async function deprecateGotcha(idOrScope, reason) {
5698
5906
  const db = await getDb();
5699
5907
  let deprecated = 0;
5700
5908
  const isId = typeof idOrScope === "number" || /^\d+$/.test(String(idOrScope).trim());
5909
+ const targetPaths = [];
5701
5910
  if (isId) {
5702
5911
  const id = Number(idOrScope);
5912
+ const findStmt = db.prepare(`SELECT file_path FROM known_gotchas WHERE id = ?`);
5913
+ findStmt.bind([id]);
5914
+ if (findStmt.step()) {
5915
+ targetPaths.push(findStmt.getAsObject().file_path);
5916
+ }
5917
+ findStmt.free();
5703
5918
  db.run(
5704
5919
  `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
5920
  [id]
@@ -5707,12 +5922,22 @@ async function deprecateGotcha(idOrScope, reason) {
5707
5922
  deprecated = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5708
5923
  } else {
5709
5924
  const scope = String(idOrScope).trim();
5925
+ targetPaths.push(scope);
5926
+ const findStmt = db.prepare(`SELECT DISTINCT file_path FROM known_gotchas WHERE (file_path = ? OR file_path LIKE ?)`);
5927
+ findStmt.bind([scope, `%${scope}%`]);
5928
+ while (findStmt.step()) {
5929
+ targetPaths.push(findStmt.getAsObject().file_path);
5930
+ }
5931
+ findStmt.free();
5710
5932
  db.run(
5711
5933
  `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
5934
  [scope, `%${scope}%`]
5713
5935
  );
5714
5936
  deprecated = Number(db.exec("SELECT changes()")[0]?.values?.[0]?.[0] || 0);
5715
5937
  }
5938
+ if (targetPaths.length > 0) {
5939
+ syncGotchaStatusToMarkdown(targetPaths, "deprecated", reason);
5940
+ }
5716
5941
  if (deprecated > 0) {
5717
5942
  saveDb();
5718
5943
  rebuildFtsIndex();
@@ -5721,6 +5946,13 @@ async function deprecateGotcha(idOrScope, reason) {
5721
5946
  message: `\u{1F9F9} Deprecated ${deprecated} gotcha(s) for "${idOrScope}"${reason ? `: ${reason}` : ""}.`
5722
5947
  };
5723
5948
  }
5949
+ const mdUpdated = syncGotchaStatusToMarkdown(targetPaths.length > 0 ? targetPaths : [String(idOrScope)], "deprecated", reason);
5950
+ if (mdUpdated) {
5951
+ return {
5952
+ deprecated: 1,
5953
+ message: `\u{1F9F9} Deprecated gotcha in KNOWN_GOTCHAS.md for "${idOrScope}"${reason ? `: ${reason}` : ""}.`
5954
+ };
5955
+ }
5724
5956
  return { deprecated: 0, message: `\u26A0\uFE0F No active gotchas found matching "${idOrScope}".` };
5725
5957
  } catch (err) {
5726
5958
  return { deprecated: 0, message: `\u274C Failed to deprecate gotcha: ${err}` };
@@ -6479,7 +6711,8 @@ var kumaFlowCache_exports = {};
6479
6711
  __export(kumaFlowCache_exports, {
6480
6712
  deriveHopsFromImports: () => deriveHopsFromImports,
6481
6713
  getFlowFreshness: () => getFlowFreshness,
6482
- getFreshDomainFlow: () => getFreshDomainFlow
6714
+ getFreshDomainFlow: () => getFreshDomainFlow,
6715
+ getHopsForDomain: () => getHopsForDomain
6483
6716
  });
6484
6717
  import fs15 from "fs";
6485
6718
  import path16 from "path";
@@ -6570,9 +6803,86 @@ function deriveHopsFromImports(entryFile, maxHops = 6) {
6570
6803
  }
6571
6804
  return hops;
6572
6805
  }
6806
+ function escapeRegex(str) {
6807
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6808
+ }
6809
+ async function getHopsForDomain(domain) {
6810
+ const hops = [];
6811
+ try {
6812
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
6813
+ const db = await getDb2();
6814
+ const stmt = db.prepare(`SELECT metadata FROM nodes WHERE type = 'feature_domain' AND name = ? LIMIT 1`);
6815
+ stmt.bind([domain]);
6816
+ if (stmt.step()) {
6817
+ const meta = JSON.parse(stmt.getAsObject().metadata || "{}");
6818
+ if (Array.isArray(meta.hops) && meta.hops.length > 0 && typeof meta.hops[0] === "object") {
6819
+ stmt.free();
6820
+ return meta.hops;
6821
+ }
6822
+ }
6823
+ stmt.free();
6824
+ const nodeStmt = db.prepare(`
6825
+ SELECT name, metadata FROM nodes
6826
+ WHERE type = 'cross_service_link' AND (metadata LIKE ? OR id LIKE ?)
6827
+ LIMIT 25
6828
+ `);
6829
+ nodeStmt.bind([`%"domain":"${domain}"%`, `%${domain}::%`]);
6830
+ const rawNodes = [];
6831
+ while (nodeStmt.step()) {
6832
+ const row = nodeStmt.getAsObject();
6833
+ try {
6834
+ const meta = JSON.parse(row.metadata || "{}");
6835
+ rawNodes.push({
6836
+ name: row.name,
6837
+ hopIndex: typeof meta.hopIndex === "number" ? meta.hopIndex : 999,
6838
+ relation: meta.relation,
6839
+ description: meta.description
6840
+ });
6841
+ } catch {
6842
+ }
6843
+ }
6844
+ nodeStmt.free();
6845
+ if (rawNodes.length >= 2) {
6846
+ rawNodes.sort((a, b) => a.hopIndex - b.hopIndex);
6847
+ const uniqueSteps = [];
6848
+ for (const n of rawNodes) {
6849
+ if (uniqueSteps.length === 0 || uniqueSteps[uniqueSteps.length - 1] !== n.name) {
6850
+ uniqueSteps.push(n.name);
6851
+ }
6852
+ }
6853
+ for (let i = 0; i < uniqueSteps.length - 1; i++) {
6854
+ hops.push({ from: uniqueSteps[i], to: uniqueSteps[i + 1] });
6855
+ }
6856
+ if (hops.length > 0) return hops;
6857
+ }
6858
+ } catch {
6859
+ }
6860
+ try {
6861
+ const root = getProjectRoot();
6862
+ const archPath = path16.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
6863
+ if (fs15.existsSync(archPath)) {
6864
+ const content = fs15.readFileSync(archPath, "utf-8");
6865
+ const lines = content.split("\n");
6866
+ for (const line of lines) {
6867
+ const domMatch = line.match(new RegExp(`domain:\\s*${escapeRegex(domain)}\\s*\\|\\s*hops:\\s*(.+)`, "i"));
6868
+ if (domMatch) {
6869
+ const hopsPart = domMatch[1].trim();
6870
+ const arrowSteps = hopsPart.split(/→|->/).map((s) => s.trim()).filter(Boolean);
6871
+ for (let i = 0; i < arrowSteps.length - 1; i++) {
6872
+ hops.push({ from: arrowSteps[i], to: arrowSteps[i + 1] });
6873
+ }
6874
+ if (hops.length > 0) return hops;
6875
+ }
6876
+ }
6877
+ }
6878
+ } catch {
6879
+ }
6880
+ return hops;
6881
+ }
6573
6882
  async function getFreshDomainFlow(domain) {
6574
6883
  const freshness = await getFlowFreshness(domain);
6575
- if (!freshness) {
6884
+ const hops = await getHopsForDomain(domain);
6885
+ if (!freshness && hops.length === 0) {
6576
6886
  return `\u2139\uFE0F No arch_flow recorded for domain "${domain}".
6577
6887
  Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} | hops: a.ts \u2192 b.ts \u2192 c.ts' }).`;
6578
6888
  }
@@ -6581,33 +6891,55 @@ Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} |
6581
6891
  `\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
6892
  ""
6583
6893
  ];
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).");
6894
+ if (freshness) {
6895
+ if (freshness.fresh) {
6896
+ lines.push(`\u2705 Cache FRESH \u2014 served as-is (${freshness.filePaths.length} file(s) tracked).`);
6897
+ } else {
6898
+ lines.push(`\u{1F501} Cache STALE (${freshness.staleFiles.length} file(s) changed) \u2014 re-deriving from imports...`);
6899
+ try {
6900
+ const entry = freshness.filePaths[0];
6901
+ const derivedHops = deriveHopsFromImports(entry);
6902
+ if (derivedHops.length > 0) {
6903
+ const { recordDomainFlow: recordDomainFlow2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
6904
+ await recordDomainFlow2({
6905
+ domain,
6906
+ hops: derivedHops,
6907
+ filePaths: freshness.filePaths
6908
+ });
6909
+ lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path16.basename(entry)} \u2014 cache refreshed.`);
6910
+ hops.splice(0, hops.length, ...derivedHops);
6911
+ } else {
6912
+ lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
6913
+ }
6914
+ } catch (err) {
6915
+ lines.push(`\u26A0\uFE0F Re-derivation failed: ${err}`);
6601
6916
  }
6602
- } catch (err) {
6603
- lines.push(`\u26A0\uFE0F Re-derivation failed: ${err}`);
6604
6917
  }
6918
+ } else {
6919
+ lines.push(`\u2139\uFE0F Serving flow from markdown definition.`);
6605
6920
  }
6606
6921
  lines.push("");
6607
- for (const fp of freshness.filePaths) {
6608
- lines.push(` \u{1F4C4} ${fp}${freshness.staleFiles.includes(fp) ? " \u26A0\uFE0F changed" : ""}`);
6922
+ if (hops.length > 0) {
6923
+ lines.push("\u{1F504} **Flow Sequence (Hops):**");
6924
+ for (let i = 0; i < hops.length; i++) {
6925
+ const h = hops[i];
6926
+ const detail = h.relation && h.relation !== "flows" ? ` (${h.relation})` : "";
6927
+ if (i === 0) {
6928
+ lines.push(` 1. ${h.from} \u2192`);
6929
+ }
6930
+ const isLast = i === hops.length - 1;
6931
+ lines.push(` ${i + 2}. ${h.to}${detail}${isLast ? "" : " \u2192"}`);
6932
+ }
6933
+ lines.push("");
6609
6934
  }
6610
- return lines.join("\n");
6935
+ if (freshness && freshness.filePaths.length > 0) {
6936
+ lines.push("\u{1F4C1} **Tracked Files:**");
6937
+ for (const fp of freshness.filePaths) {
6938
+ lines.push(` \u{1F4C4} ${fp}${freshness.staleFiles.includes(fp) ? " \u26A0\uFE0F changed" : ""}`);
6939
+ }
6940
+ lines.push("");
6941
+ }
6942
+ return lines.join("\n").trimEnd();
6611
6943
  }
6612
6944
  var init_kumaFlowCache = __esm({
6613
6945
  "src/engine/kumaFlowCache.ts"() {
@@ -7170,25 +7502,66 @@ async function createCheckpoint(label, description) {
7170
7502
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
7171
7503
  }
7172
7504
  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
- );
7505
+ const candidatePaths = /* @__PURE__ */ new Set();
7506
+ try {
7507
+ const { execSync: execSync6 } = await import("child_process");
7508
+ const gitOut = execSync6("git status --porcelain", {
7509
+ cwd: root,
7510
+ encoding: "utf-8",
7511
+ timeout: 4e3,
7512
+ stdio: ["pipe", "pipe", "pipe"]
7513
+ }).trim();
7514
+ if (gitOut) {
7515
+ for (const line of gitOut.split("\n")) {
7516
+ const trimmed = line.trim();
7517
+ if (!trimmed) continue;
7518
+ const rawPath = trimmed.slice(2).trim();
7519
+ const cleanPath = rawPath.replace(/^["']|["']$/g, "").split(" -> ").pop();
7520
+ if (cleanPath.startsWith(".kuma/") || cleanPath.startsWith(".git/")) continue;
7521
+ candidatePaths.add(cleanPath);
7522
+ }
7523
+ }
7524
+ } catch {
7525
+ }
7526
+ try {
7527
+ for (const m of sessionMemory.getModifiedFiles()) {
7528
+ if (!m.filePath.startsWith(".kuma/") && !m.filePath.startsWith(".git/")) {
7529
+ candidatePaths.add(m.filePath);
7530
+ }
7531
+ }
7532
+ } catch {
7533
+ }
7534
+ try {
7535
+ const db2 = await getDb();
7536
+ const filesStmt = db2.prepare(
7537
+ "SELECT DISTINCT file_path FROM change_log ORDER BY id DESC LIMIT 100"
7538
+ );
7539
+ while (filesStmt.step()) {
7540
+ const row = filesStmt.getAsObject();
7541
+ const fp = row.file_path;
7542
+ if (fp && !fp.startsWith(".kuma/") && !fp.startsWith(".git/")) {
7543
+ candidatePaths.add(fp);
7544
+ }
7545
+ }
7546
+ filesStmt.free();
7547
+ } catch {
7548
+ }
7177
7549
  const files = [];
7178
- while (filesStmt.step()) {
7179
- const row = filesStmt.getAsObject();
7180
- const fp = row.file_path;
7550
+ for (const fp of candidatePaths) {
7181
7551
  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 });
7552
+ try {
7553
+ if (fs22.existsSync(fullPath) && fs22.statSync(fullPath).isFile()) {
7554
+ const content = fs22.readFileSync(fullPath, "utf-8");
7555
+ const hash = simpleHash2(content);
7556
+ const fileDir = path23.dirname(path23.join(cpDir, "files", fp));
7557
+ if (!fs22.existsSync(fileDir)) fs22.mkdirSync(fileDir, { recursive: true });
7558
+ fs22.writeFileSync(path23.join(cpDir, "files", fp), content, "utf-8");
7559
+ files.push({ path: fp, hash });
7560
+ }
7561
+ } catch {
7189
7562
  }
7190
7563
  }
7191
- filesStmt.free();
7564
+ const db = await getDb();
7192
7565
  const dbData = db.export();
7193
7566
  fs22.writeFileSync(path23.join(cpDir, "kuma.db"), Buffer.from(dbData));
7194
7567
  const manifest = {
@@ -10480,11 +10853,32 @@ async function handleHistory(params) {
10480
10853
  try {
10481
10854
  const { getFileTrace: getFileTrace2, formatFileTrace: formatFileTrace2, getFreshGotchasForFile: getFreshGotchasForFile2, getDecisionsForFile: getDecisionsForFile2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
10482
10855
  const trace = await getFileTrace2(target, 8);
10483
- lines.push(formatFileTrace2(trace, target) || " (no changes recorded for this file yet)");
10856
+ const traceFormatted = formatFileTrace2(trace, target);
10857
+ if (traceFormatted) {
10858
+ lines.push(traceFormatted);
10859
+ } else {
10860
+ try {
10861
+ const { execSync: execSync6 } = await import("child_process");
10862
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
10863
+ const root = getProjectRoot2();
10864
+ const gitHistory = execSync6(
10865
+ `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
10866
+ { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
10867
+ ).trim();
10868
+ if (gitHistory) {
10869
+ lines.push(`\u{1F570}\uFE0F **Git Commit History (latest):**
10870
+ ${gitHistory}`);
10871
+ } else {
10872
+ lines.push(" (no changes recorded for this file yet)");
10873
+ }
10874
+ } catch {
10875
+ lines.push(" (no changes recorded for this file yet)");
10876
+ }
10877
+ }
10484
10878
  lines.push("");
10485
- const gotchas = await getFreshGotchasForFile2(target, 5);
10879
+ const gotchas = await getFreshGotchasForFile2(target, 5, true);
10486
10880
  if (gotchas.length > 0) {
10487
- lines.push("\u26A0\uFE0F **Active gotchas** (fresh):");
10881
+ lines.push("\u26A0\uFE0F **Active gotchas:**");
10488
10882
  for (const g of gotchas) {
10489
10883
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
10490
10884
  lines.push(` ${icon} [${g.severity}] ${g.description}`);
@@ -10499,11 +10893,34 @@ async function handleHistory(params) {
10499
10893
  try {
10500
10894
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
10501
10895
  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)}%`]);
10896
+ const base = path17.basename(target);
10897
+ const stmt = db.prepare(`
10898
+ SELECT description, last_verified_at FROM known_gotchas
10899
+ WHERE status = 'resolved' AND (file_path LIKE ? OR file_path LIKE ?)
10900
+ ORDER BY last_verified_at DESC LIMIT 5
10901
+ `);
10902
+ stmt.bind([`%${base}%`, `%${target}%`]);
10504
10903
  const resolved = [];
10505
10904
  while (stmt.step()) resolved.push(stmt.getAsObject());
10506
10905
  stmt.free();
10906
+ if (resolved.length === 0) {
10907
+ try {
10908
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
10909
+ const mdPath = path17.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
10910
+ if (fs16.existsSync(mdPath)) {
10911
+ const mdContent = fs16.readFileSync(mdPath, "utf-8");
10912
+ const sections = mdContent.split(/^###\s+/m);
10913
+ for (const sec of sections) {
10914
+ const secLower = sec.toLowerCase();
10915
+ if ((secLower.includes(base.toLowerCase()) || secLower.includes(target.toLowerCase())) && secLower.includes("status**: resolved")) {
10916
+ const descLine = sec.split("\n")[0].trim();
10917
+ resolved.push({ description: descLine, last_verified_at: null });
10918
+ }
10919
+ }
10920
+ }
10921
+ } catch {
10922
+ }
10923
+ }
10507
10924
  if (resolved.length > 0) {
10508
10925
  lines.push("\u2705 **Resolved gotchas** (fixed):");
10509
10926
  for (const r of resolved) {
@@ -10516,7 +10933,7 @@ async function handleHistory(params) {
10516
10933
  }
10517
10934
  const decisions = await getDecisionsForFile2(target, 3);
10518
10935
  if (decisions.length > 0) {
10519
- lines.push("\u{1F4CC} **Relevant decisions**");
10936
+ lines.push("\u{1F4CC} **Relevant decisions:**");
10520
10937
  for (const d of decisions) lines.push(` ${d}`);
10521
10938
  lines.push("");
10522
10939
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
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.6",
4
+ "version": "2.4.7",
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",