@plumpslabs/kuma 2.4.16 → 2.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1220,8 +1220,8 @@ async function runDoctor() {
1220
1220
  } catch {
1221
1221
  }
1222
1222
  try {
1223
- const { execSync: execSync10 } = await import("child_process");
1224
- const psOutput = execSync10(
1223
+ const { execSync: execSync11 } = await import("child_process");
1224
+ const psOutput = execSync11(
1225
1225
  `ps -eo pid,ppid,command | grep -E "(jest|pnpm test|npm test|yarn test)" | grep -v grep | head -10`,
1226
1226
  { encoding: "utf-8", timeout: 5e3 }
1227
1227
  ).trim();
@@ -3366,6 +3366,33 @@ async function scanCodebase(options = {}) {
3366
3366
  }
3367
3367
  }
3368
3368
  const db = await getDb();
3369
+ const ensureFileNode = async (relPath) => {
3370
+ const fid = nodeId("file", relPath);
3371
+ try {
3372
+ const chk = db.prepare(`SELECT 1 FROM nodes WHERE id = ? LIMIT 1`);
3373
+ chk.bind([fid]);
3374
+ const exists = chk.step();
3375
+ chk.free();
3376
+ if (!exists) {
3377
+ await upsertNode({ id: fid, type: "file", name: relPath, filePath: relPath, metadata: { backbone: true } });
3378
+ result.nodeCount++;
3379
+ }
3380
+ } catch {
3381
+ }
3382
+ return fid;
3383
+ };
3384
+ const nodeExists = (id) => {
3385
+ try {
3386
+ const chk = db.prepare(`SELECT 1 FROM nodes WHERE id = ? LIMIT 1`);
3387
+ chk.bind([id]);
3388
+ const exists = chk.step();
3389
+ chk.free();
3390
+ return exists;
3391
+ } catch {
3392
+ return false;
3393
+ }
3394
+ };
3395
+ const pendingCalls = [];
3369
3396
  for (const parsed of parsedFiles) {
3370
3397
  const { filePath, symbols, imports, calledSymbols, isTest } = parsed;
3371
3398
  const fileId = nodeId("file", filePath);
@@ -3421,14 +3448,16 @@ async function scanCodebase(options = {}) {
3421
3448
  if (sym.extends) {
3422
3449
  const parentFile = symbolMap.get(sym.extends) || filePath;
3423
3450
  const parentId = `class::${parentFile}::${sym.extends}`;
3424
- await addEdge({ sourceId: symId, targetId: parentId, type: "extends" });
3425
- result.edgeCount++;
3451
+ if (nodeExists(parentId)) {
3452
+ await addEdge({ sourceId: symId, targetId: parentId, type: "extends" });
3453
+ result.edgeCount++;
3454
+ }
3426
3455
  }
3427
3456
  }
3428
3457
  for (const imp of imports) {
3429
3458
  const resolved = resolveImportPath(filePath, imp.source, root);
3430
3459
  if (resolved) {
3431
- const targetFileId = nodeId("file", resolved);
3460
+ const targetFileId = await ensureFileNode(resolved);
3432
3461
  await addEdge({
3433
3462
  sourceId: fileId,
3434
3463
  targetId: targetFileId,
@@ -3449,17 +3478,17 @@ async function scanCodebase(options = {}) {
3449
3478
  for (const callee of calledSymbols) {
3450
3479
  const calleeFile = symbolMap.get(callee);
3451
3480
  if (calleeFile && calleeFile !== filePath) {
3452
- const calleeId = `function::${calleeFile}::${callee}`;
3453
- await addEdge({
3454
- sourceId: fileId,
3455
- targetId: calleeId,
3456
- type: "calls"
3457
- });
3458
- result.edgeCount++;
3481
+ pendingCalls.push({ fileId, callee, calleeFile, filePath });
3459
3482
  }
3460
3483
  }
3461
3484
  result.filesScanned++;
3462
3485
  }
3486
+ for (const p of pendingCalls) {
3487
+ const calleeId = `function::${p.calleeFile}::${p.callee}`;
3488
+ if (!nodeExists(calleeId)) continue;
3489
+ await addEdge({ sourceId: p.fileId, targetId: calleeId, type: "calls" });
3490
+ result.edgeCount++;
3491
+ }
3463
3492
  saveDb(db);
3464
3493
  return result;
3465
3494
  }
@@ -3957,13 +3986,18 @@ var init_kumaSelfHeal = __esm({
3957
3986
  // src/engine/kumaDriftDetector.ts
3958
3987
  var kumaDriftDetector_exports = {};
3959
3988
  __export(kumaDriftDetector_exports, {
3989
+ DRIFT_REPORT_FILE: () => DRIFT_REPORT_FILE,
3990
+ DRIFT_REPORT_MAX_ITEMS: () => DRIFT_REPORT_MAX_ITEMS,
3960
3991
  computeProjectHash: () => computeProjectHash,
3961
3992
  computeScopeHash: () => computeScopeHash,
3962
3993
  detectDrift: () => detectDrift,
3963
3994
  flagStaleRecords: () => flagStaleRecords,
3964
3995
  formatDriftReport: () => formatDriftReport,
3965
3996
  getDriftSummary: () => getDriftSummary,
3966
- hashFile: () => hashFile
3997
+ hashFile: () => hashFile,
3998
+ readDriftReport: () => readDriftReport,
3999
+ toDriftActionItem: () => toDriftActionItem,
4000
+ writeDriftReport: () => writeDriftReport
3967
4001
  });
3968
4002
  import fs7 from "fs";
3969
4003
  import path9 from "path";
@@ -4184,13 +4218,51 @@ async function getDriftSummary() {
4184
4218
  if (staleCount === 0) return "\u2705 All records fresh";
4185
4219
  return `\u26A0\uFE0F ${staleCount} stale record(s) detected. Use kuma_memory({ action: 'heal' }) to repair.`;
4186
4220
  }
4187
- var computeProjectHash;
4221
+ function toDriftActionItem(r) {
4222
+ const stale = r.severity === "stale";
4223
+ const missing = r.severity === "missing";
4224
+ const action = r.source === "research_cache" ? `re-run kuma_context({ action: 'research', scope: '${r.description}' })` : r.filePath ? `rescan '${r.filePath}' (touch via any context call) or run heal` : `run kuma_memory({ action: 'heal' }) to repair`;
4225
+ return {
4226
+ id: `${r.source}#${r.id}`,
4227
+ kind: r.source,
4228
+ target: r.filePath || r.description,
4229
+ reason: missing ? "source gone (file/record missing)" : `content hash mismatch vs current (age ${r.age})`,
4230
+ action: stale || missing ? action : "monitor"
4231
+ };
4232
+ }
4233
+ async function writeDriftReport(maxItems = DRIFT_REPORT_MAX_ITEMS) {
4234
+ const fp = path9.join(getProjectRoot(), DRIFT_REPORT_FILE);
4235
+ const records = await detectDrift();
4236
+ const actionable = records.filter((r) => r.severity === "stale" || r.severity === "missing");
4237
+ const items = actionable.slice(0, maxItems).map(toDriftActionItem);
4238
+ try {
4239
+ fs7.writeFileSync(
4240
+ fp,
4241
+ JSON.stringify({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), count: actionable.length, items }, null, 2),
4242
+ "utf-8"
4243
+ );
4244
+ } catch {
4245
+ }
4246
+ return { items: items.length, path: fp };
4247
+ }
4248
+ function readDriftReport() {
4249
+ try {
4250
+ const fp = path9.join(getProjectRoot(), DRIFT_REPORT_FILE);
4251
+ if (!fs7.existsSync(fp)) return null;
4252
+ return JSON.parse(fs7.readFileSync(fp, "utf-8"));
4253
+ } catch {
4254
+ return null;
4255
+ }
4256
+ }
4257
+ var computeProjectHash, DRIFT_REPORT_FILE, DRIFT_REPORT_MAX_ITEMS;
4188
4258
  var init_kumaDriftDetector = __esm({
4189
4259
  "src/engine/kumaDriftDetector.ts"() {
4190
4260
  "use strict";
4191
4261
  init_kumaDb();
4192
4262
  init_pathValidator();
4193
4263
  computeProjectHash = (scope) => computeScopeHash(scope) || Date.now().toString(16);
4264
+ DRIFT_REPORT_FILE = ".kuma/drift.json";
4265
+ DRIFT_REPORT_MAX_ITEMS = 20;
4194
4266
  }
4195
4267
  });
4196
4268
 
@@ -4598,12 +4670,17 @@ __export(mapBackbone_exports, {
4598
4670
  ensureMapped: () => ensureMapped,
4599
4671
  extractImportSources: () => extractImportSources,
4600
4672
  formatBackbone: () => formatBackbone,
4673
+ getWorktreeChangedFiles: () => getWorktreeChangedFiles,
4601
4674
  isMapEmpty: () => isMapEmpty,
4602
- readMapMeta: () => readMapMeta
4675
+ isWorktreeDirty: () => isWorktreeDirty,
4676
+ readMapMeta: () => readMapMeta,
4677
+ readWorktreeDirty: () => readWorktreeDirty,
4678
+ updateWorktreeDirty: () => updateWorktreeDirty
4603
4679
  });
4604
4680
  import fs9 from "fs";
4605
4681
  import path11 from "path";
4606
4682
  import fastGlob3 from "fast-glob";
4683
+ import { execSync as execSync4 } from "child_process";
4607
4684
  function extractImportSources(line) {
4608
4685
  const out = [];
4609
4686
  const t = line.trim();
@@ -4743,6 +4820,52 @@ async function ensureBackbone() {
4743
4820
  function formatBackbone(r) {
4744
4821
  return `\u{1F5FA}\uFE0F Backbone: ${r.files} file(s)${r.capped ? ` (capped at ${BACKBONE_MAX_FILES})` : ""}, ${r.edges} import edge(s) in ${r.ms}ms (~${r.filesPerSec} files/s) \u2014 approximate tier, symbols fill in on demand.`;
4745
4822
  }
4823
+ function getWorktreeChangedFiles(root, cap = WORKTREE_CAP) {
4824
+ const out = [];
4825
+ try {
4826
+ const cwd = root || getProjectRoot();
4827
+ const porcelain = execSync4("git status --porcelain --untracked-files=all", {
4828
+ cwd,
4829
+ encoding: "utf-8",
4830
+ timeout: 5e3,
4831
+ stdio: ["pipe", "pipe", "pipe"]
4832
+ });
4833
+ if (!porcelain.trim()) return out;
4834
+ for (const rawLine of porcelain.split("\n")) {
4835
+ if (out.length >= cap) break;
4836
+ const line = rawLine.replace(/\s+$/, "");
4837
+ if (!line) continue;
4838
+ const m = line.match(/^(.{2})\s+(.+)$/);
4839
+ if (!m) continue;
4840
+ const code = m[1];
4841
+ let p = m[2].trim();
4842
+ const arrow = p.indexOf(" -> ");
4843
+ if (arrow >= 0) p = p.slice(arrow + 4).trim();
4844
+ p = p.replace(/^"|"$/g, "");
4845
+ if (!p) continue;
4846
+ const ext = p.slice(p.lastIndexOf(".")).toLowerCase();
4847
+ if (!WORKTREE_EXTS.has(ext)) continue;
4848
+ const untracked = code[0] === "?" || code[1] === "?";
4849
+ out.push({ file: p, state: untracked ? "untracked" : "modified" });
4850
+ }
4851
+ } catch {
4852
+ }
4853
+ return out;
4854
+ }
4855
+ function updateWorktreeDirty(files, root) {
4856
+ writeMapMeta({ worktreeDirty: [...new Set(files)].slice(0, WORKTREE_CAP), worktreeDirtyAt: Date.now() }, root);
4857
+ }
4858
+ function readWorktreeDirty(root) {
4859
+ const meta = readMapMeta(root);
4860
+ return Array.isArray(meta.worktreeDirty) ? meta.worktreeDirty.filter((f) => typeof f === "string") : [];
4861
+ }
4862
+ function isWorktreeDirty(target, root) {
4863
+ const norm = target.replace(/\\/g, "/");
4864
+ return readWorktreeDirty(root).some((f) => {
4865
+ const nf = f.replace(/\\/g, "/");
4866
+ return nf === norm || nf.endsWith(`/${norm}`) || norm.endsWith(`/${nf}`);
4867
+ });
4868
+ }
4746
4869
  async function ensureMapped(files) {
4747
4870
  const targets = [...new Set(files.map((f) => f.trim()).filter(Boolean))].slice(0, 10);
4748
4871
  if (targets.length === 0) return { filled: 0 };
@@ -4773,7 +4896,7 @@ async function ensureMapped(files) {
4773
4896
  return { filled: 0 };
4774
4897
  }
4775
4898
  }
4776
- var BACKBONE_MAX_FILES, MAP_META, IMPORT_RES;
4899
+ var BACKBONE_MAX_FILES, MAP_META, IMPORT_RES, WORKTREE_CAP, WORKTREE_EXTS;
4777
4900
  var init_mapBackbone = __esm({
4778
4901
  "src/engine/mapBackbone.ts"() {
4779
4902
  "use strict";
@@ -4788,6 +4911,31 @@ var init_mapBackbone = __esm({
4788
4911
  /(?:from)\s+["']?([a-zA-Z0-9_./-]+)["']?\s+import\s+/g,
4789
4912
  /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
4790
4913
  ];
4914
+ WORKTREE_CAP = 50;
4915
+ WORKTREE_EXTS = /* @__PURE__ */ new Set([
4916
+ ".ts",
4917
+ ".tsx",
4918
+ ".js",
4919
+ ".jsx",
4920
+ ".mjs",
4921
+ ".cjs",
4922
+ ".py",
4923
+ ".go",
4924
+ ".rs",
4925
+ ".java",
4926
+ ".rb",
4927
+ ".php",
4928
+ ".swift",
4929
+ ".kt",
4930
+ ".cs",
4931
+ ".vue",
4932
+ ".svelte",
4933
+ ".json",
4934
+ ".yml",
4935
+ ".yaml",
4936
+ ".toml",
4937
+ ".md"
4938
+ ]);
4791
4939
  }
4792
4940
  });
4793
4941
 
@@ -4799,7 +4947,7 @@ __export(symbolReach_exports, {
4799
4947
  });
4800
4948
  import fs10 from "fs";
4801
4949
  import path12 from "path";
4802
- import { execSync as execSync4 } from "child_process";
4950
+ import { execSync as execSync5 } from "child_process";
4803
4951
  async function graphReach(symbol, limit) {
4804
4952
  const rows = [];
4805
4953
  try {
@@ -4851,7 +4999,7 @@ async function graphReach(symbol, limit) {
4851
4999
  function ripgrepReach(symbol, root, limit) {
4852
5000
  const rows = [];
4853
5001
  try {
4854
- const out = execSync4(
5002
+ const out = execSync5(
4855
5003
  `grep -rn -F --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.py" --include="*.go" --include="*.rs" "${symbol.replace(/"/g, "")}" . 2>/dev/null | grep -v node_modules | grep -v "/dist/" | grep -v "/.git/" | grep -v "/.kuma/" | head -40`,
4856
5004
  { cwd: root, encoding: "utf-8", timeout: 8e3, maxBuffer: 256 * 1024 }
4857
5005
  ).trim();
@@ -5026,7 +5174,7 @@ __export(impactAnalysis_exports, {
5026
5174
  });
5027
5175
  import fs11 from "fs";
5028
5176
  import path13 from "path";
5029
- import { execSync as execSync5 } from "child_process";
5177
+ import { execSync as execSync6 } from "child_process";
5030
5178
  import fastGlob4 from "fast-glob";
5031
5179
  function extOf(target) {
5032
5180
  const base = path13.basename(target);
@@ -5047,7 +5195,7 @@ function classifyDiffShape(target, root) {
5047
5195
  }
5048
5196
  try {
5049
5197
  const cwd = root || getProjectRoot();
5050
- const diff = execSync5(`git diff --unified=0 -- "${target}"`, {
5198
+ const diff = execSync6(`git diff --unified=0 -- "${target}"`, {
5051
5199
  cwd,
5052
5200
  encoding: "utf-8",
5053
5201
  timeout: 4e3,
@@ -5313,6 +5461,14 @@ async function calculateBlastRadius(target, options = {}) {
5313
5461
  const rev = getReverseDependencies(owningPackage.name, wsInfo);
5314
5462
  downstreamPackages.push(...rev.map((p) => p.name));
5315
5463
  }
5464
+ let worktreeNote = "";
5465
+ try {
5466
+ const { isWorktreeDirty: isWorktreeDirty2 } = await Promise.resolve().then(() => (init_mapBackbone(), mapBackbone_exports));
5467
+ if (targetType === "file" && isWorktreeDirty2(target, root)) {
5468
+ worktreeNote = "\u{1F4DD} Uncommitted worktree changes on this target \u2014 map reflects work in progress, not committed state";
5469
+ }
5470
+ } catch {
5471
+ }
5316
5472
  if (targetType === "file") {
5317
5473
  try {
5318
5474
  const { ensureMapped: ensureMapped2 } = await Promise.resolve().then(() => (init_mapBackbone(), mapBackbone_exports));
@@ -5380,6 +5536,7 @@ async function calculateBlastRadius(target, options = {}) {
5380
5536
  `\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`,
5381
5537
  `\u26A0\uFE0F **Risk Level: ${risk.toUpperCase()}**`,
5382
5538
  `\u{1F4D0} Diff shape: \`${diffShape}\` (${shapeDetail})`,
5539
+ ...worktreeNote ? [worktreeNote] : [],
5383
5540
  owningPackage ? `\u{1F4E6} Owning package: \`${owningPackage.name}\` (${owningPackage.path})` : ""
5384
5541
  ];
5385
5542
  if (riskFlags.length > 0) {
@@ -5643,6 +5800,7 @@ __export(kumaGraph_exports, {
5643
5800
  getGraphStats: () => getGraphStats,
5644
5801
  nodeId: () => nodeId,
5645
5802
  propagateImpact: () => propagateImpact,
5803
+ pruneOrphans: () => pruneOrphans,
5646
5804
  queryGraph: () => queryGraph,
5647
5805
  recordAffects: () => recordAffects,
5648
5806
  recordApiRoute: () => recordApiRoute,
@@ -5719,6 +5877,48 @@ async function pruneGraphIfNeeded() {
5719
5877
  return { pruned: false, removedNodes: 0, removedEdges: 0 };
5720
5878
  }
5721
5879
  }
5880
+ async function pruneOrphans(opts = {}) {
5881
+ const olderThanSec = opts.olderThanSec ?? 3600;
5882
+ const dryRun = opts.dryRun ?? false;
5883
+ let orphanNodes = 0;
5884
+ let danglingEdges = 0;
5885
+ try {
5886
+ const db = await getDb();
5887
+ const cutoff = Math.floor(Date.now() / 1e3) - olderThanSec;
5888
+ const dangling = db.exec(`
5889
+ SELECT COUNT(*) FROM edges e
5890
+ WHERE NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = e.target_id)
5891
+ OR NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = e.source_id)
5892
+ `);
5893
+ danglingEdges = Number(dangling[0]?.values?.[0]?.[0]) || 0;
5894
+ if (!dryRun && danglingEdges > 0) {
5895
+ db.exec(`
5896
+ DELETE FROM edges
5897
+ WHERE NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = edges.target_id)
5898
+ OR NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = edges.source_id)
5899
+ `);
5900
+ }
5901
+ const orphans = db.exec(`
5902
+ SELECT COUNT(*) FROM nodes n
5903
+ WHERE n.type IN ('file','function','class','method','interface','test','feature_domain','arch_flow')
5904
+ AND n.updated_at < ${cutoff}
5905
+ AND NOT EXISTS (SELECT 1 FROM edges e WHERE e.source_id = n.id OR e.target_id = n.id)
5906
+ `);
5907
+ orphanNodes = Number(orphans[0]?.values?.[0]?.[0]) || 0;
5908
+ if (!dryRun && orphanNodes > 0) {
5909
+ db.exec(`
5910
+ DELETE FROM nodes
5911
+ WHERE type IN ('file','function','class','method','interface','test','feature_domain','arch_flow')
5912
+ AND updated_at < ${cutoff}
5913
+ AND id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges)
5914
+ `);
5915
+ }
5916
+ if (!dryRun && (orphanNodes > 0 || danglingEdges > 0)) saveDb(db);
5917
+ } catch (err) {
5918
+ console.error(`[KumaGraph] pruneOrphans failed: ${err}`);
5919
+ }
5920
+ return { orphanNodes, danglingEdges };
5921
+ }
5722
5922
  async function upsertNode(node) {
5723
5923
  try {
5724
5924
  const db = await getDb();
@@ -5833,6 +6033,9 @@ async function recordApiRoute(route, handler) {
5833
6033
  async function recordDomainFlow(params) {
5834
6034
  let nodeCount = 0;
5835
6035
  let edgeCount = 0;
6036
+ if (!params.hops || params.hops.length === 0) {
6037
+ return { nodeCount, edgeCount };
6038
+ }
5836
6039
  try {
5837
6040
  const domainId = nodeId("feature_domain", params.domain);
5838
6041
  let fileHashes = {};
@@ -6712,9 +6915,9 @@ ${summary}`;
6712
6915
  }
6713
6916
  async function codebaseImpactFallback(target) {
6714
6917
  try {
6715
- const { execSync: execSync10 } = await import("child_process");
6918
+ const { execSync: execSync11 } = await import("child_process");
6716
6919
  const root = process.cwd();
6717
- const grepResult = execSync10(
6920
+ const grepResult = execSync11(
6718
6921
  `grep -rn ${grepIncludeFlags()} --include="*.json" --include="*.md" -l "${target.replace(/"/g, '\\"')}" . 2>/dev/null | grep -v node_modules | grep -v .git | grep -v dist | grep -v .kuma | head -50`,
6719
6922
  { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
6720
6923
  ).trim();
@@ -6860,7 +7063,7 @@ __export(kumaShared_exports, {
6860
7063
  isReadTool: () => isReadTool,
6861
7064
  isTestTool: () => isTestTool
6862
7065
  });
6863
- import { execSync as execSync6 } from "child_process";
7066
+ import { execSync as execSync7 } from "child_process";
6864
7067
  import fs12 from "fs";
6865
7068
  import path14 from "path";
6866
7069
  function isEditTool(toolName) {
@@ -6900,7 +7103,7 @@ function getSessionStats(inputGoal) {
6900
7103
  function getGitDiffStat(timeout = 3e3) {
6901
7104
  try {
6902
7105
  const root = getProjectRoot();
6903
- return execSync6("git diff --stat", {
7106
+ return execSync7("git diff --stat", {
6904
7107
  cwd: root,
6905
7108
  encoding: "utf-8",
6906
7109
  timeout
@@ -6913,13 +7116,16 @@ function getUncommittedFiles(timeout = 3e3) {
6913
7116
  const files = /* @__PURE__ */ new Set();
6914
7117
  try {
6915
7118
  const root = getProjectRoot();
6916
- const porcelain = execSync6("git status --porcelain", {
7119
+ const porcelain = execSync7("git status --porcelain", {
6917
7120
  cwd: root,
6918
7121
  encoding: "utf-8",
6919
7122
  timeout,
6920
7123
  stdio: ["pipe", "pipe", "pipe"]
6921
- }).trim();
6922
- for (const line of porcelain.split("\n")) {
7124
+ });
7125
+ if (!porcelain.trim()) return files;
7126
+ for (const rawLine of porcelain.split("\n")) {
7127
+ const line = rawLine.replace(/\s+$/, "");
7128
+ if (!line) continue;
6923
7129
  const m = line.match(/^.{3}(.+?)(?:\s+->\s+.+)?$/);
6924
7130
  if (m && m[1]) files.add(m[1].trim().replace(/^"|"$/g, ""));
6925
7131
  }
@@ -6932,17 +7138,17 @@ function getBranchCommittedFiles(timeout = 5e3) {
6932
7138
  let base = "";
6933
7139
  try {
6934
7140
  const root = getProjectRoot();
6935
- const branch = execSync6("git rev-parse --abbrev-ref HEAD", {
7141
+ const branch = execSync7("git rev-parse --abbrev-ref HEAD", {
6936
7142
  cwd: root,
6937
7143
  encoding: "utf-8",
6938
7144
  timeout,
6939
7145
  stdio: ["pipe", "pipe", "pipe"]
6940
7146
  }).trim();
6941
- const baseRef = execSync6(
7147
+ const baseRef = execSync7(
6942
7148
  `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo origin/${branch === "main" || branch === "master" ? "HEAD" : "main"}`,
6943
7149
  { cwd: root, encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
6944
7150
  ).trim().replace("refs/remotes/", "");
6945
- const mergeBase = execSync6(`git merge-base HEAD ${baseRef} 2>/dev/null || git rev-list --max-parents=0 HEAD`, {
7151
+ const mergeBase = execSync7(`git merge-base HEAD ${baseRef} 2>/dev/null || git rev-list --max-parents=0 HEAD`, {
6946
7152
  cwd: root,
6947
7153
  encoding: "utf-8",
6948
7154
  timeout,
@@ -6950,7 +7156,7 @@ function getBranchCommittedFiles(timeout = 5e3) {
6950
7156
  }).trim().split("\n")[0];
6951
7157
  base = baseRef;
6952
7158
  if (mergeBase) {
6953
- const out = execSync6(`git diff --name-only ${mergeBase} HEAD`, {
7159
+ const out = execSync7(`git diff --name-only ${mergeBase} HEAD`, {
6954
7160
  cwd: root,
6955
7161
  encoding: "utf-8",
6956
7162
  timeout,
@@ -7074,7 +7280,7 @@ function detectTestWeakening(files, root) {
7074
7280
  for (const f of files) {
7075
7281
  if (!TEST_FILE_PATTERN.test(f)) continue;
7076
7282
  try {
7077
- const diff = execSync6(`git diff --unified=0 -- "${f}"`, {
7283
+ const diff = execSync7(`git diff --unified=0 -- "${f}"`, {
7078
7284
  cwd,
7079
7285
  encoding: "utf-8",
7080
7286
  timeout: 4e3,
@@ -10307,7 +10513,7 @@ __export(docDrift_exports, {
10307
10513
  });
10308
10514
  import fs29 from "fs";
10309
10515
  import path32 from "path";
10310
- import { execSync as execSync9 } from "child_process";
10516
+ import { execSync as execSync10 } from "child_process";
10311
10517
  function collectDocFiles(root) {
10312
10518
  const cwd = root || getProjectRoot();
10313
10519
  const out = [];
@@ -10366,7 +10572,7 @@ function codeContainsBatch(root, needles) {
10366
10572
  const chunk = clean.slice(i, i + 50);
10367
10573
  try {
10368
10574
  const args = chunk.map((n) => `-e "${n}"`).join(" ");
10369
- const out = execSync9(
10575
+ const out = execSync10(
10370
10576
  `grep -rn --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.py" --include="*.go" --include="*.rs" --include="*.json" --include="*.yml" --include="*.yaml" --include="*.toml" --include=".env*" -F ${args} . 2>/dev/null | grep -v node_modules | grep -v "/dist/" | grep -v "/.git/" | grep -v "/.kuma/" | head -100`,
10371
10577
  { cwd: root, encoding: "utf-8", timeout: 8e3, maxBuffer: 256 * 1024 }
10372
10578
  ).trim();
@@ -10570,14 +10776,14 @@ async function createCheckpoint(label, description) {
10570
10776
  let gitHead;
10571
10777
  let isGitClean = false;
10572
10778
  try {
10573
- const { execSync: execSync10 } = await import("child_process");
10574
- gitHead = execSync10("git rev-parse HEAD", {
10779
+ const { execSync: execSync11 } = await import("child_process");
10780
+ gitHead = execSync11("git rev-parse HEAD", {
10575
10781
  cwd: root,
10576
10782
  encoding: "utf-8",
10577
10783
  timeout: 3e3,
10578
10784
  stdio: ["pipe", "pipe", "pipe"]
10579
10785
  }).trim();
10580
- const statusOut = execSync10("git status --porcelain", {
10786
+ const statusOut = execSync11("git status --porcelain", {
10581
10787
  cwd: root,
10582
10788
  encoding: "utf-8",
10583
10789
  timeout: 3e3,
@@ -10588,8 +10794,8 @@ async function createCheckpoint(label, description) {
10588
10794
  }
10589
10795
  const candidatePaths = /* @__PURE__ */ new Set();
10590
10796
  try {
10591
- const { execSync: execSync10 } = await import("child_process");
10592
- const gitOut = execSync10("git status --porcelain", {
10797
+ const { execSync: execSync11 } = await import("child_process");
10798
+ const gitOut = execSync11("git status --porcelain", {
10593
10799
  cwd: root,
10594
10800
  encoding: "utf-8",
10595
10801
  timeout: 4e3,
@@ -10718,8 +10924,8 @@ ${listCheckpoints()}`;
10718
10924
  }
10719
10925
  if (manifest.files.length === 0 && manifest.gitHead) {
10720
10926
  try {
10721
- const { execSync: execSync10 } = await import("child_process");
10722
- execSync10(`git checkout ${manifest.gitHead} -- .`, {
10927
+ const { execSync: execSync11 } = await import("child_process");
10928
+ execSync11(`git checkout ${manifest.gitHead} -- .`, {
10723
10929
  cwd: root,
10724
10930
  encoding: "utf-8",
10725
10931
  timeout: 8e3,
@@ -11266,6 +11472,29 @@ function opencodePluginTemplate() {
11266
11472
  " },",
11267
11473
  " };",
11268
11474
  "};",
11475
+ "",
11476
+ "// Server-runtime definition (required by newer OpenCode server).",
11477
+ "// Reuses the same hooks via the named export above.",
11478
+ "const kumaHooks = {",
11479
+ ' "tool.execute.before": async (input, output) => {',
11480
+ " const hooks = await KumaPlugin();",
11481
+ ' return hooks["tool.execute.before"](input, output);',
11482
+ " },",
11483
+ ' "file.edited": async (input) => {',
11484
+ " const hooks = await KumaPlugin();",
11485
+ ' return hooks["file.edited"](input);',
11486
+ " },",
11487
+ ' "experimental.session.compacting": async (input, output) => {',
11488
+ " const hooks = await KumaPlugin();",
11489
+ ' return hooks["experimental.session.compacting"](input, output);',
11490
+ " },",
11491
+ "};",
11492
+ "",
11493
+ "export default {",
11494
+ ' id: "kuma",',
11495
+ " setup: async () => kumaHooks,",
11496
+ " effect: async () => kumaHooks,",
11497
+ "};",
11269
11498
  ""
11270
11499
  ].join("\n");
11271
11500
  }
@@ -12769,6 +12998,20 @@ async function daemonTick(root) {
12769
12998
  } catch (err) {
12770
12999
  notes.push(`map: skipped (${err})`);
12771
13000
  }
13001
+ try {
13002
+ const { getWorktreeChangedFiles: getWorktreeChangedFiles2, updateWorktreeDirty: updateWorktreeDirty2 } = await Promise.resolve().then(() => (init_mapBackbone(), mapBackbone_exports));
13003
+ const { scanCodebase: scanCodebase2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
13004
+ const dirty = getWorktreeChangedFiles2(r, 50);
13005
+ if (dirty.length > 0) {
13006
+ const res = await scanCodebase2({ include: dirty.map((d) => d.file), force: true, maxFiles: dirty.length });
13007
+ updateWorktreeDirty2(dirty.map((d) => d.file), r);
13008
+ notes.push(`worktree: ${dirty.length} dirty forced (${res.filesScanned} scanned)`);
13009
+ } else {
13010
+ updateWorktreeDirty2([], r);
13011
+ }
13012
+ } catch (err) {
13013
+ notes.push(`worktree: skipped (${err})`);
13014
+ }
12772
13015
  try {
12773
13016
  const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
12774
13017
  const { count } = await refreshPackageBriefs2();
@@ -12784,6 +13027,20 @@ async function daemonTick(root) {
12784
13027
  } catch {
12785
13028
  notes.push("drift: skipped");
12786
13029
  }
13030
+ try {
13031
+ const { writeDriftReport: writeDriftReport2 } = await Promise.resolve().then(() => (init_kumaDriftDetector(), kumaDriftDetector_exports));
13032
+ const { items } = await writeDriftReport2();
13033
+ notes.push(`drift-report: ${items} item(s) \u2192 .kuma/drift.json`);
13034
+ } catch {
13035
+ }
13036
+ try {
13037
+ const { pruneOrphans: pruneOrphans2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
13038
+ const { orphanNodes, danglingEdges } = await pruneOrphans2({ olderThanSec: 3600 });
13039
+ if (orphanNodes > 0 || danglingEdges > 0) {
13040
+ notes.push(`orphans: -${orphanNodes}n/-${danglingEdges}e pruned`);
13041
+ }
13042
+ } catch {
13043
+ }
12787
13044
  try {
12788
13045
  const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12789
13046
  clearDirty2();
@@ -14023,10 +14280,10 @@ async function handleHistory(params) {
14023
14280
  lines.push(traceFormatted);
14024
14281
  } else {
14025
14282
  try {
14026
- const { execSync: execSync10 } = await import("child_process");
14283
+ const { execSync: execSync11 } = await import("child_process");
14027
14284
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
14028
14285
  const root = getProjectRoot2();
14029
- const gitHistory = execSync10(
14286
+ const gitHistory = execSync11(
14030
14287
  `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
14031
14288
  { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
14032
14289
  ).trim();
@@ -14457,7 +14714,7 @@ init_sessionMemory();
14457
14714
  init_kumaShared();
14458
14715
  import fs26 from "fs";
14459
14716
  import path29 from "path";
14460
- import { execSync as execSync7 } from "child_process";
14717
+ import { execSync as execSync8 } from "child_process";
14461
14718
  var SCRIPT_PATCH_PATTERNS = [
14462
14719
  "writeFileSync",
14463
14720
  "writeFile",
@@ -14551,7 +14808,7 @@ function scanRecentFiles(projectRoot) {
14551
14808
  function detectGitPatchScripts(projectRoot) {
14552
14809
  const warnings = [];
14553
14810
  try {
14554
- const statusStdout = execSync7("git status --porcelain", {
14811
+ const statusStdout = execSync8("git status --porcelain", {
14555
14812
  cwd: projectRoot,
14556
14813
  encoding: "utf-8",
14557
14814
  timeout: 3e3
@@ -14584,7 +14841,7 @@ function detectGitPatchScripts(projectRoot) {
14584
14841
  }
14585
14842
  }
14586
14843
  }
14587
- const deletedStdout = execSync7("git diff --name-only --diff-filter=D HEAD", {
14844
+ const deletedStdout = execSync8("git diff --name-only --diff-filter=D HEAD", {
14588
14845
  cwd: projectRoot,
14589
14846
  encoding: "utf-8",
14590
14847
  timeout: 3e3
@@ -14656,7 +14913,7 @@ init_sessionMemory();
14656
14913
  init_pathValidator();
14657
14914
  import fs27 from "fs";
14658
14915
  import path30 from "path";
14659
- import { execSync as execSync8 } from "child_process";
14916
+ import { execSync as execSync9 } from "child_process";
14660
14917
  var SNAPSHOT_DIR = "context-snapshots";
14661
14918
  function snapshotDir() {
14662
14919
  return path30.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
@@ -14728,7 +14985,7 @@ function formatSnapshot(snapshot) {
14728
14985
  function getGitDiffStat2() {
14729
14986
  try {
14730
14987
  const root = getProjectRoot();
14731
- const stdout = execSync8("git diff --stat", {
14988
+ const stdout = execSync9("git diff --stat", {
14732
14989
  cwd: root,
14733
14990
  encoding: "utf-8",
14734
14991
  timeout: 3e3
@@ -15513,25 +15770,25 @@ async function main() {
15513
15770
  } catch {
15514
15771
  }
15515
15772
  try {
15516
- const { execSync: execSync10 } = await import("child_process");
15773
+ const { execSync: execSync11 } = await import("child_process");
15517
15774
  try {
15518
- execSync10("pkill -f 'jest' 2>/dev/null || true");
15775
+ execSync11("pkill -f 'jest' 2>/dev/null || true");
15519
15776
  } catch {
15520
15777
  }
15521
15778
  try {
15522
- execSync10("pkill -f 'jest-worker' 2>/dev/null || true");
15779
+ execSync11("pkill -f 'jest-worker' 2>/dev/null || true");
15523
15780
  } catch {
15524
15781
  }
15525
15782
  try {
15526
- execSync10("pkill -f 'pnpm test' 2>/dev/null || true");
15783
+ execSync11("pkill -f 'pnpm test' 2>/dev/null || true");
15527
15784
  } catch {
15528
15785
  }
15529
15786
  try {
15530
- execSync10("pkill -f 'npm test' 2>/dev/null || true");
15787
+ execSync11("pkill -f 'npm test' 2>/dev/null || true");
15531
15788
  } catch {
15532
15789
  }
15533
15790
  try {
15534
- execSync10("pkill -f 'yarn test' 2>/dev/null || true");
15791
+ execSync11("pkill -f 'yarn test' 2>/dev/null || true");
15535
15792
  } catch {
15536
15793
  }
15537
15794
  console.error("\u2705 Killed orphaned test processes");
@@ -15680,6 +15937,12 @@ ${context}` : "";
15680
15937
  await refreshPackageBriefs2();
15681
15938
  } catch {
15682
15939
  }
15940
+ try {
15941
+ const { getWorktreeChangedFiles: getWorktreeChangedFiles2, updateWorktreeDirty: updateWorktreeDirty2 } = await Promise.resolve().then(() => (init_mapBackbone(), mapBackbone_exports));
15942
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
15943
+ updateWorktreeDirty2(getWorktreeChangedFiles2(getProjectRoot2(), 50).map((d) => d.file), getProjectRoot2());
15944
+ } catch {
15945
+ }
15683
15946
  try {
15684
15947
  const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
15685
15948
  clearDirty2();
@@ -15757,8 +16020,8 @@ ${line}
15757
16020
  } catch {
15758
16021
  }
15759
16022
  try {
15760
- const { execSync: execSync10 } = await import("child_process");
15761
- const branch = execSync10("git rev-parse --abbrev-ref HEAD", {
16023
+ const { execSync: execSync11 } = await import("child_process");
16024
+ const branch = execSync11("git rev-parse --abbrev-ref HEAD", {
15762
16025
  cwd: root,
15763
16026
  encoding: "utf-8",
15764
16027
  timeout: 2e3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.4.16",
3
+ "version": "2.4.19",
4
4
  "description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
3
  "name": "kuma",
4
- "version": "2.4.16",
4
+ "version": "2.4.19",
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",