@plumpslabs/kuma 2.4.4 β†’ 2.4.5

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.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: kuma-guardian
3
+ description: Runtime safety guardian. Pre-flight check, architecture boundary audit, circular dependency detection, and anti-pattern prevention. Read-only.
4
+ mode: subagent
5
+ mainAgent: false
6
+ subagent: true
7
+ permission:
8
+ read: allow
9
+ grep: allow
10
+ glob: allow
11
+ list: allow
12
+ bash: deny
13
+ webfetch: deny
14
+ websearch: deny
15
+ task: deny
16
+ edit:
17
+ "*": deny
18
+ disallowedTools: Write, Edit, Task
19
+ ---
20
+
21
+ <agent_persona>
22
+ You are the Kuma Safety Guardian. Your directive is pre-modification safety enforcement and architectural protection.
23
+ You analyze proposed changes, detect anti-patterns, circular dependencies, and verify that the agent does not edit code blindly.
24
+ </agent_persona>
25
+
26
+ <strict_boundaries>
27
+ - READ-ONLY: Never modify any source code files.
28
+ - Inspect codebase via kuma_context, kuma_safety, grep, glob, and read tools.
29
+ - If high risk or circular dependency is detected, raise explicit safety blocking recommendation.
30
+ </strict_boundaries>
31
+
32
+ <workflow>
33
+ 1. Run `kuma_safety({ action: "guard" })` to check session state and edit loops.
34
+ 2. Check file boundaries and package dependencies using `kuma_context({ action: "map" })`.
35
+ 3. If circular imports or boundary violations are found, report exact import chains.
36
+ 4. Render verdict: PASS or BLOCK.
37
+ </workflow>
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: kuma-researcher
3
+ description: Codebase intelligence & blast radius researcher. Maps monorepo packages, traces dependency trees, inspects past decisions, and gathers gotchas before changes. Read-only.
4
+ mode: subagent
5
+ mainAgent: false
6
+ subagent: true
7
+ permission:
8
+ read: allow
9
+ grep: allow
10
+ glob: allow
11
+ list: allow
12
+ bash: deny
13
+ webfetch: deny
14
+ websearch: deny
15
+ task: deny
16
+ edit:
17
+ "*": deny
18
+ disallowedTools: Write, Edit, Task
19
+ ---
20
+
21
+ <agent_persona>
22
+ You are the Kuma Codebase Researcher. You explore unfamiliar code, compute blast radius, discover domain flows, and extract historical gotchas so the main agent has 100% clarity before modifying code.
23
+ </agent_persona>
24
+
25
+ <strict_boundaries>
26
+ - READ-ONLY: Never modify any source code files.
27
+ - Focus strictly on research, dependency traversal, and impact scoring.
28
+ </strict_boundaries>
29
+
30
+ <workflow>
31
+ 1. Initialize context via `kuma_context({ action: "init" })`.
32
+ 2. Inspect area via `kuma_context({ action: "research", scope: "<target>" })`.
33
+ 3. Calculate blast radius via `kuma_context({ action: "impact", target: "<target>" })`.
34
+ 4. Check cross-session history via `kuma_context({ action: "history", target: "<file>" })`.
35
+ 5. Return structured report with affected files, consumers, and known gotchas.
36
+ </workflow>
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: "πŸ“Έ Kuma Checkpoint β€” Create an atomic named snapshot before risky work"
3
+ ---
4
+ # /kuma:checkpoint
5
+
6
+ Take an atomic rollback snapshot of current files and knowledge graph.
7
+
8
+ ```javascript
9
+ kuma_safety({ action: "checkpoint", label: "[pre-refactor-feature-x]" })
10
+ ```
11
+
12
+ Creates `<label>/` snapshot with `kuma.db` + modified files.
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: "πŸ›‘οΈ Kuma Guard β€” Pre-flight safety check against anti-patterns, drift, and circular deps"
3
+ ---
4
+ # /kuma:guard
5
+
6
+ Verify safety boundaries before applying complex refactors or multi-file edits.
7
+
8
+ ```javascript
9
+ kuma_safety({ action: "guard", guardGoal: "[what you are doing]" })
10
+ ```
11
+
12
+ **What it checks:**
13
+ - Missing pre-modification research
14
+ - Architecture boundary violations & circular imports
15
+ - Scope drift (editing files outside declared goal)
16
+ - Runaway execution loops
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: "πŸ’₯ Kuma Impact β€” Calculate blast radius, affected consumers, and safety risk score"
3
+ ---
4
+ # /kuma:impact
5
+
6
+ Evaluate blast radius before modifying high-leverage files or shared packages.
7
+
8
+ ```javascript
9
+ kuma_context({ action: "impact", target: "[file or package path]" })
10
+ ```
11
+
12
+ **What it returns:**
13
+ - Direct and transitive consumers
14
+ - Downstream affected packages in monorepos
15
+ - Mapped test suites to run
16
+ - Safety risk level (`low`, `medium`, `high`, `critical`)
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: "🐻 Kuma Init β€” Initialize session context, lean brief (<500 tokens), and branch tracking"
3
+ ---
4
+ # /kuma:init
5
+
6
+ Initialize Kuma session context before writing any code.
7
+
8
+ ```javascript
9
+ kuma_context({ action: "init", goal: "[current task goal]" })
10
+ ```
11
+
12
+ **What it does:**
13
+ - Restores cross-session shadow memory
14
+ - Loads fresh gotchas for the active branch
15
+ - Evaluates repository rules and path constraints
16
+ - Informs about branch switches
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: "πŸ”¬ Kuma Research β€” 5-step pre-edit research pipeline before editing unfamiliar code"
3
+ ---
4
+ # /kuma:research
5
+
6
+ Run research pipeline before editing code you haven't touched before.
7
+
8
+ ```javascript
9
+ kuma_context({ action: "research", scope: "[module or feature area]" })
10
+ ```
11
+
12
+ **Pipeline Stages:**
13
+ 1. Check research cache in `.kuma/kuma.db`
14
+ 2. Query knowledge graph for related nodes and edges
15
+ 3. Scan references and entry points
16
+ 4. Compute blast radius and test coverage
17
+ 5. Retrieve historical ADR decisions and gotchas
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: "βͺ Kuma Rollback β€” Restore codebase from a labeled checkpoint snapshot"
3
+ ---
4
+ # /kuma:rollback
5
+
6
+ Restore codebase to the exact state captured in a previous checkpoint.
7
+
8
+ ```javascript
9
+ kuma_safety({ action: "rollback_label", label: "[label-name]" })
10
+ ```
11
+
12
+ If the label is not found, Kuma lists all available snapshots.
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: "πŸ“Š Kuma Studio β€” Web dashboard for gotchas, flows, workspace matrix, and metrics"
3
+ ---
4
+ # /kuma:studio
5
+
6
+ Launch or view Kuma Studio web dashboard.
7
+
8
+ ```bash
9
+ npx @plumpslabs/kuma studio
10
+ ```
11
+
12
+ Opens `http://localhost:3322` featuring:
13
+ - **Gotcha Shield** (status filters & 1-click workaround copy)
14
+ - **Domain Sequence Flows**
15
+ - **Workspace & Blast Radius Simulator**
16
+ - **Health & Efficiency Metrics**
17
+ - **Calm Knowledge Graph** (physics toggle)
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: "πŸ§ͺ Kuma Verify β€” Post-edit scoped test verification and validation"
3
+ ---
4
+ # /kuma:verify
5
+
6
+ Run scoped tests and verify that changes did not introduce regressions.
7
+
8
+ ```javascript
9
+ kuma_safety({ action: "verify", scope: "[modified scope or file]" })
10
+ ```
11
+
12
+ **What it does:**
13
+ - Automatically identifies appropriate test runner (Jest, Vitest, Pytest, Go test, Cargo)
14
+ - Executes tests restricted to the modified blast radius
15
+ - Records pass/fail metrics in session memory
package/dist/index.js CHANGED
@@ -3297,7 +3297,8 @@ __export(kumaGraph_exports, {
3297
3297
  });
