@plumpslabs/kuma 2.3.4 → 2.3.6

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.
@@ -320,7 +320,7 @@ No unresolved issues.`;
320
320
  */
321
321
  async autoTrackToDb(toolName, params, timestamp) {
322
322
  try {
323
- const { getDb, saveDb } = await import("./kumaDb-I4AJ5A7H.js");
323
+ const { getDb, saveDb } = await import("./kumaDb-DJUDLYBJ.js");
324
324
  const db = await getDb();
325
325
  db.run(
326
326
  `INSERT INTO tool_calls (tool_name, params, success, duration_ms, created_at) VALUES (?, ?, 1, 0, ?)`,
@@ -466,14 +466,14 @@ No unresolved issues.`;
466
466
  const memoriesDir = this.memoriesDir();
467
467
  if (fs.existsSync(memoriesDir)) {
468
468
  try {
469
- const files = fs.readdirSync(memoriesDir);
469
+ const files = fs.readdirSync(memoriesDir).filter((f) => f.endsWith(".md")).slice(0, 3);
470
470
  for (const file of files) {
471
- if (!file.endsWith(".md")) continue;
472
471
  const filePath = path.join(memoriesDir, file);
473
472
  try {
474
473
  const content = fs.readFileSync(filePath, "utf-8");
475
474
  const lines = content.split("\n");
476
- for (let i = 0; i < lines.length; i++) {
475
+ const maxLinesPerFile = 500;
476
+ for (let i = 0; i < Math.min(lines.length, maxLinesPerFile); i++) {
477
477
  if (lines[i].toLowerCase().includes(q)) {
478
478
  const topicName = file.replace(/\.md$/, "");
479
479
  results.push({
@@ -571,10 +571,18 @@ No unresolved issues.`;
571
571
  if (recentCalls.length < 6) return { isLooping: false };
572
572
  const toolCounts = /* @__PURE__ */ new Map();
573
573
  for (const call of recentCalls) {
574
- toolCounts.set(call.toolName, (toolCounts.get(call.toolName) ?? 0) + 1);
574
+ const existing = toolCounts.get(call.toolName);
575
+ const action = call.params.action || "";
576
+ if (existing) {
577
+ existing.count++;
578
+ existing.actions.add(action);
579
+ } else {
580
+ toolCounts.set(call.toolName, { count: 1, actions: /* @__PURE__ */ new Set([action]) });
581
+ }
575
582
  }
576
- for (const [toolName, count] of toolCounts) {
583
+ for (const [toolName, { count, actions }] of toolCounts) {
577
584
  if (count >= 4) {
585
+ if (actions.size >= 3) continue;
578
586
  return {
579
587
  isLooping: true,
580
588
  toolName,
@@ -597,7 +605,7 @@ No unresolved issues.`;
597
605
  this.addModifiedFile(filePath);
598
606
  }
599
607
  try {
600
- const { recordChange } = await import("./kumaDb-I4AJ5A7H.js");
608
+ const { recordChange } = await import("./kumaDb-DJUDLYBJ.js");
601
609
  const gitHash = this.getGitHead();
602
610
  await recordChange({ filePath, changeType, symbol, gitCommitHash: gitHash || void 0 });
603
611
  } catch {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sessionMemory
3
- } from "./chunk-77G3APNG.js";
3
+ } from "./chunk-BI7KD3SG.js";
4
4
  import {
5
5
  getProjectRoot
6
6
  } from "./chunk-E2KFPEBT.js";
@@ -36,18 +36,29 @@ async function initDb() {
36
36
  dbInstance = db;
37
37
  return db;
38
38
  }
39
+ var _saveTimeout = null;
40
+ var _pendingDb = null;
41
+ var SAVE_DEBOUNCE_MS = 100;
39
42
  function saveDb(db) {
40
43
  const d = db ?? dbInstance;
41
44
  if (!d) return;
42
- try {
43
- const kumaDir = getKumaDir();
44
- const dbPath = path.join(kumaDir, DB_FILENAME);
45
- const data = d.export();
46
- const buffer = Buffer.from(data);
47
- fs.writeFileSync(dbPath, buffer);
48
- } catch (err) {
49
- console.error(`[KumaDB] Failed to save database: ${err}`);
50
- }
45
+ _pendingDb = d;
46
+ if (_saveTimeout) return;
47
+ _saveTimeout = setTimeout(() => {
48
+ _saveTimeout = null;
49
+ const targetDb = _pendingDb;
50
+ _pendingDb = null;
51
+ if (!targetDb) return;
52
+ try {
53
+ const kumaDir = getKumaDir();
54
+ const dbPath = path.join(kumaDir, DB_FILENAME);
55
+ const data = targetDb.export();
56
+ const buffer = Buffer.from(data);
57
+ fs.writeFileSync(dbPath, buffer);
58
+ } catch (err) {
59
+ console.error(`[KumaDB] Failed to save database: ${err}`);
60
+ }
61
+ }, SAVE_DEBOUNCE_MS);
51
62
  }
52
63
  function createSchema(db) {
53
64
  db.run(`CREATE TABLE IF NOT EXISTS nodes (
@@ -835,21 +846,6 @@ async function runDoctor() {
835
846
  }
836
847
  } catch {
837
848
  }
838
- try {
839
- const { isVerificationRunning, msSinceLastVerification, getRecentCallCount } = await import("./kumaVerifier-SSS4ZTJY.js");
840
- const running = isVerificationRunning();
841
- const sinceLast = msSinceLastVerification();
842
- const callCount = getRecentCallCount();
843
- checks.push("");
844
- checks.push("**Verifier Status:**");
845
- checks.push(` ${running ? "\u{1F504}" : "\u{1F4A4}"} Currently ${running ? "running" : "idle"}`);
846
- checks.push(` \u23F1\uFE0F Last run: ${sinceLast < 0 ? "never" : `${Math.floor(sinceLast / 1e3)}s ago`}`);
847
- checks.push(` \u{1F4CA} Calls in 5min: ${callCount} (threshold: 3)`);
848
- if (callCount >= 3) {
849
- checks.push(` \u{1F534} **Runaway protection active** \u2014 verifier is blocking further calls`);
850
- }
851
- } catch {
852
- }
853
849
  return checks.join("\n");
854
850
  } catch (err) {
855
851
  return `\u274C Doctor failed: ${err}`;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getDb,
3
3
  saveDb
4
- } from "./chunk-NKNXJG2E.js";
4
+ } from "./chunk-GDNAWLHF.js";
5
5
  import {
6
6
  getProjectRoot
7
7
  } from "./chunk-E2KFPEBT.js";
@@ -458,7 +458,7 @@ The two nodes may not be connected in the knowledge graph yet. Use more tools to
458
458
  }
459
459
  async function buildFromSessionMemory() {
460
460
  try {
461
- const { sessionMemory } = await import("./sessionMemory-SKKYUGB6.js");
461
+ const { sessionMemory } = await import("./sessionMemory-HBBXUSIP.js");
462
462
  const toolCalls = sessionMemory.getToolCallHistory(50);
463
463
  let edgeCount = 0;
464
464
  for (const call of toolCalls) {
@@ -586,8 +586,8 @@ async function codebaseSearchFallback(query, limit = 20) {
586
586
  return lines.join("\n");
587
587
  } catch {
588
588
  try {
589
- let walk2 = function(dir) {
590
- if (results.length >= limit) return;
589
+ let walk2 = function(dir, depth = 0) {
590
+ if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
591
591
  let entries = [];
592
592
  try {
593
593
  entries = fs2.readdirSync(dir);
@@ -600,7 +600,7 @@ async function codebaseSearchFallback(query, limit = 20) {
600
600
  try {
601
601
  const stat = fs2.statSync(full);
602
602
  if (stat.isDirectory()) {
603
- walk2(full);
603
+ walk2(full, depth + 1);
604
604
  } else if (entry.toLowerCase().includes(query.toLowerCase())) {
605
605
  results.push(path2.relative(root, full));
606
606
  }
@@ -613,7 +613,8 @@ async function codebaseSearchFallback(query, limit = 20) {
613
613
  const path2 = await import("path");
614
614
  const root = process.cwd();
615
615
  const results = [];
616
- walk2(root);
616
+ const MAX_WALK_DEPTH = 8;
617
+ walk2(root, 0);
617
618
  if (results.length === 0) {
618
619
  return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term.`;
619
620
  }
@@ -744,7 +745,7 @@ async function codebaseImpactFallback(target) {
744
745
  const root = process.cwd();
745
746
  const grepResult = execSync2(
746
747
  `grep -rn --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" -l "${target.replace(/"/g, '\\"')}" . 2>/dev/null | grep -v node_modules | grep -v .git | grep -v dist | grep -v .kuma | head -50`,
747
- { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 1024 * 1024 }
748
+ { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
748
749
  ).trim();
749
750
  const files = grepResult ? grepResult.split("\n").filter(Boolean).filter((f) => !f.startsWith("./")) : [];
750
751
  if (files.length === 0) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sessionMemory
3
- } from "./chunk-77G3APNG.js";
3
+ } from "./chunk-BI7KD3SG.js";
4
4
  import {
5
5
  getProjectRoot
6
6
  } from "./chunk-E2KFPEBT.js";
@@ -82,7 +82,7 @@ function recordDecision(decision) {
82
82
  }
83
83
  async function recordDecisionToGraph(decision) {
84
84
  try {
85
- const { upsertNode, addEdge } = await import("./kumaGraph-KXI3Q7QF.js");
85
+ const { upsertNode, addEdge } = await import("./kumaGraph-35TAIBWD.js");
86
86
  const decisionId = `decision::${decision.title.replace(/[^a-zA-Z0-9_\-\s]/g, "").trim().replace(/\s+/g, "-")}`;
87
87
  await upsertNode({
88
88
  id: decisionId,
package/dist/index.js CHANGED
@@ -14,7 +14,13 @@ import {
14
14
  getGraphStats,
15
15
  searchGraph,
16
16
  traceFlow
17
- } from "./chunk-5YCQG7V7.js";
17
+ } from "./chunk-K64NSHBR.js";
18
+ import {
19
+ buildDriftMessages,
20
+ getGitDiffStat,
21
+ getSessionStats,
22
+ getUnresolvedCount
23
+ } from "./chunk-FOQQ2CSL.js";
18
24
  import {
19
25
  addContextNote,
20
26
  addSecurityFinding,
@@ -40,22 +46,16 @@ import {
40
46
  saveResearchCache,
41
47
  updateDecisionStatus,
42
48
  updateTodoStatus
43
- } from "./chunk-NKNXJG2E.js";
49
+ } from "./chunk-GDNAWLHF.js";
44
50
  import {
45
51
  formatDecisionTemplate,
46
52
  getProactiveMemories,
47
53
  recordDecision,
48
54
  scoreMemoryRelevance
49
- } from "./chunk-4CZSPSDW.js";
50
- import {
51
- buildDriftMessages,
52
- getGitDiffStat,
53
- getSessionStats,
54
- getUnresolvedCount
55
- } from "./chunk-BKEDQFEH.js";
55
+ } from "./chunk-X5TPBDKO.js";
56
56
  import {
57
57
  sessionMemory
58
- } from "./chunk-77G3APNG.js";
58
+ } from "./chunk-BI7KD3SG.js";
59
59
  import {
60
60
  getProjectRoot,
61
61
  validateFilePath
@@ -111,7 +111,13 @@ var CONTEXT_ALIASES = {
111
111
  // Researches synonyms
112
112
  "researches": "researches",
113
113
  "research-list": "researches",
114
- "list-research": "researches"
114
+ "list-research": "researches",
115
+ // Sync/Batch synonyms (Issue #12)
116
+ "sync": "sync",
117
+ "batch": "sync",
118
+ "kuma_sync": "sync",
119
+ "unified": "sync",
120
+ "state": "sync"
115
121
  };
116
122
  async function handleContext(params) {
117
123
  const rawAction = params.action || "init";
@@ -134,8 +140,10 @@ async function handleContext(params) {
134
140
  return handleResearches(params);
135
141
  case "health":
136
142
  return handleHealth(params);
143
+ case "sync":
144
+ return handleSync(params);
137
145
  default:
138
- return `Unknown action "${action}". Use: init, research, impact, navigate, changes, rollback, researches, health`;
146
+ return `Unknown action "${action}". Use: init, research, impact, navigate, changes, rollback, researches, health, sync`;
139
147
  }
140
148
  }
141
149
  async function handleInit(_params) {
@@ -196,8 +204,8 @@ async function handleInit(_params) {
196
204
  lines.push(` \u{1F4DD} Modified: ${summary.modifiedFiles?.length || 0} file(s)`);
197
205
  lines.push(` \u{1F6E0}\uFE0F Tool calls: ${summary.toolCallCount}`);
198
206
  try {
199
- const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-ZAOUPG6A.js");
200
- const { saveHealthSnapshot: saveHealthSnapshot2 } = await import("./kumaDb-I4AJ5A7H.js");
207
+ const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-PJGRRBWP.js");
208
+ const { saveHealthSnapshot: saveHealthSnapshot2 } = await import("./kumaDb-DJUDLYBJ.js");
201
209
  const score = await computeSafetyScore(_params.goal);
202
210
  const checksStr = JSON.stringify(score.checks);
203
211
  await saveHealthSnapshot2(score.score, score.risk, checksStr, score.summary);
@@ -370,8 +378,8 @@ async function handleResearches(_params) {
370
378
  async function handleHealth(_params) {
371
379
  sessionMemory.recordToolCall("kuma_context_health", {});
372
380
  try {
373
- const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-ZAOUPG6A.js");
374
- const { saveHealthSnapshot: saveHealthSnapshot2 } = await import("./kumaDb-I4AJ5A7H.js");
381
+ const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-PJGRRBWP.js");
382
+ const { saveHealthSnapshot: saveHealthSnapshot2 } = await import("./kumaDb-DJUDLYBJ.js");
375
383
  const score = await computeSafetyScore();
376
384
  const checksStr = JSON.stringify(score.checks);
377
385
  await saveHealthSnapshot2(score.score, score.risk, checksStr, score.summary);
@@ -380,6 +388,55 @@ async function handleHealth(_params) {
380
388
  return `Error computing health: ${err}`;
381
389
  }
382
390
  }
391
+ async function handleSync(params) {
392
+ sessionMemory.setGoal(params.goal || "Sync session");
393
+ sessionMemory.recordToolCall("kuma_context_sync", { goal: params.goal });
394
+ const lines = [
395
+ "\u{1F504} **Kuma Sync \u2014 Unified State**",
396
+ `\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`,
397
+ "",
398
+ `\u{1F4C1} Project: ${getProjectRoot().split("/").pop() || "unknown"}`,
399
+ `\u{1F550} Session: ${(/* @__PURE__ */ new Date()).toISOString()}`,
400
+ ""
401
+ ];
402
+ const summary = sessionMemory.getSummary();
403
+ lines.push("**Session State**");
404
+ lines.push(` \u{1F3AF} Goal: ${summary.currentGoal || "not set"}`);
405
+ lines.push(` \u{1F4DD} Modified: ${summary.modifiedFiles?.length || 0} file(s)`);
406
+ lines.push(` \u{1F6E0}\uFE0F Tool calls: ${summary.toolCallCount}`);
407
+ lines.push("");
408
+ try {
409
+ const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-PJGRRBWP.js");
410
+ const { saveHealthSnapshot: saveHealthSnapshot2 } = await import("./kumaDb-DJUDLYBJ.js");
411
+ const score = await computeSafetyScore(params.goal);
412
+ const checksStr = JSON.stringify(score.checks);
413
+ await saveHealthSnapshot2(score.score, score.risk, checksStr, score.summary);
414
+ lines.push("**Health Score**");
415
+ lines.push(formatSafetyScore(score));
416
+ } catch {
417
+ lines.push("**Health Score**: \u26A0\uFE0F Could not compute");
418
+ }
419
+ lines.push("");
420
+ try {
421
+ const { getGraphStats: getGraphStats2 } = await import("./kumaGraph-35TAIBWD.js");
422
+ lines.push("**Knowledge Graph**");
423
+ lines.push(await getGraphStats2());
424
+ } catch {
425
+ }
426
+ lines.push("");
427
+ try {
428
+ const { getProactiveMemories: getProactiveMemories2 } = await import("./kumaMemory-5SR3TGRL.js");
429
+ const memories = getProactiveMemories2();
430
+ if (memories) {
431
+ lines.push("**Relevant Memories**");
432
+ lines.push(memories);
433
+ lines.push("");
434
+ }
435
+ } catch {
436
+ }
437
+ lines.push("\u{1F4A1} Sync complete \u2014 all state captured in a single roundtrip.");
438
+ return lines.join("\n");
439
+ }
383
440
  function computeProjectHash(scope) {
384
441
  try {
385
442
  const root = getProjectRoot();
@@ -501,7 +558,7 @@ async function handleDecision(params) {
501
558
  case "template":
502
559
  return formatDecisionTemplate();
503
560
  case "suggest": {
504
- const { shouldRecordDecision } = await import("./kumaMemory-2HW4JLJU.js");
561
+ const { shouldRecordDecision } = await import("./kumaMemory-5SR3TGRL.js");
505
562
  const check = shouldRecordDecision();
506
563
  return check.worth ? `\u{1F4A1} Decision suggested: "${check.title}"
507
564
  Use kuma_memory({ action: 'decision', title: '...', context: '...', rationale: '...', outcome: '...' }) to record.` : "\u2705 No decision needed at this time.";
@@ -579,7 +636,7 @@ async function handleSearch(params) {
579
636
  if (!query) return "\u26A0\uFE0F query or scope parameter required.";
580
637
  const limit = params.limit || 20;
581
638
  const memResults = sessionMemory.searchMemory(query, limit);
582
- const { searchGraph: searchGraph2 } = await import("./kumaGraph-KXI3Q7QF.js");
639
+ const { searchGraph: searchGraph2 } = await import("./kumaGraph-35TAIBWD.js");
583
640
  const graphResults = await searchGraph2(query, Math.min(limit, 10));
584
641
  const lines = [`\u{1F50D} **Search Results** \u2014 "${query}"
585
642
  \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
@@ -654,7 +711,7 @@ async function handleDecisionLog(params) {
654
711
  }
655
712
  async function handleMine(params) {
656
713
  sessionMemory.recordToolCall("kuma_memory_mine", { scope: params.scope });
657
- const { mineHistoricalDecisions } = await import("./kumaMiner-HZUBNYYP.js");
714
+ const { mineHistoricalDecisions } = await import("./kumaMiner-BIDSZE3Q.js");
658
715
  return await mineHistoricalDecisions({
659
716
  scope: params.scope,
660
717
  since: typeof params.since === "string" ? params.since : void 0,
@@ -1791,7 +1848,7 @@ async function handleLock(params) {
1791
1848
  }
1792
1849
  async function handleHealth2(_params) {
1793
1850
  try {
1794
- const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-ZAOUPG6A.js");
1851
+ const { computeSafetyScore, formatSafetyScore } = await import("./safetyScore-PJGRRBWP.js");
1795
1852
  const score = await computeSafetyScore();
1796
1853
  await saveHealthSnapshot(score.score, score.risk, JSON.stringify(score.checks), score.summary);
1797
1854
  return formatSafetyScore(score);
@@ -1905,7 +1962,7 @@ async function handleVerify(params) {
1905
1962
  }
1906
1963
  _lastVerifyCall = now;
1907
1964
  sessionMemory.recordToolCall("kuma_safety_verify", { scope: params.scope || params.filePath });
1908
- const { runAutoVerification } = await import("./kumaVerifier-SSS4ZTJY.js");
1965
+ const { runAutoVerification } = await import("./kumaVerifier-B4D7NOFT.js");
1909
1966
  return await runAutoVerification({
1910
1967
  scope: params.scope || params.filePath,
1911
1968
  force: params.force,
@@ -2102,8 +2159,9 @@ async function main() {
2102
2159
  if (args[0] === "stop" && (args[1] === "--force" || args[1] === "-f")) {
2103
2160
  console.error(`\u{1F43B} Kuma v${SERVER_VERSION} \u2014 Kill Switch`);
2104
2161
  console.error("");
2162
+ let killedCount = 0;
2105
2163
  try {
2106
- const { getRunningVerificationPid } = await import("./kumaVerifier-SSS4ZTJY.js");
2164
+ const { getRunningVerificationPid } = await import("./kumaVerifier-B4D7NOFT.js");
2107
2165
  const pid = getRunningVerificationPid();
2108
2166
  if (pid) {
2109
2167
  try {
@@ -2114,10 +2172,41 @@ async function main() {
2114
2172
  process.kill(pid, "SIGKILL");
2115
2173
  } catch {
2116
2174
  }
2117
- console.error(`\u2705 Killed verification process (PID: ${pid})`);
2175
+ console.error(`\u2705 Killed local verification process (PID: ${pid})`);
2176
+ killedCount++;
2118
2177
  }
2119
- } catch (err) {
2120
- console.error(`\u26A0\uFE0F Could not check verifier: ${err}`);
2178
+ } catch {
2179
+ }
2180
+ try {
2181
+ const path7 = await import("path");
2182
+ const fs7 = await import("fs");
2183
+ const lockDir = path7.resolve(process.cwd(), ".kuma/verifier.lock");
2184
+ if (fs7.existsSync(lockDir)) {
2185
+ const pidFile = path7.join(lockDir, "pid");
2186
+ if (fs7.existsSync(pidFile)) {
2187
+ try {
2188
+ const pid = parseInt(fs7.readFileSync(pidFile, "utf-8"), 10);
2189
+ if (pid && pid !== process.pid) {
2190
+ try {
2191
+ process.kill(-pid, "SIGKILL");
2192
+ } catch {
2193
+ }
2194
+ try {
2195
+ process.kill(pid, "SIGKILL");
2196
+ } catch {
2197
+ }
2198
+ console.error(`\u2705 Killed other instance verification (PID: ${pid})`);
2199
+ killedCount++;
2200
+ }
2201
+ } catch {
2202
+ }
2203
+ }
2204
+ fs7.rmSync(lockDir, { recursive: true, force: true });
2205
+ }
2206
+ if (killedCount === 0) {
2207
+ console.error("\u2705 No running verifications found to kill.");
2208
+ }
2209
+ } catch {
2121
2210
  }
2122
2211
  try {
2123
2212
  const { execSync: execSync3 } = await import("child_process");
@@ -2311,7 +2400,7 @@ async function main() {
2311
2400
  console.error(`[${SERVER_NAME}] \u2705 Restored session (${sessionInfo.toolCallCount} previous tool calls)`);
2312
2401
  }
2313
2402
  try {
2314
- const { buildFromSessionMemory } = await import("./kumaGraph-KXI3Q7QF.js");
2403
+ const { buildFromSessionMemory } = await import("./kumaGraph-35TAIBWD.js");
2315
2404
  const edgeCount = await buildFromSessionMemory();
2316
2405
  if (edgeCount > 0) {
2317
2406
  console.error(`[${SERVER_NAME}] \u2705 Graph auto-populated with ${edgeCount} entries from session memory`);
@@ -2331,7 +2420,7 @@ async function main() {
2331
2420
  console.error(`[${SERVER_NAME}] \u26A0\uFE0F Scratch directory setup: ${err}`);
2332
2421
  }
2333
2422
  try {
2334
- const { getDb: getDb2, saveDb: saveDb2 } = await import("./kumaDb-I4AJ5A7H.js");
2423
+ const { getDb: getDb2, saveDb: saveDb2 } = await import("./kumaDb-DJUDLYBJ.js");
2335
2424
  const db = await getDb2();
2336
2425
  db.run(
2337
2426
  `INSERT INTO sessions (started_at, goal, tool_calls) VALUES (?, ?, ?)`,
@@ -28,7 +28,7 @@ import {
28
28
  saveVerification,
29
29
  updateDecisionStatus,
30
30
  updateTodoStatus
31
- } from "./chunk-NKNXJG2E.js";
31
+ } from "./chunk-GDNAWLHF.js";
32
32
  import "./chunk-E2KFPEBT.js";
33
33
  export {
34
34
  addContextNote,
@@ -14,8 +14,8 @@ import {
14
14
  searchGraph,
15
15
  traceFlow,
16
16
  upsertNode
17
- } from "./chunk-5YCQG7V7.js";
18
- import "./chunk-NKNXJG2E.js";
17
+ } from "./chunk-K64NSHBR.js";
18
+ import "./chunk-GDNAWLHF.js";
19
19
  import "./chunk-E2KFPEBT.js";
20
20
  export {
21
21
  addEdge,
@@ -4,8 +4,8 @@ import {
4
4
  recordDecision,
5
5
  scoreMemoryRelevance,
6
6
  shouldRecordDecision
7
- } from "./chunk-4CZSPSDW.js";
8
- import "./chunk-77G3APNG.js";
7
+ } from "./chunk-X5TPBDKO.js";
8
+ import "./chunk-BI7KD3SG.js";
9
9
  import "./chunk-E2KFPEBT.js";
10
10
  export {
11
11
  formatDecisionTemplate,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  recordDecisionLog
3
- } from "./chunk-NKNXJG2E.js";
3
+ } from "./chunk-GDNAWLHF.js";
4
4
  import {
5
5
  recordDecision
6
- } from "./chunk-4CZSPSDW.js";
7
- import "./chunk-77G3APNG.js";
6
+ } from "./chunk-X5TPBDKO.js";
7
+ import "./chunk-BI7KD3SG.js";
8
8
  import "./chunk-E2KFPEBT.js";
9
9
 
10
10
  // src/engine/kumaMiner.ts
@@ -19,9 +19,10 @@ async function mineHistoricalDecisions(options = {}) {
19
19
  try {
20
20
  const sinceFlag = options.since ? `--since="${options.since}"` : `--since="1 year"`;
21
21
  const gitCmd = `git log ${sinceFlag} -n 100 --pretty=format:"%h|%an|%ad|%s" --date=short`;
22
- const gitOutput = execSync(gitCmd, { cwd: root, encoding: "utf-8" });
22
+ const gitOutput = execSync(gitCmd, { cwd: root, encoding: "utf-8", timeout: 15e3 });
23
23
  const keywords = ["fix", "revert", "hack", "workaround", "urgent", "hotfix", "don't touch", "temporary", "deprecated", "workaround"];
24
24
  const lines = gitOutput.split("\n").filter(Boolean);
25
+ const matchingHashes = [];
25
26
  for (const line of lines) {
26
27
  const parts = line.split("|");
27
28
  if (parts.length < 4) continue;
@@ -29,9 +30,42 @@ async function mineHistoricalDecisions(options = {}) {
29
30
  const lowerSubj = subject.toLowerCase();
30
31
  const matchedKeyword = keywords.find((kw) => lowerSubj.includes(kw));
31
32
  if (matchedKeyword) {
32
- let changedFiles = "";
33
+ matchingHashes.push({ hash, author, date, subject, keyword: matchedKeyword });
34
+ if (matchingHashes.length >= limit) break;
35
+ }
36
+ }
37
+ let changedFilesMap = /* @__PURE__ */ new Map();
38
+ if (matchingHashes.length > 0) {
39
+ try {
40
+ const hashesStr = matchingHashes.map((h) => h.hash).join(" ");
41
+ const batchOutput = execSync(
42
+ `git show --name-only --oneline ${hashesStr} 2>/dev/null | head -200`,
43
+ { cwd: root, encoding: "utf-8", timeout: 1e4, maxBuffer: 256 * 1024 }
44
+ );
45
+ let currentHash = "";
46
+ const files = [];
47
+ for (const line of batchOutput.split("\n")) {
48
+ if (matchingHashes.find((h) => line.startsWith(h.hash))) {
49
+ if (currentHash && files.length > 0) {
50
+ changedFilesMap.set(currentHash, files.slice(0, 3).join(", "));
51
+ }
52
+ currentHash = line.split(" ")[0];
53
+ files.length = 0;
54
+ } else if (line.trim() && !line.startsWith("diff")) {
55
+ files.push(line.trim());
56
+ }
57
+ }
58
+ if (currentHash && files.length > 0) {
59
+ changedFilesMap.set(currentHash, files.slice(0, 3).join(", "));
60
+ }
61
+ } catch {
62
+ }
63
+ }
64
+ for (const { hash, author, date, subject, keyword: matchedKeyword } of matchingHashes) {
65
+ let changedFiles = changedFilesMap.get(hash) || "";
66
+ if (!changedFiles) {
33
67
  try {
34
- changedFiles = execSync(`git show --name-only --oneline ${hash}`, { cwd: root, encoding: "utf-8" }).split("\n").slice(1).filter(Boolean).slice(0, 3).join(", ");
68
+ changedFiles = execSync(`git show --name-only --oneline ${hash}`, { cwd: root, encoding: "utf-8", timeout: 5e3 }).split("\n").slice(1).filter(Boolean).slice(0, 3).join(", ");
35
69
  } catch {
36
70
  }
37
71
  candidates.push({
@@ -0,0 +1,235 @@
1
+ import {
2
+ getLatestVerifications,
3
+ saveVerification
4
+ } from "./chunk-GDNAWLHF.js";
5
+ import {
6
+ sessionMemory
7
+ } from "./chunk-BI7KD3SG.js";
8
+ import "./chunk-E2KFPEBT.js";
9
+
10
+ // src/engine/kumaVerifier.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { exec } from "child_process";
14
+ var LOCK_DIR_NAME = ".kuma/verifier.lock";
15
+ function getLockDir(root = process.cwd()) {
16
+ return path.resolve(root, LOCK_DIR_NAME);
17
+ }
18
+ function getPidFile(root = process.cwd()) {
19
+ return path.join(getLockDir(root), "pid");
20
+ }
21
+ function acquireFileLock(root) {
22
+ const lockDir = getLockDir(root);
23
+ try {
24
+ fs.mkdirSync(lockDir, { recursive: false });
25
+ fs.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
26
+ return true;
27
+ } catch {
28
+ try {
29
+ if (fs.existsSync(getPidFile(root))) {
30
+ const pid = parseInt(fs.readFileSync(getPidFile(root), "utf-8"), 10);
31
+ try {
32
+ process.kill(pid, 0);
33
+ return false;
34
+ } catch {
35
+ fs.rmSync(lockDir, { recursive: true, force: true });
36
+ return acquireFileLock(root);
37
+ }
38
+ }
39
+ } catch {
40
+ }
41
+ try {
42
+ fs.rmSync(lockDir, { recursive: true, force: true });
43
+ return acquireFileLock(root);
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ }
49
+ function releaseFileLock(root) {
50
+ try {
51
+ fs.rmSync(getLockDir(root), { recursive: true, force: true });
52
+ } catch {
53
+ }
54
+ }
55
+ var _localRunning = false;
56
+ var _currentProcess = null;
57
+ var STALE_RESULT_MS = 3e5;
58
+ var DEFAULT_TIMEOUT_MS = 3e4;
59
+ function getRunningVerificationPid() {
60
+ return _currentProcess?.pid ?? null;
61
+ }
62
+ function detectTestRunner(root = process.cwd()) {
63
+ const pkgPath = path.join(root, "package.json");
64
+ if (fs.existsSync(pkgPath)) {
65
+ try {
66
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
67
+ if (pkg.scripts && pkg.scripts.test) {
68
+ if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) return { runner: "pnpm", baseCommand: "pnpm test" };
69
+ if (fs.existsSync(path.join(root, "yarn.lock"))) return { runner: "yarn", baseCommand: "yarn test" };
70
+ return { runner: "npm", baseCommand: "npm test" };
71
+ }
72
+ } catch {
73
+ }
74
+ }
75
+ if (fs.existsSync(path.join(root, "pytest.ini")) || fs.existsSync(path.join(root, "pyproject.toml"))) return { runner: "pytest", baseCommand: "pytest" };
76
+ if (fs.existsSync(path.join(root, "Cargo.toml"))) return { runner: "cargo", baseCommand: "cargo test" };
77
+ if (fs.existsSync(path.join(root, "go.mod"))) return { runner: "go", baseCommand: "go test ./..." };
78
+ if (fs.existsSync(path.join(root, "Makefile"))) return { runner: "make", baseCommand: "make test" };
79
+ if (fs.existsSync(path.join(root, "package.json"))) {
80
+ try {
81
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
82
+ if (pkg.dependencies || pkg.devDependencies) {
83
+ if (fs.existsSync(path.join(root, "tsconfig.json"))) return { runner: "tsc", baseCommand: "npx tsc --noEmit" };
84
+ const srcDir = path.join(root, "src");
85
+ if (fs.existsSync(srcDir)) return { runner: "node", baseCommand: `node -c "${srcDir}/**/*.js" 2>/dev/null || node --check $(find src -name '*.js' 2>/dev/null | head -5)` };
86
+ }
87
+ } catch {
88
+ }
89
+ }
90
+ return { runner: "unknown", baseCommand: "npm test || echo 'No test runner configured'" };
91
+ }
92
+ function checkAllowed(root) {
93
+ if (_localRunning) return "\u23F3 Verification already in progress (this instance).";
94
+ if (!acquireFileLock(root)) return "\u23F3 Another Kuma instance is running verification.";
95
+ return null;
96
+ }
97
+ async function checkStaleness(scope) {
98
+ if (scope !== "session-impact" && scope !== void 0) return null;
99
+ try {
100
+ const recent = await getLatestVerifications(1);
101
+ if (recent.length === 0) return null;
102
+ const age = Date.now() - recent[0].created_at * 1e3;
103
+ if (age >= STALE_RESULT_MS) return null;
104
+ const ageSeconds = Math.floor(age / 1e3);
105
+ return [
106
+ `\u23E9 **Using cached verification** (${ageSeconds}s old, < 5 min threshold)`,
107
+ `\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`,
108
+ "",
109
+ `\u{1F6E0}\uFE0F Runner: \`${recent[0].runner}\``,
110
+ `\u{1F4BB} Command: \`${recent[0].test_command}\``,
111
+ `\u{1F3AF} Scope: \`${recent[0].scope}\``,
112
+ `\u23F1\uFE0F Original duration: ${recent[0].duration_ms}ms`,
113
+ "",
114
+ recent[0].passed ? "\u2705 **Result: PASSED** (served from cache)" : "\u{1F534} **Result: FAILED** (served from cache)",
115
+ "",
116
+ "\u{1F4A1} Use `force: true` to bypass cache and run tests again."
117
+ ].join("\n");
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ async function runAutoVerification(options = {}) {
123
+ const startTime = Date.now();
124
+ const root = process.cwd();
125
+ const scope = options.scope || "session-impact";
126
+ const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
127
+ const denial = checkAllowed(root);
128
+ if (denial) return denial;
129
+ try {
130
+ if (!options.force) {
131
+ const cached = await checkStaleness(scope);
132
+ if (cached) {
133
+ releaseFileLock(root);
134
+ return cached;
135
+ }
136
+ }
137
+ _localRunning = true;
138
+ const { runner, baseCommand } = detectTestRunner(root);
139
+ let testFiles = [];
140
+ const modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
141
+ if (options.scope) {
142
+ const term = options.scope.toLowerCase();
143
+ testFiles = modified.filter((f) => f.toLowerCase().includes(term));
144
+ if (testFiles.length === 0) {
145
+ try {
146
+ const { default: glob } = await import("fast-glob");
147
+ testFiles = await glob([`**/*${term}*test*.*`, `**/*test*/*${term}*.*`], { cwd: root, ignore: ["node_modules/**", "dist/**"] });
148
+ } catch {
149
+ }
150
+ }
151
+ } else {
152
+ testFiles = modified.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test."));
153
+ }
154
+ let fullCommand = baseCommand;
155
+ if (testFiles.length > 0 && runner === "npm") {
156
+ fullCommand = `${baseCommand} -- ${testFiles.map((f) => `"${f}"`).join(" ")}`;
157
+ }
158
+ return await new Promise((resolve) => {
159
+ const killTimer = setTimeout(() => {
160
+ if (_currentProcess && !_currentProcess.killed) {
161
+ try {
162
+ const childPid = _currentProcess.pid;
163
+ if (childPid) {
164
+ try {
165
+ process.kill(-childPid, "SIGKILL");
166
+ } catch {
167
+ }
168
+ try {
169
+ _currentProcess.kill("SIGKILL");
170
+ } catch {
171
+ }
172
+ }
173
+ } catch {
174
+ }
175
+ }
176
+ }, timeoutMs + 5e3);
177
+ const heartbeatTimer = setInterval(() => {
178
+ if (getLockDir(root)) {
179
+ try {
180
+ fs.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
181
+ } catch {
182
+ }
183
+ }
184
+ }, 15e3);
185
+ const child = exec(fullCommand, { cwd: root, timeout: timeoutMs }, async (error, stdout, stderr) => {
186
+ const durationMs = Date.now() - startTime;
187
+ const rawOutput = (stdout + "\n" + stderr).trim();
188
+ const passed = !error;
189
+ const truncatedOutput = rawOutput.length > 2e3 ? rawOutput.substring(rawOutput.length - 2e3) : rawOutput;
190
+ await saveVerification(scope, runner, fullCommand, passed, truncatedOutput, durationMs);
191
+ _localRunning = false;
192
+ _currentProcess = null;
193
+ releaseFileLock(root);
194
+ clearInterval(heartbeatTimer);
195
+ const statusSymbol = passed ? "\u2705" : "\u{1F534}";
196
+ const lines = [
197
+ `${statusSymbol} **Verification ${passed ? "PASSED" : "FAILED"}**`,
198
+ `\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`,
199
+ "",
200
+ `\u{1F6E0}\uFE0F **Runner**: \`${runner}\``,
201
+ `\u{1F4BB} **Command**: \`${fullCommand}\``,
202
+ `\u{1F3AF} **Scope**: \`${scope}\`${testFiles.length > 0 ? ` (${testFiles.length} file(s) matched)` : ""}`,
203
+ `\u23F1\uFE0F **Duration**: ${durationMs}ms`,
204
+ ""
205
+ ];
206
+ if (!passed) {
207
+ lines.push("\u26A0\uFE0F **Verification failed!** Please fix test failures before shipping.");
208
+ if (error && error.killed) {
209
+ lines.push(`\u23F0 **Process was killed after ${timeoutMs}ms timeout**`);
210
+ }
211
+ lines.push("```text", rawOutput.substring(0, 1500), "```");
212
+ } else {
213
+ lines.push("\u{1F389} All scoped tests passed!");
214
+ if (rawOutput) lines.push("```text", rawOutput.substring(0, 800), "```");
215
+ }
216
+ resolve(lines.join("\n"));
217
+ });
218
+ _currentProcess = child;
219
+ _localRunning = true;
220
+ child.on("close", () => {
221
+ clearTimeout(killTimer);
222
+ });
223
+ });
224
+ } catch (err) {
225
+ releaseFileLock(root);
226
+ _localRunning = false;
227
+ _currentProcess = null;
228
+ return `\u274C Verification failed with error: ${err}`;
229
+ }
230
+ }
231
+ export {
232
+ detectTestRunner,
233
+ getRunningVerificationPid,
234
+ runAutoVerification
235
+ };
@@ -2,10 +2,10 @@ import {
2
2
  getGitDiffStat,
3
3
  getSessionStats,
4
4
  getUnresolvedCount
5
- } from "./chunk-BKEDQFEH.js";
5
+ } from "./chunk-FOQQ2CSL.js";
6
6
  import {
7
7
  sessionMemory
8
- } from "./chunk-77G3APNG.js";
8
+ } from "./chunk-BI7KD3SG.js";
9
9
  import "./chunk-E2KFPEBT.js";
10
10
 
11
11
  // src/engine/safetyScore.ts
@@ -15,7 +15,7 @@ async function computeSafetyScore(inputGoal) {
15
15
  let totalScore = 0;
16
16
  const maxScore = 100;
17
17
  const gitStat = getGitDiffStat();
18
- if (gitStat) {
18
+ if (gitStat && gitStat !== "clean") {
19
19
  const lines = gitStat.split("\n").filter(Boolean).length;
20
20
  if (lines === 0) {
21
21
  checks.push({
@@ -42,6 +42,14 @@ async function computeSafetyScore(inputGoal) {
42
42
  });
43
43
  totalScore += 10;
44
44
  }
45
+ } else if (gitStat === "clean") {
46
+ checks.push({
47
+ label: "Git Clean",
48
+ status: "pass",
49
+ message: "Working tree is clean",
50
+ weight: 20
51
+ });
52
+ totalScore += 20;
45
53
  } else {
46
54
  checks.push({
47
55
  label: "Git Status",
@@ -52,7 +60,7 @@ async function computeSafetyScore(inputGoal) {
52
60
  totalScore += 20;
53
61
  }
54
62
  try {
55
- const { getDb } = await import("./kumaDb-I4AJ5A7H.js");
63
+ const { getDb } = await import("./kumaDb-DJUDLYBJ.js");
56
64
  const db = await getDb();
57
65
  const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
58
66
  const edgeCount = db.exec("SELECT COUNT(*) as c FROM edges")[0]?.values[0][0] ?? 0;
@@ -83,7 +91,7 @@ async function computeSafetyScore(inputGoal) {
83
91
  totalScore += 5;
84
92
  }
85
93
  try {
86
- const { getDb } = await import("./kumaDb-I4AJ5A7H.js");
94
+ const { getDb } = await import("./kumaDb-DJUDLYBJ.js");
87
95
  const db = await getDb();
88
96
  const researchCount = db.exec("SELECT COUNT(*) as c FROM research_cache")[0]?.values[0][0] ?? 0;
89
97
  checks.push({
@@ -108,7 +116,7 @@ async function computeSafetyScore(inputGoal) {
108
116
  const hasRunTests = stats.hasRunTests;
109
117
  let latestVerif = null;
110
118
  try {
111
- const { getLatestVerifications } = await import("./kumaDb-I4AJ5A7H.js");
119
+ const { getLatestVerifications } = await import("./kumaDb-DJUDLYBJ.js");
112
120
  const verifs = await getLatestVerifications(1);
113
121
  if (verifs.length > 0) latestVerif = verifs[0];
114
122
  } catch {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sessionMemory
3
- } from "./chunk-77G3APNG.js";
3
+ } from "./chunk-BI7KD3SG.js";
4
4
  export {
5
5
  sessionMemory
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,243 +0,0 @@
1
- import {
2
- getLatestVerifications,
3
- saveVerification
4
- } from "./chunk-NKNXJG2E.js";
5
- import {
6
- sessionMemory
7
- } from "./chunk-77G3APNG.js";
8
- import "./chunk-E2KFPEBT.js";
9
-
10
- // src/engine/kumaVerifier.ts
11
- import fs from "fs";
12
- import path from "path";
13
- import { exec } from "child_process";
14
- var _isRunning = false;
15
- var _lastCompletedAt = 0;
16
- var _callHistory = [];
17
- var RUNAWAY_WINDOW_MS = 5 * 60 * 1e3;
18
- var RUNAWAY_THRESHOLD = 3;
19
- var MIN_INTERVAL_MS = 6e4;
20
- var STALE_RESULT_MS = 3e5;
21
- var DEFAULT_TIMEOUT_MS = 3e4;
22
- var _currentProcess = null;
23
- function getRunningVerificationPid() {
24
- return _currentProcess?.pid ?? null;
25
- }
26
- function isVerificationRunning() {
27
- return _isRunning;
28
- }
29
- function msSinceLastVerification() {
30
- if (_lastCompletedAt === 0) return -1;
31
- return Date.now() - _lastCompletedAt;
32
- }
33
- function getRecentCallCount() {
34
- const cutoff = Date.now() - RUNAWAY_WINDOW_MS;
35
- return _callHistory.filter((t) => t > cutoff).length;
36
- }
37
- function detectTestRunner(root = process.cwd()) {
38
- const pkgPath = path.join(root, "package.json");
39
- if (fs.existsSync(pkgPath)) {
40
- try {
41
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
42
- if (pkg.scripts && pkg.scripts.test) {
43
- if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) {
44
- return { runner: "pnpm", baseCommand: "pnpm test" };
45
- }
46
- if (fs.existsSync(path.join(root, "yarn.lock"))) {
47
- return { runner: "yarn", baseCommand: "yarn test" };
48
- }
49
- return { runner: "npm", baseCommand: "npm test" };
50
- }
51
- } catch {
52
- }
53
- }
54
- if (fs.existsSync(path.join(root, "pytest.ini")) || fs.existsSync(path.join(root, "pyproject.toml"))) {
55
- return { runner: "pytest", baseCommand: "pytest" };
56
- }
57
- if (fs.existsSync(path.join(root, "Cargo.toml"))) {
58
- return { runner: "cargo", baseCommand: "cargo test" };
59
- }
60
- if (fs.existsSync(path.join(root, "go.mod"))) {
61
- return { runner: "go", baseCommand: "go test ./..." };
62
- }
63
- if (fs.existsSync(path.join(root, "Makefile"))) {
64
- return { runner: "make", baseCommand: "make test" };
65
- }
66
- if (fs.existsSync(path.join(root, "package.json"))) {
67
- try {
68
- const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
69
- if (pkg.dependencies || pkg.devDependencies) {
70
- if (fs.existsSync(path.join(root, "tsconfig.json"))) {
71
- return { runner: "tsc", baseCommand: "npx tsc --noEmit" };
72
- }
73
- const srcDir = path.join(root, "src");
74
- if (fs.existsSync(srcDir)) {
75
- return { runner: "node", baseCommand: `node -c "${srcDir}/**/*.js" 2>/dev/null || node --check $(find src -name '*.js' 2>/dev/null | head -5)` };
76
- }
77
- }
78
- } catch {
79
- }
80
- }
81
- return { runner: "unknown", baseCommand: "npm test || echo 'No test runner configured'" };
82
- }
83
- function checkRateLimit() {
84
- if (_isRunning) {
85
- return "\u23F3 Verification already in progress \u2014 only 1 verification at a time. Wait for it to complete, or try again later.";
86
- }
87
- const cutoff = Date.now() - RUNAWAY_WINDOW_MS;
88
- const recent = _callHistory.filter((t) => t > cutoff);
89
- if (recent.length >= RUNAWAY_THRESHOLD) {
90
- return `\u{1F534} **Runaway Detection Triggered**
91
-
92
- Verification has been called ${recent.length} times in the last 5 minutes. This looks like an uncontrolled loop. Verification has been **blocked** to prevent resource exhaustion.
93
-
94
- \u{1F4A1} If you need to verify, wait ${Math.ceil((RUNAWAY_WINDOW_MS - (Date.now() - recent[0])) / 6e4)} minutes, or run \`kuma_safety({ action: "doctor" })\` to check system status.
95
-
96
- \u{1F6A8} If this is unexpected, run \`pkill -f "pnpm test"\` to kill orphaned processes.`;
97
- }
98
- if (_lastCompletedAt > 0) {
99
- const elapsed = Date.now() - _lastCompletedAt;
100
- if (elapsed < MIN_INTERVAL_MS) {
101
- const remaining = Math.ceil((MIN_INTERVAL_MS - elapsed) / 1e3);
102
- return `\u23F3 Rate limit: verification was run ${Math.floor(elapsed / 1e3)}s ago. Please wait ${remaining}s before running verification again. (Minimum interval: ${MIN_INTERVAL_MS / 1e3}s)`;
103
- }
104
- }
105
- return null;
106
- }
107
- async function checkStaleness(scope) {
108
- if (scope === "session-impact" || scope === void 0) {
109
- try {
110
- const recent = await getLatestVerifications(1);
111
- if (recent.length > 0) {
112
- const age = Date.now() - recent[0].created_at * 1e3;
113
- if (age < STALE_RESULT_MS) {
114
- const ageSeconds = Math.floor(age / 1e3);
115
- return [
116
- `\u23E9 **Using cached verification** (${ageSeconds}s old, < 5 min threshold)`,
117
- `\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`,
118
- "",
119
- `\u{1F6E0}\uFE0F Runner: \`${recent[0].runner}\``,
120
- `\u{1F4BB} Command: \`${recent[0].test_command}\``,
121
- `\u{1F3AF} Scope: \`${recent[0].scope}\``,
122
- `\u23F1\uFE0F Original duration: ${recent[0].duration_ms}ms`,
123
- "",
124
- recent[0].passed ? "\u2705 **Result: PASSED** (served from cache)" : "\u{1F534} **Result: FAILED** (served from cache)",
125
- "",
126
- "\u{1F4A1} Use `force: true` to bypass cache and run tests again."
127
- ].join("\n");
128
- }
129
- }
130
- } catch {
131
- }
132
- }
133
- return null;
134
- }
135
- async function runAutoVerification(options = {}) {
136
- const startTime = Date.now();
137
- const root = process.cwd();
138
- const scope = options.scope || "session-impact";
139
- const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
140
- const denial = checkRateLimit();
141
- if (denial) {
142
- return denial;
143
- }
144
- if (!options.force) {
145
- const cached = await checkStaleness(scope);
146
- if (cached) return cached;
147
- }
148
- _isRunning = true;
149
- const { runner, baseCommand } = detectTestRunner(root);
150
- let testFiles = [];
151
- const modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
152
- if (options.scope) {
153
- const term = options.scope.toLowerCase();
154
- testFiles = modified.filter((f) => f.toLowerCase().includes(term));
155
- if (testFiles.length === 0) {
156
- try {
157
- const { default: glob } = await import("fast-glob");
158
- const found = await glob([`**/*${term}*test*.*`, `**/*test*/*${term}*.*`], { cwd: root, ignore: ["node_modules/**", "dist/**"] });
159
- testFiles = found;
160
- } catch {
161
- }
162
- }
163
- } else {
164
- testFiles = modified.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test."));
165
- }
166
- let fullCommand = baseCommand;
167
- if (testFiles.length > 0 && runner === "npm") {
168
- fullCommand = `${baseCommand} -- ${testFiles.map((f) => `"${f}"`).join(" ")}`;
169
- }
170
- return new Promise((resolve) => {
171
- const child = exec(fullCommand, { cwd: root, timeout: timeoutMs }, async (error, stdout, stderr) => {
172
- const durationMs = Date.now() - startTime;
173
- const rawOutput = (stdout + "\n" + stderr).trim();
174
- const passed = !error;
175
- const truncatedOutput = rawOutput.length > 2e3 ? rawOutput.substring(rawOutput.length - 2e3) : rawOutput;
176
- await saveVerification(scope, runner, fullCommand, passed, truncatedOutput, durationMs);
177
- _isRunning = false;
178
- _lastCompletedAt = Date.now();
179
- _callHistory.push(Date.now());
180
- while (_callHistory.length > 100) _callHistory.shift();
181
- _currentProcess = null;
182
- const statusSymbol = passed ? "\u2705" : "\u{1F534}";
183
- const summaryHeader = `${statusSymbol} **Verification ${passed ? "PASSED" : "FAILED"}**
184
- \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`;
185
- const details = [
186
- summaryHeader,
187
- `\u{1F6E0}\uFE0F **Runner**: \`${runner}\``,
188
- `\u{1F4BB} **Command**: \`${fullCommand}\``,
189
- `\u{1F3AF} **Scope**: \`${scope}\`${testFiles.length > 0 ? ` (${testFiles.length} file(s) matched)` : ""}`,
190
- `\u23F1\uFE0F **Duration**: ${durationMs}ms`,
191
- ""
192
- ];
193
- if (!passed) {
194
- details.push("\u26A0\uFE0F **Verification failed!** Please fix test failures before shipping.");
195
- if (error && error.killed) {
196
- details.push(`\u23F0 **Process was killed after ${timeoutMs}ms timeout**`);
197
- }
198
- details.push("```text");
199
- details.push(rawOutput.substring(0, 1500));
200
- details.push("```");
201
- } else {
202
- details.push("\u{1F389} All scoped tests passed!");
203
- if (rawOutput) {
204
- details.push("```text");
205
- details.push(rawOutput.substring(0, 800));
206
- details.push("```");
207
- }
208
- }
209
- resolve(details.join("\n"));
210
- });
211
- _currentProcess = child;
212
- _isRunning = true;
213
- const killTimer = setTimeout(() => {
214
- if (_currentProcess && !_currentProcess.killed) {
215
- try {
216
- const childPid = _currentProcess.pid;
217
- if (childPid) {
218
- try {
219
- process.kill(-childPid, "SIGKILL");
220
- } catch {
221
- }
222
- try {
223
- _currentProcess.kill("SIGKILL");
224
- } catch {
225
- }
226
- }
227
- } catch {
228
- }
229
- }
230
- }, timeoutMs + 5e3);
231
- child.on("close", () => {
232
- clearTimeout(killTimer);
233
- });
234
- });
235
- }
236
- export {
237
- detectTestRunner,
238
- getRecentCallCount,
239
- getRunningVerificationPid,
240
- isVerificationRunning,
241
- msSinceLastVerification,
242
- runAutoVerification
243
- };