@plumpslabs/kuma 2.3.22 → 2.3.24

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
@@ -311,7 +311,7 @@ function createSchema(db) {
311
311
  id INTEGER PRIMARY KEY AUTOINCREMENT,
312
312
  source_id TEXT NOT NULL REFERENCES nodes(id),
313
313
  target_id TEXT NOT NULL REFERENCES nodes(id),
314
- type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with')),
314
+ type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with','affects')),
315
315
  weight REAL DEFAULT 1.0,
316
316
  metadata TEXT DEFAULT '{}',
317
317
  created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
@@ -326,7 +326,7 @@ function createSchema(db) {
326
326
  id INTEGER PRIMARY KEY AUTOINCREMENT,
327
327
  source_id TEXT NOT NULL REFERENCES nodes(id),
328
328
  target_id TEXT NOT NULL REFERENCES nodes(id),
329
- type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with')),
329
+ type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with','affects')),
330
330
  weight REAL DEFAULT 1.0,
331
331
  metadata TEXT DEFAULT '{}',
332
332
  created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
@@ -717,22 +717,20 @@ async function listResearchCache() {
717
717
  async function recordChange(entry) {
718
718
  try {
719
719
  const db = await getDb();
720
- const root = process.cwd();
721
- const fullPath = path2.resolve(root, entry.filePath);
722
- let previousContent = null;
723
- if (entry.changeType === "modified" && fs2.existsSync(fullPath)) {
724
- try {
725
- previousContent = fs2.readFileSync(fullPath, "utf-8");
726
- } catch {
727
- }
728
- } else if (entry.changeType === "created") {
729
- previousContent = "";
730
- }
731
720
  db.run(
732
721
  `INSERT INTO change_log (session_id, file_path, change_type, symbol, diff_summary, git_commit_hash, previous_content) VALUES (?, ?, ?, ?, ?, ?, ?)`,
733
- [entry.sessionId ?? null, entry.filePath, entry.changeType, entry.symbol ?? null, entry.diffSummary ?? null, entry.gitCommitHash ?? null, previousContent]
722
+ [entry.sessionId ?? null, entry.filePath, entry.changeType, entry.symbol ?? null, entry.diffSummary ?? null, entry.gitCommitHash ?? null, null]
734
723
  );
735
724
  saveDb();
725
+ try {
726
+ const countResult = db.exec("SELECT COUNT(*) FROM change_log");
727
+ const count = countResult[0]?.values?.[0] || 0;
728
+ if (count > 600) {
729
+ db.exec("DELETE FROM change_log WHERE id IN (SELECT id FROM change_log ORDER BY created_at ASC LIMIT 200)");
730
+ saveDb();
731
+ }
732
+ } catch {
733
+ }
736
734
  } catch (err) {
737
735
  console.error(`[KumaDB] Failed to record change: ${err}`);
738
736
  }
@@ -1077,16 +1075,69 @@ async function runGarbageCollection() {
1077
1075
  try {
1078
1076
  const db = await getDb();
1079
1077
  let removed = 0;
1078
+ const getCount = (sql) => {
1079
+ try {
1080
+ const result = db.exec(sql);
1081
+ return result[0]?.values?.[0] || 0;
1082
+ } catch {
1083
+ return 0;
1084
+ }
1085
+ };
1080
1086
  const orphanResult = db.exec(`DELETE FROM nodes WHERE id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges) AND file_path IS NULL AND updated_at < strftime('%s','now','-30 days')`);
1081
1087
  removed += orphanResult[0]?.values?.length || 0;
1088
+ const staleEdges = db.exec(`DELETE FROM edges WHERE weight < 0.1 AND updated_at < strftime('%s','now','-30 days')`);
1089
+ removed += staleEdges[0]?.values?.length || 0;
1082
1090
  db.exec(`DELETE FROM tool_calls WHERE created_at < strftime('%s','now','-90 days')`);
1083
1091
  db.exec(`DELETE FROM experiences WHERE created_at < strftime('%s','now','-90 days')`);
1092
+ const changeCount = getCount("SELECT COUNT(*) FROM change_log");
1093
+ if (changeCount > 500) {
1094
+ db.exec(`DELETE FROM change_log WHERE id IN (SELECT id FROM change_log ORDER BY created_at ASC LIMIT ${changeCount - 500})`);
1095
+ }
1096
+ const verifCount = getCount("SELECT COUNT(*) FROM verifications");
1097
+ if (verifCount > 100) {
1098
+ db.exec(`DELETE FROM verifications WHERE id IN (SELECT id FROM verifications ORDER BY created_at ASC LIMIT ${verifCount - 100})`);
1099
+ }
1100
+ const healthCount = getCount("SELECT COUNT(*) FROM health_snapshots");
1101
+ if (healthCount > 50) {
1102
+ db.exec(`DELETE FROM health_snapshots WHERE id IN (SELECT id FROM health_snapshots ORDER BY created_at ASC LIMIT ${healthCount - 50})`);
1103
+ }
1104
+ const auditCount = getCount("SELECT COUNT(*) FROM safety_audit");
1105
+ if (auditCount > 200) {
1106
+ db.exec(`DELETE FROM safety_audit WHERE id IN (SELECT id FROM safety_audit ORDER BY created_at ASC LIMIT ${auditCount - 200})`);
1107
+ }
1108
+ db.exec(`DELETE FROM research_cache WHERE updated_at < strftime('%s','now','-60 days')`);
1109
+ const secCount = getCount("SELECT COUNT(*) FROM security_findings");
1110
+ if (secCount > 100) {
1111
+ db.exec(`DELETE FROM security_findings WHERE id IN (SELECT id FROM security_findings ORDER BY created_at ASC LIMIT ${secCount - 100})`);
1112
+ }
1113
+ const ctxCount = getCount("SELECT COUNT(*) FROM context_notes");
1114
+ if (ctxCount > 200) {
1115
+ db.exec(`DELETE FROM context_notes WHERE id IN (SELECT id FROM context_notes ORDER BY created_at ASC LIMIT ${ctxCount - 200})`);
1116
+ }
1117
+ const benchCount = getCount("SELECT COUNT(*) FROM benchmarks");
1118
+ if (benchCount > 100) {
1119
+ db.exec(`DELETE FROM benchmarks WHERE id IN (SELECT id FROM benchmarks ORDER BY created_at ASC LIMIT ${benchCount - 100})`);
1120
+ }
1121
+ const decCount = getCount("SELECT COUNT(*) FROM decision_log");
1122
+ if (decCount > 200) {
1123
+ db.exec(`DELETE FROM decision_log WHERE id IN (SELECT id FROM decision_log ORDER BY created_at ASC LIMIT ${decCount - 200})`);
1124
+ }
1125
+ db.exec(`DELETE FROM known_gotchas WHERE file_path NOT IN (SELECT DISTINCT file_path FROM nodes WHERE file_path IS NOT NULL) AND file_path NOT LIKE '%::%'`);
1126
+ db.exec(`DELETE FROM scratch_entries WHERE created_at < strftime('%s','now','-7 days')`);
1127
+ const costCount = getCount("SELECT COUNT(*) FROM cost_tracking");
1128
+ if (costCount > 500) {
1129
+ db.exec(`DELETE FROM cost_tracking WHERE id IN (SELECT id FROM cost_tracking ORDER BY created_at ASC LIMIT ${costCount - 500})`);
1130
+ }
1131
+ const sessCount = getCount("SELECT COUNT(*) FROM sessions");
1132
+ if (sessCount > 50) {
1133
+ db.exec(`DELETE FROM sessions WHERE id IN (SELECT id FROM sessions ORDER BY started_at ASC LIMIT ${sessCount - 50})`);
1134
+ }
1084
1135
  try {
1085
1136
  db.exec("VACUUM");
1086
1137
  } catch {
1087
1138
  }
1088
1139
  saveDb();
1089
- return `\u{1F9F9} GC complete. Removed orphaned data. Database vacuumed.`;
1140
+ return `\u{1F9F9} GC complete \u2014 cleaned 17 tables. Database vacuumed.`;
1090
1141
  } catch (err) {
1091
1142
  return `\u274C GC failed: ${err}`;
1092
1143
  }
@@ -1324,7 +1375,9 @@ var init_sessionMemory = __esm({
1324
1375
  searchResults: new Map(parsed.searchResults || []),
1325
1376
  dependencyGraph: new Map(parsed.dependencyGraph || []),
1326
1377
  toolCalls: parsed.toolCalls || [],
1327
- conventions: parsed.conventions
1378
+ conventions: parsed.conventions,
1379
+ recordings: parsed.recordings || { archFlows: 0, gotchas: 0, decisions: 0, researchSaves: 0, total: 0 },
1380
+ metrics: parsed.metrics || { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 }
1328
1381
  };
1329
1382
  this.initialized = true;
1330
1383
  console.error(`[SessionMemory] Loaded persistent session memory.json`);
@@ -1342,7 +1395,9 @@ var init_sessionMemory = __esm({
1342
1395
  failedFiles: /* @__PURE__ */ new Map(),
1343
1396
  searchResults: /* @__PURE__ */ new Map(),
1344
1397
  dependencyGraph: /* @__PURE__ */ new Map(),
1345
- toolCalls: []
1398
+ toolCalls: [],
1399
+ recordings: { archFlows: 0, gotchas: 0, decisions: 0, researchSaves: 0, total: 0 },
1400
+ metrics: { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 }
1346
1401
  };
1347
1402
  this.initialized = true;
1348
1403
  this.save();
@@ -1490,7 +1545,9 @@ No unresolved issues.`;
1490
1545
  searchResults: Array.from(this.state.searchResults.entries()),
1491
1546
  dependencyGraph: Array.from(this.state.dependencyGraph.entries()),
1492
1547
  toolCalls: this.state.toolCalls,
1493
- conventions: this.state.conventions
1548
+ conventions: this.state.conventions,
1549
+ recordings: this.state.recordings,
1550
+ metrics: this.state.metrics
1494
1551
  };
1495
1552
  fs3.writeFileSync(path3.join(kumaDir, "memory.json"), JSON.stringify(serialized, null, 2), "utf-8");
1496
1553
  } catch (err) {
@@ -1516,6 +1573,10 @@ No unresolved issues.`;
1516
1573
  existing.modifiedAt = Date.now();
1517
1574
  existing.status = "modified";
1518
1575
  } else {
1576
+ if (this.state.modifiedFiles.size >= 200) {
1577
+ const oldest = this.state.modifiedFiles.keys().next().value;
1578
+ if (oldest) this.state.modifiedFiles.delete(oldest);
1579
+ }
1519
1580
  this.state.modifiedFiles.set(filePath, {
1520
1581
  filePath,
1521
1582
  modifiedAt: Date.now(),
@@ -1547,7 +1608,14 @@ No unresolved issues.`;
1547
1608
  timestamp: Date.now(),
1548
1609
  resolved: false
1549
1610
  });
1611
+ if (failures.length > 10) {
1612
+ failures.splice(0, failures.length - 10);
1613
+ }
1550
1614
  this.state.failedFiles.set(task, failures);
1615
+ if (this.state.failedFiles.size > 100) {
1616
+ const oldest = this.state.failedFiles.keys().next().value;
1617
+ if (oldest) this.state.failedFiles.delete(oldest);
1618
+ }
1551
1619
  this.save();
1552
1620
  this.ensureMemoriesDir();
1553
1621
  this.writeMemoryFile("known-issues", this.generateKnownIssuesMd());
@@ -1572,6 +1640,10 @@ No unresolved issues.`;
1572
1640
  }
1573
1641
  addSearchResult(query, files) {
1574
1642
  this.ensureInit();
1643
+ if (this.state.searchResults.size >= 50) {
1644
+ const oldest = this.state.searchResults.keys().next().value;
1645
+ if (oldest) this.state.searchResults.delete(oldest);
1646
+ }
1575
1647
  this.state.searchResults.set(query, files);
1576
1648
  this.save();
1577
1649
  }
@@ -1580,6 +1652,9 @@ No unresolved issues.`;
1580
1652
  const deps = this.state.dependencyGraph.get(file) ?? [];
1581
1653
  if (!deps.includes(dependsOn)) {
1582
1654
  deps.push(dependsOn);
1655
+ if (deps.length > 20) {
1656
+ deps.splice(0, deps.length - 20);
1657
+ }
1583
1658
  this.state.dependencyGraph.set(file, deps);
1584
1659
  this.save();
1585
1660
  }
@@ -1879,6 +1954,95 @@ No unresolved issues.`;
1879
1954
  return { isLooping: false };
1880
1955
  }
1881
1956
  // ============================================================
1957
+ // RECORDING TRACKING — Enforcement & Feedback
1958
+ // ============================================================
1959
+ /**
1960
+ * Increment recording counter for a specific type.
1961
+ * Called by kumaMemoryTool after successful record operations.
1962
+ */
1963
+ recordMemoryAction(type) {
1964
+ this.ensureInit();
1965
+ switch (type) {
1966
+ case "arch_flow":
1967
+ this.state.recordings.archFlows++;
1968
+ break;
1969
+ case "gotcha":
1970
+ this.state.recordings.gotchas++;
1971
+ break;
1972
+ case "decision":
1973
+ this.state.recordings.decisions++;
1974
+ break;
1975
+ case "research_save":
1976
+ this.state.recordings.researchSaves++;
1977
+ break;
1978
+ }
1979
+ this.state.recordings.total++;
1980
+ this.save();
1981
+ }
1982
+ /**
1983
+ * Get recording summary for this session.
1984
+ * Used by guard to detect if agent hasn't recorded anything.
1985
+ */
1986
+ getRecordingSummary() {
1987
+ this.ensureInit();
1988
+ const r = this.state.recordings;
1989
+ const missing = [];
1990
+ if (r.archFlows === 0) missing.push("arch_flow");
1991
+ if (r.gotchas === 0) missing.push("gotcha");
1992
+ if (r.decisions === 0) missing.push("decision");
1993
+ if (r.researchSaves === 0) missing.push("research_save");
1994
+ return {
1995
+ ...r,
1996
+ hasAnyRecordings: r.total > 0,
1997
+ missingRecordings: missing
1998
+ };
1999
+ }
2000
+ // ============================================================
2001
+ // SESSION METRICS — Time saved tracking
2002
+ // ============================================================
2003
+ /**
2004
+ * Track a file read operation.
2005
+ * Estimates time saved if research_cache exists for this file.
2006
+ */
2007
+ trackFileRead(filePath) {
2008
+ this.ensureInit();
2009
+ this.state.metrics.filesRead++;
2010
+ try {
2011
+ const researchDir = path3.join(this.state.projectRoot, ".kuma", "research");
2012
+ const cacheFile = path3.join(researchDir, `${filePath}.json`);
2013
+ if (fs3.existsSync(cacheFile)) {
2014
+ this.state.metrics.researchTimeSaved += 4e3;
2015
+ }
2016
+ } catch {
2017
+ }
2018
+ this.save();
2019
+ }
2020
+ /**
2021
+ * Track a file edit operation.
2022
+ */
2023
+ trackFileEdit(_filePath) {
2024
+ this.ensureInit();
2025
+ this.state.metrics.filesEdited++;
2026
+ this.save();
2027
+ }
2028
+ /**
2029
+ * Get metrics summary for session.
2030
+ */
2031
+ getMetricsSummary() {
2032
+ this.ensureInit();
2033
+ const m = this.state.metrics;
2034
+ const durationMs = Date.now() - this.state.startTime;
2035
+ const durationMin = Math.round(durationMs / 6e4);
2036
+ const savedSec = Math.round(m.researchTimeSaved / 1e3);
2037
+ return {
2038
+ filesRead: m.filesRead,
2039
+ filesEdited: m.filesEdited,
2040
+ researchTimeSaved: m.researchTimeSaved,
2041
+ researchTimeSavedFormatted: savedSec > 60 ? `${Math.round(savedSec / 60)}m ${savedSec % 60}s` : `${savedSec}s`,
2042
+ sessionDuration: durationMin > 60 ? `${Math.round(durationMin / 60)}h ${durationMin % 60}m` : `${durationMin}m`
2043
+ };
2044
+ }
2045
+ // ============================================================
1882
2046
  // V3: Change Tracking (Selective Undo)
1883
2047
  // ============================================================
1884
2048
  /**
@@ -1997,7 +2161,7 @@ async function detectStaleNodes() {
1997
2161
  SELECT id, type, name, file_path FROM nodes
1998
2162
  WHERE file_path IS NOT NULL
1999
2163
  AND length(file_path) > 0
2000
- ORDER BY updated_at DESC LIMIT 500
2164
+ ORDER BY updated_at DESC LIMIT 2000
2001
2165
  `);
2002
2166
  while (stmt.step()) {
2003
2167
  const row = stmt.getAsObject();
@@ -2182,16 +2346,18 @@ __export(kumaGraph_exports, {
2182
2346
  formatFlow: () => formatFlow,
2183
2347
  formatImpact: () => formatImpact,
2184
2348
  getGraphStats: () => getGraphStats,
2185
- listFederatedReferences: () => listFederatedReferences,
2186
2349
  nodeId: () => nodeId,
2350
+ propagateImpact: () => propagateImpact,
2187
2351
  queryGraph: () => queryGraph,
2352
+ recordAffects: () => recordAffects,
2188
2353
  recordApiRoute: () => recordApiRoute,
2189
2354
  recordDomainFlow: () => recordDomainFlow,
2355
+ recordFeature: () => recordFeature,
2190
2356
  recordFileDefinition: () => recordFileDefinition,
2191
2357
  recordFunctionCall: () => recordFunctionCall,
2192
2358
  recordImport: () => recordImport,
2193
2359
  recordTestRelation: () => recordTestRelation,
2194
- resolveFederatedNode: () => resolveFederatedNode,
2360
+ retrieveForTask: () => retrieveForTask,
2195
2361
  searchGraph: () => searchGraph,
2196
2362
  traceFlow: () => traceFlow,
2197
2363
  upsertNode: () => upsertNode
@@ -2199,6 +2365,60 @@ __export(kumaGraph_exports, {
2199
2365
  function nodeId(type, name) {
2200
2366
  return `${type}::${name}`;
2201
2367
  }
2368
+ async function pruneGraphIfNeeded() {
2369
+ try {
2370
+ const db = await getDb();
2371
+ const getCount = (sql) => {
2372
+ try {
2373
+ const result = db.exec(sql);
2374
+ return result[0]?.values?.[0] || 0;
2375
+ } catch {
2376
+ return 0;
2377
+ }
2378
+ };
2379
+ const nodeCount = getCount("SELECT COUNT(*) FROM nodes");
2380
+ const edgeCount = getCount("SELECT COUNT(*) FROM edges");
2381
+ if (nodeCount <= GRAPH_LIMITS.maxNodes && edgeCount <= GRAPH_LIMITS.maxEdges) {
2382
+ return { pruned: false, removedNodes: 0, removedEdges: 0 };
2383
+ }
2384
+ let removedNodes = 0;
2385
+ let removedEdges = 0;
2386
+ if (edgeCount > GRAPH_LIMITS.maxEdges) {
2387
+ const result = db.exec(`DELETE FROM edges WHERE weight < ${GRAPH_LIMITS.minWeight} AND metadata NOT LIKE '%stale:0%'`);
2388
+ removedEdges = result[0]?.values?.length || 0;
2389
+ }
2390
+ const currentNodes = getCount("SELECT COUNT(*) FROM nodes");
2391
+ if (currentNodes > GRAPH_LIMITS.maxNodes) {
2392
+ const result = db.exec(`
2393
+ DELETE FROM nodes WHERE id IN (
2394
+ SELECT id FROM nodes
2395
+ WHERE id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges)
2396
+ ORDER BY updated_at ASC
2397
+ LIMIT ${GRAPH_LIMITS.pruneBatch}
2398
+ )
2399
+ `);
2400
+ removedNodes = result[0]?.values?.length || 0;
2401
+ }
2402
+ const finalNodes = getCount("SELECT COUNT(*) FROM nodes");
2403
+ if (finalNodes > GRAPH_LIMITS.maxNodes) {
2404
+ const excess = finalNodes - GRAPH_LIMITS.maxNodes + 100;
2405
+ const result = db.exec(`
2406
+ DELETE FROM nodes WHERE id IN (
2407
+ SELECT id FROM nodes ORDER BY updated_at ASC LIMIT ${excess}
2408
+ )
2409
+ `);
2410
+ removedNodes += result[0]?.values?.length || 0;
2411
+ }
2412
+ if (removedNodes > 0 || removedEdges > 0) {
2413
+ saveDb(db);
2414
+ return { pruned: true, removedNodes, removedEdges };
2415
+ }
2416
+ return { pruned: false, removedNodes: 0, removedEdges: 0 };
2417
+ } catch (err) {
2418
+ console.error(`[KumaGraph] Prune failed: ${err}`);
2419
+ return { pruned: false, removedNodes: 0, removedEdges: 0 };
2420
+ }
2421
+ }
2202
2422
  async function upsertNode(node) {
2203
2423
  try {
2204
2424
  const db = await getDb();
@@ -2219,6 +2439,14 @@ async function upsertNode(node) {
2219
2439
  } catch {
2220
2440
  }
2221
2441
  saveDb(db);
2442
+ try {
2443
+ const countResult = db.exec("SELECT COUNT(*) FROM nodes");
2444
+ const nodeCount = countResult[0]?.values?.[0] || 0;
2445
+ if (nodeCount % 50 === 0 && nodeCount > GRAPH_LIMITS.maxNodes * 0.8) {
2446
+ await pruneGraphIfNeeded();
2447
+ }
2448
+ } catch {
2449
+ }
2222
2450
  } catch (err) {
2223
2451
  console.error(`[KumaGraph] Failed to upsert node: ${err}`);
2224
2452
  }
@@ -2327,6 +2555,14 @@ async function recordDomainFlow(params) {
2327
2555
  metadata: { relation: hop.relation, description: hop.description }
2328
2556
  });
2329
2557
  edgeCount++;
2558
+ await addEdge({
2559
+ sourceId: fromId,
2560
+ targetId: toId,
2561
+ type: "affects",
2562
+ weight: 0.8,
2563
+ metadata: { reason: "arch_flow", domain: params.domain }
2564
+ });
2565
+ edgeCount++;
2330
2566
  await addEdge({
2331
2567
  sourceId: domainId,
2332
2568
  targetId: fromId,
@@ -2403,6 +2639,14 @@ async function recordDomainFlow(params) {
2403
2639
  metadata: { domain: params.domain }
2404
2640
  });
2405
2641
  edgeCount++;
2642
+ await addEdge({
2643
+ sourceId: domainId,
2644
+ targetId: fileId,
2645
+ type: "affects",
2646
+ weight: 0.7,
2647
+ metadata: { reason: "arch_flow" }
2648
+ });
2649
+ edgeCount++;
2406
2650
  } catch {
2407
2651
  }
2408
2652
  }
@@ -2430,6 +2674,214 @@ async function clearGraph() {
2430
2674
  return -1;
2431
2675
  }
2432
2676
  }
2677
+ async function recordFeature(params) {
2678
+ let nodeCount = 0;
2679
+ let edgeCount = 0;
2680
+ try {
2681
+ const featureId = nodeId("feature", params.name);
2682
+ await upsertNode({
2683
+ id: featureId,
2684
+ type: "feature",
2685
+ name: params.name,
2686
+ metadata: {
2687
+ description: params.description || "",
2688
+ tags: params.tags || [],
2689
+ risk: params.risk || "medium"
2690
+ }
2691
+ });
2692
+ nodeCount++;
2693
+ if (params.files) {
2694
+ for (const fp of params.files) {
2695
+ const fileId = nodeId("file", fp);
2696
+ await upsertNode({ id: fileId, type: "file", name: fp });
2697
+ await addEdge({
2698
+ sourceId: featureId,
2699
+ targetId: fileId,
2700
+ type: "owns",
2701
+ weight: 1,
2702
+ metadata: { feature: params.name }
2703
+ });
2704
+ nodeCount++;
2705
+ edgeCount++;
2706
+ }
2707
+ }
2708
+ flushDb();
2709
+ return { nodeCount, edgeCount };
2710
+ } catch (err) {
2711
+ console.error(`[KumaGraph] Failed to record feature: ${err}`);
2712
+ return { nodeCount, edgeCount };
2713
+ }
2714
+ }
2715
+ async function recordAffects(sourceId, targetId, weight = 0.8, reason) {
2716
+ await addEdge({
2717
+ sourceId,
2718
+ targetId,
2719
+ type: "affects",
2720
+ weight,
2721
+ metadata: { reason: reason || "manual" }
2722
+ });
2723
+ }
2724
+ async function propagateImpact(startNodeId, maxDepth = 5, minWeight = 0.1) {
2725
+ try {
2726
+ const db = await getDb();
2727
+ const visited = /* @__PURE__ */ new Set();
2728
+ const results = [];
2729
+ const queue = [[startNodeId, 0, 1]];
2730
+ while (queue.length > 0) {
2731
+ const [currentId, depth, accWeight] = queue.shift();
2732
+ if (visited.has(currentId) || depth > maxDepth) continue;
2733
+ visited.add(currentId);
2734
+ const stmt = db.prepare(`
2735
+ SELECT e.target_id, e.weight, n.type, n.name, n.file_path
2736
+ FROM edges e
2737
+ LEFT JOIN nodes n ON n.id = e.target_id
2738
+ WHERE e.source_id = ? AND e.type = 'affects' AND e.weight >= ?
2739
+ `);
2740
+ stmt.bind([currentId, minWeight]);
2741
+ while (stmt.step()) {
2742
+ const row = stmt.getAsObject();
2743
+ const targetId = row.target_id;
2744
+ const edgeWeight = row.weight || 0.8;
2745
+ const propagatedWeight = accWeight * edgeWeight;
2746
+ if (!visited.has(targetId) && propagatedWeight >= minWeight) {
2747
+ results.push({
2748
+ id: targetId,
2749
+ type: row.type,
2750
+ name: row.name,
2751
+ depth: depth + 1,
2752
+ weight: Math.round(propagatedWeight * 100) / 100,
2753
+ filePath: row.file_path || void 0
2754
+ });
2755
+ queue.push([targetId, depth + 1, propagatedWeight]);
2756
+ }
2757
+ }
2758
+ stmt.free();
2759
+ }
2760
+ return results.sort((a, b) => b.weight - a.weight);
2761
+ } catch (err) {
2762
+ console.error(`[KumaGraph] Failed to propagate impact: ${err}`);
2763
+ return [];
2764
+ }
2765
+ }
2766
+ async function retrieveForTask(taskDescription, maxNodes = 10) {
2767
+ try {
2768
+ const db = await getDb();
2769
+ const keywords = taskDescription.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2);
2770
+ if (keywords.length === 0) {
2771
+ return `\u{1F50D} **Task Retrieval** \u2014 No meaningful keywords in "${taskDescription}".`;
2772
+ }
2773
+ const allNodes = [];
2774
+ try {
2775
+ const ftsQuery = keywords.join(" OR ");
2776
+ const stmt = db.prepare(`
2777
+ SELECT n.id, n.type, n.name, n.file_path
2778
+ FROM nodes_fts f
2779
+ JOIN nodes n ON n.rowid = f.rowid
2780
+ WHERE nodes_fts MATCH ?
2781
+ LIMIT ?
2782
+ `);
2783
+ stmt.bind([ftsQuery, maxNodes * 2]);
2784
+ while (stmt.step()) {
2785
+ const row = stmt.getAsObject();
2786
+ allNodes.push({
2787
+ id: row.id,
2788
+ type: row.type,
2789
+ name: row.name,
2790
+ filePath: row.file_path || void 0,
2791
+ relevance: 0.8
2792
+ });
2793
+ }
2794
+ stmt.free();
2795
+ } catch {
2796
+ }
2797
+ if (allNodes.length === 0) {
2798
+ for (const kw of keywords) {
2799
+ const stmt = db.prepare(`
2800
+ SELECT id, type, name, file_path FROM nodes
2801
+ WHERE name LIKE ? OR file_path LIKE ?
2802
+ LIMIT ?
2803
+ `);
2804
+ stmt.bind([`%${kw}%`, `%${kw}%`, maxNodes]);
2805
+ while (stmt.step()) {
2806
+ const row = stmt.getAsObject();
2807
+ allNodes.push({
2808
+ id: row.id,
2809
+ type: row.type,
2810
+ name: row.name,
2811
+ filePath: row.file_path || void 0,
2812
+ relevance: 0.6
2813
+ });
2814
+ }
2815
+ stmt.free();
2816
+ }
2817
+ }
2818
+ const seen = /* @__PURE__ */ new Set();
2819
+ const prioritized = [];
2820
+ const typePriority = {
2821
+ feature: 10,
2822
+ feature_domain: 9,
2823
+ class: 8,
2824
+ api_route: 7,
2825
+ db_table: 6,
2826
+ file: 5,
2827
+ function: 4,
2828
+ component: 3,
2829
+ gotcha: 2,
2830
+ decision: 1
2831
+ };
2832
+ for (const node of allNodes) {
2833
+ if (!seen.has(node.id)) {
2834
+ seen.add(node.id);
2835
+ node.relevance += typePriority[node.type] || 0;
2836
+ prioritized.push(node);
2837
+ }
2838
+ }
2839
+ prioritized.sort((a, b) => b.relevance - a.relevance);
2840
+ const topNodes = prioritized.slice(0, maxNodes);
2841
+ if (topNodes.length === 0) {
2842
+ return `\u{1F50D} **Task Retrieval** \u2014 No relevant nodes found for "${taskDescription}".
2843
+
2844
+ \u{1F4A1} Try recording features, arch_flows, or gotchas first.`;
2845
+ }
2846
+ const lines = [
2847
+ `\u{1F50D} **Task Context** \u2014 "${taskDescription}"`,
2848
+ `\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`,
2849
+ `\u{1F4CA} ${topNodes.length} relevant node(s) found
2850
+ `
2851
+ ];
2852
+ for (const node of topNodes) {
2853
+ const emoji = node.type === "feature" ? "\u2B50" : node.type === "feature_domain" ? "\u{1F3DB}\uFE0F" : node.type === "class" ? "\u{1F3D7}\uFE0F" : node.type === "api_route" ? "\u{1F310}" : node.type === "db_table" ? "\u{1F5C4}\uFE0F" : node.type === "file" ? "\u{1F4C4}" : node.type === "function" ? "\u{1F527}" : node.type === "component" ? "\u25C7" : node.type === "gotcha" ? "\u26A0\uFE0F" : "\u{1F4CC}";
2854
+ lines.push(`${emoji} **${node.name}** (${node.type})`);
2855
+ if (node.filePath) lines.push(` \u{1F4CD} ${node.filePath}`);
2856
+ try {
2857
+ const stmt = db.prepare(`
2858
+ SELECT e.type AS edgeType, n.type AS nodeType, n.name AS nodeName
2859
+ FROM edges e
2860
+ LEFT JOIN nodes n ON n.id = (CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END)
2861
+ WHERE (e.source_id = ? OR e.target_id = ?) AND n.id IS NOT NULL
2862
+ ORDER BY e.weight DESC
2863
+ LIMIT 3
2864
+ `);
2865
+ stmt.bind([node.id, node.id, node.id]);
2866
+ const connections = [];
2867
+ while (stmt.step()) {
2868
+ const row = stmt.getAsObject();
2869
+ connections.push(`${row.edgeType}\u2192${row.nodeName}`);
2870
+ }
2871
+ stmt.free();
2872
+ if (connections.length > 0) {
2873
+ lines.push(` \u{1F517} ${connections.join(", ")}`);
2874
+ }
2875
+ } catch {
2876
+ }
2877
+ lines.push("");
2878
+ }
2879
+ lines.push("\u{1F4A1} Use this context instead of scanning the full codebase.");
2880
+ return lines.join("\n");
2881
+ } catch (err) {
2882
+ return `Error retrieving task context: ${err}`;
2883
+ }
2884
+ }
2433
2885
  async function queryGraph(params) {
2434
2886
  try {
2435
2887
  const db = await getDb();
@@ -2701,27 +3153,27 @@ async function codebaseSearchFallback(query, limit = 20) {
2701
3153
  if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
2702
3154
  let entries = [];
2703
3155
  try {
2704
- entries = fs27.readdirSync(dir);
3156
+ entries = fs26.readdirSync(dir);
2705
3157
  } catch {
2706
3158
  return;
2707
3159
  }
2708
3160
  for (const entry of entries) {
2709
3161
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
2710
- const full = path27.join(dir, entry);
3162
+ const full = path26.join(dir, entry);
2711
3163
  try {
2712
- const stat = fs27.statSync(full);
3164
+ const stat = fs26.statSync(full);
2713
3165
  if (stat.isDirectory()) {
2714
3166
  walk2(full, depth + 1);
2715
3167
  } else if (entry.toLowerCase().includes(query.toLowerCase())) {
2716
- results.push(path27.relative(root, full));
3168
+ results.push(path26.relative(root, full));
2717
3169
  }
2718
3170
  } catch {
2719
3171
  }
2720
3172
  }
2721
3173
  };
2722
3174
  var walk = walk2;
2723
- const fs27 = await import("fs");
2724
- const path27 = await import("path");
3175
+ const fs26 = await import("fs");
3176
+ const path26 = await import("path");
2725
3177
  const root = process.cwd();
2726
3178
  const results = [];
2727
3179
  const MAX_WALK_DEPTH = 8;
@@ -2741,114 +3193,19 @@ async function codebaseSearchFallback(query, limit = 20) {
2741
3193
  }
2742
3194
  }
2743
3195
  }
2744
- async function resolveFederatedNode(uri) {
2745
- try {
2746
- if (!uri.startsWith("kuma://")) {
2747
- return `\u26A0\uFE0F Invalid URI scheme: "${uri.substring(0, 20)}...". Use kuma://project/node-id`;
2748
- }
2749
- const pathPart = uri.replace("kuma://", "");
2750
- const firstSlash = pathPart.indexOf("/");
2751
- if (firstSlash === -1) {
2752
- return `\u26A0\uFE0F Invalid federated URI: "${uri}". Format: kuma://<project>/<node-id>`;
2753
- }
2754
- const projectName = pathPart.substring(0, firstSlash);
2755
- const nodeRef = pathPart.substring(firstSlash + 1);
2756
- const db = await getDb();
2757
- const stmt = db.prepare(`
2758
- SELECT id, type, name, file_path, metadata FROM nodes
2759
- WHERE id = ? OR name LIKE ?
2760
- LIMIT 5
2761
- `);
2762
- stmt.bind([nodeRef, `%${nodeRef}%`]);
2763
- const results = [];
2764
- while (stmt.step()) results.push(stmt.getAsObject());
2765
- stmt.free();
2766
- if (results.length > 0) {
2767
- const lines = [
2768
- `\u{1F517} **Federated Node Resolved** (local)`,
2769
- `\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`,
2770
- `\u{1F4C1} Project: ${projectName}`,
2771
- `\u{1F517} URI: ${uri}`,
2772
- ""
2773
- ];
2774
- for (const r of results) {
2775
- lines.push(` \u2022 **${r.name}** (${r.type}) \u2014 ${r.file_path || "no path"}`);
2776
- }
2777
- lines.push("", "\u{1F4A1} Federated cross-repo resolution is active. Remote resolution via HTTP coming soon.");
2778
- return lines.join("\n");
2779
- }
2780
- const federatedNodes = await registerFederatedReference(uri, projectName, nodeRef);
2781
- return [
2782
- `\u{1F517} **Federated Reference**`,
2783
- `\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`,
2784
- `\u{1F4C1} Remote project: ${projectName}`,
2785
- `\u{1F517} URI: ${uri}`,
2786
- `\u{1F4CC} Node ref: ${nodeRef}`,
2787
- "",
2788
- `\u{1F4DD} Registered as federated reference for future resolution.`,
2789
- `\u{1F4A1} Remote graph resolution will be available when the target project is reachable.`,
2790
- federatedNodes > 0 ? `\u2705 Created ${federatedNodes} federated node(s) in local graph.` : ""
2791
- ].filter(Boolean).join("\n");
2792
- } catch (err) {
2793
- return `\u274C Federated resolution failed: ${err}`;
2794
- }
2795
- }
2796
- async function registerFederatedReference(uri, projectName, nodeRef) {
2797
- try {
2798
- await upsertNode({
2799
- id: `federated::${uri}`,
2800
- type: "module",
2801
- name: `[${projectName}] ${nodeRef}`,
2802
- metadata: {
2803
- federated: true,
2804
- uri,
2805
- project: projectName,
2806
- ref: nodeRef,
2807
- resolved: false
2808
- }
2809
- });
2810
- return 1;
2811
- } catch {
2812
- return 0;
2813
- }
2814
- }
2815
- async function listFederatedReferences() {
2816
- try {
2817
- const db = await getDb();
2818
- const stmt = db.prepare(`
2819
- SELECT id, name, metadata FROM nodes
2820
- WHERE metadata LIKE '%"federated":true%'
2821
- ORDER BY updated_at DESC
2822
- LIMIT 20
2823
- `);
2824
- const results = [];
2825
- while (stmt.step()) results.push(stmt.getAsObject());
2826
- stmt.free();
2827
- if (results.length === 0) {
2828
- return "\u{1F517} **No federated references.** Use kuma_memory({ action: 'federated', uri: 'kuma://project/node-id' }) to add one.";
2829
- }
2830
- const lines = [
2831
- "\u{1F517} **Federated References**",
2832
- "\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",
2833
- ""
2834
- ];
2835
- for (const r of results) {
2836
- const meta = JSON.parse(r.metadata || "{}");
2837
- lines.push(` \u{1F517} **${r.name}**`);
2838
- lines.push(` URI: ${meta.uri || "unknown"}`);
2839
- lines.push(` Resolved: ${meta.resolved ? "\u2705" : "\u23F3"}`);
2840
- lines.push("");
2841
- }
2842
- return lines.join("\n");
2843
- } catch (err) {
2844
- return `Error: ${err}`;
2845
- }
2846
- }
2847
3196
  async function getGraphStats() {
2848
3197
  try {
2849
3198
  const db = await getDb();
2850
- const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
2851
- const edgeCount = db.exec("SELECT COUNT(*) as c FROM edges")[0]?.values[0][0] ?? 0;
3199
+ const getCount = (sql) => {
3200
+ try {
3201
+ const result = db.exec(sql);
3202
+ return result[0]?.values?.[0] || 0;
3203
+ } catch {
3204
+ return 0;
3205
+ }
3206
+ };
3207
+ const nodeCount = getCount("SELECT COUNT(*) as c FROM nodes");
3208
+ const edgeCount = getCount("SELECT COUNT(*) as c FROM edges");
2852
3209
  const typeCounts = [];
2853
3210
  try {
2854
3211
  const stmt = db.prepare("SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type ORDER BY cnt DESC");
@@ -2863,6 +3220,7 @@ async function getGraphStats() {
2863
3220
  `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
2864
3221
  "",
2865
3222
  `\u{1F4CA} **${nodeCount} nodes** | **${edgeCount} edges**`,
3223
+ `\u{1F4CF} **Limits:** ${nodeCount}/${GRAPH_LIMITS.maxNodes} nodes (${Math.round(nodeCount / GRAPH_LIMITS.maxNodes * 100)}%) | ${edgeCount}/${GRAPH_LIMITS.maxEdges} edges (${Math.round(edgeCount / GRAPH_LIMITS.maxEdges * 100)}%)`,
2866
3224
  ""
2867
3225
  ];
2868
3226
  if (typeCounts.length > 0) {
@@ -3059,11 +3417,22 @@ function formatFlow(entryPoint, steps) {
3059
3417
  lines.push("", `\u{1F4A1} ${steps.length} step(s) traversed.`);
3060
3418
  return lines.join("\n");
3061
3419
  }
3420
+ var GRAPH_LIMITS;
3062
3421
  var init_kumaGraph = __esm({
3063
3422
  "src/engine/kumaGraph.ts"() {
3064
3423
  "use strict";
3065
3424
  init_kumaDb();
3066
3425
  init_kumaSelfHeal();
3426
+ GRAPH_LIMITS = {
3427
+ maxNodes: 5e3,
3428
+ // Max nodes before pruning
3429
+ maxEdges: 1e4,
3430
+ // Max edges before pruning
3431
+ pruneBatch: 500,
3432
+ // Remove this many old nodes when limit hit
3433
+ minWeight: 0.1
3434
+ // Minimum edge weight before deletion
3435
+ };
3067
3436
  }
3068
3437
  });
3069
3438
 
@@ -5965,525 +6334,23 @@ async function listGotchas(params) {
5965
6334
  }
5966
6335
  sql += " ORDER BY severity DESC, created_at DESC LIMIT 50";
5967
6336
  const stmt = db.prepare(sql);
5968
- stmt.bind(bind);
5969
- const results = [];
5970
- while (stmt.step()) results.push(stmt.getAsObject());
5971
- stmt.free();
5972
- if (results.length === 0) {
5973
- return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
5974
- }
5975
- const lines = [
5976
- `\u26A0\uFE0F **Known Gotchas** \u2014 ${results.length} recorded`,
5977
- "\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",
5978
- ""
5979
- ];
5980
- for (const g of results) {
5981
- const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
5982
- lines.push(`${icon} [${g.severity}] ${g.file_path}`);
5983
- lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
5984
- if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
5985
- lines.push("");
5986
- }
5987
- return lines.join("\n");
5988
- } catch (err) {
5989
- return `Error: ${err}`;
5990
- }
5991
- }
5992
- function checkGotchasForFile(filePath) {
5993
- const warnings = [];
5994
- const markdownWarnings = checkFileGotchas(filePath);
5995
- try {
5996
- const gotchas = getActiveGotchas();
5997
- for (const g of gotchas) {
5998
- if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
5999
- if (!markdownWarnings.some((w) => w.includes(g.description))) {
6000
- const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
6001
- warnings.push(`${icon} **Known Gotcha**: ${g.description} (${g.severity})`);
6002
- }
6003
- }
6004
- }
6005
- } catch {
6006
- }
6007
- return [.../* @__PURE__ */ new Set([...markdownWarnings, ...warnings])];
6008
- }
6009
- async function syncGotchasToDb() {
6010
- try {
6011
- await ensureGotchasSchema();
6012
- const db = await getDb();
6013
- const gotchas = getActiveGotchas();
6014
- let synced = 0;
6015
- for (const g of gotchas) {
6016
- const checkStmt = db.prepare(
6017
- `SELECT id FROM known_gotchas WHERE file_path = ? AND description = ?`
6018
- );
6019
- checkStmt.bind([g.filePath, g.description]);
6020
- const exists = checkStmt.step();
6021
- checkStmt.free();
6022
- if (exists) continue;
6023
- db.run(
6024
- `INSERT INTO known_gotchas (file_path, description, severity) VALUES (?, ?, ?)`,
6025
- [g.filePath, g.description, g.severity]
6026
- );
6027
- synced++;
6028
- }
6029
- saveDb();
6030
- return { synced };
6031
- } catch {
6032
- return { synced: 0 };
6033
- }
6034
- }
6035
- var init_kumaGotchas = __esm({
6036
- "src/engine/kumaGotchas.ts"() {
6037
- "use strict";
6038
- init_sessionMemory();
6039
- init_kumaDb();
6040
- init_domainRules();
6041
- }
6042
- });
6043
-
6044
- // src/engine/kumaTrajectory.ts
6045
- var kumaTrajectory_exports = {};
6046
- __export(kumaTrajectory_exports, {
6047
- findSimilarTrajectories: () => findSimilarTrajectories,
6048
- generateTestFromTrajectory: () => generateTestFromTrajectory,
6049
- generateTestFromTrajectoryId: () => generateTestFromTrajectoryId,
6050
- listDistilledSkills: () => listDistilledSkills,
6051
- listGeneratedTests: () => listGeneratedTests,
6052
- listTrajectories: () => listTrajectories,
6053
- recordTrajectory: () => recordTrajectory
6054
- });
6055
- import fs14 from "fs";
6056
- import path14 from "path";
6057
- async function ensureTrajectorySchema() {
6058
- const db = await getDb();
6059
- db.exec(`
6060
- CREATE TABLE IF NOT EXISTS trajectories (
6061
- id INTEGER PRIMARY KEY AUTOINCREMENT,
6062
- goal TEXT NOT NULL,
6063
- steps TEXT NOT NULL DEFAULT '[]',
6064
- total_duration_ms INTEGER DEFAULT 0,
6065
- success_rate REAL DEFAULT 0.0,
6066
- complexity INTEGER DEFAULT 0,
6067
- created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
6068
- );
6069
- CREATE INDEX IF NOT EXISTS idx_traj_goal ON trajectories(goal);
6070
- CREATE INDEX IF NOT EXISTS idx_traj_complexity ON trajectories(complexity);
6071
- CREATE INDEX IF NOT EXISTS idx_traj_created ON trajectories(created_at);
6072
- `);
6073
- db.exec(`
6074
- CREATE TABLE IF NOT EXISTS distilled_skills (
6075
- id INTEGER PRIMARY KEY AUTOINCREMENT,
6076
- name TEXT NOT NULL UNIQUE,
6077
- description TEXT NOT NULL,
6078
- pattern TEXT NOT NULL, -- JSON: tool sequence pattern
6079
- parameters TEXT DEFAULT '[]', -- JSON: parameterized inputs
6080
- success_count INTEGER DEFAULT 1,
6081
- avg_duration_ms INTEGER DEFAULT 0,
6082
- source_trajectory_id INTEGER,
6083
- last_used_at INTEGER,
6084
- created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
6085
- );
6086
- CREATE INDEX IF NOT EXISTS idx_skills_name ON distilled_skills(name);
6087
- CREATE INDEX IF NOT EXISTS idx_skills_used ON distilled_skills(last_used_at);
6088
- `);
6089
- db.exec(`
6090
- CREATE TABLE IF NOT EXISTS generated_tests (
6091
- id INTEGER PRIMARY KEY AUTOINCREMENT,
6092
- trajectory_id INTEGER,
6093
- file_path TEXT NOT NULL,
6094
- test_framework TEXT NOT NULL,
6095
- description TEXT,
6096
- test_code TEXT NOT NULL,
6097
- created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
6098
- );
6099
- CREATE INDEX IF NOT EXISTS idx_gen_tests_traj ON generated_tests(trajectory_id);
6100
- `);
6101
- saveDb();
6102
- }
6103
- async function recordTrajectory(goal) {
6104
- try {
6105
- await ensureTrajectorySchema();
6106
- const calls = sessionMemory.getToolCallHistory(100);
6107
- const steps = calls.map((c) => ({
6108
- toolName: c.toolName,
6109
- params: c.params,
6110
- success: true,
6111
- durationMs: 0,
6112
- timestamp: c.timestamp
6113
- }));
6114
- const totalDurationMs = steps.length > 0 ? steps[steps.length - 1].timestamp - steps[0].timestamp : 0;
6115
- const errorCalls = steps.filter((s) => {
6116
- const action = s.params.action;
6117
- return action === "verify" && s.params.success === false;
6118
- }).length;
6119
- const successRate = steps.length > 0 ? (steps.length - errorCalls) / steps.length : 1;
6120
- const complexity = Math.min(
6121
- 100,
6122
- Math.round(
6123
- steps.length * 5 + // 5 pts per step
6124
- errorCalls * 15 + // 15 pts per error
6125
- Math.min(totalDurationMs / 6e4 * 10, 30)
6126
- // 10 pts per minute (max 30)
6127
- )
6128
- );
6129
- const db2 = await getDb();
6130
- db2.run(
6131
- `INSERT INTO trajectories (goal, steps, total_duration_ms, success_rate, complexity)
6132
- VALUES (?, ?, ?, ?, ?)`,
6133
- [
6134
- goal.substring(0, 200),
6135
- JSON.stringify(steps),
6136
- totalDurationMs,
6137
- successRate,
6138
- complexity
6139
- ]
6140
- );
6141
- saveDb();
6142
- const result = db2.exec("SELECT last_insert_rowid() as id");
6143
- const id = result[0]?.values[0]?.[0];
6144
- if (id !== null && complexity > 40 && successRate > 0.8) {
6145
- await distillSkill(id, goal, steps).catch(() => {
6146
- });
6147
- if (goal.toLowerCase().includes("fix") || goal.toLowerCase().includes("bug") || goal.toLowerCase().includes("error")) {
6148
- await generateTestFromTrajectory(id, goal, steps).catch(() => {
6149
- });
6150
- }
6151
- }
6152
- return id ?? null;
6153
- } catch (err) {
6154
- console.error(`[Trajectory] Record error: ${err}`);
6155
- return null;
6156
- }
6157
- }
6158
- async function distillSkill(trajectoryId, goal, steps) {
6159
- try {
6160
- const db = await getDb();
6161
- const toolSequence = steps.map((s) => s.toolName);
6162
- const patternKey = toolSequence.join(" \u2192 ");
6163
- const uniqueTools = [...new Set(toolSequence)];
6164
- const skillName = `trajectory:${uniqueTools.slice(0, 3).join("-")}`;
6165
- const checkStmt = db.prepare(
6166
- "SELECT id, success_count, avg_duration_ms FROM distilled_skills WHERE name = ?"
6167
- );
6168
- checkStmt.bind([skillName]);
6169
- const exists = checkStmt.step();
6170
- let existingRow = null;
6171
- if (exists) {
6172
- existingRow = checkStmt.getAsObject();
6173
- }
6174
- checkStmt.free();
6175
- const totalDuration = steps.reduce((sum, s) => sum + s.durationMs, 0);
6176
- if (existingRow) {
6177
- const existingId = existingRow.id;
6178
- const existingCount = existingRow.success_count;
6179
- const existingAvg = existingRow.avg_duration_ms;
6180
- db.run(
6181
- `UPDATE distilled_skills
6182
- SET success_count = ?, avg_duration_ms = ?, last_used_at = strftime('%s','now')
6183
- WHERE id = ?`,
6184
- [
6185
- existingCount + 1,
6186
- Math.round((existingAvg * existingCount + totalDuration) / (existingCount + 1)),
6187
- existingId
6188
- ]
6189
- );
6190
- } else {
6191
- const description = [
6192
- `Distilled trajectory pattern for "${goal.substring(0, 60)}"`,
6193
- `Tool sequence: ${patternKey}`,
6194
- `${steps.length} steps, ${uniqueTools.length} unique tools`
6195
- ].join(". ");
6196
- db.run(
6197
- `INSERT INTO distilled_skills (name, description, pattern, parameters, success_count, avg_duration_ms, source_trajectory_id, last_used_at)
6198
- VALUES (?, ?, ?, ?, 1, ?, ?, strftime('%s','now'))`,
6199
- [
6200
- skillName,
6201
- description,
6202
- JSON.stringify(toolSequence),
6203
- JSON.stringify(extractParameters(steps)),
6204
- totalDuration,
6205
- trajectoryId
6206
- ]
6207
- );
6208
- }
6209
- saveDb();
6210
- console.error(`[Trajectory] Distilled skill "${skillName}" from trajectory #${trajectoryId}`);
6211
- } catch (err) {
6212
- console.error(`[Trajectory] Distill error: ${err}`);
6213
- }
6214
- }
6215
- function extractParameters(steps) {
6216
- const params = /* @__PURE__ */ new Set();
6217
- for (const step of steps) {
6218
- for (const [key] of Object.entries(step.params)) {
6219
- if (["scope", "target", "query", "filePath", "action"].includes(key)) {
6220
- params.add(key);
6221
- }
6222
- }
6223
- }
6224
- return Array.from(params);
6225
- }
6226
- async function generateTestFromTrajectory(trajectoryId, goal, steps) {
6227
- try {
6228
- await ensureTrajectorySchema();
6229
- const modifiedFiles = steps.filter((s) => s.params.filePath || s.params.target).map((s) => s.params.filePath || s.params.target).filter(Boolean);
6230
- if (modifiedFiles.length === 0) return null;
6231
- const framework = detectTestFramework();
6232
- const primaryFile = modifiedFiles[0];
6233
- const testCode = synthesizeTestCode(goal, primaryFile, modifiedFiles, framework);
6234
- const testFilePath = determineTestPath(primaryFile, framework);
6235
- const genTest = {
6236
- filePath: testFilePath,
6237
- framework,
6238
- code: testCode,
6239
- description: `Auto-generated from trajectory #${trajectoryId}: ${goal.substring(0, 80)}`
6240
- };
6241
- const db = await getDb();
6242
- db.run(
6243
- `INSERT INTO generated_tests (trajectory_id, file_path, test_framework, description, test_code)
6244
- VALUES (?, ?, ?, ?, ?)`,
6245
- [trajectoryId, testFilePath, framework, genTest.description, testCode]
6246
- );
6247
- saveDb();
6248
- const root = getProjectRoot();
6249
- const fullPath = path14.resolve(root, testFilePath);
6250
- const dir = path14.dirname(fullPath);
6251
- if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
6252
- fs14.writeFileSync(fullPath, testCode, "utf-8");
6253
- console.error(`[Trajectory] Generated test: ${testFilePath} from trajectory #${trajectoryId}`);
6254
- return genTest;
6255
- } catch (err) {
6256
- console.error(`[Trajectory] Test generation error: ${err}`);
6257
- return null;
6258
- }
6259
- }
6260
- async function generateTestFromTrajectoryId(trajectoryId) {
6261
- try {
6262
- await ensureTrajectorySchema();
6263
- const db = await getDb();
6264
- const stmt = db.prepare("SELECT id, goal, steps FROM trajectories WHERE id = ?");
6265
- stmt.bind([trajectoryId]);
6266
- if (!stmt.step()) {
6267
- stmt.free();
6268
- return `\u274C Trajectory #${trajectoryId} not found.`;
6269
- }
6270
- const row = stmt.getAsObject();
6271
- stmt.free();
6272
- const goal = row.goal;
6273
- const steps = JSON.parse(row.steps);
6274
- const result = await generateTestFromTrajectory(trajectoryId, goal, steps);
6275
- if (!result) {
6276
- return "\u26A0\uFE0F Could not generate test \u2014 no modified files found in trajectory.";
6277
- }
6278
- return [
6279
- `\u{1F9EA} **Test Generated** from trajectory #${trajectoryId}`,
6280
- `\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\u2501\u2501\u2501`,
6281
- ``,
6282
- `\u{1F4C1} **File**: ${result.filePath}`,
6283
- `\u26A1 **Framework**: ${result.framework}`,
6284
- `\u{1F4DD} **Description**: ${result.description}`,
6285
- ``,
6286
- `\`\`\`${result.framework === "jest" ? "typescript" : "javascript"}`,
6287
- result.code.substring(0, 800),
6288
- result.code.length > 800 ? "..." : "",
6289
- `\`\`\``,
6290
- ``,
6291
- `\u{1F4A1} Test file has been written to disk. Run your test suite to verify.`
6292
- ].filter(Boolean).join("\n");
6293
- } catch (err) {
6294
- return `\u274C Test generation failed: ${err}`;
6295
- }
6296
- }
6297
- async function listGeneratedTests() {
6298
- try {
6299
- await ensureTrajectorySchema();
6300
- const db = await getDb();
6301
- const stmt = db.prepare(`
6302
- SELECT gt.*, t.goal FROM generated_tests gt
6303
- LEFT JOIN trajectories t ON t.id = gt.trajectory_id
6304
- ORDER BY gt.created_at DESC LIMIT 20
6305
- `);
6306
- const results = [];
6307
- while (stmt.step()) results.push(stmt.getAsObject());
6308
- stmt.free();
6309
- if (results.length === 0) {
6310
- return "\u{1F9EA} **No generated tests yet.** Tests are auto-generated from successful fix trajectories.";
6311
- }
6312
- const lines = [
6313
- "\u{1F9EA} **Generated Tests**",
6314
- "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
6315
- ""
6316
- ];
6317
- for (const r of results) {
6318
- const time = new Date(r.created_at * 1e3).toLocaleString();
6319
- lines.push(` \u{1F4C4} **${r.file_path}**`);
6320
- lines.push(` \u26A1 ${r.test_framework} | \u{1F550} ${time}`);
6321
- if (r.goal) lines.push(` \u{1F3AF} ${r.goal.substring(0, 60)}`);
6322
- lines.push("");
6323
- }
6324
- return lines.join("\n");
6325
- } catch (err) {
6326
- return `Error: ${err}`;
6327
- }
6328
- }
6329
- function detectTestFramework() {
6330
- const root = getProjectRoot();
6331
- try {
6332
- const pkg = JSON.parse(fs14.readFileSync(path14.join(root, "package.json"), "utf-8"));
6333
- if (pkg.devDependencies?.vitest) return "vitest";
6334
- if (pkg.devDependencies?.jest || pkg.devDependencies?.["@jest/globals"]) return "jest";
6335
- } catch {
6336
- }
6337
- if (fs14.existsSync(path14.join(root, "vitest.config.ts")) || fs14.existsSync(path14.join(root, "vitest.config.js"))) return "vitest";
6338
- if (fs14.existsSync(path14.join(root, "jest.config.ts")) || fs14.existsSync(path14.join(root, "jest.config.js"))) return "jest";
6339
- return "node";
6340
- }
6341
- function determineTestPath(filePath, _framework) {
6342
- const dir = path14.dirname(filePath);
6343
- const basename = path14.basename(filePath, path14.extname(filePath));
6344
- return path14.join(dir, `__tests__`, `${basename}.fix.test.ts`);
6345
- }
6346
- function synthesizeTestCode(goal, primaryFile, modifiedFiles, framework) {
6347
- const importPath = primaryFile.replace(/\.ts$/, "").replace(/\.js$/, "");
6348
- const describeBlock = `describe('Fix: ${goal.substring(0, 60)}', () => {`;
6349
- const testBody = `
6350
- it('should not regress the fix applied in ${path14.basename(primaryFile)}', async () => {
6351
- // Auto-generated from Kuma trajectory \u2014 regression test
6352
- // Source file: ${primaryFile}
6353
- // Goal: ${goal}
6354
- // Modified files: ${modifiedFiles.slice(0, 3).join(", ")}
6355
-
6356
- // TODO: Replace with actual test logic
6357
- // const { yourFunction } = await import('${importPath}');
6358
- // const result = yourFunction();
6359
- // expect(result).toBeDefined();
6360
-
6361
- expect(true).toBe(true);
6362
- });
6363
- `;
6364
- const closeBlock = `});`;
6365
- switch (framework) {
6366
- case "vitest":
6367
- case "jest":
6368
- return [
6369
- `// ============================================================`,
6370
- `// AUTO-GENERATED REGRESSION TEST`,
6371
- `// Generated by Kuma Trajectory-to-Test Generator (Issue #28)`,
6372
- `// ${(/* @__PURE__ */ new Date()).toISOString()}`,
6373
- `// Goal: ${goal}`,
6374
- `// Files: ${modifiedFiles.join(", ")}`,
6375
- `// ============================================================`,
6376
- ``,
6377
- `import { describe, it, expect } from '@jest/globals';`,
6378
- ``,
6379
- describeBlock,
6380
- testBody,
6381
- closeBlock,
6382
- ``
6383
- ].join("\n");
6384
- default:
6385
- return [
6386
- `// AUTO-GENERATED REGRESSION TEST`,
6387
- `// Goal: ${goal}`,
6388
- `// Files: ${modifiedFiles.join(", ")}`,
6389
- ``,
6390
- `const assert = require('assert');`,
6391
- ``,
6392
- describeBlock,
6393
- testBody.replace("import", "// ").replace("expect", "// expect"),
6394
- closeBlock,
6395
- ``
6396
- ].join("\n");
6397
- }
6398
- }
6399
- async function listDistilledSkills() {
6400
- try {
6401
- await ensureTrajectorySchema();
6402
- const db = await getDb();
6403
- const stmt = db.prepare(`
6404
- SELECT * FROM distilled_skills ORDER BY success_count DESC, last_used_at DESC LIMIT 20
6405
- `);
6406
- const results = [];
6407
- while (stmt.step()) results.push(stmt.getAsObject());
6408
- stmt.free();
6409
- if (results.length === 0) {
6410
- return "\u{1F9E0} **No distilled skills yet** \u2014 skills are auto-generated from successful trajectories.";
6411
- }
6412
- const lines = [
6413
- "\u{1F9E0} **Distilled Skills** \u2014 auto-generated from trajectories",
6414
- "\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\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
6415
- ""
6416
- ];
6417
- for (const s of results) {
6418
- const pattern = JSON.parse(s.pattern || "[]");
6419
- const patternStr = pattern.slice(0, 5).join(" \u2192 ");
6420
- lines.push(` \u26A1 **${s.name}**`);
6421
- lines.push(` Pattern: ${patternStr}${pattern.length > 5 ? ` (+${pattern.length - 5} more)` : ""}`);
6422
- lines.push(` Used ${s.success_count}x | Avg ${s.avg_duration_ms}ms`);
6423
- lines.push("");
6424
- }
6425
- return lines.join("\n");
6426
- } catch (err) {
6427
- return `Error: ${err}`;
6428
- }
6429
- }
6430
- async function findSimilarTrajectories(errorPattern, limit = 5) {
6431
- try {
6432
- await ensureTrajectorySchema();
6433
- const db = await getDb();
6434
- const stmt = db.prepare(`
6435
- SELECT id, goal, steps, total_duration_ms, success_rate, complexity, created_at
6436
- FROM trajectories
6437
- WHERE steps LIKE ?
6438
- ORDER BY complexity DESC, created_at DESC
6439
- LIMIT ?
6440
- `);
6441
- stmt.bind([`%${errorPattern}%`, limit]);
6442
- const results = [];
6443
- while (stmt.step()) results.push(stmt.getAsObject());
6444
- stmt.free();
6445
- if (results.length === 0) return "";
6446
- const lines = [
6447
- "\u{1F504} **Similar Past Trajectories Found**",
6448
- "\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\u2501\u2501",
6449
- ""
6450
- ];
6451
- for (const r of results) {
6452
- const successIcon = r.success_rate > 0.8 ? "\u2705" : "\u26A0\uFE0F";
6453
- lines.push(`${successIcon} #${r.id} \u2014 ${r.goal.substring(0, 60)}`);
6454
- lines.push(` Complexity: ${r.complexity} | Duration: ${Math.round(r.total_duration_ms / 1e3)}s | Rate: ${Math.round(r.success_rate * 100)}%`);
6455
- lines.push("");
6456
- }
6457
- return lines.join("\n");
6458
- } catch {
6459
- return "";
6460
- }
6461
- }
6462
- async function listTrajectories(limit = 10) {
6463
- try {
6464
- await ensureTrajectorySchema();
6465
- const db = await getDb();
6466
- const stmt = db.prepare(`
6467
- SELECT id, goal, total_duration_ms, success_rate, complexity, created_at
6468
- FROM trajectories ORDER BY created_at DESC LIMIT ?
6469
- `);
6470
- stmt.bind([limit]);
6337
+ stmt.bind(bind);
6471
6338
  const results = [];
6472
6339
  while (stmt.step()) results.push(stmt.getAsObject());
6473
6340
  stmt.free();
6474
6341
  if (results.length === 0) {
6475
- return "\u{1F4C8} **No trajectories recorded yet.** Trajectories are logged automatically as agents work.";
6342
+ return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
6476
6343
  }
6477
6344
  const lines = [
6478
- "\u{1F4C8} **Agent Trajectories**",
6479
- "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
6345
+ `\u26A0\uFE0F **Known Gotchas** \u2014 ${results.length} recorded`,
6346
+ "\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",
6480
6347
  ""
6481
6348
  ];
6482
- for (const r of results) {
6483
- const time = new Date(r.created_at * 1e3).toLocaleString();
6484
- const successIcon = r.success_rate > 0.8 ? "\u2705" : "\u26A0\uFE0F";
6485
- lines.push(` ${successIcon} #${r.id} \u2014 ${r.goal.substring(0, 50)} @ ${time}`);
6486
- lines.push(` ${Math.round(r.total_duration_ms / 1e3)}s | ${Math.round(r.success_rate * 100)}% success | complexity: ${r.complexity}`);
6349
+ for (const g of results) {
6350
+ const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
6351
+ lines.push(`${icon} [${g.severity}] ${g.file_path}`);
6352
+ lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
6353
+ if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
6487
6354
  lines.push("");
6488
6355
  }
6489
6356
  return lines.join("\n");
@@ -6491,12 +6358,55 @@ async function listTrajectories(limit = 10) {
6491
6358
  return `Error: ${err}`;
6492
6359
  }
6493
6360
  }
6494
- var init_kumaTrajectory = __esm({
6495
- "src/engine/kumaTrajectory.ts"() {
6361
+ function checkGotchasForFile(filePath) {
6362
+ const warnings = [];
6363
+ const markdownWarnings = checkFileGotchas(filePath);
6364
+ try {
6365
+ const gotchas = getActiveGotchas();
6366
+ for (const g of gotchas) {
6367
+ if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
6368
+ if (!markdownWarnings.some((w) => w.includes(g.description))) {
6369
+ const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
6370
+ warnings.push(`${icon} **Known Gotcha**: ${g.description} (${g.severity})`);
6371
+ }
6372
+ }
6373
+ }
6374
+ } catch {
6375
+ }
6376
+ return [.../* @__PURE__ */ new Set([...markdownWarnings, ...warnings])];
6377
+ }
6378
+ async function syncGotchasToDb() {
6379
+ try {
6380
+ await ensureGotchasSchema();
6381
+ const db = await getDb();
6382
+ const gotchas = getActiveGotchas();
6383
+ let synced = 0;
6384
+ for (const g of gotchas) {
6385
+ const checkStmt = db.prepare(
6386
+ `SELECT id FROM known_gotchas WHERE file_path = ? AND description = ?`
6387
+ );
6388
+ checkStmt.bind([g.filePath, g.description]);
6389
+ const exists = checkStmt.step();
6390
+ checkStmt.free();
6391
+ if (exists) continue;
6392
+ db.run(
6393
+ `INSERT INTO known_gotchas (file_path, description, severity) VALUES (?, ?, ?)`,
6394
+ [g.filePath, g.description, g.severity]
6395
+ );
6396
+ synced++;
6397
+ }
6398
+ saveDb();
6399
+ return { synced };
6400
+ } catch {
6401
+ return { synced: 0 };
6402
+ }
6403
+ }
6404
+ var init_kumaGotchas = __esm({
6405
+ "src/engine/kumaGotchas.ts"() {
6496
6406
  "use strict";
6497
- init_kumaDb();
6498
6407
  init_sessionMemory();
6499
- init_pathValidator();
6408
+ init_kumaDb();
6409
+ init_domainRules();
6500
6410
  }
6501
6411
  });
6502
6412
 
@@ -6669,19 +6579,19 @@ __export(kumaPolicyEngine_exports, {
6669
6579
  processOverride: () => processOverride,
6670
6580
  savePolicyConfig: () => savePolicyConfig
6671
6581
  });
6672
- import fs20 from "fs";
6673
- import path20 from "path";
6582
+ import fs19 from "fs";
6583
+ import path19 from "path";
6674
6584
  function loadPolicyConfig() {
6675
6585
  try {
6676
6586
  const root = getProjectRoot();
6677
- const policyPath = path20.join(root, ".kuma", "POLICY.json");
6678
- if (fs20.existsSync(policyPath)) {
6679
- const content = fs20.readFileSync(policyPath, "utf-8");
6587
+ const policyPath = path19.join(root, ".kuma", "POLICY.json");
6588
+ if (fs19.existsSync(policyPath)) {
6589
+ const content = fs19.readFileSync(policyPath, "utf-8");
6680
6590
  const config = JSON.parse(content);
6681
6591
  return { ...DEFAULT_POLICY_CONFIG, ...config };
6682
6592
  }
6683
- const ymlPath = path20.join(root, ".kuma", "policy.yml");
6684
- if (fs20.existsSync(ymlPath)) {
6593
+ const ymlPath = path19.join(root, ".kuma", "policy.yml");
6594
+ if (fs19.existsSync(ymlPath)) {
6685
6595
  console.error("[PolicyEngine] Found legacy policy.yml \u2014 consider migrating to POLICY.json");
6686
6596
  }
6687
6597
  } catch (err) {
@@ -6692,10 +6602,10 @@ function loadPolicyConfig() {
6692
6602
  function savePolicyConfig(config) {
6693
6603
  try {
6694
6604
  const root = getProjectRoot();
6695
- const policyPath = path20.join(root, ".kuma", "POLICY.json");
6696
- const dir = path20.dirname(policyPath);
6697
- if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
6698
- fs20.writeFileSync(policyPath, JSON.stringify(config, null, 2), "utf-8");
6605
+ const policyPath = path19.join(root, ".kuma", "POLICY.json");
6606
+ const dir = path19.dirname(policyPath);
6607
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
6608
+ fs19.writeFileSync(policyPath, JSON.stringify(config, null, 2), "utf-8");
6699
6609
  return `\u2705 Policy config saved to .kuma/POLICY.json (${config.rules.length} rules)`;
6700
6610
  } catch (err) {
6701
6611
  return `\u274C Failed to save policy: ${err}`;
@@ -6980,14 +6890,14 @@ __export(kumaAstValidator_exports, {
6980
6890
  validateDiff: () => validateDiff,
6981
6891
  validateFile: () => validateFile
6982
6892
  });
6983
- import fs21 from "fs";
6984
- import path21 from "path";
6893
+ import fs20 from "fs";
6894
+ import path20 from "path";
6985
6895
  function loadImportWhitelist() {
6986
6896
  try {
6987
6897
  const root = getProjectRoot();
6988
- const whitelistPath = path21.join(root, ".kuma", "import-whitelist.json");
6989
- if (fs21.existsSync(whitelistPath)) {
6990
- return JSON.parse(fs21.readFileSync(whitelistPath, "utf-8"));
6898
+ const whitelistPath = path20.join(root, ".kuma", "import-whitelist.json");
6899
+ if (fs20.existsSync(whitelistPath)) {
6900
+ return JSON.parse(fs20.readFileSync(whitelistPath, "utf-8"));
6991
6901
  }
6992
6902
  } catch {
6993
6903
  }
@@ -7061,9 +6971,9 @@ function validateCodeContent(code, filePath) {
7061
6971
  function validateFile(filePath) {
7062
6972
  try {
7063
6973
  const root = getProjectRoot();
7064
- const fullPath = path21.resolve(root, filePath);
7065
- if (!fs21.existsSync(fullPath)) return [];
7066
- const content = fs21.readFileSync(fullPath, "utf-8");
6974
+ const fullPath = path20.resolve(root, filePath);
6975
+ if (!fs20.existsSync(fullPath)) return [];
6976
+ const content = fs20.readFileSync(fullPath, "utf-8");
7067
6977
  return validateCodeContent(content, filePath);
7068
6978
  } catch {
7069
6979
  return [];
@@ -7134,16 +7044,16 @@ __export(kumaCheckpoint_exports, {
7134
7044
  pruneCheckpoints: () => pruneCheckpoints,
7135
7045
  rollbackToCheckpoint: () => rollbackToCheckpoint
7136
7046
  });
7137
- import fs22 from "fs";
7138
- import path22 from "path";
7047
+ import fs21 from "fs";
7048
+ import path21 from "path";
7139
7049
  async function createCheckpoint(label, description) {
7140
7050
  try {
7141
7051
  const root = getProjectRoot();
7142
- const cpDir = path22.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
7143
- if (fs22.existsSync(cpDir)) {
7052
+ const cpDir = path21.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
7053
+ if (fs21.existsSync(cpDir)) {
7144
7054
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
7145
7055
  }
7146
- fs22.mkdirSync(cpDir, { recursive: true });
7056
+ fs21.mkdirSync(cpDir, { recursive: true });
7147
7057
  const db = await getDb();
7148
7058
  const filesStmt = db.prepare(
7149
7059
  "SELECT DISTINCT file_path FROM change_log ORDER BY id DESC LIMIT 100"
@@ -7152,19 +7062,19 @@ async function createCheckpoint(label, description) {
7152
7062
  while (filesStmt.step()) {
7153
7063
  const row = filesStmt.getAsObject();
7154
7064
  const fp = row.file_path;
7155
- const fullPath = path22.resolve(root, fp);
7156
- if (fs22.existsSync(fullPath)) {
7157
- const content = fs22.readFileSync(fullPath, "utf-8");
7065
+ const fullPath = path21.resolve(root, fp);
7066
+ if (fs21.existsSync(fullPath)) {
7067
+ const content = fs21.readFileSync(fullPath, "utf-8");
7158
7068
  const hash = simpleHash2(content);
7159
- const fileDir = path22.dirname(path22.join(cpDir, "files", fp));
7160
- if (!fs22.existsSync(fileDir)) fs22.mkdirSync(fileDir, { recursive: true });
7161
- fs22.writeFileSync(path22.join(cpDir, "files", fp), content, "utf-8");
7069
+ const fileDir = path21.dirname(path21.join(cpDir, "files", fp));
7070
+ if (!fs21.existsSync(fileDir)) fs21.mkdirSync(fileDir, { recursive: true });
7071
+ fs21.writeFileSync(path21.join(cpDir, "files", fp), content, "utf-8");
7162
7072
  files.push({ path: fp, hash });
7163
7073
  }
7164
7074
  }
7165
7075
  filesStmt.free();
7166
7076
  const dbData = db.export();
7167
- fs22.writeFileSync(path22.join(cpDir, "kuma.db"), Buffer.from(dbData));
7077
+ fs21.writeFileSync(path21.join(cpDir, "kuma.db"), Buffer.from(dbData));
7168
7078
  const manifest = {
7169
7079
  label,
7170
7080
  timestamp: Date.now(),
@@ -7172,8 +7082,8 @@ async function createCheckpoint(label, description) {
7172
7082
  dbSnapshot: true,
7173
7083
  description
7174
7084
  };
7175
- fs22.writeFileSync(
7176
- path22.join(cpDir, "manifest.json"),
7085
+ fs21.writeFileSync(
7086
+ path21.join(cpDir, "manifest.json"),
7177
7087
  JSON.stringify(manifest, null, 2),
7178
7088
  "utf-8"
7179
7089
  );
@@ -7199,37 +7109,37 @@ async function createCheckpoint(label, description) {
7199
7109
  async function rollbackToCheckpoint(label) {
7200
7110
  try {
7201
7111
  const root = getProjectRoot();
7202
- const cpDir = path22.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
7203
- const manifestPath = path22.join(cpDir, "manifest.json");
7204
- if (!fs22.existsSync(manifestPath)) {
7112
+ const cpDir = path21.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
7113
+ const manifestPath = path21.join(cpDir, "manifest.json");
7114
+ if (!fs21.existsSync(manifestPath)) {
7205
7115
  return `\u274C Checkpoint "${label}" not found. Use kuma_safety({ action: 'checkpoint_list' }) to see available checkpoints.`;
7206
7116
  }
7207
7117
  const manifest = JSON.parse(
7208
- fs22.readFileSync(manifestPath, "utf-8")
7118
+ fs21.readFileSync(manifestPath, "utf-8")
7209
7119
  );
7210
7120
  let restored = 0;
7211
7121
  let failed = 0;
7212
7122
  for (const f of manifest.files) {
7213
- const snapshotPath = path22.join(cpDir, "files", f.path);
7214
- if (fs22.existsSync(snapshotPath)) {
7123
+ const snapshotPath = path21.join(cpDir, "files", f.path);
7124
+ if (fs21.existsSync(snapshotPath)) {
7215
7125
  try {
7216
- const content = fs22.readFileSync(snapshotPath, "utf-8");
7217
- const targetPath = path22.resolve(root, f.path);
7218
- const targetDir = path22.dirname(targetPath);
7219
- if (!fs22.existsSync(targetDir)) fs22.mkdirSync(targetDir, { recursive: true });
7220
- fs22.writeFileSync(targetPath, content, "utf-8");
7126
+ const content = fs21.readFileSync(snapshotPath, "utf-8");
7127
+ const targetPath = path21.resolve(root, f.path);
7128
+ const targetDir = path21.dirname(targetPath);
7129
+ if (!fs21.existsSync(targetDir)) fs21.mkdirSync(targetDir, { recursive: true });
7130
+ fs21.writeFileSync(targetPath, content, "utf-8");
7221
7131
  restored++;
7222
7132
  } catch {
7223
7133
  failed++;
7224
7134
  }
7225
7135
  }
7226
7136
  }
7227
- const dbSnapshotPath = path22.join(cpDir, "kuma.db");
7228
- if (fs22.existsSync(dbSnapshotPath)) {
7229
- const kumaDir = path22.join(root, ".kuma");
7230
- const dbPath = path22.join(kumaDir, "kuma.db");
7231
- const snapshotData = fs22.readFileSync(dbSnapshotPath);
7232
- fs22.writeFileSync(dbPath, snapshotData);
7137
+ const dbSnapshotPath = path21.join(cpDir, "kuma.db");
7138
+ if (fs21.existsSync(dbSnapshotPath)) {
7139
+ const kumaDir = path21.join(root, ".kuma");
7140
+ const dbPath = path21.join(kumaDir, "kuma.db");
7141
+ const snapshotData = fs21.readFileSync(dbSnapshotPath);
7142
+ fs21.writeFileSync(dbPath, snapshotData);
7233
7143
  resetDbInstance();
7234
7144
  }
7235
7145
  sessionMemory.recordToolCall("kuma_checkpoint_rollback", {
@@ -7254,18 +7164,18 @@ async function rollbackToCheckpoint(label) {
7254
7164
  function listCheckpoints() {
7255
7165
  try {
7256
7166
  const root = getProjectRoot();
7257
- const cpDir = path22.join(root, CHECKPOINT_DIR);
7258
- if (!fs22.existsSync(cpDir)) {
7167
+ const cpDir = path21.join(root, CHECKPOINT_DIR);
7168
+ if (!fs21.existsSync(cpDir)) {
7259
7169
  return "\u{1F4ED} No checkpoints found. Use kuma_safety({ action: 'checkpoint', label: 'pre-feature-x' }) to create one.";
7260
7170
  }
7261
- const entries = fs22.readdirSync(cpDir);
7171
+ const entries = fs21.readdirSync(cpDir);
7262
7172
  const checkpoints = [];
7263
7173
  for (const entry of entries) {
7264
- const manifestPath = path22.join(cpDir, entry, "manifest.json");
7265
- if (fs22.existsSync(manifestPath)) {
7174
+ const manifestPath = path21.join(cpDir, entry, "manifest.json");
7175
+ if (fs21.existsSync(manifestPath)) {
7266
7176
  try {
7267
7177
  const manifest = JSON.parse(
7268
- fs22.readFileSync(manifestPath, "utf-8")
7178
+ fs21.readFileSync(manifestPath, "utf-8")
7269
7179
  );
7270
7180
  checkpoints.push({ label: entry, manifest });
7271
7181
  } catch {
@@ -7297,12 +7207,12 @@ function listCheckpoints() {
7297
7207
  function pruneCheckpoints(keep = 5) {
7298
7208
  try {
7299
7209
  const root = getProjectRoot();
7300
- const cpDir = path22.join(root, CHECKPOINT_DIR);
7301
- if (!fs22.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
7302
- const entries = fs22.readdirSync(cpDir).map((e) => ({ name: e, time: fs22.statSync(path22.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
7210
+ const cpDir = path21.join(root, CHECKPOINT_DIR);
7211
+ if (!fs21.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
7212
+ const entries = fs21.readdirSync(cpDir).map((e) => ({ name: e, time: fs21.statSync(path21.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
7303
7213
  let removed = 0;
7304
7214
  for (let i = keep; i < entries.length; i++) {
7305
- fs22.rmSync(path22.join(cpDir, entries[i].name), { recursive: true, force: true });
7215
+ fs21.rmSync(path21.join(cpDir, entries[i].name), { recursive: true, force: true });
7306
7216
  removed++;
7307
7217
  }
7308
7218
  return removed > 0 ? `\u{1F9F9} Pruned **${removed} old checkpoint(s)**. Kept ${Math.min(keep, entries.length)} most recent.` : "\u2705 No checkpoints needed pruning.";
@@ -7342,18 +7252,18 @@ __export(kumaContractEngine_exports, {
7342
7252
  loadContracts: () => loadContracts,
7343
7253
  runContractChecks: () => runContractChecks
7344
7254
  });
7345
- import fs23 from "fs";
7346
- import path23 from "path";
7255
+ import fs22 from "fs";
7256
+ import path22 from "path";
7347
7257
  function contractsDir() {
7348
- return path23.join(getProjectRoot(), ".kuma", "contracts");
7258
+ return path22.join(getProjectRoot(), ".kuma", "contracts");
7349
7259
  }
7350
7260
  function ensureContractsDir() {
7351
7261
  const dir = contractsDir();
7352
- if (!fs23.existsSync(dir)) {
7353
- fs23.mkdirSync(dir, { recursive: true });
7354
- const defaultPath = path23.join(dir, "default.json");
7355
- if (!fs23.existsSync(defaultPath)) {
7356
- fs23.writeFileSync(
7262
+ if (!fs22.existsSync(dir)) {
7263
+ fs22.mkdirSync(dir, { recursive: true });
7264
+ const defaultPath = path22.join(dir, "default.json");
7265
+ if (!fs22.existsSync(defaultPath)) {
7266
+ fs22.writeFileSync(
7357
7267
  defaultPath,
7358
7268
  JSON.stringify(DEFAULT_CONTRACTS, null, 2),
7359
7269
  "utf-8"
@@ -7366,10 +7276,10 @@ function loadContracts() {
7366
7276
  const dir = contractsDir();
7367
7277
  const contracts = [];
7368
7278
  try {
7369
- const files = fs23.readdirSync(dir).filter((f) => f.endsWith(".json"));
7279
+ const files = fs22.readdirSync(dir).filter((f) => f.endsWith(".json"));
7370
7280
  for (const file of files) {
7371
7281
  try {
7372
- const content = fs23.readFileSync(path23.join(dir, file), "utf-8");
7282
+ const content = fs22.readFileSync(path22.join(dir, file), "utf-8");
7373
7283
  const config = JSON.parse(content);
7374
7284
  contracts.push(config);
7375
7285
  } catch (err) {
@@ -7442,7 +7352,7 @@ function evaluateCondition(condition, contextPath) {
7442
7352
  switch (condition.type) {
7443
7353
  case "file_exists": {
7444
7354
  const targetPath = resolveTarget(condition.target, contextPath, root);
7445
- const exists = targetPath.some((p) => fs23.existsSync(p));
7355
+ const exists = targetPath.some((p) => fs22.existsSync(p));
7446
7356
  return {
7447
7357
  passed: exists,
7448
7358
  message: exists ? `\u2705 File exists: ${condition.target}` : `\u274C Required file not found: ${condition.target} (${condition.description})`
@@ -7450,7 +7360,7 @@ function evaluateCondition(condition, contextPath) {
7450
7360
  }
7451
7361
  case "file_not_exists": {
7452
7362
  const targetPath = resolveTarget(condition.target, contextPath, root);
7453
- const exists = targetPath.some((p) => fs23.existsSync(p));
7363
+ const exists = targetPath.some((p) => fs22.existsSync(p));
7454
7364
  return {
7455
7365
  passed: !exists,
7456
7366
  message: !exists ? `\u2705 File does not exist: ${condition.target}` : `\u274C File should not exist: ${condition.target} (${condition.description})`
@@ -7458,9 +7368,9 @@ function evaluateCondition(condition, contextPath) {
7458
7368
  }
7459
7369
  case "contains": {
7460
7370
  if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
7461
- const fullPath = path23.resolve(root, contextPath);
7462
- if (!fs23.existsSync(fullPath)) return { passed: true, message: "\u26A0\uFE0F File not found" };
7463
- const content = fs23.readFileSync(fullPath, "utf-8");
7371
+ const fullPath = path22.resolve(root, contextPath);
7372
+ if (!fs22.existsSync(fullPath)) return { passed: true, message: "\u26A0\uFE0F File not found" };
7373
+ const content = fs22.readFileSync(fullPath, "utf-8");
7464
7374
  const hasContent = condition.value ? content.includes(condition.value) : false;
7465
7375
  return {
7466
7376
  passed: hasContent,
@@ -7472,12 +7382,12 @@ function evaluateCondition(condition, contextPath) {
7472
7382
  const root2 = getProjectRoot();
7473
7383
  const targets = resolveTarget(condition.target, contextPath, root2);
7474
7384
  for (const t of targets) {
7475
- if (fs23.existsSync(t)) {
7476
- const content = fs23.readFileSync(t, "utf-8");
7385
+ if (fs22.existsSync(t)) {
7386
+ const content = fs22.readFileSync(t, "utf-8");
7477
7387
  if (condition.value && content.includes(condition.value)) {
7478
7388
  return {
7479
7389
  passed: false,
7480
- message: `\u274C Found forbidden pattern "${condition.value}" in ${path23.relative(root2, t)} (${condition.description})`
7390
+ message: `\u274C Found forbidden pattern "${condition.value}" in ${path22.relative(root2, t)} (${condition.description})`
7481
7391
  };
7482
7392
  }
7483
7393
  }
@@ -7486,15 +7396,15 @@ function evaluateCondition(condition, contextPath) {
7486
7396
  }
7487
7397
  case "has_test_file": {
7488
7398
  if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
7489
- const basename = path23.basename(contextPath, path23.extname(contextPath));
7490
- const dir = path23.dirname(contextPath);
7399
+ const basename = path22.basename(contextPath, path22.extname(contextPath));
7400
+ const dir = path22.dirname(contextPath);
7491
7401
  const possibleTests = [
7492
- path23.join(dir, `${basename}.test.ts`),
7493
- path23.join(dir, `${basename}.spec.ts`),
7494
- path23.join(dir, `__tests__/${basename}.test.ts`),
7495
- path23.join(dir, `__tests__/${basename}.spec.ts`)
7402
+ path22.join(dir, `${basename}.test.ts`),
7403
+ path22.join(dir, `${basename}.spec.ts`),
7404
+ path22.join(dir, `__tests__/${basename}.test.ts`),
7405
+ path22.join(dir, `__tests__/${basename}.spec.ts`)
7496
7406
  ];
7497
- const hasTest = possibleTests.some((t) => fs23.existsSync(t));
7407
+ const hasTest = possibleTests.some((t) => fs22.existsSync(t));
7498
7408
  return {
7499
7409
  passed: hasTest,
7500
7410
  message: hasTest ? `\u2705 Test file exists` : `\u274C No test file found for ${contextPath} (${condition.description})`
@@ -7518,18 +7428,18 @@ function resolveTarget(pattern, contextPath, root) {
7518
7428
  const r = root || getProjectRoot();
7519
7429
  if (pattern.startsWith("*")) {
7520
7430
  if (contextPath) {
7521
- const dir = path23.dirname(path23.resolve(r, contextPath));
7431
+ const dir = path22.dirname(path22.resolve(r, contextPath));
7522
7432
  const suffix = pattern.replace("*", "");
7523
7433
  try {
7524
- const files = fs23.readdirSync(dir);
7525
- return files.filter((f) => f.endsWith(suffix) || f.includes(suffix)).map((f) => path23.join(dir, f));
7434
+ const files = fs22.readdirSync(dir);
7435
+ return files.filter((f) => f.endsWith(suffix) || f.includes(suffix)).map((f) => path22.join(dir, f));
7526
7436
  } catch {
7527
- return [path23.join(dir, pattern.replace("*", ""))];
7437
+ return [path22.join(dir, pattern.replace("*", ""))];
7528
7438
  }
7529
7439
  }
7530
7440
  return [];
7531
7441
  }
7532
- return [path23.resolve(r, pattern)];
7442
+ return [path22.resolve(r, pattern)];
7533
7443
  }
7534
7444
  async function runContractChecks(toolName, filePath, phase = "pre") {
7535
7445
  const verdicts = phase === "pre" ? evaluatePreConditions(toolName, filePath) : evaluatePostConditions(toolName, filePath);
@@ -7650,37 +7560,37 @@ __export(kumaVerifier_exports, {
7650
7560
  getRunningVerificationPid: () => getRunningVerificationPid,
7651
7561
  runAutoVerification: () => runAutoVerification
7652
7562
  });
7653
- import fs24 from "fs";
7654
- import path24 from "path";
7563
+ import fs23 from "fs";
7564
+ import path23 from "path";
7655
7565
  import { exec } from "child_process";
7656
7566
  function getLockDir(root = process.cwd()) {
7657
- return path24.resolve(root, LOCK_DIR_NAME);
7567
+ return path23.resolve(root, LOCK_DIR_NAME);
7658
7568
  }
7659
7569
  function getPidFile(root = process.cwd()) {
7660
- return path24.join(getLockDir(root), "pid");
7570
+ return path23.join(getLockDir(root), "pid");
7661
7571
  }
7662
7572
  function acquireFileLock(root) {
7663
7573
  const lockDir = getLockDir(root);
7664
7574
  try {
7665
- fs24.mkdirSync(lockDir, { recursive: false });
7666
- fs24.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
7575
+ fs23.mkdirSync(lockDir, { recursive: false });
7576
+ fs23.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
7667
7577
  return true;
7668
7578
  } catch {
7669
7579
  try {
7670
- if (fs24.existsSync(getPidFile(root))) {
7671
- const pid = parseInt(fs24.readFileSync(getPidFile(root), "utf-8"), 10);
7580
+ if (fs23.existsSync(getPidFile(root))) {
7581
+ const pid = parseInt(fs23.readFileSync(getPidFile(root), "utf-8"), 10);
7672
7582
  try {
7673
7583
  process.kill(pid, 0);
7674
7584
  return false;
7675
7585
  } catch {
7676
- fs24.rmSync(lockDir, { recursive: true, force: true });
7586
+ fs23.rmSync(lockDir, { recursive: true, force: true });
7677
7587
  return acquireFileLock(root);
7678
7588
  }
7679
7589
  }
7680
7590
  } catch {
7681
7591
  }
7682
7592
  try {
7683
- fs24.rmSync(lockDir, { recursive: true, force: true });
7593
+ fs23.rmSync(lockDir, { recursive: true, force: true });
7684
7594
  return acquireFileLock(root);
7685
7595
  } catch {
7686
7596
  return false;
@@ -7689,7 +7599,7 @@ function acquireFileLock(root) {
7689
7599
  }
7690
7600
  function releaseFileLock(root) {
7691
7601
  try {
7692
- fs24.rmSync(getLockDir(root), { recursive: true, force: true });
7602
+ fs23.rmSync(getLockDir(root), { recursive: true, force: true });
7693
7603
  } catch {
7694
7604
  }
7695
7605
  }
@@ -7712,36 +7622,36 @@ function getRunningVerificationPid() {
7712
7622
  return _currentProcess?.pid ?? null;
7713
7623
  }
7714
7624
  function detectTestRunner(root = process.cwd()) {
7715
- const pkgPath = path24.join(root, "package.json");
7716
- if (fs24.existsSync(pkgPath)) {
7625
+ const pkgPath = path23.join(root, "package.json");
7626
+ if (fs23.existsSync(pkgPath)) {
7717
7627
  try {
7718
- const pkg = JSON.parse(fs24.readFileSync(pkgPath, "utf-8"));
7628
+ const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
7719
7629
  if (pkg.scripts && pkg.scripts.test) {
7720
- if (fs24.existsSync(path24.join(root, "pnpm-lock.yaml"))) return { runner: "pnpm", baseCommand: "pnpm test" };
7721
- if (fs24.existsSync(path24.join(root, "yarn.lock"))) return { runner: "yarn", baseCommand: "yarn test" };
7630
+ if (fs23.existsSync(path23.join(root, "pnpm-lock.yaml"))) return { runner: "pnpm", baseCommand: "pnpm test" };
7631
+ if (fs23.existsSync(path23.join(root, "yarn.lock"))) return { runner: "yarn", baseCommand: "yarn test" };
7722
7632
  return { runner: "npm", baseCommand: "npm test" };
7723
7633
  }
7724
7634
  } catch {
7725
7635
  }
7726
7636
  }
7727
- if (fs24.existsSync(path24.join(root, "pytest.ini")) || fs24.existsSync(path24.join(root, "pyproject.toml"))) return { runner: "pytest", baseCommand: "pytest" };
7728
- if (fs24.existsSync(path24.join(root, "Cargo.toml"))) return { runner: "cargo", baseCommand: "cargo test" };
7729
- if (fs24.existsSync(path24.join(root, "go.mod"))) return { runner: "go", baseCommand: "go test ./..." };
7730
- if (fs24.existsSync(path24.join(root, "Makefile"))) {
7637
+ if (fs23.existsSync(path23.join(root, "pytest.ini")) || fs23.existsSync(path23.join(root, "pyproject.toml"))) return { runner: "pytest", baseCommand: "pytest" };
7638
+ if (fs23.existsSync(path23.join(root, "Cargo.toml"))) return { runner: "cargo", baseCommand: "cargo test" };
7639
+ if (fs23.existsSync(path23.join(root, "go.mod"))) return { runner: "go", baseCommand: "go test ./..." };
7640
+ if (fs23.existsSync(path23.join(root, "Makefile"))) {
7731
7641
  try {
7732
- const makefileContent = fs24.readFileSync(path24.join(root, "Makefile"), "utf-8");
7642
+ const makefileContent = fs23.readFileSync(path23.join(root, "Makefile"), "utf-8");
7733
7643
  const hasTestTarget = /^test:/m.test(makefileContent) || /^\s+test:/.test(makefileContent);
7734
7644
  if (hasTestTarget) return { runner: "make", baseCommand: "make test" };
7735
7645
  } catch {
7736
7646
  }
7737
7647
  }
7738
- if (fs24.existsSync(path24.join(root, "package.json"))) {
7648
+ if (fs23.existsSync(path23.join(root, "package.json"))) {
7739
7649
  try {
7740
- const pkg = JSON.parse(fs24.readFileSync(path24.join(root, "package.json"), "utf-8"));
7650
+ const pkg = JSON.parse(fs23.readFileSync(path23.join(root, "package.json"), "utf-8"));
7741
7651
  if (pkg.dependencies || pkg.devDependencies) {
7742
- if (fs24.existsSync(path24.join(root, "tsconfig.json"))) return { runner: "tsc", baseCommand: "npx tsc --noEmit" };
7743
- const srcDir = path24.join(root, "src");
7744
- if (fs24.existsSync(srcDir)) return { runner: "node", baseCommand: `node -c "${srcDir}/**/*.js" 2>/dev/null || node --check $(find src -name '*.js' 2>/dev/null | head -5)` };
7652
+ if (fs23.existsSync(path23.join(root, "tsconfig.json"))) return { runner: "tsc", baseCommand: "npx tsc --noEmit" };
7653
+ const srcDir = path23.join(root, "src");
7654
+ if (fs23.existsSync(srcDir)) return { runner: "node", baseCommand: `node -c "${srcDir}/**/*.js" 2>/dev/null || node --check $(find src -name '*.js' 2>/dev/null | head -5)` };
7745
7655
  }
7746
7656
  } catch {
7747
7657
  }
@@ -7841,7 +7751,7 @@ async function runAutoVerification(options = {}) {
7841
7751
  const heartbeatTimer = setInterval(() => {
7842
7752
  if (getLockDir(root)) {
7843
7753
  try {
7844
- fs24.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
7754
+ fs23.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
7845
7755
  } catch {
7846
7756
  }
7847
7757
  }
@@ -7918,8 +7828,8 @@ __export(init_exports, {
7918
7828
  generateInitMdContent: () => generateInitMdContent,
7919
7829
  runInit: () => runInit
7920
7830
  });
7921
- import fs25 from "fs";
7922
- import path25 from "path";
7831
+ import fs24 from "fs";
7832
+ import path24 from "path";
7923
7833
  function configFilePath(type) {
7924
7834
  switch (type) {
7925
7835
  case "claude":
@@ -8366,14 +8276,11 @@ function generateInitMdContent() {
8366
8276
  "| `todo` | Persistent todo CRUD | When managing tasks |",
8367
8277
  "| `context` | Inject context notes | Additional notes when needed |",
8368
8278
  "| `domain_rules` | Layer 1: business rules | Record business rules when discovered |",
8369
- "| `federated` | Resolve kuma:// URIs | Cross-project references |",
8370
- "| `gen_test` | Generate test from trajectory | After trajectory analysis |",
8371
- "| `trajectory` | List trajectories | Review past agent trajectories |",
8372
- "| `skills` | List distilled skills | See auto-learned patterns |",
8279
+ "| `feature` | Record high-level feature with owns edges | Define features like 'Authentication', 'Billing' |",
8373
8280
  "| `add_node` | Create structural nodes (low impact \u2014 grep faster) | Rarely; only for visualize debugging |",
8374
8281
  "",
8375
8282
  "| `delete_node` | Delete specific nodes, gotchas, todos, or decisions by ID | When you saved wrong data or need cleanup |",
8376
- "| `clear` | Wipe ALL graph nodes, edges, gotchas, trajectories | Emergency reset \u2014 irreversible |",
8283
+ "| `clear` | Wipe ALL graph nodes, edges, gotchas | Emergency reset \u2014 irreversible |",
8377
8284
  "| `layers` | Show all 3 layers summary | Overview memory layers |",
8378
8285
  "",
8379
8286
  "### \u{1F6E1}\uFE0F kuma_safety \u2014 Safety & Policy",
@@ -8445,6 +8352,7 @@ function generateInitMdContent() {
8445
8352
  "| \u{1F539} `small_square` | variable | Variables/constants | AUTO by scanner |",
8446
8353
  "| \u{1F504} `circle` | workflow | Workflow definitions | AUTO by scanner |",
8447
8354
  "| \u{1F4C4} `note` | context | Manual context notes | MANUAL \u2014 kuma_memory context |",
8355
+ "| \u2B50 `star` | feature | High-level feature (Authentication, Billing) | MANUAL \u2014 kuma_memory feature |",
8448
8356
  "| \u{1F3DB}\uFE0F `pillar` | feature_domain | High-level feature domain anchor | MANUAL \u2014 kuma_memory arch_flow (domain:) |",
8449
8357
  "| \u{1F517} `link` | cross_service_link | Cross-service/inter-file flow step | MANUAL \u2014 kuma_memory arch_flow (hops:) |",
8450
8358
  "| \u{1F3F7}\uFE0F `tag` | domain_rule | Business rules (Layer 1) | MANUAL \u2014 kuma_memory domain_rules |",
@@ -8461,7 +8369,8 @@ function generateInitMdContent() {
8461
8369
  "- `extends` \u2014 class extends class (AUTO)",
8462
8370
  "- `contains` \u2014 file contains function (AUTO)",
8463
8371
  "- `composes` \u2014 component contains sub-component (AUTO)",
8464
- "- `owns` \u2014 domain owns file (MANUAL \u2014 arch_flow)",
8372
+ "- `owns` \u2014 domain/file owns file (MANUAL \u2014 arch_flow, feature)",
8373
+ "- `affects` \u2014 impact propagation chain (MANUAL \u2014 arch_flow, feature)",
8465
8374
  "- `flows_through` \u2014 flow step to next step (MANUAL \u2014 arch_flow)",
8466
8375
  "- `triggers` \u2014 flow triggers action (MANUAL)",
8467
8376
  "- `syncs_with` \u2014 cross-service sync (MANUAL)",
@@ -8490,11 +8399,17 @@ function generateInitMdContent() {
8490
8399
  "\u{1F9E0} Graph is persistent \u2014 nodes/edges accumulate across sessions.",
8491
8400
  "",
8492
8401
  "ON DISCOVERY (When needed):",
8402
+ " kuma_memory \u2192 feature \u2014 Record high-level feature (Authentication, Billing)",
8493
8403
  " kuma_memory \u2192 domain_rules \u2014 Business rules (Layer 1)",
8494
8404
  " kuma_memory \u2192 context \u2014 Additional notes",
8495
8405
  " kuma_memory \u2192 todo \u2014 Task management",
8496
8406
  " kuma_memory \u2192 mine \u2014 Historical context",
8497
8407
  " kuma_memory \u2192 add_node \u2014 Manual function/class/component node (for visualize)",
8408
+ "",
8409
+ "GRAPH-AS-RETRIEVAL (search now includes):",
8410
+ " \u2022 Task-focused retrieval: top N relevant nodes for a task description",
8411
+ " \u2022 Impact analysis: which nodes are affected by a change",
8412
+ " \u2022 arch_flow now auto-creates affects edges for impact propagation",
8498
8413
  "```",
8499
8414
  "",
8500
8415
  "## \u{1F9E0} How the Scanner Works",
@@ -8589,10 +8504,10 @@ function generateInitMdContent() {
8589
8504
  ].join("\n");
8590
8505
  }
8591
8506
  function handleOpencodeSecondary(root, results) {
8592
- const skillPath = path25.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
8507
+ const skillPath = path24.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
8593
8508
  if (results.some((r) => r.filePath === ".agents/skills/kuma/SKILL.md")) return;
8594
8509
  try {
8595
- const dir = path25.dirname(skillPath);
8510
+ const dir = path24.dirname(skillPath);
8596
8511
  const content = [
8597
8512
  "---",
8598
8513
  "name: kuma-mcp",
@@ -8625,18 +8540,18 @@ function handleOpencodeSecondary(root, results) {
8625
8540
  "",
8626
8541
  "\u{1F4D6} Full rules: `.kuma/init.md`"
8627
8542
  ].join("\n");
8628
- if (fs25.existsSync(skillPath)) {
8629
- const existing = fs25.readFileSync(skillPath, "utf-8");
8543
+ if (fs24.existsSync(skillPath)) {
8544
+ const existing = fs24.readFileSync(skillPath, "utf-8");
8630
8545
  if (existing.includes("kuma-mcp")) {
8631
8546
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "skipped" });
8632
8547
  return;
8633
8548
  }
8634
8549
  const newContent = existing.trimEnd() + "\n\n---\n\n" + content;
8635
- fs25.writeFileSync(skillPath, newContent, "utf-8");
8550
+ fs24.writeFileSync(skillPath, newContent, "utf-8");
8636
8551
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "appended" });
8637
8552
  } else {
8638
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
8639
- fs25.writeFileSync(skillPath, content, "utf-8");
8553
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
8554
+ fs24.writeFileSync(skillPath, content, "utf-8");
8640
8555
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "created" });
8641
8556
  }
8642
8557
  } catch (err) {
@@ -8649,21 +8564,21 @@ function handleOpencodeSecondary(root, results) {
8649
8564
  }
8650
8565
  }
8651
8566
  function handleCodexSecondary(root, results) {
8652
- const tomlPath = path25.resolve(root, ".codex/config.toml");
8567
+ const tomlPath = path24.resolve(root, ".codex/config.toml");
8653
8568
  if (results.some((r) => r.filePath === ".codex/config.toml")) return;
8654
8569
  try {
8655
- const dir = path25.dirname(tomlPath);
8656
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
8657
- if (fs25.existsSync(tomlPath)) {
8658
- const existingContent = fs25.readFileSync(tomlPath, "utf-8");
8570
+ const dir = path24.dirname(tomlPath);
8571
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
8572
+ if (fs24.existsSync(tomlPath)) {
8573
+ const existingContent = fs24.readFileSync(tomlPath, "utf-8");
8659
8574
  if (existingContent.includes("kuma")) {
8660
8575
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
8661
8576
  return;
8662
8577
  }
8663
- fs25.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
8578
+ fs24.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
8664
8579
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
8665
8580
  } else {
8666
- fs25.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
8581
+ fs24.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
8667
8582
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
8668
8583
  }
8669
8584
  } catch (err) {
@@ -8676,11 +8591,11 @@ function handleCodexSecondary(root, results) {
8676
8591
  }
8677
8592
  }
8678
8593
  function handleQwenSecondary(root, results) {
8679
- const settingsPath = path25.resolve(root, "settings.json");
8594
+ const settingsPath = path24.resolve(root, "settings.json");
8680
8595
  if (results.some((r) => r.filePath === "settings.json")) return;
8681
8596
  try {
8682
- if (fs25.existsSync(settingsPath)) {
8683
- const existingContent = fs25.readFileSync(settingsPath, "utf-8");
8597
+ if (fs24.existsSync(settingsPath)) {
8598
+ const existingContent = fs24.readFileSync(settingsPath, "utf-8");
8684
8599
  if (existingContent.includes("kuma")) {
8685
8600
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
8686
8601
  try {
@@ -8688,7 +8603,7 @@ function handleQwenSecondary(root, results) {
8688
8603
  parsed.mcpServers = parsed.mcpServers || {};
8689
8604
  if (!parsed.mcpServers.kuma) {
8690
8605
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
8691
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8606
+ fs24.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8692
8607
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
8693
8608
  return;
8694
8609
  }
@@ -8703,13 +8618,13 @@ function handleQwenSecondary(root, results) {
8703
8618
  parsed.mcpServers = parsed.mcpServers || {};
8704
8619
  if (!parsed.mcpServers.kuma) {
8705
8620
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
8706
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8621
+ fs24.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8707
8622
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
8708
8623
  }
8709
8624
  } catch {
8710
8625
  }
8711
8626
  } else {
8712
- fs25.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
8627
+ fs24.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
8713
8628
  results.push({ type: "qwen", filePath: "settings.json", action: "created" });
8714
8629
  }
8715
8630
  } catch (err) {
@@ -8812,18 +8727,18 @@ function getCombinedAgentsMd(selectedTypes) {
8812
8727
  return lines.join("\n");
8813
8728
  }
8814
8729
  function handleAntigravityMcpConfig(root, results) {
8815
- const mcpPath = path25.resolve(root, ".agents/mcp_config.json");
8730
+ const mcpPath = path24.resolve(root, ".agents/mcp_config.json");
8816
8731
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
8817
8732
  try {
8818
- const mcpDir = path25.dirname(mcpPath);
8819
- if (fs25.existsSync(mcpPath)) {
8820
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
8733
+ const mcpDir = path24.dirname(mcpPath);
8734
+ if (fs24.existsSync(mcpPath)) {
8735
+ const existingContent = fs24.readFileSync(mcpPath, "utf-8");
8821
8736
  if (existingContent.includes("kuma")) {
8822
8737
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
8823
8738
  const trimmed = existingContent.trimEnd();
8824
8739
  if (trimmed.endsWith("}")) {
8825
8740
  const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
8826
- fs25.writeFileSync(mcpPath, updated, "utf-8");
8741
+ fs24.writeFileSync(mcpPath, updated, "utf-8");
8827
8742
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
8828
8743
  }
8829
8744
  }
@@ -8832,11 +8747,11 @@ function handleAntigravityMcpConfig(root, results) {
8832
8747
  const parsed = JSON.parse(existingContent);
8833
8748
  parsed.mcpServers = parsed.mcpServers || {};
8834
8749
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
8835
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8750
+ fs24.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8836
8751
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
8837
8752
  } else {
8838
- if (!fs25.existsSync(mcpDir)) fs25.mkdirSync(mcpDir, { recursive: true });
8839
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
8753
+ if (!fs24.existsSync(mcpDir)) fs24.mkdirSync(mcpDir, { recursive: true });
8754
+ fs24.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
8840
8755
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
8841
8756
  }
8842
8757
  } catch (err) {
@@ -8849,18 +8764,18 @@ function handleAntigravityMcpConfig(root, results) {
8849
8764
  }
8850
8765
  }
8851
8766
  function handleAiderSecondary(root, results) {
8852
- const ymlPath = path25.resolve(root, ".aider.conf.yml");
8767
+ const ymlPath = path24.resolve(root, ".aider.conf.yml");
8853
8768
  if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
8854
8769
  try {
8855
8770
  const conventionsRef = "read: CONVENTIONS.md";
8856
- if (fs25.existsSync(ymlPath)) {
8857
- const existingContent = fs25.readFileSync(ymlPath, "utf-8");
8771
+ if (fs24.existsSync(ymlPath)) {
8772
+ const existingContent = fs24.readFileSync(ymlPath, "utf-8");
8858
8773
  if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
8859
8774
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
8860
8775
  return;
8861
8776
  }
8862
8777
  const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
8863
- fs25.writeFileSync(ymlPath, newContent, "utf-8");
8778
+ fs24.writeFileSync(ymlPath, newContent, "utf-8");
8864
8779
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
8865
8780
  } else {
8866
8781
  const content = [
@@ -8870,7 +8785,7 @@ function handleAiderSecondary(root, results) {
8870
8785
  conventionsRef,
8871
8786
  ""
8872
8787
  ].join("\n");
8873
- fs25.writeFileSync(ymlPath, content, "utf-8");
8788
+ fs24.writeFileSync(ymlPath, content, "utf-8");
8874
8789
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
8875
8790
  }
8876
8791
  } catch (err) {
@@ -8883,10 +8798,10 @@ function handleAiderSecondary(root, results) {
8883
8798
  }
8884
8799
  }
8885
8800
  function handleCopilotSecondary(root, results) {
8886
- const skillPath = path25.resolve(root, ".github/skills/kuma/SKILL.md");
8801
+ const skillPath = path24.resolve(root, ".github/skills/kuma/SKILL.md");
8887
8802
  if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
8888
8803
  try {
8889
- const dir = path25.dirname(skillPath);
8804
+ const dir = path24.dirname(skillPath);
8890
8805
  const content = [
8891
8806
  "---",
8892
8807
  "name: kuma-mcp",
@@ -8897,17 +8812,17 @@ function handleCopilotSecondary(root, results) {
8897
8812
  "",
8898
8813
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules."
8899
8814
  ].join("\n");
8900
- if (fs25.existsSync(skillPath)) {
8901
- const existingContent = fs25.readFileSync(skillPath, "utf-8");
8815
+ if (fs24.existsSync(skillPath)) {
8816
+ const existingContent = fs24.readFileSync(skillPath, "utf-8");
8902
8817
  if (existingContent.includes("kuma")) {
8903
8818
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
8904
8819
  return;
8905
8820
  }
8906
- fs25.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
8821
+ fs24.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
8907
8822
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
8908
8823
  } else {
8909
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
8910
- fs25.writeFileSync(skillPath, content, "utf-8");
8824
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
8825
+ fs24.writeFileSync(skillPath, content, "utf-8");
8911
8826
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
8912
8827
  }
8913
8828
  } catch (err) {
@@ -8920,21 +8835,21 @@ function handleCopilotSecondary(root, results) {
8920
8835
  }
8921
8836
  }
8922
8837
  function handleOpenclawSecondary(root, results) {
8923
- const mcpPath = path25.resolve(root, ".agents/mcp_config.json");
8838
+ const mcpPath = path24.resolve(root, ".agents/mcp_config.json");
8924
8839
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
8925
8840
  try {
8926
- const dir = path25.dirname(mcpPath);
8927
- if (fs25.existsSync(mcpPath)) {
8928
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
8841
+ const dir = path24.dirname(mcpPath);
8842
+ if (fs24.existsSync(mcpPath)) {
8843
+ const existingContent = fs24.readFileSync(mcpPath, "utf-8");
8929
8844
  if (existingContent.includes("kuma")) return;
8930
8845
  const parsed = JSON.parse(existingContent);
8931
8846
  parsed.mcpServers = parsed.mcpServers || {};
8932
8847
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
8933
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8848
+ fs24.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
8934
8849
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
8935
8850
  } else {
8936
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
8937
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
8851
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
8852
+ fs24.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
8938
8853
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
8939
8854
  }
8940
8855
  } catch (err) {
@@ -8947,53 +8862,42 @@ function handleOpenclawSecondary(root, results) {
8947
8862
  }
8948
8863
  }
8949
8864
  function handleQuickrefGeneration(root, results) {
8950
- const quickrefPath = path25.resolve(root, ".kuma/quickref.md");
8865
+ const quickrefPath = path24.resolve(root, ".kuma/quickref.md");
8951
8866
  const content = [
8952
8867
  "# Kuma Quick Reference",
8953
8868
  "",
8954
- "_(Auto-generated by `kuma init` \u2014 simplified workflow cheat sheet)_",
8869
+ "## Lean Mode (default \u2014 for speed)",
8870
+ "1. `init` \u2192 edit \u2192 record (if needed)",
8955
8871
  "",
8956
- "## Every Session (Mandatory \u2014 including research-only)",
8872
+ "## Standard Mode (for safety)",
8873
+ "1. `init` \u2192 `guard` \u2192 `research` \u2192 edit \u2192 `record` \u2192 `verify`",
8957
8874
  "",
8958
- ' 1. `kuma_context({ action: "init" })` \u2014 Load context',
8959
- ' 2. `kuma_safety({ action: "guard" })` \u2014 Safety check',
8960
- ' 3. `kuma_context({ action: "research", scope: "<area>" })` \u2014 Research',
8961
- " 4. *(edit/read using native tools)*",
8962
- ' 5. `kuma_memory({ action: "research_save", ... })` \u2014 After exploring area (creates search cache)',
8963
- ' 6. `kuma_memory({ action: "gotcha" })` \u2014 \u{1F525} IMMEDIATELY when bug found (exponential)',
8964
- ' 7. `kuma_memory({ action: "arch_flow" })` \u2014 \u{1F525} IMMEDIATELY after each flow hop (exponential)',
8965
- ' 8. `kuma_memory({ action: "decision" })` \u2014 When choosing (preserves rationale)',
8966
- ' 9. `kuma_safety({ action: "verify", ... })` \u2014 Run tests / confirm nothing broken',
8967
- ' 10. `kuma_context({ action: "changes" })` \u2014 Review session activity',
8968
- "",
8969
- "\u{1F525} `arch_flow` + `gotcha` = EXPONENTIAL value. Each record saves 5-10 files next session.",
8970
- "\u{1F7E2} SKIP recording function/class/component nodes \u2014 grep/glob is faster.",
8971
- "\u{1F9E0} Graph is persistent \u2014 nodes/edges accumulate across sessions.",
8875
+ "## Full Mode (for complex changes)",
8876
+ "1. `init` \u2192 `guard` \u2192 `research` \u2192 `impact` \u2192 edit \u2192 `record` \u2192 `verify` \u2192 `changes`",
8972
8877
  "",
8973
- "\u26D4 Steps 5-10 are mandatory even for research-only sessions!",
8878
+ "## Record Rules",
8879
+ "- **MUST:** decision, gotcha",
8880
+ "- **SKIP:** function, class, import, route",
8974
8881
  "",
8975
8882
  "## Platform Tool Names",
8883
+ "- OpenCode / Antigravity: `kuma_kuma_*`",
8884
+ "- Others: `kuma_*`",
8976
8885
  "",
8977
- "- **OpenCode:** Use `kuma_kuma_*` prefix (e.g. `kuma_kuma_context`)",
8978
- "- **Other platforms:** Use `kuma_*` directly (e.g. `kuma_context`)",
8979
- "",
8980
- "\u{1F4D6} Full rules: `.kuma/init.md`",
8886
+ "\u{1F4D6} Full: `.kuma/init.md` | \u{1F4CA} Modes: `.kuma/MODE.md` | \u{1F7E2} Skip: `.kuma/SKIP_RULES.md`",
8981
8887
  "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
8982
8888
  ].join("\n");
8983
8889
  try {
8984
- const kumaDir = path25.dirname(quickrefPath);
8985
- if (!fs25.existsSync(kumaDir)) fs25.mkdirSync(kumaDir, { recursive: true });
8986
- if (fs25.existsSync(quickrefPath)) {
8987
- const existing = fs25.readFileSync(quickrefPath, "utf-8");
8890
+ const kumaDir = path24.dirname(quickrefPath);
8891
+ if (!fs24.existsSync(kumaDir)) fs24.mkdirSync(kumaDir, { recursive: true });
8892
+ if (fs24.existsSync(quickrefPath)) {
8893
+ const existing = fs24.readFileSync(quickrefPath, "utf-8");
8988
8894
  if (existing.includes("_Generated by Kuma MCP_")) {
8989
8895
  results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "skipped" });
8990
8896
  return;
8991
8897
  }
8992
- results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "skipped" });
8993
- } else {
8994
- fs25.writeFileSync(quickrefPath, content, "utf-8");
8995
- results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "created" });
8996
8898
  }
8899
+ fs24.writeFileSync(quickrefPath, content, "utf-8");
8900
+ results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "created" });
8997
8901
  } catch (err) {
8998
8902
  results.push({
8999
8903
  type: "claude",
@@ -9003,21 +8907,165 @@ function handleQuickrefGeneration(root, results) {
9003
8907
  });
9004
8908
  }
9005
8909
  }
8910
+ function handleModeMdGeneration(root, results) {
8911
+ const filePath = path24.resolve(root, ".kuma/MODE.md");
8912
+ const content = [
8913
+ "# Adaptive Mode Selector",
8914
+ "",
8915
+ "## Mode Definitions",
8916
+ "",
8917
+ "| Mode | When to Use | Token Cost | Steps |",
8918
+ "|------|-------------|------------|-------|",
8919
+ "| **Lean** (default) | Small fixes, familiar code, hotfixes, < 3 files | ~100 | 3 |",
8920
+ "| **Standard** | New features, unfamiliar modules, 3-10 files | ~300 | 5 |",
8921
+ "| **Full** | Cross-module refactors, architecture changes, > 10 files | ~500 | 7 |",
8922
+ "",
8923
+ "## Auto-Detection Rules",
8924
+ "",
8925
+ "```",
8926
+ "IF file_count < 3 AND area_familiar == true:",
8927
+ ' mode = "lean"',
8928
+ "ELSE IF file_count <= 10 OR area_familiar == false:",
8929
+ ' mode = "standard"',
8930
+ "ELSE IF file_count > 10 OR cross_module == true:",
8931
+ ' mode = "full"',
8932
+ "```",
8933
+ "",
8934
+ "## Mode Behaviors",
8935
+ "",
8936
+ "### Lean Mode (Default)",
8937
+ "- Skip: guard, research, verify, changes",
8938
+ "- Record: only if decision/gotcha explicit",
8939
+ "- Focus: speed, minimal overhead",
8940
+ "",
8941
+ "### Standard Mode",
8942
+ "- Include: guard (before unfamiliar), research (before edit)",
8943
+ "- Record: decision, gotcha, arch_flow (if complex)",
8944
+ "- Focus: safety + knowledge capture",
8945
+ "",
8946
+ "### Full Mode",
8947
+ "- Include: all steps including impact, changes",
8948
+ "- Record: everything valuable",
8949
+ "- Focus: completeness, audit trail",
8950
+ "",
8951
+ "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
8952
+ ].join("\n");
8953
+ try {
8954
+ const dir = path24.dirname(filePath);
8955
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
8956
+ if (!fs24.existsSync(filePath)) {
8957
+ fs24.writeFileSync(filePath, content, "utf-8");
8958
+ results.push({ type: "claude", filePath: ".kuma/MODE.md", action: "created" });
8959
+ }
8960
+ } catch (err) {
8961
+ results.push({ type: "claude", filePath: ".kuma/MODE.md", action: "error", error: err instanceof Error ? err.message : String(err) });
8962
+ }
8963
+ }
8964
+ function handleSkipRulesMdGeneration(root, results) {
8965
+ const filePath = path24.resolve(root, ".kuma/SKIP_RULES.md");
8966
+ const content = [
8967
+ "# Skip Rules \u2014 What NOT to Record",
8968
+ "",
8969
+ "## \u{1F7E2} NEVER Record (grep/glob is faster)",
8970
+ "",
8971
+ "| What | Why Skip | Better Tool |",
8972
+ "|------|----------|-------------|",
8973
+ "| Function definitions | `grep funcName(` | Grep |",
8974
+ "| Class definitions | `grep class ClassName` | Grep |",
8975
+ "| Import statements | Read import block | Read |",
8976
+ "| Component definitions | `glob **/*Component*` | Glob |",
8977
+ "| Route definitions | Check router file | Read |",
8978
+ "| Type/interface definitions | `grep interface TypeName` | Grep |",
8979
+ "| Variable/const declarations | `grep const varName` | Grep |",
8980
+ "| Test file locations | `glob **/*.test.*` | Glob |",
8981
+ "",
8982
+ "## \u{1F534} ALWAYS Record (high value)",
8983
+ "",
8984
+ "| What | Why Record | Tool |",
8985
+ "|------|------------|------|",
8986
+ "| Architecture decisions with rationale | Preserves context | `decision` |",
8987
+ "| Bugs/quirks found and fixed | Prevents re-discovery | `gotcha` |",
8988
+ "| Cross-module flow paths | Saves 5-10 files next session | `arch_flow` |",
8989
+ "| Business rules discovered | Domain knowledge | `domain_rules` |",
8990
+ "",
8991
+ "## \u{1F7E1} Record ONLY If Complex",
8992
+ "",
8993
+ "| What | Condition | Tool |",
8994
+ "|------|-----------|------|",
8995
+ "| Research findings | Multi-file exploration (> 3 files) | `research_save` |",
8996
+ "| Architecture flow | Cross-service or > 3 hops | `arch_flow` |",
8997
+ "| Feature definition | High-level feature with owns edges | `feature` |",
8998
+ "",
8999
+ "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
9000
+ ].join("\n");
9001
+ try {
9002
+ const dir = path24.dirname(filePath);
9003
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
9004
+ if (!fs24.existsSync(filePath)) {
9005
+ fs24.writeFileSync(filePath, content, "utf-8");
9006
+ results.push({ type: "claude", filePath: ".kuma/SKIP_RULES.md", action: "created" });
9007
+ }
9008
+ } catch (err) {
9009
+ results.push({ type: "claude", filePath: ".kuma/SKIP_RULES.md", action: "error", error: err instanceof Error ? err.message : String(err) });
9010
+ }
9011
+ }
9012
+ function handleStalenessMdGeneration(root, results) {
9013
+ const filePath = path24.resolve(root, ".kuma/STALENESS.md");
9014
+ const content = [
9015
+ "# Auto-Staleness Detection",
9016
+ "",
9017
+ "## Staleness Signals",
9018
+ "",
9019
+ "| Signal | Detection | Action |",
9020
+ "|--------|-----------|--------|",
9021
+ "| File modified after save | Compare file mtime vs research_save timestamp | Mark STALE, skip cache |",
9022
+ "| Git commit touches file | `git log --since` on researched files | Mark STALE, warn |",
9023
+ '| Age > 7 days | Current time - research_save timestamp | Warn: "Cache may be stale" |',
9024
+ "| Age > 30 days | Current time - research_save timestamp | Auto-invalidate, re-read |",
9025
+ "",
9026
+ "## Recovery Protocol",
9027
+ "",
9028
+ "```",
9029
+ "ON stale_detected:",
9030
+ " 1. Skip cached data",
9031
+ " 2. Re-read file from disk",
9032
+ " 3. Update research_save timestamp",
9033
+ " 4. Continue with fresh data",
9034
+ "",
9035
+ "ON search_conflict:",
9036
+ " 1. Prefer recent research (newer timestamp)",
9037
+ " 2. If same age, prefer live file over cache",
9038
+ " 3. Log conflict for review",
9039
+ "```",
9040
+ "",
9041
+ "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
9042
+ ].join("\n");
9043
+ try {
9044
+ const dir = path24.dirname(filePath);
9045
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
9046
+ if (!fs24.existsSync(filePath)) {
9047
+ fs24.writeFileSync(filePath, content, "utf-8");
9048
+ results.push({ type: "claude", filePath: ".kuma/STALENESS.md", action: "created" });
9049
+ }
9050
+ } catch (err) {
9051
+ results.push({ type: "claude", filePath: ".kuma/STALENESS.md", action: "error", error: err instanceof Error ? err.message : String(err) });
9052
+ }
9053
+ }
9006
9054
  function handleInitMdGeneration(root, results) {
9007
- const initMdPath = path25.resolve(root, ".kuma/init.md");
9055
+ const initMdPath = path24.resolve(root, ".kuma/init.md");
9008
9056
  try {
9009
- const kumaDir = path25.dirname(initMdPath);
9010
- if (!fs25.existsSync(kumaDir)) fs25.mkdirSync(kumaDir, { recursive: true });
9011
- if (fs25.existsSync(initMdPath)) {
9012
- const existing = fs25.readFileSync(initMdPath, "utf-8");
9057
+ const kumaDir = path24.dirname(initMdPath);
9058
+ if (!fs24.existsSync(kumaDir)) fs24.mkdirSync(kumaDir, { recursive: true });
9059
+ if (fs24.existsSync(initMdPath)) {
9060
+ const existing = fs24.readFileSync(initMdPath, "utf-8");
9013
9061
  if (existing.includes("_Generated by Kuma MCP_")) {
9014
9062
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "skipped" });
9015
9063
  return;
9016
9064
  }
9017
- fs25.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
9065
+ fs24.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
9018
9066
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "appended" });
9019
9067
  } else {
9020
- fs25.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
9068
+ fs24.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
9021
9069
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "created" });
9022
9070
  }
9023
9071
  } catch (err) {
@@ -9029,22 +9077,25 @@ function handleInitMdGeneration(root, results) {
9029
9077
  });
9030
9078
  }
9031
9079
  handleQuickrefGeneration(root, results);
9080
+ handleModeMdGeneration(root, results);
9081
+ handleSkipRulesMdGeneration(root, results);
9082
+ handleStalenessMdGeneration(root, results);
9032
9083
  }
9033
9084
  function handleCodewhaleSecondary(root, results) {
9034
- const mcpPath = path25.resolve(root, ".codewhale/mcp.json");
9085
+ const mcpPath = path24.resolve(root, ".codewhale/mcp.json");
9035
9086
  if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
9036
9087
  try {
9037
- const dir = path25.dirname(mcpPath);
9038
- if (fs25.existsSync(mcpPath)) {
9039
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
9088
+ const dir = path24.dirname(mcpPath);
9089
+ if (fs24.existsSync(mcpPath)) {
9090
+ const existingContent = fs24.readFileSync(mcpPath, "utf-8");
9040
9091
  if (existingContent.includes("kuma")) return;
9041
9092
  const parsed = JSON.parse(existingContent);
9042
9093
  parsed.mcpServers = parsed.mcpServers || {};
9043
9094
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
9044
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
9095
+ fs24.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
9045
9096
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
9046
9097
  } else {
9047
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
9098
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
9048
9099
  const config = {
9049
9100
  mcpServers: {
9050
9101
  kuma: {
@@ -9054,7 +9105,7 @@ function handleCodewhaleSecondary(root, results) {
9054
9105
  }
9055
9106
  }
9056
9107
  };
9057
- fs25.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
9108
+ fs24.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
9058
9109
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
9059
9110
  }
9060
9111
  } catch (err) {
@@ -9076,28 +9127,28 @@ function runInit(options) {
9076
9127
  let agentsMdHandled = false;
9077
9128
  for (const type of selected) {
9078
9129
  const relativePath = configFilePath(type);
9079
- const fullPath = path25.resolve(root, relativePath);
9130
+ const fullPath = path24.resolve(root, relativePath);
9080
9131
  const getTemplate = TEMPLATES[type];
9081
9132
  try {
9082
9133
  if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
9083
9134
  agentsMdHandled = true;
9084
9135
  const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
9085
- if (fs25.existsSync(fullPath)) {
9136
+ if (fs24.existsSync(fullPath)) {
9086
9137
  if (options.skipExisting) {
9087
9138
  results.push({ type, filePath: relativePath, action: "skipped" });
9088
9139
  } else {
9089
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
9140
+ const existingContent = fs24.readFileSync(fullPath, "utf-8");
9090
9141
  if (existingContent.includes("_Generated by Kuma MCP_")) {
9091
9142
  results.push({ type, filePath: relativePath, action: "skipped" });
9092
9143
  } else {
9093
- fs25.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
9144
+ fs24.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
9094
9145
  results.push({ type, filePath: relativePath, action: "appended" });
9095
9146
  }
9096
9147
  }
9097
9148
  } else {
9098
- const dir = path25.dirname(fullPath);
9099
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
9100
- fs25.writeFileSync(fullPath, combinedContent, "utf-8");
9149
+ const dir = path24.dirname(fullPath);
9150
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
9151
+ fs24.writeFileSync(fullPath, combinedContent, "utf-8");
9101
9152
  results.push({ type, filePath: relativePath, action: "created" });
9102
9153
  }
9103
9154
  if (selectedSet.has("opencode")) handleOpencodeSecondary(root, results);
@@ -9109,12 +9160,12 @@ function runInit(options) {
9109
9160
  continue;
9110
9161
  } else {
9111
9162
  const template = getTemplate();
9112
- if (fs25.existsSync(fullPath)) {
9163
+ if (fs24.existsSync(fullPath)) {
9113
9164
  if (options.skipExisting) {
9114
9165
  results.push({ type, filePath: relativePath, action: "skipped" });
9115
9166
  continue;
9116
9167
  }
9117
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
9168
+ const existingContent = fs24.readFileSync(fullPath, "utf-8");
9118
9169
  if (existingContent.includes("_Generated by Kuma MCP_")) {
9119
9170
  if (type === "antigravity") {
9120
9171
  handleAntigravityMcpConfig(root, results);
@@ -9127,14 +9178,14 @@ function runInit(options) {
9127
9178
  continue;
9128
9179
  }
9129
9180
  const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
9130
- fs25.writeFileSync(fullPath, newContent, "utf-8");
9181
+ fs24.writeFileSync(fullPath, newContent, "utf-8");
9131
9182
  results.push({ type, filePath: relativePath, action: "appended" });
9132
9183
  } else {
9133
- const dir = path25.dirname(fullPath);
9134
- if (!fs25.existsSync(dir)) {
9135
- fs25.mkdirSync(dir, { recursive: true });
9184
+ const dir = path24.dirname(fullPath);
9185
+ if (!fs24.existsSync(dir)) {
9186
+ fs24.mkdirSync(dir, { recursive: true });
9136
9187
  }
9137
- fs25.writeFileSync(fullPath, template, "utf-8");
9188
+ fs24.writeFileSync(fullPath, template, "utf-8");
9138
9189
  results.push({ type, filePath: relativePath, action: "created" });
9139
9190
  }
9140
9191
  if (type === "antigravity") {
@@ -9290,19 +9341,19 @@ __export(agentDetector_exports, {
9290
9341
  getSkillPath: () => getSkillPath,
9291
9342
  skillExists: () => skillExists
9292
9343
  });
9293
- import fs26 from "fs";
9294
- import path26 from "path";
9344
+ import fs25 from "fs";
9345
+ import path25 from "path";
9295
9346
  function checkFile(root, filePath) {
9296
9347
  try {
9297
- return fs26.existsSync(path26.join(root, filePath));
9348
+ return fs25.existsSync(path25.join(root, filePath));
9298
9349
  } catch {
9299
9350
  return false;
9300
9351
  }
9301
9352
  }
9302
9353
  function checkDir(root, dirPath) {
9303
9354
  try {
9304
- const fullPath = path26.join(root, dirPath);
9305
- return fs26.existsSync(fullPath) && fs26.statSync(fullPath).isDirectory();
9355
+ const fullPath = path25.join(root, dirPath);
9356
+ return fs25.existsSync(fullPath) && fs25.statSync(fullPath).isDirectory();
9306
9357
  } catch {
9307
9358
  return false;
9308
9359
  }
@@ -9370,7 +9421,7 @@ function getAgentLabel(type) {
9370
9421
  function skillExists(type, projectRoot) {
9371
9422
  const root = projectRoot ?? getProjectRoot();
9372
9423
  const skillPath = getSkillPath(type);
9373
- return fs26.existsSync(path26.join(root, skillPath));
9424
+ return fs25.existsSync(path25.join(root, skillPath));
9374
9425
  }
9375
9426
  var AGENT_DETECTORS;
9376
9427
  var init_agentDetector = __esm({
@@ -9766,30 +9817,18 @@ var init_skillGenerator = __esm({
9766
9817
  "use strict";
9767
9818
  BOOTSTRAP = [
9768
9819
  "Kuma MCP tools are available (kuma_context, kuma_memory, kuma_safety).",
9769
- '**Before coding, call `kuma_context({ action: "init" })`** to load project context and session memory.',
9820
+ '**Before coding, call `kuma_context({ action: "init" })`** to load project context.',
9770
9821
  "Project knowledge persists in `.kuma/memories/*.md` across sessions.",
9771
9822
  "",
9772
- "\u26A0\uFE0F **MANDATORY WORKFLOW** \u2014 Follow in order every session (including research-only):",
9773
- "",
9774
- ' 1. `kuma_context({ action: "init" })` \u2014 Load context (START HERE)',
9775
- ' 2. `kuma_safety({ action: "guard" })` \u2014 Safety check before work',
9776
- ' 3. `kuma_context({ action: "research", scope: "<area>" })` \u2014 Research before editing',
9777
- " 4. *(edit/read using native tools)*",
9778
- ' 5. `kuma_memory({ action: "research_save", ... })` \u2014 After exploring area (creates search cache)',
9779
- ' 6. `kuma_memory({ action: "gotcha" })` \u2014 \u{1F525} IMMEDIATELY when bug found (exponential value)',
9780
- ' 7. `kuma_memory({ action: "arch_flow" })` \u2014 \u{1F525} IMMEDIATELY after each flow hop (exponential value)',
9781
- ' 8. `kuma_memory({ action: "decision" })` \u2014 When choosing between options (preserves rationale)',
9782
- ' 9. `kuma_safety({ action: "verify", ... })` \u2014 Run tests / confirm nothing broken',
9783
- ' 10. `kuma_context({ action: "changes" })` \u2014 Review session activity',
9823
+ "\u26A1 **ADAPTIVE WORKFLOW (Lean Mode by Default):**",
9824
+ " \u2022 **Lean Mode (Default, < 3 files):** `init` \u2192 edit/read \u2192 record (`decision`/`gotcha` if needed)",
9825
+ " \u2022 **Standard Mode (3-10 files/unfamiliar):** `init` \u2192 `guard` \u2192 `research` \u2192 edit \u2192 `record` \u2192 `verify`",
9826
+ " \u2022 **Full Mode (> 10 files/refactor):** `init` \u2192 `guard` \u2192 `research` \u2192 `impact` \u2192 edit \u2192 `record` \u2192 `verify` \u2192 `changes`",
9784
9827
  "",
9785
- "\u{1F525} **Power Curve:** `arch_flow` + `gotcha` are EXPONENTIAL \u2014 each record saves 5-10 files next session.",
9786
- " The more you record, the more overpowered the agent becomes. These are the highest-value actions.",
9787
- "\u{1F7E2} **SKIP** recording function/class/component nodes \u2014 grep/glob is faster.",
9788
- "\u{1F9E0} Knowledge graph is persistent \u2014 nodes/edges accumulate across sessions, getting richer over time.",
9828
+ "\u{1F534} **MUST RECORD:** `decision` (rationale), `gotcha` (bug/quirk), `arch_flow` (complex flow)",
9829
+ "\u{1F7E2} **SKIP RECORDING:** function, class, import, route (use native grep/glob instead)",
9789
9830
  "",
9790
- "\u26D4 Do NOT skip steps 5-10 after research \u2014 even if no code was changed.",
9791
- "\u{1F5D1}\uFE0F **Delete/Clear:** `delete_node` (delete node/gotcha/todo/decision) | `clear` (wipe entire graph)",
9792
- "\u{1F4D6} Full rules: `.kuma/init.md`",
9831
+ "\u{1F4D6} Full rules & modes: `.kuma/init.md` | `.kuma/MODE.md` | `.kuma/SKIP_RULES.md`",
9793
9832
  "\u{1F9E0} Tools: `kuma_context` | `kuma_memory` | `kuma_safety`"
9794
9833
  ].join("\n");
9795
9834
  BOOTSTRAP_OPENCODE = [
@@ -9797,27 +9836,15 @@ var init_skillGenerator = __esm({
9797
9836
  '**Before coding, call `kuma_kuma_context({ action: "init" })`** to load project context.',
9798
9837
  "Project knowledge persists in `.kuma/memories/*.md` across sessions.",
9799
9838
  "",
9800
- "\u26A0\uFE0F **MANDATORY WORKFLOW** \u2014 Follow in order every session (including research-only):",
9801
- "",
9802
- ' 1. `kuma_kuma_context({ action: "init" })` \u2014 Load context (START HERE)',
9803
- ' 2. `kuma_kuma_safety({ action: "guard" })` \u2014 Safety check before work',
9804
- ' 3. `kuma_kuma_context({ action: "research", scope: "<area>" })` \u2014 Research before editing',
9805
- " 4. *(edit/read using native tools)*",
9806
- ' 5. `kuma_kuma_memory({ action: "research_save", ... })` \u2014 After exploring area (creates search cache)',
9807
- ' 6. `kuma_kuma_memory({ action: "gotcha" })` \u2014 \u{1F525} IMMEDIATELY when bug found (exponential value)',
9808
- ' 7. `kuma_kuma_memory({ action: "arch_flow" })` \u2014 \u{1F525} IMMEDIATELY after each flow hop (exponential value)',
9809
- ' 8. `kuma_kuma_memory({ action: "decision" })` \u2014 When choosing between options (preserves rationale)',
9810
- ' 9. `kuma_kuma_safety({ action: "verify", ... })` \u2014 Run tests / confirm nothing broken',
9811
- ' 10. `kuma_kuma_context({ action: "changes" })` \u2014 Review session activity',
9839
+ "\u26A1 **ADAPTIVE WORKFLOW (Lean Mode by Default):**",
9840
+ ' \u2022 **Lean Mode (Default, < 3 files):** `kuma_kuma_context({ action: "init" })` \u2192 edit/read \u2192 record (`decision`/`gotcha`)',
9841
+ " \u2022 **Standard Mode (3-10 files/unfamiliar):** `init` \u2192 `guard` \u2192 `research` \u2192 edit \u2192 `record` \u2192 `verify`",
9842
+ " \u2022 **Full Mode (> 10 files/refactor):** `init` \u2192 `guard` \u2192 `research` \u2192 `impact` \u2192 edit \u2192 `record` \u2192 `verify` \u2192 `changes`",
9812
9843
  "",
9813
- "\u{1F525} **Power Curve:** `arch_flow` + `gotcha` are EXPONENTIAL \u2014 each record saves 5-10 files next session.",
9814
- " The more you record, the more overpowered the agent becomes. These are the highest-value actions.",
9815
- "\u{1F7E2} **SKIP** recording function/class/component nodes \u2014 grep/glob is faster.",
9816
- "\u{1F9E0} Knowledge graph is persistent \u2014 nodes/edges accumulate across sessions, getting richer over time.",
9844
+ "\u{1F534} **MUST RECORD:** `decision` (rationale), `gotcha` (bug/quirk), `arch_flow` (complex flow)",
9845
+ "\u{1F7E2} **SKIP RECORDING:** function, class, import, route (use native grep/glob instead)",
9817
9846
  "",
9818
- "\u26D4 Do NOT skip steps 5-10 after research \u2014 even if no code was changed.",
9819
- "\u{1F5D1}\uFE0F **Delete/Clear:** `delete_node` (delete node/gotcha/todo/decision) | `clear` (wipe entire graph)",
9820
- "\u{1F4D6} Full rules: `.kuma/init.md`",
9847
+ "\u{1F4D6} Full rules & modes: `.kuma/init.md` | `.kuma/MODE.md` | `.kuma/SKIP_RULES.md`",
9821
9848
  "\u{1F9E0} Tools: `kuma_kuma_context` | `kuma_kuma_memory` | `kuma_kuma_safety`"
9822
9849
  ].join("\n");
9823
9850
  }
@@ -10339,8 +10366,8 @@ init_kumaDb();
10339
10366
  init_kumaSelfHeal();
10340
10367
  init_kumaMemory();
10341
10368
  init_pathValidator();
10342
- import fs15 from "fs";
10343
- import path15 from "path";
10369
+ import fs14 from "fs";
10370
+ import path14 from "path";
10344
10371
  var MEMORY_ALIASES = {
10345
10372
  // Session synonyms
10346
10373
  "session": "session",
@@ -10421,29 +10448,14 @@ var MEMORY_ALIASES = {
10421
10448
  "layers": "layers",
10422
10449
  "3-layer": "layers",
10423
10450
  "memory-layers": "layers",
10424
- // Federated synonyms (Issue #27)
10425
- "federated": "federated",
10426
- "federated-graph": "federated",
10427
- "kuma-uri": "federated",
10428
- "remote-graph": "federated",
10429
- // Test generation synonyms (Issue #28)
10430
- "gen_test": "gen_test",
10431
- "gen-test": "gen_test",
10432
- "generate-test": "gen_test",
10433
- "gentest": "gen_test",
10434
- // Trajectory synonyms
10435
- "trajectory": "trajectory",
10436
- "trajectories": "trajectory",
10437
- "traj": "trajectory",
10438
- // Skills synonyms
10439
- "skills": "skills",
10440
- "distilled-skills": "skills",
10441
- "skill-list": "skills",
10442
10451
  // Add node synonyms
10443
- "add_node": "add_node",
10444
10452
  "add-node": "add_node",
10445
- "node": "add_node",
10446
10453
  "create-node": "add_node",
10454
+ // Feature synonyms
10455
+ "feature": "feature",
10456
+ "features": "feature",
10457
+ "record-feature": "feature",
10458
+ "define-feature": "feature",
10447
10459
  "record-node": "add_node",
10448
10460
  // Delete node synonyms
10449
10461
  "delete_node": "delete_node",
@@ -10495,22 +10507,16 @@ async function handleMemory(params) {
10495
10507
  return handleGotchaAction(params);
10496
10508
  case "layers":
10497
10509
  return handleLayersSummary(params);
10498
- case "federated":
10499
- return handleFederated(params);
10500
- case "gen_test":
10501
- return handleGenTest(params);
10502
- case "trajectory":
10503
- return handleTrajectoryList(params);
10504
- case "skills":
10505
- return handleSkillsList(params);
10506
10510
  case "add_node":
10507
10511
  return handleAddNode(params);
10512
+ case "feature":
10513
+ return handleFeature(params);
10508
10514
  case "delete_node":
10509
10515
  return handleDeleteNode(params);
10510
10516
  case "clear": {
10511
10517
  const { clearGraph: clearGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10512
10518
  await clearGraph2();
10513
- return "\u{1F5D1}\uFE0F **Knowledge Graph Cleared** \u2014 All nodes, edges, gotchas, and trajectories have been wiped from disk and memory.";
10519
+ return "\u{1F5D1}\uFE0F **Knowledge Graph Cleared** \u2014 All nodes, edges, and gotchas have been wiped from disk and memory.";
10514
10520
  }
10515
10521
  default:
10516
10522
  return `Unknown action "${action}".`;
@@ -10528,8 +10534,13 @@ async function handleDecision(params) {
10528
10534
  Use kuma_memory({ action: 'decision', title: '...', context: '...', rationale: '...', outcome: '...' }) to record.` : "\u2705 No decision needed at this time.";
10529
10535
  }
10530
10536
  case "record":
10531
- if (!params.title || !params.rationale) return "\u26A0\uFE0F title and rationale are required.";
10532
- return recordDecision({
10537
+ if (!params.title) return `\u274C **decision format error:** title is required.
10538
+
10539
+ \u2705 Correct: kuma_memory({ action: "decision", decisionAction: "record", title: "Use bcrypt vs argon2", rationale: "bcrypt is battle-tested, argon2 is newer but less adoption", context: "Login system security", outcome: "Chose bcrypt" })`;
10540
+ if (!params.rationale) return `\u274C **decision format error:** rationale is required.
10541
+
10542
+ \u2705 Correct: kuma_memory({ action: "decision", decisionAction: "record", title: "${params.title}", rationale: "WHY you chose this option", context: "optional context", outcome: "optional outcome" })`;
10543
+ const decisionResult = await recordDecision({
10533
10544
  title: params.title,
10534
10545
  context: params.context || "",
10535
10546
  options: [],
@@ -10537,6 +10548,8 @@ Use kuma_memory({ action: 'decision', title: '...', context: '...', rationale: '
10537
10548
  outcome: params.outcome || "implemented",
10538
10549
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10539
10550
  });
10551
+ sessionMemory.recordMemoryAction("decision");
10552
+ return decisionResult;
10540
10553
  default:
10541
10554
  return formatDecisionTemplate();
10542
10555
  }
@@ -10562,11 +10575,12 @@ async function handleResearchSave(params) {
10562
10575
  } catch {
10563
10576
  }
10564
10577
  try {
10565
- const researchDir = path15.join(getProjectRoot(), ".kuma", "research");
10566
- if (!fs15.existsSync(researchDir)) fs15.mkdirSync(researchDir, { recursive: true });
10567
- fs15.writeFileSync(path15.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
10578
+ const researchDir = path14.join(getProjectRoot(), ".kuma", "research");
10579
+ if (!fs14.existsSync(researchDir)) fs14.mkdirSync(researchDir, { recursive: true });
10580
+ fs14.writeFileSync(path14.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
10568
10581
  } catch {
10569
10582
  }
10583
+ sessionMemory.recordMemoryAction("research_save");
10570
10584
  return `\u2705 Research "${scope}" saved.`;
10571
10585
  }
10572
10586
  async function handleSession(params) {
@@ -10575,13 +10589,23 @@ async function handleSession(params) {
10575
10589
  if (topic) return typeof summary.content === "string" ? summary.content : JSON.stringify(summary, null, 2);
10576
10590
  const modifiedFiles = summary.modifiedFiles || [];
10577
10591
  const failures = summary.unresolvedFailures || [];
10592
+ const recordingSummary = sessionMemory.getRecordingSummary();
10593
+ const metricsSummary = sessionMemory.getMetricsSummary();
10578
10594
  const lines = [
10579
10595
  "\u{1F4CB} **Session Summary**\n\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\n",
10580
10596
  `\u{1F3AF} Goal: ${summary.currentGoal || "not set"}`,
10581
10597
  `\u{1F550} Duration: ${summary.sessionDuration}`,
10582
10598
  `\u{1F6E0}\uFE0F Tool calls: ${summary.toolCallCount}
10583
- `
10599
+ `,
10600
+ `\u{1F9E0} **Recordings:** ${recordingSummary.total} total \u2014 ${recordingSummary.archFlows} arch_flow, ${recordingSummary.gotchas} gotcha, ${recordingSummary.decisions} decision, ${recordingSummary.researchSaves} research_save`,
10601
+ `\u{1F4CA} **Metrics:** ${metricsSummary.filesRead} files read, ${metricsSummary.filesEdited} files edited`,
10602
+ `\u23F1\uFE0F **Time saved:** ~${metricsSummary.researchTimeSavedFormatted} (from research cache)`
10584
10603
  ];
10604
+ if (recordingSummary.missingRecordings.length > 0 && recordingSummary.total === 0) {
10605
+ lines.push(`
10606
+ \u26A0\uFE0F **No recordings yet!** Missing: ${recordingSummary.missingRecordings.join(", ")}`);
10607
+ lines.push(`\u{1F4A1} Tip: Record findings after reading files. arch_flow + gotcha are exponential value.`);
10608
+ }
10585
10609
  if (modifiedFiles.length > 0) {
10586
10610
  lines.push(`**Modified Files** (${modifiedFiles.length}):`);
10587
10611
  for (const f of modifiedFiles.slice(0, 10)) {
@@ -10613,6 +10637,33 @@ async function handleSearch(params) {
10613
10637
  const memResults = sessionMemory.searchMemory(query, limit);
10614
10638
  const { searchGraph: searchGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10615
10639
  const graphResults = await searchGraph2(query, Math.min(limit, 10));
10640
+ let taskContext = "";
10641
+ try {
10642
+ const { retrieveForTask: retrieveForTask2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10643
+ taskContext = await retrieveForTask2(query, 10);
10644
+ } catch {
10645
+ }
10646
+ let impactAnalysis = "";
10647
+ try {
10648
+ const { propagateImpact: propagateImpact2, searchGraph: sg } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10649
+ const searchRes = await sg(query, 1);
10650
+ const nodeIdMatch = searchRes.match(/\*\*([^*]+)\*\* \(([^)]+)\)/);
10651
+ if (nodeIdMatch) {
10652
+ const nodeType = nodeIdMatch[2];
10653
+ const nodeName = nodeIdMatch[1];
10654
+ const nid = `${nodeType}::${nodeName}`;
10655
+ const impacts = await propagateImpact2(nid, 3, 0.2);
10656
+ if (impacts.length > 0) {
10657
+ const impactLines = impacts.slice(0, 5).map(
10658
+ (i) => ` \u2022 ${i.name} (${i.type}) \u2014 depth ${i.depth}, weight ${i.weight}`
10659
+ );
10660
+ impactAnalysis = `
10661
+ \u{1F4A5} **Impact Analysis** \u2014 ${impacts.length} node(s) affected by "${nodeName}":
10662
+ ${impactLines.join("\n")}`;
10663
+ }
10664
+ }
10665
+ } catch {
10666
+ }
10616
10667
  let hybridResults = "";
10617
10668
  try {
10618
10669
  const { hybridSearch: hybridSearch2, formatHybridResults: formatHybridResults2 } = await Promise.resolve().then(() => (init_kumaSearch(), kumaSearch_exports));
@@ -10630,7 +10681,14 @@ async function handleSearch(params) {
10630
10681
  for (const r of memResults.slice(0, 5)) lines.push(` \u2022 ${r.content.substring(0, 120)}`);
10631
10682
  lines.push("");
10632
10683
  }
10684
+ if (taskContext) {
10685
+ lines.push(taskContext);
10686
+ lines.push("");
10687
+ }
10633
10688
  lines.push("**Knowledge Graph:\n" + graphResults);
10689
+ if (impactAnalysis) {
10690
+ lines.push(impactAnalysis);
10691
+ }
10634
10692
  if (hybridResults) {
10635
10693
  lines.push("");
10636
10694
  lines.push(hybridResults);
@@ -10722,12 +10780,24 @@ async function handleLayerAction(layer, params) {
10722
10780
  const gotchasStr = gotchasMatch ? gotchasMatch[1].trim() : "";
10723
10781
  const decisionsStr = decisionsMatch ? decisionsMatch[1].trim() : "";
10724
10782
  const filesStr = filesMatch ? filesMatch[1].trim() : "";
10783
+ if (hopsStr && !content.includes("\u2192")) {
10784
+ return `\u274C **arch_flow format error:** hops must use \u2192 separator.
10785
+
10786
+ \u274C Wrong: hops: file1.js, file2.js
10787
+ \u274C Wrong: hops: file1.js\\nfile2.js
10788
+ \u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js \u2192 file3.js`;
10789
+ }
10725
10790
  const hops = hopsStr ? hopsStr.split("\u2192").map((h) => h.trim()).filter(Boolean).map((h, i, arr) => ({
10726
10791
  from: i === 0 ? domain : arr[i - 1],
10727
10792
  to: h,
10728
10793
  relation: "flows",
10729
10794
  description: h
10730
10795
  })) : [];
10796
+ if (hops.length === 0) {
10797
+ return `\u274C **arch_flow format error:** no hops found after \u2192 separator.
10798
+
10799
+ \u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js`;
10800
+ }
10731
10801
  const gotchas = gotchasStr ? gotchasStr.split(",").map((g) => g.trim()).filter(Boolean) : [];
10732
10802
  const decisions = decisionsStr ? decisionsStr.split(",").map((d) => d.trim()).filter(Boolean) : [];
10733
10803
  const filePaths = filesStr ? filesStr.split(",").map((f) => f.trim()).filter(Boolean) : [];
@@ -10735,6 +10805,7 @@ async function handleLayerAction(layer, params) {
10735
10805
  const { recordDomainFlow: recordDomainFlow2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10736
10806
  const flow = await recordDomainFlow2({ domain, hops, gotchas, decisions, filePaths });
10737
10807
  await writeLayer2("arch_flow", content);
10808
+ sessionMemory.recordMemoryAction("arch_flow");
10738
10809
  return `\u2705 Domain flow "${domain}" recorded \u2014 ${flow.nodeCount} nodes, ${flow.edgeCount} edges created.
10739
10810
  \u{1F3DB}\uFE0F **Domain Anchor:** ${domain}
10740
10811
  \u{1F504} **Hops:** ${hops.length}
@@ -10745,7 +10816,15 @@ async function handleLayerAction(layer, params) {
10745
10816
  return `\u2705 Architecture flow saved (text only). Graph recording failed: ${err}`;
10746
10817
  }
10747
10818
  }
10748
- return writeLayer2("arch_flow", content);
10819
+ return `\u274C **arch_flow format not recognized.** Use this format:
10820
+
10821
+ domain: <DomainName> | hops: <file1.tsx> \u2192 <file2.js> \u2192 <file3.ts>
10822
+
10823
+ Fields:
10824
+ - domain: short name (e.g. InviteUserFlow)
10825
+ - hops: file names separated by \u2192 (NOT commas, NOT newlines)
10826
+ - gotchas: optional, comma-separated
10827
+ - decisions: optional, comma-separated`;
10749
10828
  }
10750
10829
  if (params.content) {
10751
10830
  return writeLayer2(layer, params.content);
@@ -10754,13 +10833,25 @@ async function handleLayerAction(layer, params) {
10754
10833
  }
10755
10834
  async function handleGotchaAction(params) {
10756
10835
  if (params.content && params.scope) {
10836
+ if (!params.scope.trim()) {
10837
+ return `\u274C **gotcha format error:** scope (file path) is required.
10838
+
10839
+ \u2705 Correct: kuma_memory({ action: "gotcha", scope: "path/to/file.ts", content: "bug description", status: "high" })`;
10840
+ }
10841
+ if (!params.content.trim()) {
10842
+ return `\u274C **gotcha format error:** content (description) is required.
10843
+
10844
+ \u2705 Correct: kuma_memory({ action: "gotcha", scope: "path/to/file.ts", content: "bug description", status: "high" })`;
10845
+ }
10757
10846
  const { addGotcha: addGotcha2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
10758
- return await addGotcha2({
10847
+ const result = await addGotcha2({
10759
10848
  filePath: params.scope,
10760
10849
  description: params.content,
10761
10850
  severity: params.status || "medium",
10762
10851
  workaround: params.description
10763
10852
  });
10853
+ sessionMemory.recordMemoryAction("gotcha");
10854
+ return result;
10764
10855
  }
10765
10856
  const { listGotchas: listGotchas2, syncGotchasToDb: syncGotchasToDb2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
10766
10857
  await syncGotchasToDb2();
@@ -10796,19 +10887,13 @@ async function handleDeleteNode(params) {
10796
10887
  saveDb2(db);
10797
10888
  return `\u{1F5D1}\uFE0F **Decision #${id} deleted.**`;
10798
10889
  }
10799
- case "trajectory":
10800
- case "trajectories": {
10801
- db.run("DELETE FROM trajectories WHERE id = ?", [id]);
10802
- saveDb2(db);
10803
- return `\u{1F5D1}\uFE0F **Trajectory #${id} deleted.**`;
10804
- }
10805
10890
  case "checkpoint": {
10806
10891
  db.run("DELETE FROM health_snapshots WHERE id = ?", [id]);
10807
10892
  saveDb2(db);
10808
10893
  return `\u{1F5D1}\uFE0F **Checkpoint #${id} deleted.**`;
10809
10894
  }
10810
10895
  default:
10811
- return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision, trajectory, checkpoint`;
10896
+ return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision, checkpoint`;
10812
10897
  }
10813
10898
  }
10814
10899
  if (params.target) {
@@ -10841,38 +10926,7 @@ async function handleDeleteNode(params) {
10841
10926
  return `\u{1F5D1}\uFE0F **Node #${id} deleted.**`;
10842
10927
  }
10843
10928
  }
10844
- return "\u26A0\uFE0F Provide `target` (node ID) to delete, or `scope` + `target` (numeric ID) for gotcha/todo/decision/trajectory/checkpoint.\n\nExamples:\n- `delete_node`, target: 'function::sendMessage'\n- `delete_node`, scope: 'gotcha', target: '42'\n- `delete_node`, scope: 'todo', target: '7'\n- `delete_node`, scope: 'decision', target: '3'\n- `delete_node`, scope: 'trajectory', target: '5'\n- `delete_node`, scope: 'checkpoint', target: '1'";
10845
- }
10846
- async function handleFederated(params) {
10847
- sessionMemory.recordToolCall("kuma_memory_federated", { scope: params.scope, uri: params.uri });
10848
- if (params.uri) {
10849
- const { resolveFederatedNode: resolveFederatedNode2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10850
- return await resolveFederatedNode2(params.uri);
10851
- }
10852
- const { listFederatedReferences: listFederatedReferences2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10853
- return await listFederatedReferences2();
10854
- }
10855
- async function handleGenTest(params) {
10856
- sessionMemory.recordToolCall("kuma_memory_gen_test", { target: params.target });
10857
- if (params.target) {
10858
- const id = parseInt(params.target, 10);
10859
- if (!isNaN(id)) {
10860
- const { generateTestFromTrajectoryId: generateTestFromTrajectoryId2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
10861
- return await generateTestFromTrajectoryId2(id);
10862
- }
10863
- }
10864
- const { listGeneratedTests: listGeneratedTests2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
10865
- return await listGeneratedTests2();
10866
- }
10867
- async function handleTrajectoryList(params) {
10868
- sessionMemory.recordToolCall("kuma_memory_trajectory", {});
10869
- const { listTrajectories: listTrajectories2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
10870
- return await listTrajectories2(params.limit || 10);
10871
- }
10872
- async function handleSkillsList(_params) {
10873
- sessionMemory.recordToolCall("kuma_memory_skills", {});
10874
- const { listDistilledSkills: listDistilledSkills2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
10875
- return await listDistilledSkills2();
10929
+ return "\u26A0\uFE0F Provide `target` (node ID) to delete, or `scope` + `target` (numeric ID) for gotcha/todo/decision/checkpoint.\n\nExamples:\n- `delete_node`, target: 'function::sendMessage'\n- `delete_node`, scope: 'gotcha', target: '42'\n- `delete_node`, scope: 'todo', target: '7'\n- `delete_node`, scope: 'decision', target: '3'\n- `delete_node`, scope: 'checkpoint', target: '1'";
10876
10930
  }
10877
10931
  async function handleAddNode(params) {
10878
10932
  const type = params.title;
@@ -10907,6 +10961,29 @@ async function handleAddNode(params) {
10907
10961
  return `\u274C Failed to create node: ${err}`;
10908
10962
  }
10909
10963
  }
10964
+ async function handleFeature(params) {
10965
+ sessionMemory.recordToolCall("kuma_memory_feature", { title: params.title, scope: params.scope });
10966
+ if (!params.title) {
10967
+ return "\u26A0\uFE0F `title` (feature name) required.\n\n\u2705 Correct: kuma_memory({ action: 'feature', title: 'Authentication', content: 'description', scope: 'src/auth/login.ts,src/auth/session.ts' })\n\n\u2022 title = feature name (e.g. 'Authentication', 'Billing')\n\u2022 content = description (optional)\n\u2022 scope = comma-separated file paths (optional)\n\u2022 tags = comma-separated tags (optional)\n\u2022 status = risk level: low/medium/high/critical (optional)";
10968
+ }
10969
+ const { recordFeature: recordFeature2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10970
+ const files = params.scope ? params.scope.split(",").map((f) => f.trim()).filter(Boolean) : [];
10971
+ const tags = params.tags ? params.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
10972
+ const risk = params.status || "medium";
10973
+ const result = await recordFeature2({
10974
+ name: params.title,
10975
+ description: params.content,
10976
+ files,
10977
+ tags,
10978
+ risk
10979
+ });
10980
+ sessionMemory.recordMemoryAction("research_save");
10981
+ return `\u2705 **Feature "${params.title}" recorded** \u2014 ${result.nodeCount} node(s), ${result.edgeCount} edge(s)
10982
+ \u2B50 Level: feature
10983
+ \u{1F4C1} Files: ${files.length}
10984
+ \u{1F3F7}\uFE0F Tags: ${tags.length}
10985
+ \u26A0\uFE0F Risk: ${risk}`;
10986
+ }
10910
10987
  async function handleLayersSummary(_params) {
10911
10988
  const { getLayersSummary: getLayersSummary2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
10912
10989
  return getLayersSummary2();
@@ -10917,72 +10994,72 @@ init_sessionMemory();
10917
10994
 
10918
10995
  // src/engine/kumaLock.ts
10919
10996
  init_pathValidator();
10920
- import fs16 from "fs";
10921
- import path16 from "path";
10997
+ import fs15 from "fs";
10998
+ import path15 from "path";
10922
10999
  var LOCKS_DIR = ".kuma/locks";
10923
11000
  function locksDir() {
10924
- return path16.join(getProjectRoot(), LOCKS_DIR);
11001
+ return path15.join(getProjectRoot(), LOCKS_DIR);
10925
11002
  }
10926
11003
  function ensureLocksDir() {
10927
11004
  const dir = locksDir();
10928
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
11005
+ if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
10929
11006
  }
10930
11007
  function lockPath(filePath) {
10931
11008
  const safeName = filePath.replace(/[^a-zA-Z0-9_./-]/g, "_");
10932
- return path16.join(locksDir(), `${safeName}.lock.json`);
11009
+ return path15.join(locksDir(), `${safeName}.lock.json`);
10933
11010
  }
10934
11011
  function acquireLock(filePath, agentId) {
10935
11012
  ensureLocksDir();
10936
11013
  const lp = lockPath(filePath);
10937
11014
  const id = agentId || `agent-${process.pid}`;
10938
- if (fs16.existsSync(lp)) {
11015
+ if (fs15.existsSync(lp)) {
10939
11016
  try {
10940
- const existing = JSON.parse(fs16.readFileSync(lp, "utf-8"));
11017
+ const existing = JSON.parse(fs15.readFileSync(lp, "utf-8"));
10941
11018
  if (existing.agentId === id) {
10942
11019
  return `\u{1F512} **Already locked** by you (${id}) on ${new Date(existing.acquiredAt).toISOString()}`;
10943
11020
  }
10944
11021
  const elapsed = Math.floor((Date.now() - existing.acquiredAt) / 1e3);
10945
11022
  if (elapsed > 300) {
10946
- fs16.unlinkSync(lp);
11023
+ fs15.unlinkSync(lp);
10947
11024
  } else {
10948
11025
  return `\u{1F512} **Locked** by ${existing.agentId} since ${new Date(existing.acquiredAt).toISOString()} (${elapsed}s ago)`;
10949
11026
  }
10950
11027
  } catch {
10951
- fs16.unlinkSync(lp);
11028
+ fs15.unlinkSync(lp);
10952
11029
  }
10953
11030
  }
10954
11031
  const entry = { filePath, agentId: id, acquiredAt: Date.now(), status: "locked" };
10955
- fs16.writeFileSync(lp, JSON.stringify(entry, null, 2), "utf-8");
11032
+ fs15.writeFileSync(lp, JSON.stringify(entry, null, 2), "utf-8");
10956
11033
  return `\u{1F513} **Lock acquired** on \`${filePath}\` by ${id}`;
10957
11034
  }
10958
11035
  function releaseLock(filePath, agentId) {
10959
11036
  ensureLocksDir();
10960
11037
  const lp = lockPath(filePath);
10961
11038
  const id = agentId || `agent-${process.pid}`;
10962
- if (!fs16.existsSync(lp)) {
11039
+ if (!fs15.existsSync(lp)) {
10963
11040
  return `\u26A0\uFE0F No lock found for \`${filePath}\``;
10964
11041
  }
10965
11042
  try {
10966
- const existing = JSON.parse(fs16.readFileSync(lp, "utf-8"));
11043
+ const existing = JSON.parse(fs15.readFileSync(lp, "utf-8"));
10967
11044
  if (existing.agentId !== id) {
10968
11045
  return `\u26A0\uFE0F Cannot release lock held by ${existing.agentId}. Use force:true to override.`;
10969
11046
  }
10970
- fs16.unlinkSync(lp);
11047
+ fs15.unlinkSync(lp);
10971
11048
  return `\u{1F513} **Lock released** on \`${filePath}\``;
10972
11049
  } catch {
10973
- fs16.unlinkSync(lp);
11050
+ fs15.unlinkSync(lp);
10974
11051
  return `\u{1F513} **Lock released** (force cleanup) on \`${filePath}\``;
10975
11052
  }
10976
11053
  }
10977
11054
  function listLocks() {
10978
11055
  ensureLocksDir();
10979
11056
  const dir = locksDir();
10980
- const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".lock.json"));
11057
+ const files = fs15.readdirSync(dir).filter((f) => f.endsWith(".lock.json"));
10981
11058
  if (files.length === 0) return "\u{1F513} No active locks.";
10982
11059
  const lines = ["\u{1F512} **Active Locks:**", ""];
10983
11060
  for (const f of files) {
10984
11061
  try {
10985
- const entry = JSON.parse(fs16.readFileSync(path16.join(dir, f), "utf-8"));
11062
+ const entry = JSON.parse(fs15.readFileSync(path15.join(dir, f), "utf-8"));
10986
11063
  const elapsed = Math.floor((Date.now() - entry.acquiredAt) / 1e3);
10987
11064
  lines.push(` \u2022 \`${entry.filePath}\` \u2014 locked by ${entry.agentId} (${elapsed}s ago)`);
10988
11065
  } catch {
@@ -10993,9 +11070,9 @@ function listLocks() {
10993
11070
  }
10994
11071
  function isLocked(filePath) {
10995
11072
  const lp = lockPath(filePath);
10996
- if (!fs16.existsSync(lp)) return { locked: false };
11073
+ if (!fs15.existsSync(lp)) return { locked: false };
10997
11074
  try {
10998
- const entry = JSON.parse(fs16.readFileSync(lp, "utf-8"));
11075
+ const entry = JSON.parse(fs15.readFileSync(lp, "utf-8"));
10999
11076
  return { locked: true, by: entry.agentId, since: entry.acquiredAt };
11000
11077
  } catch {
11001
11078
  return { locked: false };
@@ -11005,16 +11082,16 @@ function cleanStaleLocks() {
11005
11082
  ensureLocksDir();
11006
11083
  const dir = locksDir();
11007
11084
  let count = 0;
11008
- for (const f of fs16.readdirSync(dir).filter((f2) => f2.endsWith(".lock.json"))) {
11085
+ for (const f of fs15.readdirSync(dir).filter((f2) => f2.endsWith(".lock.json"))) {
11009
11086
  try {
11010
- const entry = JSON.parse(fs16.readFileSync(path16.join(dir, f), "utf-8"));
11087
+ const entry = JSON.parse(fs15.readFileSync(path15.join(dir, f), "utf-8"));
11011
11088
  if (Date.now() - entry.acquiredAt > 3e5) {
11012
- fs16.unlinkSync(path16.join(dir, f));
11089
+ fs15.unlinkSync(path15.join(dir, f));
11013
11090
  count++;
11014
11091
  }
11015
11092
  } catch {
11016
11093
  try {
11017
- fs16.unlinkSync(path16.join(dir, f));
11094
+ fs15.unlinkSync(path15.join(dir, f));
11018
11095
  count++;
11019
11096
  } catch {
11020
11097
  }
@@ -11030,8 +11107,8 @@ init_safetyAudit();
11030
11107
 
11031
11108
  // src/tools/kumaPolicy.ts
11032
11109
  init_pathValidator();
11033
- import fs17 from "fs";
11034
- import path17 from "path";
11110
+ import fs16 from "fs";
11111
+ import path16 from "path";
11035
11112
  var DEFAULT_POLICY = {
11036
11113
  never_touch: [
11037
11114
  ".env",
@@ -11057,15 +11134,15 @@ var DEFAULT_POLICY = {
11057
11134
  };
11058
11135
  function loadPolicy() {
11059
11136
  const root = getProjectRoot();
11060
- const ymlPath = path17.join(root, ".kuma", "policy.yml");
11061
- const yamlPath = path17.join(root, ".kuma", "policy.yaml");
11137
+ const ymlPath = path16.join(root, ".kuma", "policy.yml");
11138
+ const yamlPath = path16.join(root, ".kuma", "policy.yaml");
11062
11139
  let policyContent = null;
11063
11140
  let policyPath = null;
11064
- if (fs17.existsSync(ymlPath)) {
11065
- policyContent = fs17.readFileSync(ymlPath, "utf-8");
11141
+ if (fs16.existsSync(ymlPath)) {
11142
+ policyContent = fs16.readFileSync(ymlPath, "utf-8");
11066
11143
  policyPath = ymlPath;
11067
- } else if (fs17.existsSync(yamlPath)) {
11068
- policyContent = fs17.readFileSync(yamlPath, "utf-8");
11144
+ } else if (fs16.existsSync(yamlPath)) {
11145
+ policyContent = fs16.readFileSync(yamlPath, "utf-8");
11069
11146
  policyPath = yamlPath;
11070
11147
  }
11071
11148
  if (!policyContent) {
@@ -11175,9 +11252,9 @@ function checkFilePathPolicy(filePath, policy) {
11175
11252
  if (policy.max_file_size && policy.max_file_size > 0) {
11176
11253
  try {
11177
11254
  const root = getProjectRoot();
11178
- const fullPath = path17.join(root, filePath);
11179
- if (fs17.existsSync(fullPath)) {
11180
- const sizeKB = fs17.statSync(fullPath).size / 1024;
11255
+ const fullPath = path16.join(root, filePath);
11256
+ if (fs16.existsSync(fullPath)) {
11257
+ const sizeKB = fs16.statSync(fullPath).size / 1024;
11181
11258
  if (sizeKB > policy.max_file_size) {
11182
11259
  violations.push({
11183
11260
  rule: "max_file_size",
@@ -11344,12 +11421,12 @@ async function safetyCheck(action, filePath, command) {
11344
11421
  }
11345
11422
  if (filePath) {
11346
11423
  try {
11347
- const fs27 = await import("fs");
11348
- const path27 = await import("path");
11424
+ const fs26 = await import("fs");
11425
+ const path26 = await import("path");
11349
11426
  const root = process.cwd();
11350
- const fullPath = path27.resolve(root, filePath);
11351
- if (fs27.existsSync(fullPath)) {
11352
- const stat = fs27.statSync(fullPath);
11427
+ const fullPath = path26.resolve(root, filePath);
11428
+ if (fs26.existsSync(fullPath)) {
11429
+ const stat = fs26.statSync(fullPath);
11353
11430
  const sizeKB = Math.round(stat.size / 1024);
11354
11431
  if (sizeKB > 500) {
11355
11432
  checks.push({
@@ -11359,12 +11436,12 @@ async function safetyCheck(action, filePath, command) {
11359
11436
  });
11360
11437
  }
11361
11438
  if (filePath.endsWith(".ts") || filePath.endsWith(".js")) {
11362
- const baseName = path27.basename(filePath, path27.extname(filePath));
11439
+ const baseName = path26.basename(filePath, path26.extname(filePath));
11363
11440
  const testPatterns = [
11364
11441
  filePath.replace(/\.(ts|js)$/, ".test.$1"),
11365
11442
  filePath.replace(/\.(ts|js)$/, ".spec.$1"),
11366
11443
  filePath.replace(/^src\//, "tests/").replace(/\.(ts|js)$/, ".test.$1"),
11367
- `**/__tests__/**/${baseName}.test.${path27.extname(filePath).slice(1)}`
11444
+ `**/__tests__/**/${baseName}.test.${path26.extname(filePath).slice(1)}`
11368
11445
  ];
11369
11446
  let hasTests = false;
11370
11447
  const fg = await import("fast-glob");
@@ -11380,7 +11457,7 @@ async function safetyCheck(action, filePath, command) {
11380
11457
  name: "Tests Check",
11381
11458
  passed: true,
11382
11459
  // Not a blocker, just informative
11383
- detail: `\u{1F9EA} No test file found for "${path27.basename(filePath)}" \u2014 consider adding tests`
11460
+ detail: `\u{1F9EA} No test file found for "${path26.basename(filePath)}" \u2014 consider adding tests`
11384
11461
  });
11385
11462
  } else {
11386
11463
  checks.push({
@@ -11469,8 +11546,8 @@ init_sessionMemory();
11469
11546
  // src/guards/antiPatternDetector.ts
11470
11547
  init_pathValidator();
11471
11548
  init_sessionMemory();
11472
- import fs18 from "fs";
11473
- import path18 from "path";
11549
+ import fs17 from "fs";
11550
+ import path17 from "path";
11474
11551
  import { execSync as execSync5 } from "child_process";
11475
11552
  var SCRIPT_PATCH_PATTERNS = [
11476
11553
  "writeFileSync",
@@ -11497,20 +11574,20 @@ function findPatchScripts(projectRoot) {
11497
11574
  const warnings = [];
11498
11575
  const recentFiles = scanRecentFiles(projectRoot);
11499
11576
  for (const file of recentFiles) {
11500
- const ext = path18.extname(file).toLowerCase();
11577
+ const ext = path17.extname(file).toLowerCase();
11501
11578
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
11502
- const relativePath = path18.relative(projectRoot, file);
11579
+ const relativePath = path17.relative(projectRoot, file);
11503
11580
  if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".") || relativePath.startsWith(".kuma/scratch")) {
11504
11581
  continue;
11505
11582
  }
11506
11583
  try {
11507
- const content = fs18.readFileSync(file, "utf-8").toLowerCase();
11584
+ const content = fs17.readFileSync(file, "utf-8").toLowerCase();
11508
11585
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
11509
11586
  if (matchedPattern) {
11510
11587
  warnings.push({
11511
11588
  severity: "high",
11512
11589
  pattern: "script-patching",
11513
- message: `Created script file that modifies other files: ${path18.basename(file)}`,
11590
+ message: `Created script file that modifies other files: ${path17.basename(file)}`,
11514
11591
  suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
11515
11592
  evidence: `File: ${relativePath} contains '${matchedPattern}'`,
11516
11593
  filePath: relativePath
@@ -11544,15 +11621,15 @@ function detectBashGrepUsage() {
11544
11621
  function scanRecentFiles(projectRoot) {
11545
11622
  const recent = [];
11546
11623
  try {
11547
- const entries = fs18.readdirSync(projectRoot, { withFileTypes: true });
11624
+ const entries = fs17.readdirSync(projectRoot, { withFileTypes: true });
11548
11625
  const now = Date.now();
11549
11626
  const recentThreshold = 30 * 60 * 1e3;
11550
11627
  for (const entry of entries) {
11551
11628
  if (!entry.isFile()) continue;
11552
11629
  try {
11553
- const stat = fs18.statSync(path18.join(projectRoot, entry.name));
11630
+ const stat = fs17.statSync(path17.join(projectRoot, entry.name));
11554
11631
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
11555
- recent.push(path18.join(projectRoot, entry.name));
11632
+ recent.push(path17.join(projectRoot, entry.name));
11556
11633
  }
11557
11634
  } catch {
11558
11635
  continue;
@@ -11576,14 +11653,14 @@ function detectGitPatchScripts(projectRoot) {
11576
11653
  const prefix = line.substring(0, 2);
11577
11654
  if (prefix !== "??" && prefix !== "A ") continue;
11578
11655
  const file = line.substring(3).trim();
11579
- const ext = path18.extname(file).toLowerCase();
11656
+ const ext = path17.extname(file).toLowerCase();
11580
11657
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
11581
11658
  const isRootLevel = !file.includes("/");
11582
11659
  const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
11583
11660
  if (!isRootLevel && !isScriptsDir) continue;
11584
- const fullPath = path18.join(projectRoot, file);
11585
- if (fs18.existsSync(fullPath)) {
11586
- const content = fs18.readFileSync(fullPath, "utf-8").toLowerCase();
11661
+ const fullPath = path17.join(projectRoot, file);
11662
+ if (fs17.existsSync(fullPath)) {
11663
+ const content = fs17.readFileSync(fullPath, "utf-8").toLowerCase();
11587
11664
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
11588
11665
  if (matchedPattern) {
11589
11666
  warnings.push({
@@ -11606,7 +11683,7 @@ function detectGitPatchScripts(projectRoot) {
11606
11683
  if (deletedStdout) {
11607
11684
  const deletedFiles = deletedStdout.split("\n").filter(Boolean);
11608
11685
  for (const file of deletedFiles) {
11609
- const ext = path18.extname(file).toLowerCase();
11686
+ const ext = path17.extname(file).toLowerCase();
11610
11687
  if (SCRIPT_EXTENSIONS.includes(ext)) {
11611
11688
  warnings.push({
11612
11689
  severity: "high",
@@ -11668,21 +11745,21 @@ function detectAllAntiPatterns() {
11668
11745
  // src/engine/contextSnapshot.ts
11669
11746
  init_sessionMemory();
11670
11747
  init_pathValidator();
11671
- import fs19 from "fs";
11672
- import path19 from "path";
11748
+ import fs18 from "fs";
11749
+ import path18 from "path";
11673
11750
  import { execSync as execSync6 } from "child_process";
11674
11751
  var SNAPSHOT_DIR = "context-snapshots";
11675
11752
  function snapshotDir() {
11676
- return path19.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
11753
+ return path18.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
11677
11754
  }
11678
11755
  function ensureSnapshotDir() {
11679
11756
  const dir = snapshotDir();
11680
- if (!fs19.existsSync(dir)) {
11681
- fs19.mkdirSync(dir, { recursive: true });
11757
+ if (!fs18.existsSync(dir)) {
11758
+ fs18.mkdirSync(dir, { recursive: true });
11682
11759
  }
11683
11760
  }
11684
11761
  function snapshotFilePath(timestamp) {
11685
- return path19.join(snapshotDir(), `${timestamp}.json`);
11762
+ return path18.join(snapshotDir(), `${timestamp}.json`);
11686
11763
  }
11687
11764
  function saveSnapshot(goal) {
11688
11765
  try {
@@ -11703,7 +11780,7 @@ function saveSnapshot(goal) {
11703
11780
  hasConventions: !!summary.hasConventions
11704
11781
  };
11705
11782
  try {
11706
- fs19.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
11783
+ fs18.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
11707
11784
  } catch {
11708
11785
  }
11709
11786
  sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
@@ -11802,6 +11879,55 @@ async function handleKumaGuard(params) {
11802
11879
  });
11803
11880
  }
11804
11881
  }
11882
+ const recordingSummary = sessionMemory.getRecordingSummary();
11883
+ if (check === "all" || check === "drift") {
11884
+ const readCalls = stats.toolCalls.filter(
11885
+ (c) => c.toolName === "read" || c.toolName === "grep" || c.toolName === "glob" || c.toolName === "smart_file_picker"
11886
+ ).length;
11887
+ if (stats.toolCallCount >= 10 && !recordingSummary.hasAnyRecordings) {
11888
+ warnings.push({
11889
+ severity: "high",
11890
+ pattern: "no-recordings-critical",
11891
+ message: `\u{1F6AB} BLOCKING: ${stats.toolCallCount} tool calls with 0 knowledge recordings. You are wasting future sessions by not recording.`,
11892
+ suggestion: "STOP. Record now:\n- kuma_memory({ action: 'research_save', scope: '<file>' }) after reading files\n- kuma_memory({ action: 'gotcha', scope: '<file>', content: '<bug>' }) when finding bugs\n- kuma_memory({ action: 'arch_flow', content: 'domain: <X> | hops: <file1> \u2192 <file2>' }) when tracing flows"
11893
+ });
11894
+ } else if (stats.toolCallCount >= 5 && !recordingSummary.hasAnyRecordings) {
11895
+ warnings.push({
11896
+ severity: "medium",
11897
+ pattern: "no-recordings",
11898
+ message: `${stats.toolCallCount} tool calls made but 0 knowledge recordings. Agent is not building persistent knowledge.`,
11899
+ suggestion: "Record findings after reading files. arch_flow + gotcha are exponential value."
11900
+ });
11901
+ } else if (readCalls >= 3 && recordingSummary.researchSaves === 0) {
11902
+ warnings.push({
11903
+ severity: "low",
11904
+ pattern: "read-without-record",
11905
+ message: `${readCalls} file reads but 0 research_save calls. Reading without recording = wasted context.`,
11906
+ suggestion: "After reading unfamiliar files: kuma_memory({ action: 'research_save', scope: '<file>' })"
11907
+ });
11908
+ }
11909
+ if (recordingSummary.total > 0) {
11910
+ }
11911
+ const errorCalls = stats.toolCalls.filter(
11912
+ (c) => c.toolName === "execute_safe_command" && JSON.stringify(c.params).includes("error")
11913
+ ).length;
11914
+ if (errorCalls >= 2 && recordingSummary.gotchas === 0) {
11915
+ warnings.push({
11916
+ severity: "medium",
11917
+ pattern: "error-without-gotcha",
11918
+ message: `${errorCalls} error encounters but 0 gotchas recorded. Errors = future gotchas.`,
11919
+ suggestion: "Record gotchas for bugs you encounter:\nkuma_memory({ action: 'gotcha', scope: '<file>', content: '<what went wrong>', status: 'high' })"
11920
+ });
11921
+ }
11922
+ if (readCalls >= 4 && recordingSummary.archFlows === 0) {
11923
+ warnings.push({
11924
+ severity: "low",
11925
+ pattern: "trace-without-arch-flow",
11926
+ message: `${readCalls} file reads (possible flow tracing) but 0 arch_flows recorded. Flow knowledge dissipates fast.`,
11927
+ suggestion: "After tracing a flow, record it:\nkuma_memory({ action: 'arch_flow', content: 'domain: <Name> | hops: <file1> \u2192 <file2> \u2192 <file3>' })"
11928
+ });
11929
+ }
11930
+ }
11805
11931
  if (check === "context") {
11806
11932
  const snapshot = saveSnapshot(stats.goal);
11807
11933
  if (!snapshot) {
@@ -11843,7 +11969,24 @@ async function handleKumaGuard(params) {
11843
11969
  hasRunTests: stats.hasRunTests
11844
11970
  }
11845
11971
  };
11846
- return JSON.stringify(report, null, 2);
11972
+ const metricsSummary = sessionMemory.getMetricsSummary();
11973
+ const reportWithRecordings = {
11974
+ ...report,
11975
+ recordings: {
11976
+ archFlows: recordingSummary.archFlows,
11977
+ gotchas: recordingSummary.gotchas,
11978
+ decisions: recordingSummary.decisions,
11979
+ researchSaves: recordingSummary.researchSaves,
11980
+ total: recordingSummary.total
11981
+ },
11982
+ metrics: {
11983
+ filesRead: metricsSummary.filesRead,
11984
+ filesEdited: metricsSummary.filesEdited,
11985
+ researchTimeSaved: metricsSummary.researchTimeSavedFormatted,
11986
+ sessionDuration: metricsSummary.sessionDuration
11987
+ }
11988
+ };
11989
+ return JSON.stringify(reportWithRecordings, null, 2);
11847
11990
  }
11848
11991
 
11849
11992
  // src/tools/kumaSafetyTool.ts
@@ -11948,12 +12091,12 @@ async function handleSecurity(params) {
11948
12091
  sessionMemory.recordToolCall("kuma_safety_security", {});
11949
12092
  if (params.filePath) {
11950
12093
  try {
11951
- const fs27 = await import("fs");
11952
- const path27 = await import("path");
12094
+ const fs26 = await import("fs");
12095
+ const path26 = await import("path");
11953
12096
  const root = process.cwd();
11954
- const fullPath = path27.resolve(root, params.filePath);
11955
- if (fs27.existsSync(fullPath) && fs27.statSync(fullPath).isFile()) {
11956
- const content = fs27.readFileSync(fullPath, "utf-8");
12097
+ const fullPath = path26.resolve(root, params.filePath);
12098
+ if (fs26.existsSync(fullPath) && fs26.statSync(fullPath).isFile()) {
12099
+ const content = fs26.readFileSync(fullPath, "utf-8");
11957
12100
  const lines = content.split("\n");
11958
12101
  const findings = [];
11959
12102
  for (let i = 0; i < lines.length; i++) {
@@ -12007,23 +12150,23 @@ async function handleGitignore(_params) {
12007
12150
  }
12008
12151
  async function handleClean(_params) {
12009
12152
  sessionMemory.recordToolCall("kuma_safety_clean", {});
12010
- const fs27 = await import("fs");
12011
- const path27 = await import("path");
12153
+ const fs26 = await import("fs");
12154
+ const path26 = await import("path");
12012
12155
  const root = process.cwd();
12013
- const scratchDir = path27.resolve(root, ".kuma", "scratch");
12156
+ const scratchDir = path26.resolve(root, ".kuma", "scratch");
12014
12157
  let removed = 0;
12015
- if (fs27.existsSync(scratchDir)) {
12158
+ if (fs26.existsSync(scratchDir)) {
12016
12159
  try {
12017
- const entries = fs27.readdirSync(scratchDir);
12160
+ const entries = fs26.readdirSync(scratchDir);
12018
12161
  for (const entry of entries) {
12019
- const fullPath = path27.join(scratchDir, entry);
12162
+ const fullPath = path26.join(scratchDir, entry);
12020
12163
  try {
12021
- const stat = fs27.statSync(fullPath);
12164
+ const stat = fs26.statSync(fullPath);
12022
12165
  if (stat.isFile()) {
12023
- fs27.unlinkSync(fullPath);
12166
+ fs26.unlinkSync(fullPath);
12024
12167
  removed++;
12025
12168
  } else if (stat.isDirectory()) {
12026
- fs27.rmSync(fullPath, { recursive: true, force: true });
12169
+ fs26.rmSync(fullPath, { recursive: true, force: true });
12027
12170
  removed++;
12028
12171
  }
12029
12172
  } catch {
@@ -12125,13 +12268,30 @@ async function handleVerify(params) {
12125
12268
  return `\u23F3 **Handler rate limit** \u2014 verify was just called ${Math.floor((now - _lastVerifyCall) / 1e3)}s ago. Please wait ${remaining}s before calling verify again.`;
12126
12269
  }
12127
12270
  _lastVerifyCall = now;
12271
+ const recordingSummary = sessionMemory.getRecordingSummary();
12272
+ const stats = sessionMemory.getSummary();
12273
+ const toolCallCount = stats.toolCallCount || 0;
12274
+ let recordingWarning = "";
12275
+ if (toolCallCount >= 5 && !recordingSummary.hasAnyRecordings) {
12276
+ recordingWarning = `
12277
+
12278
+ \u26A0\uFE0F **RECORDING MISSING:** You made ${toolCallCount} tool calls with 0 recordings. Before switching tasks, record what you learned:
12279
+ - kuma_memory({ action: 'research_save', scope: '<file>' })
12280
+ - kuma_memory({ action: 'gotcha', ... }) if you found bugs
12281
+ - kuma_memory({ action: 'arch_flow', ... }) if you traced a flow`;
12282
+ } else if (recordingSummary.total > 0) {
12283
+ recordingWarning = `
12284
+
12285
+ \u2705 **Recordings:** ${recordingSummary.total} total (${recordingSummary.archFlows} arch_flow, ${recordingSummary.gotchas} gotcha, ${recordingSummary.decisions} decision, ${recordingSummary.researchSaves} research_save)`;
12286
+ }
12128
12287
  sessionMemory.recordToolCall("kuma_safety_verify", { scope: params.scope || params.filePath });
12129
12288
  const { runAutoVerification: runAutoVerification2 } = await Promise.resolve().then(() => (init_kumaVerifier(), kumaVerifier_exports));
12130
- return await runAutoVerification2({
12289
+ const verifyResult = await runAutoVerification2({
12131
12290
  scope: params.scope || params.filePath,
12132
12291
  force: params.force,
12133
12292
  timeoutMs: 3e4
12134
12293
  });
12294
+ return verifyResult + recordingWarning;
12135
12295
  }
12136
12296
 
12137
12297
  // src/manifest.ts
@@ -12169,8 +12329,8 @@ function registerAllTools(server) {
12169
12329
  "kuma_memory",
12170
12330
  "**Call after research/editing** (including research-only). Record what matters, skip what doesn't.\n\n\u{1F534} MUST RECORD \u2014 High Impact (saves agent time next session):\n\u2022 STEP 5: `research_save` \u2014 after exploring area (creates search cache).\n\u2022 STEP 6: `gotcha` \u2014 IMMEDIATELY when you discover bugs/quirks. No re-research.\n\u2022 STEP 7: `arch_flow` \u2014 IMMEDIATELY after EACH flow hop. Saves reading 5-10 files.\n\u2022 STEP 8: `decision` \u2014 IMMEDIATELY when choosing between options. Preserves rationale.\n\n\u{1F7E2} SKIP using MCP (agent native tools are faster):\n\u2022 Function/class nodes \u2192 grep\n\u2022 Component/route nodes \u2192 glob or check directly\n\u2022 Import edges \u2192 read imports directly\n\u2022 Visual graph \u2192 for humans, not AI agents\n\nOther actions: session, heal, search, changes, benchmark, layers.",
12171
12331
  {
12172
- action: z.enum(["decision", "mine", "research_save", "session", "heal", "search", "changes", "todo", "context", "benchmark", "decision_log", "domain_rules", "arch_flow", "gotcha", "layers", "federated", "gen_test", "trajectory", "skills", "add_node", "delete_node", "clear"]).describe("Memory action: decision=ADR, mine=mine git log & comments, research_save=save (creates file + graph node), session=summary, heal=repair, search=search, changes=log, todo=manage todos, context=inject notes, benchmark=capture/diff, decision_log=manage decisions, domain_rules=Layer 1 business rules (Issue #17), arch_flow=Layer 2 architecture flow (Issue #17), gotcha=Layer 3 known gotchas (Issue #17/#21), layers=all 3 layers summary, federated=resolve federated kuma:// URI (Issue #27), gen_test=generate test from trajectory (Issue #28), trajectory=list trajectories, skills=list distilled skills, add_node=manually create function/class/component structural nodes, delete_node=delete a specific node/gotcha/todo/decision by ID or scope+target, clear=wipe all nodes/edges/gotchas/trajectories"),
12173
- scope: z.string().optional().describe("Scope for research_save/search/todo/context/mine/federated"),
12332
+ action: z.enum(["decision", "mine", "research_save", "session", "heal", "search", "changes", "todo", "context", "benchmark", "decision_log", "domain_rules", "arch_flow", "gotcha", "layers", "add_node", "delete_node", "clear", "feature"]).describe("Memory action: decision=ADR, mine=mine git log & comments, research_save=save (creates file + graph node), session=summary, heal=repair, search=search (now with task retrieval + impact analysis), changes=log, todo=manage todos, context=inject notes, benchmark=capture/diff, decision_log=manage decisions, domain_rules=Layer 1 business rules, arch_flow=Layer 2 architecture flow (now auto-creates affects edges), gotcha=Layer 3 known gotchas, layers=all 3 layers summary, add_node=manually create function/class/component structural nodes, delete_node=delete a specific node/gotcha/todo/decision by ID or scope+target, clear=wipe all nodes/edges/gotchas, feature=record high-level feature with owns edges to files"),
12333
+ scope: z.string().optional().describe("Scope for research_save/search/todo/context/mine"),
12174
12334
  query: z.string().optional().describe("Search query for search action"),
12175
12335
  content: z.string().optional().describe("Content/notes for research_save / context"),
12176
12336
  record: z.string().optional().describe("JSON record string for research_save"),
@@ -12186,7 +12346,7 @@ function registerAllTools(server) {
12186
12346
  healAction: z.enum(["check", "heal"]).optional().describe("Heal sub-action"),
12187
12347
  topic: z.string().optional().describe("Memory topic for session"),
12188
12348
  limit: z.number().min(1).max(100).optional().describe("Result limit"),
12189
- target: z.string().optional().describe("File path for changes / decision_log ID / trajectory ID"),
12349
+ target: z.string().optional().describe("File path for changes / decision_log ID"),
12190
12350
  since: z.number().optional().describe("Timestamp filter for changes"),
12191
12351
  compact: z.boolean().optional().default(false).describe("Compact output mode"),
12192
12352
  // Todo params
@@ -12223,8 +12383,7 @@ function registerAllTools(server) {
12223
12383
  topic: params.topic,
12224
12384
  limit: params.limit,
12225
12385
  target: params.target,
12226
- since: params.since,
12227
- uri: params.uri
12386
+ since: params.since
12228
12387
  });
12229
12388
  return { content: [{ type: "text", text }] };
12230
12389
  } catch (err) {
@@ -12358,14 +12517,14 @@ async function main() {
12358
12517
  } catch {
12359
12518
  }
12360
12519
  try {
12361
- const path27 = await import("path");
12362
- const fs27 = await import("fs");
12363
- const lockDir = path27.resolve(process.cwd(), ".kuma/verifier.lock");
12364
- if (fs27.existsSync(lockDir)) {
12365
- const pidFile = path27.join(lockDir, "pid");
12366
- if (fs27.existsSync(pidFile)) {
12520
+ const path26 = await import("path");
12521
+ const fs26 = await import("fs");
12522
+ const lockDir = path26.resolve(process.cwd(), ".kuma/verifier.lock");
12523
+ if (fs26.existsSync(lockDir)) {
12524
+ const pidFile = path26.join(lockDir, "pid");
12525
+ if (fs26.existsSync(pidFile)) {
12367
12526
  try {
12368
- const pid = parseInt(fs27.readFileSync(pidFile, "utf-8"), 10);
12527
+ const pid = parseInt(fs26.readFileSync(pidFile, "utf-8"), 10);
12369
12528
  if (pid && pid !== process.pid) {
12370
12529
  try {
12371
12530
  process.kill(-pid, "SIGKILL");
@@ -12381,7 +12540,7 @@ async function main() {
12381
12540
  } catch {
12382
12541
  }
12383
12542
  }
12384
- fs27.rmSync(lockDir, { recursive: true, force: true });
12543
+ fs26.rmSync(lockDir, { recursive: true, force: true });
12385
12544
  }
12386
12545
  if (killedCount === 0) {
12387
12546
  console.error("\u2705 No running verifications found to kill.");
@@ -12521,23 +12680,23 @@ async function main() {
12521
12680
  });
12522
12681
  const output = formatInitResults(results);
12523
12682
  console.log(output);
12524
- const fs27 = await import("fs");
12525
- const path27 = await import("path");
12526
- const matchaSkills = path27.resolve(process.cwd(), "skills/matcha/SKILL.md");
12527
- const matchaAgents = path27.resolve(
12683
+ const fs26 = await import("fs");
12684
+ const path26 = await import("path");
12685
+ const matchaSkills = path26.resolve(process.cwd(), "skills/matcha/SKILL.md");
12686
+ const matchaAgents = path26.resolve(
12528
12687
  process.cwd(),
12529
12688
  ".agents/skills/matcha/SKILL.md"
12530
12689
  );
12531
- const matchaRootSkills = path27.resolve(
12690
+ const matchaRootSkills = path26.resolve(
12532
12691
  process.cwd(),
12533
12692
  "skills/matcha/SKILL.md"
12534
12693
  );
12535
- const matchaAgentsMd = path27.resolve(process.cwd(), "AGENTS.md");
12536
- const matchaWindsurfRules = path27.resolve(
12694
+ const matchaAgentsMd = path26.resolve(process.cwd(), "AGENTS.md");
12695
+ const matchaWindsurfRules = path26.resolve(
12537
12696
  process.cwd(),
12538
12697
  ".windsurf"
12539
12698
  );
12540
- if (fs27.existsSync(matchaSkills) || fs27.existsSync(matchaAgents) || fs27.existsSync(matchaRootSkills) || fs27.existsSync(matchaAgentsMd) || fs27.existsSync(matchaWindsurfRules)) {
12699
+ if (fs26.existsSync(matchaSkills) || fs26.existsSync(matchaAgents) || fs26.existsSync(matchaRootSkills) || fs26.existsSync(matchaAgentsMd) || fs26.existsSync(matchaWindsurfRules)) {
12541
12700
  console.error(
12542
12701
  "\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
12543
12702
  );
@@ -12551,13 +12710,13 @@ async function main() {
12551
12710
  (async () => {
12552
12711
  try {
12553
12712
  const { generateInitMdContent: generateInitMdContent2 } = await Promise.resolve().then(() => (init_init(), init_exports));
12554
- const fs27 = await import("fs");
12555
- const path27 = await import("path");
12556
- const initMdPath = path27.resolve(process.cwd(), ".kuma/init.md");
12557
- if (!fs27.existsSync(initMdPath)) {
12558
- const kumaDir = path27.dirname(initMdPath);
12559
- if (!fs27.existsSync(kumaDir)) fs27.mkdirSync(kumaDir, { recursive: true });
12560
- fs27.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
12713
+ const fs26 = await import("fs");
12714
+ const path26 = await import("path");
12715
+ const initMdPath = path26.resolve(process.cwd(), ".kuma/init.md");
12716
+ if (!fs26.existsSync(initMdPath)) {
12717
+ const kumaDir = path26.dirname(initMdPath);
12718
+ if (!fs26.existsSync(kumaDir)) fs26.mkdirSync(kumaDir, { recursive: true });
12719
+ fs26.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
12561
12720
  console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
12562
12721
  }
12563
12722
  } catch (err) {
@@ -12568,8 +12727,8 @@ async function main() {
12568
12727
  try {
12569
12728
  const { detectAgent: detectAgent2, getSkillPath: getSkillPath2, getAgentLabel: getAgentLabel2 } = await Promise.resolve().then(() => (init_agentDetector(), agentDetector_exports));
12570
12729
  const { generateSkill: generateSkill2, getSecondaryFiles: getSecondaryFiles2 } = await Promise.resolve().then(() => (init_skillGenerator(), skillGenerator_exports));
12571
- const fs27 = await import("fs");
12572
- const path27 = await import("path");
12730
+ const fs26 = await import("fs");
12731
+ const path26 = await import("path");
12573
12732
  const detection = detectAgent2();
12574
12733
  if (!detection.primary) {
12575
12734
  console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
@@ -12577,22 +12736,22 @@ async function main() {
12577
12736
  }
12578
12737
  const agentType = detection.primary;
12579
12738
  const skillPath = getSkillPath2(agentType);
12580
- const fullPath = path27.resolve(process.cwd(), skillPath);
12581
- if (fs27.existsSync(fullPath)) {
12739
+ const fullPath = path26.resolve(process.cwd(), skillPath);
12740
+ if (fs26.existsSync(fullPath)) {
12582
12741
  console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel2(agentType)} \u2014 skipping`);
12583
12742
  return;
12584
12743
  }
12585
- const dir = path27.dirname(fullPath);
12586
- if (!fs27.existsSync(dir)) fs27.mkdirSync(dir, { recursive: true });
12587
- fs27.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
12744
+ const dir = path26.dirname(fullPath);
12745
+ if (!fs26.existsSync(dir)) fs26.mkdirSync(dir, { recursive: true });
12746
+ fs26.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
12588
12747
  console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel2(agentType)}`);
12589
12748
  const secondaryFiles = getSecondaryFiles2(agentType);
12590
12749
  for (const sf of secondaryFiles) {
12591
- const sfPath = path27.resolve(process.cwd(), sf.path);
12592
- const sfDir = path27.dirname(sfPath);
12593
- if (!fs27.existsSync(sfPath)) {
12594
- if (!fs27.existsSync(sfDir)) fs27.mkdirSync(sfDir, { recursive: true });
12595
- fs27.writeFileSync(sfPath, sf.content, "utf-8");
12750
+ const sfPath = path26.resolve(process.cwd(), sf.path);
12751
+ const sfDir = path26.dirname(sfPath);
12752
+ if (!fs26.existsSync(sfPath)) {
12753
+ if (!fs26.existsSync(sfDir)) fs26.mkdirSync(sfDir, { recursive: true });
12754
+ fs26.writeFileSync(sfPath, sf.content, "utf-8");
12596
12755
  console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
12597
12756
  }
12598
12757
  }
@@ -12617,11 +12776,11 @@ async function main() {
12617
12776
  console.error(`[${SERVER_NAME}] \u26A0\uFE0F Graph auto-population: ${err}`);
12618
12777
  }
12619
12778
  try {
12620
- const fs27 = await import("fs");
12621
- const path27 = await import("path");
12622
- const scratchDir = path27.resolve(process.cwd(), ".kuma", "scratch");
12623
- if (!fs27.existsSync(scratchDir)) {
12624
- fs27.mkdirSync(scratchDir, { recursive: true });
12779
+ const fs26 = await import("fs");
12780
+ const path26 = await import("path");
12781
+ const scratchDir = path26.resolve(process.cwd(), ".kuma", "scratch");
12782
+ if (!fs26.existsSync(scratchDir)) {
12783
+ fs26.mkdirSync(scratchDir, { recursive: true });
12625
12784
  console.error(`[${SERVER_NAME}] \u2705 Created .kuma/scratch/ for temporary debug artifacts`);
12626
12785
  }
12627
12786
  } catch (err) {
@@ -12639,11 +12798,11 @@ async function main() {
12639
12798
  console.error(`[${SERVER_NAME}] \u26A0\uFE0F Session DB record: ${err}`);
12640
12799
  }
12641
12800
  try {
12642
- const fs27 = await import("fs");
12643
- const path27 = await import("path");
12644
- const hooksDir = path27.resolve(process.cwd(), ".kuma", "hooks");
12645
- if (!fs27.existsSync(hooksDir)) {
12646
- fs27.mkdirSync(hooksDir, { recursive: true });
12801
+ const fs26 = await import("fs");
12802
+ const path26 = await import("path");
12803
+ const hooksDir = path26.resolve(process.cwd(), ".kuma", "hooks");
12804
+ if (!fs26.existsSync(hooksDir)) {
12805
+ fs26.mkdirSync(hooksDir, { recursive: true });
12647
12806
  const hookContent = `#!/bin/bash
12648
12807
  # Kuma auto-capture hook (post-commit)
12649
12808
  # Auto-updates knowledge graph after git commits
@@ -12651,13 +12810,13 @@ if command -v npx &> /dev/null; then
12651
12810
  npx -y @plumpslabs/kuma --hook post-commit 2>/dev/null || true
12652
12811
  fi
12653
12812
  `;
12654
- const gitHooksDir = path27.resolve(process.cwd(), ".git", "hooks");
12655
- if (fs27.existsSync(gitHooksDir)) {
12656
- const postCommitPath = path27.join(gitHooksDir, "post-commit");
12657
- if (!fs27.existsSync(postCommitPath)) {
12658
- fs27.writeFileSync(postCommitPath, hookContent, "utf-8");
12813
+ const gitHooksDir = path26.resolve(process.cwd(), ".git", "hooks");
12814
+ if (fs26.existsSync(gitHooksDir)) {
12815
+ const postCommitPath = path26.join(gitHooksDir, "post-commit");
12816
+ if (!fs26.existsSync(postCommitPath)) {
12817
+ fs26.writeFileSync(postCommitPath, hookContent, "utf-8");
12659
12818
  try {
12660
- fs27.chmodSync(postCommitPath, 493);
12819
+ fs26.chmodSync(postCommitPath, 493);
12661
12820
  } catch {
12662
12821
  }
12663
12822
  console.error(`[${SERVER_NAME}] \u2705 Created .git/hooks/post-commit for auto-capture`);