3298
3298
  function nodeId(type, name) {
3299
3299
  if (type === "file") {
3300
- return `file::${name}`;
3300
+ const normalized = normalizeScope(name) || name;
3301
+ return `file::${normalized}`;
3301
3302
  }
3302
3303
  return generateNodeId(type, name);
3303
3304
  }
@@ -3358,7 +3359,9 @@ async function pruneGraphIfNeeded() {
3358
3359
  async function upsertNode(node) {
3359
3360
  try {
3360
3361
  const db = await getDb();
3361
- const id = node.id || nodeId(node.type, node.name);
3362
+ const normName = node.type === "file" ? normalizeScope(node.name) || node.name : node.name;
3363
+ const normFilePath = node.filePath ? normalizeScope(node.filePath) || node.filePath : node.type === "file" ? normName : void 0;
3364
+ const id = node.id || nodeId(node.type, normName);
3362
3365
  const metadata = JSON.stringify(node.metadata ?? {});
3363
3366
  const severity = node.metadata?.severity || "medium";
3364
3367
  const confidence = node.metadata?.confidence || 0.8;
@@ -3373,19 +3376,19 @@ async function upsertNode(node) {
3373
3376
  severity = excluded.severity,
3374
3377
  confidence = excluded.confidence,
3375
3378
  updated_at = strftime('%s','now')
3376
- `, [id, node.type, node.name, node.filePath || null, metadata, severity, confidence]);
3379
+ `, [id, node.type, normName, normFilePath || null, metadata, severity, confidence]);
3377
3380
  try {
3378
- db.run(`INSERT INTO nodes_fts (rowid, name, metadata) VALUES (last_insert_rowid(), ?, ?)`, [node.name, metadata]);
3381
+ db.run(`INSERT INTO nodes_fts (rowid, name, metadata) VALUES (last_insert_rowid(), ?, ?)`, [normName, metadata]);
3379
3382
  } catch {
3380
3383
  }
3381
- if (node.filePath && node.type !== "file") {
3382
- const fileNodeId = nodeId("file", node.filePath);
3384
+ if (normFilePath && node.type !== "file") {
3385
+ const fileNodeId = nodeId("file", normFilePath);
3383
3386
  try {
3384
3387
  db.run(`
3385
3388
  INSERT INTO nodes (id, type, name, file_path, metadata, updated_at)
3386
3389
  VALUES (?, 'file', ?, ?, '{}', strftime('%s','now'))
3387
3390
  ON CONFLICT(id) DO NOTHING
3388
- `, [fileNodeId, node.filePath, node.filePath]);
3391
+ `, [fileNodeId, normFilePath, normFilePath]);
3389
3392
  db.run(`
3390
3393
  INSERT INTO edges (source_id, target_id, type, weight, metadata, created_at)
3391
3394
  VALUES (?, ?, 'contains', 1.0, '{}', strftime('%s','now'))
@@ -4411,6 +4414,7 @@ var init_kumaGraph = __esm({
4411
4414
  init_kumaDb();
4412
4415
  init_kumaSelfHeal();
4413
4416
  init_languageSupport();
4417
+ init_pathValidator();
4414
4418
  GRAPH_LIMITS = {
4415
4419
  maxNodes: 5e3,
4416
4420
  // Max nodes before pruning
@@ -7539,10 +7543,16 @@ function detectTestRunner(root = process.cwd()) {
7539
7543
  if (fs24.existsSync(pkgPath)) {
7540
7544
  try {
7541
7545
  const pkg = JSON.parse(fs24.readFileSync(pkgPath, "utf-8"));
7546
+ const isJest = Boolean(
7547
+ pkg.devDependencies?.jest || pkg.dependencies?.jest || pkg.scripts?.test && pkg.scripts.test.includes("jest") || fs24.existsSync(path25.join(root, "jest.config.js")) || fs24.existsSync(path25.join(root, "jest.config.ts")) || fs24.existsSync(path25.join(root, "jest.config.mjs")) || fs24.existsSync(path25.join(root, "jest.config.json"))
7548
+ );
7549
+ const isVitest = Boolean(
7550
+ pkg.devDependencies?.vitest || pkg.dependencies?.vitest || pkg.scripts?.test && pkg.scripts.test.includes("vitest") || fs24.existsSync(path25.join(root, "vitest.config.ts")) || fs24.existsSync(path25.join(root, "vitest.config.js")) || fs24.existsSync(path25.join(root, "vitest.config.mjs"))
7551
+ );
7542
7552
  if (pkg.scripts && pkg.scripts.test) {
7543
- if (fs24.existsSync(path25.join(root, "pnpm-lock.yaml"))) return { runner: "pnpm", baseCommand: "pnpm test" };
7544
- if (fs24.existsSync(path25.join(root, "yarn.lock"))) return { runner: "yarn", baseCommand: "yarn test" };
7545
- return { runner: "npm", baseCommand: "npm test" };
7553
+ if (fs24.existsSync(path25.join(root, "pnpm-lock.yaml"))) return { runner: "pnpm", baseCommand: "pnpm test", isJest, isVitest };
7554
+ if (fs24.existsSync(path25.join(root, "yarn.lock"))) return { runner: "yarn", baseCommand: "yarn test", isJest, isVitest };
7555
+ return { runner: "npm", baseCommand: "npm test", isJest, isVitest };
7546
7556
  }
7547
7557
  } catch {
7548
7558
  }
@@ -7622,7 +7632,7 @@ async function runAutoVerification(options = {}) {
7622
7632
  }
7623
7633
  }
7624
7634
  _localRunning = true;
7625
- const { runner, baseCommand } = detectTestRunner(root);
7635
+ const { runner, baseCommand, isJest, isVitest } = detectTestRunner(root);
7626
7636
  if (runner === "unknown" || !baseCommand) {
7627
7637
  _localRunning = false;
7628
7638
  releaseFileLock(root);
@@ -7644,49 +7654,88 @@ async function runAutoVerification(options = {}) {
7644
7654
  }
7645
7655
  let fullCommand = baseCommand;
7646
7656
  let testFiles = [];
7647
- const modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
7648
- let workspaceScoped = false;
7649
- try {
7650
- const { getWorkspaceInfo: getWorkspaceInfo2, getAffectedPackages: getAffectedPackages2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
7651
- const wsInfo = await getWorkspaceInfo2(root);
7652
- if (wsInfo.isWorkspace && modified.length > 0 && !options.scope) {
7653
- const affected = getAffectedPackages2(modified, wsInfo);
7654
- if (affected.affectedTestCommands.length > 0) {
7655
- fullCommand = affected.affectedTestCommands[0];
7656
- workspaceScoped = true;
7657
+ let modified = sessionMemory.getModifiedFiles().map((f) => f.filePath);
7658
+ if (modified.length === 0) {
7659
+ try {
7660
+ const { execSync: execSync6 } = await import("child_process");
7661
+ const gitDiff = execSync6("git diff --name-only HEAD", { encoding: "utf-8", timeout: 3e3, cwd: root }).trim();
7662
+ if (gitDiff) {
7663
+ modified = gitDiff.split("\n").map((f) => f.trim()).filter(Boolean);
7657
7664
  }
7665
+ } catch {
7658
7666
  }
7659
- } catch {
7660
7667
  }
7661
- if (!workspaceScoped) {
7662
- if (options.scope) {
7663
- const term = options.scope.toLowerCase();
7664
- testFiles = modified.filter((f) => f.toLowerCase().includes(term));
7665
- if (testFiles.length === 0) {
7666
- try {
7667
- const { default: glob } = await import("fast-glob");
7668
- testFiles = await glob([`**/*${term}*test*.*`, `**/*test*/*${term}*.*`], { cwd: root, ignore: ["node_modules/**", "dist/**"] });
7669
- } catch {
7668
+ const targetScope = (options.scope || options.target || "").trim();
7669
+ if (options.command) {
7670
+ const cmd = options.command.trim();
7671
+ const isTestCmd = /^(npm|pnpm|yarn|bun|npx|pytest|cargo|go|make|jest|vitest)\s+/i.test(cmd);
7672
+ if (isTestCmd) {
7673
+ fullCommand = cmd;
7674
+ }
7675
+ } else {
7676
+ let workspaceScoped = false;
7677
+ try {
7678
+ const { getWorkspaceInfo: getWorkspaceInfo2, getAffectedPackages: getAffectedPackages2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
7679
+ const wsInfo = await getWorkspaceInfo2(root);
7680
+ if (wsInfo.isWorkspace && modified.length > 0 && !targetScope) {
7681
+ const affected = getAffectedPackages2(modified, wsInfo);
7682
+ if (affected.affectedTestCommands.length > 0) {
7683
+ fullCommand = affected.affectedTestCommands[0];
7684
+ workspaceScoped = true;
7670
7685
  }
7671
7686
  }
7672
- } else {
7673
- const directTests = modified.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test."));
7674
- if (directTests.length > 0) {
7675
- testFiles = directTests;
7676
- } else if (modified.length > 0) {
7677
- try {
7678
- const { default: glob } = await import("fast-glob");
7679
- for (const f of modified.slice(0, 5)) {
7680
- const base = path25.basename(f, path25.extname(f));
7681
- const matches = await glob([`**/*${base}*.test.*`, `**/*${base}*.spec.*`], { cwd: root, ignore: ["node_modules/**", "dist/**"] });
7682
- testFiles.push(...matches);
7687
+ } catch {
7688
+ }
7689
+ if (!workspaceScoped) {
7690
+ if (targetScope && targetScope !== "session-impact") {
7691
+ const resolved = path25.resolve(root, targetScope);
7692
+ if (fs24.existsSync(resolved) && fs24.statSync(resolved).isFile()) {
7693
+ testFiles = [path25.relative(root, resolved)];
7694
+ } else {
7695
+ const term = targetScope.toLowerCase();
7696
+ testFiles = modified.filter((f) => f.toLowerCase().includes(term));
7697
+ if (testFiles.length === 0) {
7698
+ try {
7699
+ const { default: glob } = await import("fast-glob");
7700
+ const baseName = path25.basename(targetScope, path25.extname(targetScope));
7701
+ testFiles = await glob([
7702
+ `**/*${baseName}*.test.*`,
7703
+ `**/*${baseName}*.spec.*`,
7704
+ `**/*${term}*test*.*`,
7705
+ `**/*test*/*${term}*.*`,
7706
+ `**/${targetScope}`
7707
+ ], { cwd: root, ignore: ["node_modules/**", "dist/**", ".git/**"] });
7708
+ } catch {
7709
+ }
7710
+ }
7711
+ }
7712
+ } else {
7713
+ const directTests = modified.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test."));
7714
+ if (directTests.length > 0) {
7715
+ testFiles = directTests;
7716
+ } else if (modified.length > 0) {
7717
+ try {
7718
+ const { default: glob } = await import("fast-glob");
7719
+ for (const f of modified.slice(0, 5)) {
7720
+ const base = path25.basename(f, path25.extname(f));
7721
+ const matches = await glob([`**/*${base}*.test.*`, `**/*${base}*.spec.*`], { cwd: root, ignore: ["node_modules/**", "dist/**"] });
7722
+ testFiles.push(...matches);
7723
+ }
7724
+ } catch {
7683
7725
  }
7684
- } catch {
7685
7726
  }
7686
7727
  }
7687
- }
7688
- if (testFiles.length > 0 && (runner === "npm" || runner === "pnpm" || runner === "yarn")) {
7689
- fullCommand = `${baseCommand} -- ${Array.from(new Set(testFiles)).map((f) => `"${f}"`).join(" ")}`;
7728
+ if (testFiles.length > 0 && (runner === "npm" || runner === "pnpm" || runner === "yarn" || runner === "bun")) {
7729
+ fullCommand = `${baseCommand} -- ${Array.from(new Set(testFiles)).map((f) => `"${f}"`).join(" ")}`;
7730
+ } else if (testFiles.length === 0 && modified.length > 0 && isJest && (runner === "npm" || runner === "pnpm" || runner === "yarn" || runner === "bun")) {
7731
+ const relFiles = Array.from(new Set(modified.slice(0, 15))).map((f) => `"${f}"`).join(" ");
7732
+ fullCommand = `${baseCommand} -- --findRelatedTests ${relFiles}`;
7733
+ } else if (testFiles.length === 0 && modified.length > 0 && isVitest && (runner === "npm" || runner === "pnpm" || runner === "yarn" || runner === "bun")) {
7734
+ const relFiles = Array.from(new Set(modified.slice(0, 15))).map((f) => `"${f}"`).join(" ");
7735
+ fullCommand = `${baseCommand} -- related ${relFiles} --run`;
7736
+ } else if (testFiles.length > 0 && runner === "pytest") {
7737
+ fullCommand = `pytest ${Array.from(new Set(testFiles)).map((f) => `"${f}"`).join(" ")}`;
7738
+ }
7690
7739
  }
7691
7740
  }
7692
7741
  return await new Promise((resolve) => {
@@ -7761,9 +7810,11 @@ async function runAutoVerification(options = {}) {
7761
7810
  _currentProcess = null;
7762
7811
  releaseFileLock(root);
7763
7812
  clearInterval(heartbeatTimer);
7764
- const statusSymbol = passed ? "\u2705" : "\u{1F534}";
7813
+ const isTimedOut = Boolean(error && error.killed);
7814
+ const statusSymbol = passed ? "\u2705" : isTimedOut ? "\u23F1\uFE0F" : "\u{1F534}";
7815
+ const statusTitle = passed ? "PASSED" : isTimedOut ? "TIMED_OUT" : "FAILED";
7765
7816
  const lines = [
7766
- `${statusSymbol} **Verification ${passed ? "PASSED" : "FAILED"}**`,
7817
+ `${statusSymbol} **Verification ${statusTitle}**`,
7767
7818
  `\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`,
7768
7819
  "",
7769
7820
  `\u{1F6E0}\uFE0F **Runner**: \`${runner}\``,
@@ -7776,12 +7827,14 @@ async function runAutoVerification(options = {}) {
7776
7827
  if (isNoTests) {
7777
7828
  lines[0] = `\u26AA **Verification: NO TESTS**`;
7778
7829
  lines.push("\u2139\uFE0F No tests ran. Install Jest, Vitest, pytest, or similar to enable verification.");
7830
+ } else if (isTimedOut) {
7831
+ lines.push(`\u23F0 **Process was killed after ${timeoutMs}ms timeout (TIMED_OUT)**`);
7832
+ lines.push("\u2139\uFE0F Tests did not fail; the test execution exceeded the allotted timeout threshold.");
7833
+ lines.push(`\u{1F4A1} *Tip*: Run a specific test file via \`scope: 'tests/my-test.test.ts'\`, specify a custom command via \`command: '...' \`, or increase timeout via \`timeoutMs: ${Math.max(timeoutMs * 2, 12e4)}\`.`);
7834
+ if (rawOutput) lines.push("```text", rawOutput.substring(0, 1500), "```");
7779
7835
  } else {
7780
7836
  lines.push("\u26A0\uFE0F **Verification failed!** Please fix test failures before shipping.");
7781
- if (error && error.killed) {
7782
- lines.push(`\u23F0 **Process was killed after ${timeoutMs}ms timeout**`);
7783
- }
7784
- lines.push("```text", rawOutput.substring(0, 1500), "```");
7837
+ if (rawOutput) lines.push("```text", rawOutput.substring(0, 1500), "```");
7785
7838
  }
7786
7839
  } else {
7787
7840
  lines.push("\u{1F389} All scoped tests passed!");
@@ -7815,7 +7868,7 @@ var init_kumaVerifier = __esm({
7815
7868
  _localRunning = false;
7816
7869
  _currentProcess = null;
7817
7870
  STALE_RESULT_MS = 3e5;
7818
- DEFAULT_TIMEOUT_MS = 3e4;
7871
+ DEFAULT_TIMEOUT_MS = 6e4;
7819
7872
  RUNAWAY_WINDOW_MS = 3e5;
7820
7873
  RUNAWAY_MAX_CALLS = 3;
7821
7874
  _verifyCallTimestamps = [];
@@ -10255,7 +10308,8 @@ async function handleInit(_params) {
10255
10308
  lines.push("**Active Gotchas (fresh & budgeted)**");
10256
10309
  for (const g of budgetedGotchas) {
10257
10310
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
10258
- lines.push(` ${icon} [${g.severity}] ${g.filePath} \u2014 ${g.description.substring(0, 90)}`);
10311
+ const desc = g.description.length <= 180 ? g.description : g.description.substring(0, 177).replace(/\s+\S*$/, "") + "...";
10312
+ lines.push(` ${icon} [${g.severity}] ${g.filePath} \u2014 ${desc}`);
10259
10313
  }
10260
10314
  if (totalActiveCount > budgetedGotchas.length) {
10261
10315
  lines.push(` \u2139\uFE0F +${totalActiveCount - budgetedGotchas.length} more active gotchas in DB (query: kuma_memory({ action: 'gotcha' }))`);
@@ -10433,7 +10487,10 @@ async function handleHistory(params) {
10433
10487
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
10434
10488
  lines.push(` ${icon} [${g.severity}] ${g.description}`);
10435
10489
  if (g.triggerCommand) lines.push(` \u2328\uFE0F when running: \`${g.triggerCommand}\``);
10436
- if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 140)}`);
10490
+ if (g.workaround) {
10491
+ const work = g.workaround.length <= 180 ? g.workaround : g.workaround.substring(0, 177).replace(/\s+\S*$/, "") + "...";
10492
+ lines.push(` \u{1F4A1} ${work}`);
10493
+ }
10437
10494
  }
10438
10495
  lines.push("");
10439
10496
  }
@@ -10447,7 +10504,10 @@ async function handleHistory(params) {
10447
10504
  stmt.free();
10448
10505
  if (resolved.length > 0) {
10449
10506
  lines.push("\u2705 **Resolved gotchas** (fixed):");
10450
- for (const r of resolved) lines.push(` \u2705 ${r.description.substring(0, 100)}`);
10507
+ for (const r of resolved) {
10508
+ const rdesc = r.description.length <= 140 ? r.description : r.description.substring(0, 137).replace(/\s+\S*$/, "") + "...";
10509
+ lines.push(` \u2705 ${rdesc}`);
10510
+ }
10451
10511
  lines.push("");
10452
10512
  }
10453
10513
  } catch {
@@ -10554,13 +10614,15 @@ async function handleMemory(params) {
10554
10614
  }
10555
10615
  }
10556
10616
  async function handleDecision(params) {
10557
- if (!params.title) return "\u26A0\uFE0F `title` is required. Use: kuma_memory({ action: 'decision', title: '...', rationale: '...' })";
10558
- if (!params.rationale) return "\u26A0\uFE0F `rationale` is required. Why did you choose this option?";
10617
+ const title = params.title || params.content || (params.target ? `Decision regarding ${params.target}` : params.scope ? `Decision for ${params.scope}` : "");
10618
+ const rationale = params.rationale || params.description || (params.content && params.content !== title ? params.content : "");
10619
+ if (!title) return "\u26A0\uFE0F `title` is required (or provide `content`/`target`). Use: kuma_memory({ action: 'decision', title: '...', rationale: '...' })";
10620
+ if (!rationale) return "\u26A0\uFE0F `rationale` is required (or provide `content`/`description`). Why did you choose this option?";
10559
10621
  const result = await recordDecision({
10560
- title: params.title,
10561
- context: params.context || "",
10622
+ title,
10623
+ context: params.context || (params.target ? `Target: ${params.target}` : params.scope ? `Scope: ${params.scope}` : ""),
10562
10624
  options: [],
10563
- rationale: params.rationale,
10625
+ rationale,
10564
10626
  outcome: params.outcome || "implemented",
10565
10627
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10566
10628
  });
@@ -11287,13 +11349,13 @@ async function handleRollbackLabel(params) {
11287
11349
  const { rollbackToCheckpoint: rollbackToCheckpoint2 } = await Promise.resolve().then(() => (init_kumaCheckpoint(), kumaCheckpoint_exports));
11288
11350
  return await rollbackToCheckpoint2(params.label);
11289
11351
  }
11290
- var _lastVerifyCall = 0;
11291
11352
  var VERIFY_COOLDOWN_MS = 3e4;
11353
+ var _lastVerifyCall = 0;
11292
11354
  async function handleVerify(params) {
11293
11355
  const now = Date.now();
11294
- if (_lastVerifyCall > 0 && now - _lastVerifyCall < VERIFY_COOLDOWN_MS) {
11356
+ if (now - _lastVerifyCall < VERIFY_COOLDOWN_MS && !params.force) {
11295
11357
  const remaining = Math.ceil((VERIFY_COOLDOWN_MS - (now - _lastVerifyCall)) / 1e3);
11296
- 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.`;
11358
+ return `\u23F3 Verification cooldown active. Please wait ${remaining}s before calling verify again, or use force: true.`;
11297
11359
  }
11298
11360
  _lastVerifyCall = now;
11299
11361
  const recordingSummary = sessionMemory.getRecordingSummary();
@@ -11312,12 +11374,16 @@ async function handleVerify(params) {
11312
11374
 
11313
11375
  \u2705 **Recordings:** ${recordingSummary.total} total (${recordingSummary.archFlows} arch_flow, ${recordingSummary.gotchas} gotcha, ${recordingSummary.decisions} decision, ${recordingSummary.researchSaves} research_save)`;
11314
11376
  }
11315
- sessionMemory.recordToolCall("kuma_safety_verify", { scope: params.scope });
11377
+ const effectiveScope = params.scope || params.target;
11378
+ const timeoutMs = typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 : process.env.KUMA_VERIFY_TIMEOUT_MS ? parseInt(process.env.KUMA_VERIFY_TIMEOUT_MS, 10) : 6e4;
11379
+ sessionMemory.recordToolCall("kuma_safety_verify", { scope: effectiveScope, timeoutMs });
11316
11380
  const { runAutoVerification: runAutoVerification2 } = await Promise.resolve().then(() => (init_kumaVerifier(), kumaVerifier_exports));
11317
11381
  const verifyResult = await runAutoVerification2({
11318
- scope: params.scope,
11382
+ scope: effectiveScope,
11383
+ target: params.target,
11384
+ command: params.command,
11319
11385
  force: params.force,
11320
- timeoutMs: 3e4
11386
+ timeoutMs
11321
11387
  });
11322
11388
  return verifyResult + recordingWarning;
11323
11389
  }
@@ -11385,13 +11451,13 @@ var CORE_NOTE = "\n\nCORE ACTIONS (use these): ";
11385
11451
  function registerAllTools(server) {
11386
11452
  server.tool(
11387
11453
  "kuma_context",
11388
- "Context & memory recall. Call FIRST each session. Lean \u2014 each call returns only what you need." + CORE_NOTE + "`init` (start of session: project brief + session state), `research` (before editing unfamiliar code), `history` (why is this file written this way), `flow` (read a recorded architecture flow).",
11454
+ "Context & memory recall. Call FIRST each session. Lean \u2014 each call returns only what you need." + CORE_NOTE + "`init` (start of session: project brief + session state), `research` (before editing unfamiliar code), `history` (why is this file written this way), `flow` (read a recorded architecture flow), `map` (repository topology), `impact` (blast radius analysis).",
11389
11455
  {
11390
- action: z.enum(["init", "research", "history", "flow"]).describe(
11391
- "init=project brief + session restore, research=research pipeline, history=file rationale + fresh gotchas, flow=read recorded domain flow"
11456
+ action: z.enum(["init", "research", "history", "flow", "map", "impact"]).describe(
11457
+ "init=project brief + session restore, research=research pipeline, history=file rationale + fresh gotchas, flow=read recorded domain flow, map=repo topology, impact=blast radius"
11392
11458
  ),
11393
- scope: z.string().optional().describe("Research scope for research action"),
11394
- target: z.string().optional().describe("Target symbol/file for history/flow"),
11459
+ scope: z.string().optional().describe("Research scope or target for impact"),
11460
+ target: z.string().optional().describe("Target symbol/file for history/flow/impact"),
11395
11461
  goal: z.string().optional().describe("Current goal (for init)")
11396
11462
  },
11397
11463
  async (params) => {
@@ -11417,17 +11483,20 @@ function registerAllTools(server) {
11417
11483
  "gotcha=record bug/quirk, decision=ADR, arch_flow=record architecture flow, research_save=save findings, search=quick memory+graph lookup"
11418
11484
  ),
11419
11485
  scope: z.string().optional().describe("File path for gotcha \u2014 or scope for research_save/search"),
11486
+ target: z.string().optional().describe("Target file, gotcha ID, or component"),
11420
11487
  trigger_command: z.string().optional().describe("Gotcha trigger: shell command that hits this gotcha"),
11421
11488
  query: z.string().optional().describe("Search query for search action"),
11422
11489
  content: z.string().optional().describe("Content/notes for research_save / gotcha description / arch_flow record"),
11423
11490
  record: z.string().optional().describe("JSON record string for research_save"),
11424
11491
  confidence: z.number().min(0).max(1).optional().describe("Confidence for research_save (0-1)"),
11425
- title: z.string().optional().describe("Decision title (required for decision)"),
11492
+ title: z.string().optional().describe("Decision title (optional if content/target provided)"),
11426
11493
  context: z.string().optional().describe("Decision context"),
11427
- rationale: z.string().optional().describe("Decision rationale (required for decision)"),
11494
+ rationale: z.string().optional().describe("Decision rationale (optional if content provided)"),
11428
11495
  outcome: z.string().optional().describe("Decision outcome (default: implemented)"),
11429
- status: z.string().optional().describe("Gotcha severity (low|medium|high|critical)"),
11430
- description: z.string().optional().describe("Gotcha workaround"),
11496
+ status: z.string().optional().describe("Gotcha severity (low|medium|high|critical) or status (resolved|deprecated)"),
11497
+ severity: z.string().optional().describe("Gotcha severity (low|medium|high|critical)"),
11498
+ description: z.string().optional().describe("Gotcha workaround / rationale"),
11499
+ id: z.union([z.number(), z.string()]).optional().describe("Gotcha ID for resolution or deprecation"),
11431
11500
  limit: z.number().min(1).max(100).optional().describe("Result limit for search")
11432
11501
  },
11433
11502
  async (params) => {
@@ -11436,6 +11505,7 @@ function registerAllTools(server) {
11436
11505
  const text = await handleMemory({
11437
11506
  action: params.action,
11438
11507
  scope: params.scope,
11508
+ target: params.target,
11439
11509
  query: params.query,
11440
11510
  content: params.content,
11441
11511
  record: params.record,
@@ -11446,8 +11516,10 @@ function registerAllTools(server) {
11446
11516
  outcome: params.outcome,
11447
11517
  limit: params.limit,
11448
11518
  status: params.status,
11519
+ severity: params.severity,
11449
11520
  description: params.description,
11450
- trigger_command: params.trigger_command
11521
+ trigger_command: params.trigger_command,
11522
+ id: params.id
11451
11523
  });
11452
11524
  return { content: [{ type: "text", text }] };
11453
11525
  } catch (err) {
@@ -11463,7 +11535,11 @@ function registerAllTools(server) {
11463
11535
  "guard=anti-patterns/drift/loops before risky work, verify=scoped tests after edits, checkpoint=labeled snapshot before risky work, rollback_label=restore a labeled snapshot"
11464
11536
  ),
11465
11537
  scope: z.string().optional().describe("Scope for verify (e.g. 'auth', file path)"),
11466
- force: z.boolean().optional().describe("Force re-run even if cache is fresh (verify)"),
11538
+ target: z.string().optional().describe("Target test file or scope for verify"),
11539
+ command: z.string().optional().describe("Custom test command to run during verify"),
11540
+ timeoutMs: z.number().optional().describe("Custom timeout in milliseconds for verify (default: 60000)"),
11541
+ timeout: z.number().optional().describe("Custom timeout in seconds for verify"),
11542
+ force: z.boolean().optional().describe("Force re-run even if cache is fresh or rate limited (verify)"),
11467
11543
  guardGoal: z.string().optional().describe("Goal for guard check"),
11468
11544
  guardCheck: z.enum(["all", "anti-pattern", "loop", "drift", "context"]).optional().describe("Guard check type"),
11469
11545
  label: z.string().optional().describe("Checkpoint label for checkpoint/rollback_label"),
@@ -11477,6 +11553,10 @@ function registerAllTools(server) {
11477
11553
  guardGoal: params.guardGoal,
11478
11554
  guardCheck: params.guardCheck,
11479
11555
  scope: params.scope,
11556
+ target: params.target,
11557
+ command: params.command,
11558
+ timeoutMs: params.timeoutMs,
11559
+ timeout: params.timeout,
11480
11560
  force: params.force,
11481
11561
  label: params.label,
11482
11562
  description: params.description
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 🐻 Kuma MCP β€” Antigravity Lifecycle Hook
3
+ * Intercepts file writes to warn about known gotchas and fragile files.
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ function runHook() {
9
+ try {
10
+ const input = process.env.AGY_TOOL_INPUT ? JSON.parse(process.env.AGY_TOOL_INPUT) : {};
11
+ const targetFile = input.TargetFile || input.file || "";
12
+
13
+ if (!targetFile) {
14
+ process.stdout.write(JSON.stringify({ decision: "allow" }) + "\n");
15
+ return;
16
+ }
17
+
18
+ const gotchasFile = path.resolve(process.cwd(), ".kuma", "KNOWN_GOTCHAS.md");
19
+ if (fs.existsSync(gotchasFile)) {
20
+ const content = fs.readFileSync(gotchasFile, "utf-8");
21
+ const baseName = path.basename(targetFile);
22
+ if (content.includes(baseName) || content.includes(targetFile)) {
23
+ process.stdout.write(JSON.stringify({
24
+ decision: "allow",
25
+ systemMessage: `⚠️ Kuma Gotcha Warning: ${baseName} has recorded gotchas in .kuma/KNOWN_GOTCHAS.md. Review before editing!`
26
+ }) + "\n");
27
+ return;
28
+ }
29
+ }
30
+
31
+ process.stdout.write(JSON.stringify({ decision: "allow" }) + "\n");
32
+ } catch {
33
+ process.stdout.write(JSON.stringify({ decision: "allow" }) + "\n");
34
+ }
35
+ }
36
+
37
+ runHook();
package/hooks.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "kuma-safety": {
3
+ "enabled": true,
4
+ "PreToolUse": [
5
+ {
6
+ "matcher": "replace_file_content|write_to_file",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node -e \"import('fs').then(fs=>{const p=['hooks/kuma-agy-hooks.js','.agents/hooks/kuma-agy-hooks.js','../hooks/kuma-agy-hooks.js'].find(x=>fs.existsSync(x));if(p)import('./'+p);else process.stdout.write(JSON.stringify({decision:'allow'})+'\\n');}).catch(()=>process.stdout.write(JSON.stringify({decision:'allow'})+'\\n'))\"",
11
+ "timeout": 15
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
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",
@@ -16,6 +16,11 @@
16
16
  "mcp_config.json",
17
17
  "install.sh",
18
18
  "INSTALL.md",
19
+ "hooks.json",
20
+ "hooks/",
21
+ "skills/",
22
+ "commands/",
23
+ "agents/",
19
24
  ".claude-plugin/"
20
25
  ],
21
26
  "dependencies": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
3
  "name": "kuma",
4
- "version": "2.4.4",
4
+ "version": "2.4.5",
5
5
  "description": "Safety-first context & orchestration engine for AI coding agents. Shadow memory, gotcha lifecycle, blast radius impact analysis, and test verification.",
6
6
  "author": {
7
7
  "name": "plumpslabs",
@@ -11,8 +11,50 @@
11
11
  "skills": [
12
12
  {
13
13
  "name": "kuma-mcp",
14
- "description": "Kuma MCP \u2014 safety toolkit for AI coding agents",
15
- "file": ".agents/skills/kuma/SKILL.md"
14
+ "description": "Kuma MCP β€” safety toolkit for AI coding agents",
15
+ "file": "skills/kuma/SKILL.md"
16
+ }
17
+ ],
18
+ "commands": [
19
+ {
20
+ "name": "kuma:init",
21
+ "description": "Initialize Kuma session context, lean brief (<500 tokens), and branch tracking",
22
+ "file": "commands/kuma:init.md"
23
+ },
24
+ {
25
+ "name": "kuma:research",
26
+ "description": "Run 5-step research pipeline before editing unfamiliar code",
27
+ "file": "commands/kuma:research.md"
28
+ },
29
+ {
30
+ "name": "kuma:impact",
31
+ "description": "Calculate blast radius, affected consumers, and safety risk score",
32
+ "file": "commands/kuma:impact.md"
33
+ },
34
+ {
35
+ "name": "kuma:guard",
36
+ "description": "Pre-flight safety check against anti-patterns, drift, and circular deps",
37
+ "file": "commands/kuma:guard.md"
38
+ },
39
+ {
40
+ "name": "kuma:verify",
41
+ "description": "Post-edit scoped test verification and validation",
42
+ "file": "commands/kuma:verify.md"
43
+ },
44
+ {
45
+ "name": "kuma:checkpoint",
46
+ "description": "Create an atomic named snapshot before risky work",
47
+ "file": "commands/kuma:checkpoint.md"
48
+ },
49
+ {
50
+ "name": "kuma:rollback",
51
+ "description": "Restore codebase from a labeled checkpoint snapshot",
52
+ "file": "commands/kuma:rollback.md"
53
+ },
54
+ {
55
+ "name": "kuma:studio",
56
+ "description": "Launch or view Kuma Studio web dashboard",
57
+ "file": "commands/kuma:studio.md"
16
58
  }
17
59
  ]
18
60
  }
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: kuma-mcp
3
+ description: Kuma MCP β€” safety toolkit for AI coding agents
4
+ ---
5
+
6
+ Kuma MCP tools: kuma_context, kuma_memory, kuma_safety.
7
+ Record what matters, skip what doesn't.
8
+
9
+ <kuma_obedience>
10
+ ## πŸ”’ Obedience (non-negotiable)
11
+ - MUST call `kuma_context({ action: "init" })` at session start.
12
+ - MUST check blast radius `kuma_context({ action: "impact", target: "<file>" })` before touching core/shared modules.
13
+ - MUST record gotchas IMMEDIATELY when a bug/quirk is found.
14
+ - MUST call `kuma_safety({ action: "verify" })` after edits.
15
+ - MUST NOT call actions outside the 15 core actions.
16
+ </kuma_obedience>
17
+
18
+ <kuma_workflow>
19
+ 🧠 **Before coding:** `kuma_context({ action: "init" })`
20
+ πŸ—ΊοΈ **Monorepo map:** `kuma_context({ action: "map" })`
21
+ πŸ”¬ **Unfamiliar code / blast radius:** `kuma_context({ action: "research", scope: "<area>" })` or `kuma_context({ action: "impact", target: "<file>" })`
22
+ πŸ› **Found a bug/quirk:** `kuma_memory({ action: "gotcha" })` (IMMEDIATELY)
23
+ 🧭 **Chose between options:** `kuma_memory({ action: "decision" })`
24
+ πŸ”€ **Traced a flow:** `kuma_memory({ action: "arch_flow" })` (max 5 files)
25
+ πŸ›‘οΈ **Before risky work / after edits:** `kuma_safety({ action: "guard" | "verify" })`
26
+ </kuma_workflow>
27
+
28
+ πŸ“– Full rules: `.kuma/init.md`