@plumpslabs/kuma 2.3.0 → 2.3.2

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.
@@ -4,8 +4,8 @@ import {
4
4
  formatInitResults,
5
5
  generateInitMdContent,
6
6
  runInit
7
- } from "./chunk-5A6AKUJZ.js";
8
- import "./chunk-IXMWW5WA.js";
7
+ } from "./chunk-L5WU2HTN.js";
8
+ import "./chunk-E2KFPEBT.js";
9
9
  export {
10
10
  ALL_CONFIG_TYPES,
11
11
  CONFIG_LABELS,
@@ -0,0 +1,63 @@
1
+ import {
2
+ addContextNote,
3
+ addSecurityFinding,
4
+ addTodo,
5
+ checkPortability,
6
+ ensureGitignore,
7
+ getBenchmarkDiff,
8
+ getChanges,
9
+ getDb,
10
+ getFileSummary,
11
+ getLatestVerifications,
12
+ getResearchCache,
13
+ getSecurityFindings,
14
+ listContextNotes,
15
+ listDecisionLog,
16
+ listResearchCache,
17
+ listTodos,
18
+ recordChange,
19
+ recordDecisionLog,
20
+ rollbackChange,
21
+ runDoctor,
22
+ runGarbageCollection,
23
+ saveBenchmark,
24
+ saveDb,
25
+ saveFileSummary,
26
+ saveHealthSnapshot,
27
+ saveResearchCache,
28
+ saveVerification,
29
+ updateDecisionStatus,
30
+ updateTodoStatus
31
+ } from "./chunk-5N6KXACT.js";
32
+ import "./chunk-E2KFPEBT.js";
33
+ export {
34
+ addContextNote,
35
+ addSecurityFinding,
36
+ addTodo,
37
+ checkPortability,
38
+ ensureGitignore,
39
+ getBenchmarkDiff,
40
+ getChanges,
41
+ getDb,
42
+ getFileSummary,
43
+ getLatestVerifications,
44
+ getResearchCache,
45
+ getSecurityFindings,
46
+ listContextNotes,
47
+ listDecisionLog,
48
+ listResearchCache,
49
+ listTodos,
50
+ recordChange,
51
+ recordDecisionLog,
52
+ rollbackChange,
53
+ runDoctor,
54
+ runGarbageCollection,
55
+ saveBenchmark,
56
+ saveDb,
57
+ saveFileSummary,
58
+ saveHealthSnapshot,
59
+ saveResearchCache,
60
+ saveVerification,
61
+ updateDecisionStatus,
62
+ updateTodoStatus
63
+ };
@@ -14,9 +14,9 @@ import {
14
14
  searchGraph,
15
15
  traceFlow,
16
16
  upsertNode
17
- } from "./chunk-IMO4QAFJ.js";
18
- import "./chunk-6NEMSUKP.js";
19
- import "./chunk-IXMWW5WA.js";
17
+ } from "./chunk-EQ2CE4UC.js";
18
+ import "./chunk-5N6KXACT.js";
19
+ import "./chunk-E2KFPEBT.js";
20
20
  export {
21
21
  addEdge,
22
22
  analyzeImpact,
@@ -4,9 +4,9 @@ import {
4
4
  recordDecision,
5
5
  scoreMemoryRelevance,
6
6
  shouldRecordDecision
7
- } from "./chunk-OUPFJQKW.js";
8
- import "./chunk-TT37TE4P.js";
9
- import "./chunk-IXMWW5WA.js";
7
+ } from "./chunk-EIK7T746.js";
8
+ import "./chunk-SU7BTOND.js";
9
+ import "./chunk-E2KFPEBT.js";
10
10
  export {
11
11
  formatDecisionTemplate,
12
12
  getProactiveMemories,
@@ -0,0 +1,142 @@
1
+ import {
2
+ recordDecisionLog
3
+ } from "./chunk-5N6KXACT.js";
4
+ import {
5
+ recordDecision
6
+ } from "./chunk-EIK7T746.js";
7
+ import "./chunk-SU7BTOND.js";
8
+ import "./chunk-E2KFPEBT.js";
9
+
10
+ // src/engine/kumaMiner.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { execSync } from "child_process";
14
+ import fastGlob from "fast-glob";
15
+ async function mineHistoricalDecisions(options = {}) {
16
+ const root = process.cwd();
17
+ const limit = options.limit || 15;
18
+ const candidates = [];
19
+ try {
20
+ const sinceFlag = options.since ? `--since="${options.since}"` : `--since="1 year"`;
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" });
23
+ const keywords = ["fix", "revert", "hack", "workaround", "urgent", "hotfix", "don't touch", "temporary", "deprecated", "workaround"];
24
+ const lines = gitOutput.split("\n").filter(Boolean);
25
+ for (const line of lines) {
26
+ const parts = line.split("|");
27
+ if (parts.length < 4) continue;
28
+ const [hash, author, date, subject] = parts;
29
+ const lowerSubj = subject.toLowerCase();
30
+ const matchedKeyword = keywords.find((kw) => lowerSubj.includes(kw));
31
+ if (matchedKeyword) {
32
+ let changedFiles = "";
33
+ 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(", ");
35
+ } catch {
36
+ }
37
+ candidates.push({
38
+ id: `git-${hash}`,
39
+ type: "git_commit",
40
+ title: `[Git Commit] ${subject}`,
41
+ context: `Commit ${hash} by ${author} on ${date}.${changedFiles ? ` Files touched: ${changedFiles}` : ""}`,
42
+ rationale: `Historical signal pattern "${matchedKeyword}" found in commit message.`,
43
+ commitHash: hash,
44
+ pattern: matchedKeyword
45
+ });
46
+ if (candidates.length >= limit) break;
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ if (candidates.length < limit) {
52
+ try {
53
+ const scopePattern = options.scope ? `**/*${options.scope}*.*` : "**/*.{ts,js,py,go,rs,java,md}";
54
+ const files = await fastGlob(scopePattern, {
55
+ cwd: root,
56
+ ignore: ["node_modules/**", "dist/**", ".kuma/**", ".git/**", "coverage/**"],
57
+ onlyFiles: true
58
+ });
59
+ const commentPatterns = [
60
+ { tag: "HACK", regex: /\/\/\s*HACK:?\s*(.*)|#\s*HACK:?\s*(.*)/i },
61
+ { tag: "FIXME", regex: /\/\/\s*FIXME:?\s*(.*)|#\s*FIXME:?\s*(.*)/i },
62
+ { tag: "TODO", regex: /\/\/\s*TODO:?\s*(.*)|#\s*TODO:?\s*(.*)/i },
63
+ { tag: "XXX", regex: /\/\/\s*XXX:?\s*(.*)|#\s*XXX:?\s*(.*)/i },
64
+ { tag: "WARNING", regex: /\/\/\s*WARNING:?\s*(.*)|#\s*WARNING:?\s*(.*)/i }
65
+ ];
66
+ for (const file of files.slice(0, 50)) {
67
+ if (candidates.length >= limit) break;
68
+ const fullPath = path.join(root, file);
69
+ if (!fs.existsSync(fullPath)) continue;
70
+ const content = fs.readFileSync(fullPath, "utf-8");
71
+ const lines = content.split("\n");
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const l = lines[i];
74
+ for (const cp of commentPatterns) {
75
+ const match = cp.regex.exec(l);
76
+ if (match) {
77
+ const text = (match[1] || match[2] || l).trim();
78
+ candidates.push({
79
+ id: `comment-${file}-${i + 1}`,
80
+ type: "inline_comment",
81
+ title: `[${cp.tag}] ${file}:${i + 1}`,
82
+ context: `Inline comment marker "${cp.tag}" found in file "${file}" at line ${i + 1}.`,
83
+ rationale: text || l.trim(),
84
+ filePath: file,
85
+ lineNumber: i + 1,
86
+ pattern: cp.tag
87
+ });
88
+ if (candidates.length >= limit) break;
89
+ }
90
+ }
91
+ if (candidates.length >= limit) break;
92
+ }
93
+ }
94
+ } catch {
95
+ }
96
+ }
97
+ if (options.confirm && candidates.length > 0) {
98
+ let recordedCount = 0;
99
+ for (const c of candidates) {
100
+ await recordDecisionLog({
101
+ title: c.title,
102
+ context: c.context,
103
+ rationale: c.rationale,
104
+ outcome: `Mined from ${c.type} (${c.pattern})`,
105
+ status: "proposed"
106
+ });
107
+ await recordDecision({
108
+ title: c.title,
109
+ context: c.context,
110
+ options: [],
111
+ rationale: c.rationale,
112
+ outcome: `Mined from ${c.type}`,
113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
114
+ });
115
+ recordedCount++;
116
+ }
117
+ return `\u26CF\uFE0F **Mined & Recorded ${recordedCount} Decisions** into decision log & graph.
118
+ Use \`kuma_memory({ action: 'decision_log' })\` to view all active & proposed decisions.`;
119
+ }
120
+ if (candidates.length === 0) {
121
+ return "\u26CF\uFE0F **Decision Mining**: No significant historical decision markers (HACK/FIXME/git fix) found for the requested scope.";
122
+ }
123
+ const report = [
124
+ `\u26CF\uFE0F **Decision Mining Candidates** (${candidates.length} candidate(s) found)`,
125
+ "\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\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
126
+ "Review these draft historical decisions mined from git history & inline comments:",
127
+ ""
128
+ ];
129
+ candidates.forEach((c, idx) => {
130
+ report.push(`**${idx + 1}. ${c.title}**`);
131
+ report.push(` \u2022 **Type**: \`${c.type}\` | **Signal**: \`${c.pattern}\``);
132
+ report.push(` \u2022 **Context**: ${c.context}`);
133
+ report.push(` \u2022 **Rationale**: ${c.rationale}`);
134
+ report.push("");
135
+ });
136
+ report.push("\u{1F4A1} *To accept and record these mined decisions into knowledge graph & decision.md:*");
137
+ report.push(`Run: \`kuma_memory({ action: 'mine', confirm: true${options.scope ? `, scope: '${options.scope}'` : ""} })\``);
138
+ return report.join("\n");
139
+ }
140
+ export {
141
+ mineHistoricalDecisions
142
+ };
@@ -0,0 +1,108 @@
1
+ import {
2
+ saveVerification
3
+ } from "./chunk-5N6KXACT.js";
4
+ import {
5
+ sessionMemory
6
+ } from "./chunk-SU7BTOND.js";
7
+ import "./chunk-E2KFPEBT.js";
8
+
9
+ // src/engine/kumaVerifier.ts
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { exec } from "child_process";
13
+ function detectTestRunner(root = process.cwd()) {
14
+ const pkgPath = path.join(root, "package.json");
15
+ if (fs.existsSync(pkgPath)) {
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
18
+ if (pkg.scripts && pkg.scripts.test) {
19
+ if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) {
20
+ return { runner: "pnpm", baseCommand: "pnpm test" };
21
+ }
22
+ if (fs.existsSync(path.join(root, "yarn.lock"))) {
23
+ return { runner: "yarn", baseCommand: "yarn test" };
24
+ }
25
+ return { runner: "npm", baseCommand: "npm test" };
26
+ }
27
+ } catch {
28
+ }
29
+ }
30
+ if (fs.existsSync(path.join(root, "pytest.ini")) || fs.existsSync(path.join(root, "pyproject.toml"))) {
31
+ return { runner: "pytest", baseCommand: "pytest" };
32
+ }
33
+ if (fs.existsSync(path.join(root, "Cargo.toml"))) {
34
+ return { runner: "cargo", baseCommand: "cargo test" };
35
+ }
36
+ if (fs.existsSync(path.join(root, "go.mod"))) {
37
+ return { runner: "go", baseCommand: "go test ./..." };
38
+ }
39
+ if (fs.existsSync(path.join(root, "Makefile"))) {
40
+ return { runner: "make", baseCommand: "make test" };
41
+ }
42
+ return { runner: "unknown", baseCommand: "npm test" };
43
+ }
44
+ async function runAutoVerification(options = {}) {
45
+ const startTime = Date.now();
46
+ const root = process.cwd();
47
+ const scope = options.scope || "session-impact";
48
+ const timeoutMs = options.timeoutMs || 3e4;
49
+ const { runner, baseCommand } = detectTestRunner(root);
50
+ let testFiles = [];
51
+ const modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
52
+ if (options.scope) {
53
+ const term = options.scope.toLowerCase();
54
+ testFiles = modified.filter((f) => f.toLowerCase().includes(term));
55
+ if (testFiles.length === 0) {
56
+ try {
57
+ const { default: glob } = await import("fast-glob");
58
+ const found = await glob([`**/*${term}*test*.*`, `**/*test*/*${term}*.*`], { cwd: root, ignore: ["node_modules/**", "dist/**"] });
59
+ testFiles = found;
60
+ } catch {
61
+ }
62
+ }
63
+ } else {
64
+ testFiles = modified.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test."));
65
+ }
66
+ let fullCommand = baseCommand;
67
+ if (testFiles.length > 0 && runner === "npm") {
68
+ fullCommand = `${baseCommand} -- ${testFiles.map((f) => `"${f}"`).join(" ")}`;
69
+ }
70
+ return new Promise((resolve) => {
71
+ exec(fullCommand, { cwd: root, timeout: timeoutMs }, async (error, stdout, stderr) => {
72
+ const durationMs = Date.now() - startTime;
73
+ const rawOutput = (stdout + "\n" + stderr).trim();
74
+ const passed = !error;
75
+ const truncatedOutput = rawOutput.length > 2e3 ? rawOutput.substring(rawOutput.length - 2e3) : rawOutput;
76
+ await saveVerification(scope, runner, fullCommand, passed, truncatedOutput, durationMs);
77
+ const statusSymbol = passed ? "\u2705" : "\u{1F534}";
78
+ const summaryHeader = `${statusSymbol} **Auto-Verification ${passed ? "PASSED" : "FAILED"}**
79
+ \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`;
80
+ const details = [
81
+ summaryHeader,
82
+ `\u{1F6E0}\uFE0F **Runner**: \`${runner}\``,
83
+ `\u{1F4BB} **Command**: \`${fullCommand}\``,
84
+ `\u{1F3AF} **Scope**: \`${scope}\`${testFiles.length > 0 ? ` (${testFiles.length} file(s) matched)` : ""}`,
85
+ `\u23F1\uFE0F **Duration**: ${durationMs}ms`,
86
+ ""
87
+ ];
88
+ if (!passed) {
89
+ details.push("\u26A0\uFE0F **BLOCKER**: Verification failed! Please fix test failures before shipping or continuing edits.");
90
+ details.push("```text");
91
+ details.push(rawOutput.substring(0, 1500));
92
+ details.push("```");
93
+ } else {
94
+ details.push("\u{1F389} All scoped tests passed cleanly!");
95
+ if (rawOutput) {
96
+ details.push("```text");
97
+ details.push(rawOutput.substring(0, 800));
98
+ details.push("```");
99
+ }
100
+ }
101
+ resolve(details.join("\n"));
102
+ });
103
+ });
104
+ }
105
+ export {
106
+ detectTestRunner,
107
+ runAutoVerification
108
+ };
@@ -0,0 +1,22 @@
1
+ import {
2
+ ensureBackupDir,
3
+ getBackupPath,
4
+ getKumaBackupsDir,
5
+ getKumaDir,
6
+ getProjectRoot,
7
+ isReadable,
8
+ isWritable,
9
+ validateFileExtension,
10
+ validateFilePath
11
+ } from "./chunk-E2KFPEBT.js";
12
+ export {
13
+ ensureBackupDir,
14
+ getBackupPath,
15
+ getKumaBackupsDir,
16
+ getKumaDir,
17
+ getProjectRoot,
18
+ isReadable,
19
+ isWritable,
20
+ validateFileExtension,
21
+ validateFilePath
22
+ };
@@ -2,11 +2,11 @@ import {
2
2
  getGitDiffStat,
3
3
  getSessionStats,
4
4
  getUnresolvedCount
5
- } from "./chunk-WP3WRPDC.js";
5
+ } from "./chunk-JIL533AM.js";
6
6
  import {
7
7
  sessionMemory
8
- } from "./chunk-TT37TE4P.js";
9
- import "./chunk-IXMWW5WA.js";
8
+ } from "./chunk-SU7BTOND.js";
9
+ import "./chunk-E2KFPEBT.js";
10
10
 
11
11
  // src/engine/safetyScore.ts
12
12
  async function computeSafetyScore(inputGoal) {
@@ -52,7 +52,7 @@ async function computeSafetyScore(inputGoal) {
52
52
  totalScore += 20;
53
53
  }
54
54
  try {
55
- const { getDb } = await import("./kumaDb-KN7Z4B5V.js");
55
+ const { getDb } = await import("./kumaDb-RC2EO3OB.js");
56
56
  const db = await getDb();
57
57
  const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
58
58
  const edgeCount = db.exec("SELECT COUNT(*) as c FROM edges")[0]?.values[0][0] ?? 0;
@@ -83,7 +83,7 @@ async function computeSafetyScore(inputGoal) {
83
83
  totalScore += 5;
84
84
  }
85
85
  try {
86
- const { getDb } = await import("./kumaDb-KN7Z4B5V.js");
86
+ const { getDb } = await import("./kumaDb-RC2EO3OB.js");
87
87
  const db = await getDb();
88
88
  const researchCount = db.exec("SELECT COUNT(*) as c FROM research_cache")[0]?.values[0][0] ?? 0;
89
89
  checks.push({
@@ -106,14 +106,38 @@ async function computeSafetyScore(inputGoal) {
106
106
  const allFailures = stats.failedFiles.flatMap((f) => f.failures);
107
107
  const testFailures = allFailures.filter((f) => f.error.toLowerCase().includes("test") || f.error.toLowerCase().includes("fail"));
108
108
  const hasRunTests = stats.hasRunTests;
109
- if (!hasRunTests) {
109
+ let latestVerif = null;
110
+ try {
111
+ const { getLatestVerifications } = await import("./kumaDb-RC2EO3OB.js");
112
+ const verifs = await getLatestVerifications(1);
113
+ if (verifs.length > 0) latestVerif = verifs[0];
114
+ } catch {
115
+ }
116
+ if (!hasRunTests && !latestVerif) {
110
117
  checks.push({
111
118
  label: "Tests Status",
112
119
  status: "warn",
113
- message: "No tests run yet this session",
120
+ message: "No tests run yet this session \u2014 run kuma_safety({ action: 'verify' })",
114
121
  weight: 10
115
122
  });
116
123
  totalScore += 10;
124
+ } else if (latestVerif) {
125
+ if (latestVerif.passed) {
126
+ checks.push({
127
+ label: "Tests Status",
128
+ status: "pass",
129
+ message: `Auto-verified passed (${latestVerif.scope})`,
130
+ weight: 15
131
+ });
132
+ totalScore += 15;
133
+ } else {
134
+ checks.push({
135
+ label: "Tests Status",
136
+ status: "fail",
137
+ message: `Auto-verification failed (${latestVerif.scope}) \u2014 fix before shipping`,
138
+ weight: 0
139
+ });
140
+ }
117
141
  } else if (testFailures.length === 0 && unresolvedCount === 0) {
118
142
  checks.push({
119
143
  label: "Tests Status",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sessionMemory
3
- } from "./chunk-TT37TE4P.js";
3
+ } from "./chunk-SU7BTOND.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.0",
3
+ "version": "2.3.2",
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",