@plumpslabs/kuma 2.4.13 → 2.4.15

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
@@ -628,7 +628,11 @@ function createSchema(db) {
628
628
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
629
629
  last_verified_at INTEGER,
630
630
  scope_package TEXT,
631
- verified_by TEXT
631
+ verified_by TEXT,
632
+ provenance TEXT,
633
+ tier TEXT NOT NULL DEFAULT 'single'
634
+ CHECK(tier IN ('human-confirmed','corroborated','single','external')),
635
+ quarantined INTEGER NOT NULL DEFAULT 0
632
636
  );
633
637
  `);
634
638
  try {
@@ -652,6 +656,15 @@ function createSchema(db) {
652
656
  if (!cols.includes("verified_by")) {
653
657
  db.run(`ALTER TABLE known_gotchas ADD COLUMN verified_by TEXT`);
654
658
  }
659
+ if (!cols.includes("provenance")) {
660
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN provenance TEXT`);
661
+ }
662
+ if (!cols.includes("tier")) {
663
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN tier TEXT NOT NULL DEFAULT 'single'`);
664
+ }
665
+ if (!cols.includes("quarantined")) {
666
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN quarantined INTEGER NOT NULL DEFAULT 0`);
667
+ }
655
668
  const schema = db.exec(`SELECT sql FROM sqlite_master WHERE type='table' AND name='known_gotchas'`);
656
669
  const gotchaSql = schema[0]?.values?.[0]?.[0] || "";
657
670
  if (gotchaSql && !gotchaSql.includes("candidate")) {
@@ -672,7 +685,11 @@ function createSchema(db) {
672
685
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
673
686
  last_verified_at INTEGER,
674
687
  scope_package TEXT,
675
- verified_by TEXT
688
+ verified_by TEXT,
689
+ provenance TEXT,
690
+ tier TEXT NOT NULL DEFAULT 'single'
691
+ CHECK(tier IN ('human-confirmed','corroborated','single','external')),
692
+ quarantined INTEGER NOT NULL DEFAULT 0
676
693
  )`);
677
694
  db.run(`INSERT OR IGNORE INTO known_gotchas (id, file_path, description, severity, workaround, added_by, created_at, updated_at, content_hash, trigger_command, status, last_verified_at) SELECT id, file_path, description, severity, workaround, added_by, created_at, updated_at, content_hash, trigger_command, status, last_verified_at FROM known_gotchas_old`);
678
695
  db.run(`DROP TABLE known_gotchas_old`);
@@ -1203,8 +1220,8 @@ async function runDoctor() {
1203
1220
  } catch {
1204
1221
  }
1205
1222
  try {
1206
- const { execSync: execSync7 } = await import("child_process");
1207
- const psOutput = execSync7(
1223
+ const { execSync: execSync10 } = await import("child_process");
1224
+ const psOutput = execSync10(
1208
1225
  `ps -eo pid,ppid,command | grep -E "(jest|pnpm test|npm test|yarn test)" | grep -v grep | head -10`,
1209
1226
  { encoding: "utf-8", timeout: 5e3 }
1210
1227
  ).trim();
@@ -2134,6 +2151,23 @@ No unresolved issues.`;
2134
2151
  this.state.metrics.filesEdited++;
2135
2152
  this.save();
2136
2153
  }
2154
+ // ============================================================
2155
+ // MICROCOMPACT — session-scoped shown-set (in-memory only)
2156
+ // ============================================================
2157
+ // Hook processes are separate OS processes, so cross-hook dedupe lives
2158
+ // in hook-state.json (15min window). THIS set covers the MCP server
2159
+ // process: same gotcha/decision text is never injected twice in one
2160
+ // server run — repeats collapse to zero tokens.
2161
+ shownKeys = /* @__PURE__ */ new Set();
2162
+ /** True when this exact payload was already injected this server run. */
2163
+ wasShown(kind, key) {
2164
+ return this.shownKeys.has(`${kind}::${key}`);
2165
+ }
2166
+ /** Mark payloads as injected. Bounded at 500 keys (FIFO-ish reset). */
2167
+ markShown(kind, keys) {
2168
+ for (const k of keys) this.shownKeys.add(`${kind}::${k}`);
2169
+ if (this.shownKeys.size > 500) this.shownKeys = new Set([...this.shownKeys].slice(-500));
2170
+ }
2137
2171
  /**
2138
2172
  * Get metrics summary for session.
2139
2173
  */
@@ -3217,6 +3251,18 @@ function parseFallbackRegex(filePath, content) {
3217
3251
  if (impMatch && (impMatch[1].startsWith(".") || impMatch[1].startsWith("/"))) {
3218
3252
  result.imports.push({ source: impMatch[1], symbols: [], isDefault: false });
3219
3253
  }
3254
+ if (!fnMatch && !clsMatch) {
3255
+ const callRe = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
3256
+ let m;
3257
+ let count = 0;
3258
+ while ((m = callRe.exec(line)) !== null && count < 5) {
3259
+ const name = m[1];
3260
+ if (!isJsBuiltin(name) && !result.calledSymbols.includes(name)) {
3261
+ result.calledSymbols.push(name);
3262
+ count++;
3263
+ }
3264
+ }
3265
+ }
3220
3266
  }
3221
3267
  return result;
3222
3268
  }
@@ -4543,16 +4589,283 @@ var init_workspaceIntelligence = __esm({
4543
4589
  }
4544
4590
  });
4545
4591
 
4592
+ // src/engine/symbolReach.ts
4593
+ var symbolReach_exports = {};
4594
+ __export(symbolReach_exports, {
4595
+ formatReachability: () => formatReachability,
4596
+ symbolReachability: () => symbolReachability
4597
+ });
4598
+ import fs9 from "fs";
4599
+ import path11 from "path";
4600
+ import { execSync as execSync4 } from "child_process";
4601
+ async function graphReach(symbol, limit) {
4602
+ const rows = [];
4603
+ try {
4604
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
4605
+ const db = await getDb2();
4606
+ const symStmt = db.prepare(
4607
+ `SELECT id, file_path FROM nodes WHERE type IN ('function','class','method') AND (name = ? OR name LIKE ?) LIMIT 5`
4608
+ );
4609
+ symStmt.bind([symbol, `%::${symbol}`]);
4610
+ const symIds = [];
4611
+ while (symStmt.step()) {
4612
+ const r = symStmt.getAsObject();
4613
+ symIds.push({ id: r.id, file: r.file_path });
4614
+ }
4615
+ symStmt.free();
4616
+ if (symIds.length === 0) return [];
4617
+ for (const s of symIds) {
4618
+ const cStmt = db.prepare(
4619
+ `SELECT DISTINCT n_src.file_path as fp FROM edges e JOIN nodes n_src ON n_src.id = e.source_id WHERE e.target_id = ? AND e.type = 'calls' LIMIT 20`
4620
+ );
4621
+ cStmt.bind([s.id]);
4622
+ const callers = /* @__PURE__ */ new Set();
4623
+ while (cStmt.step()) {
4624
+ const fp = cStmt.getAsObject().fp;
4625
+ if (fp) callers.add(fp);
4626
+ }
4627
+ cStmt.free();
4628
+ for (const fp of [...callers, s.file]) {
4629
+ const tStmt = db.prepare(
4630
+ `SELECT DISTINCT n.file_path as tfp FROM edges e JOIN nodes n ON n.id = e.source_id WHERE e.type = 'tests' AND (e.target_id LIKE ? OR e.target_id LIKE ?) LIMIT 5`
4631
+ );
4632
+ tStmt.bind([`%${fp}%`, `%${s.id}%`]);
4633
+ while (tStmt.step()) {
4634
+ const tfp = tStmt.getAsObject().tfp;
4635
+ if (tfp && !rows.some((r) => r.test === tfp)) {
4636
+ rows.push({ test: tfp, via: fp, evidence: `${tfp} tests ${fp} (graph edge)` });
4637
+ }
4638
+ if (rows.length >= limit) break;
4639
+ }
4640
+ tStmt.free();
4641
+ if (rows.length >= limit) break;
4642
+ }
4643
+ if (rows.length >= limit) break;
4644
+ }
4645
+ } catch {
4646
+ }
4647
+ return rows;
4648
+ }
4649
+ function ripgrepReach(symbol, root, limit) {
4650
+ const rows = [];
4651
+ try {
4652
+ const out = execSync4(
4653
+ `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`,
4654
+ { cwd: root, encoding: "utf-8", timeout: 8e3, maxBuffer: 256 * 1024 }
4655
+ ).trim();
4656
+ if (!out) return [];
4657
+ const callers = /* @__PURE__ */ new Set();
4658
+ for (const line of out.split("\n")) {
4659
+ const m = line.match(/^([^:]+):(\d+):(.*)$/);
4660
+ if (!m) continue;
4661
+ const [, fp, ln, code] = m;
4662
+ if (new RegExp(`\\b${symbol}\\s*\\(`).test(code)) callers.add(`${fp}:${ln}`);
4663
+ }
4664
+ for (const c of [...callers].slice(0, BERBEDA)) {
4665
+ const [fp] = c.split(":");
4666
+ const dir = path11.dirname(fp);
4667
+ const stem = path11.basename(fp, path11.extname(fp)).split(/[-_.]/)[0];
4668
+ try {
4669
+ const dirFull = path11.join(root, dir);
4670
+ if (!fs9.existsSync(dirFull)) continue;
4671
+ for (const f of fs9.readdirSync(dirFull)) {
4672
+ if (!/(\.test\.|\.spec\.|_test\.)/i.test(f)) continue;
4673
+ const low = f.toLowerCase();
4674
+ if (low.includes(stem.toLowerCase()) || low.includes(symbol.toLowerCase())) {
4675
+ const rel = path11.join(dir, f);
4676
+ if (!rows.some((r) => r.test === rel)) {
4677
+ rows.push({ test: rel, via: fp, evidence: `${rel} sibling-matches caller ${fp} (called at ${c})` });
4678
+ }
4679
+ }
4680
+ if (rows.length >= limit) break;
4681
+ }
4682
+ } catch {
4683
+ }
4684
+ if (rows.length >= limit) break;
4685
+ }
4686
+ } catch {
4687
+ }
4688
+ return rows;
4689
+ }
4690
+ async function symbolReachability(symbol, limit = 10, root) {
4691
+ const g = await graphReach(symbol, limit);
4692
+ if (g.length > 0) return { tier: 1, rows: g };
4693
+ return { tier: 2, rows: ripgrepReach(symbol, root || getProjectRoot(), limit) };
4694
+ }
4695
+ function formatReachability(symbol, tier, rows) {
4696
+ if (rows.length === 0) return `\u{1F50E} Reachability \`${symbol}\` (tier ${tier}): no tests reach this symbol.`;
4697
+ const lines = [`\u{1F50E} Reachability \`${symbol}\` (tier ${tier}, ${rows.length} test(s)):`];
4698
+ for (const r of rows.slice(0, 5)) lines.push(` \u2022 \`${r.test}\` via \`${r.via}\` \u2014 ${r.evidence}`);
4699
+ return lines.join("\n");
4700
+ }
4701
+ var BERBEDA;
4702
+ var init_symbolReach = __esm({
4703
+ "src/engine/symbolReach.ts"() {
4704
+ "use strict";
4705
+ init_pathValidator();
4706
+ BERBEDA = 10;
4707
+ }
4708
+ });
4709
+
4710
+ // src/engine/testHistory.ts
4711
+ var testHistory_exports = {};
4712
+ __export(testHistory_exports, {
4713
+ fileSig: () => fileSig,
4714
+ markTestFlaky: () => markTestFlaky,
4715
+ rankTestsByHistory: () => rankTestsByHistory,
4716
+ recordTestOutcome: () => recordTestOutcome
4717
+ });
4718
+ async function ensureTestHistorySchema() {
4719
+ const db = await getDb();
4720
+ db.exec(`
4721
+ CREATE TABLE IF NOT EXISTS test_history (
4722
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
4723
+ file_sig TEXT NOT NULL,
4724
+ test_path TEXT NOT NULL,
4725
+ failed INTEGER NOT NULL DEFAULT 0,
4726
+ flaky INTEGER NOT NULL DEFAULT 0,
4727
+ runs INTEGER NOT NULL DEFAULT 1,
4728
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
4729
+ );
4730
+ CREATE INDEX IF NOT EXISTS idx_th_sig ON test_history(file_sig);
4731
+ CREATE INDEX IF NOT EXISTS idx_th_test ON test_history(test_path);
4732
+ `);
4733
+ saveDb();
4734
+ }
4735
+ function fileSig(filePath) {
4736
+ const parts = filePath.replace(/\\/g, "/").split("/");
4737
+ const base = (parts.pop() || "").split(".")[0];
4738
+ const dir = parts.slice(-2).join("/");
4739
+ return `${dir}/${base}`.toLowerCase();
4740
+ }
4741
+ async function recordTestOutcome(changedFiles, failedTests) {
4742
+ try {
4743
+ await ensureTestHistorySchema();
4744
+ const db = await getDb();
4745
+ let recorded = 0;
4746
+ for (const f of changedFiles.slice(0, 10)) {
4747
+ const sig = fileSig(f);
4748
+ for (const t of failedTests.slice(0, 10)) {
4749
+ const find = db.prepare(`SELECT id, failed, flaky, runs FROM test_history WHERE file_sig = ? AND test_path = ?`);
4750
+ find.bind([sig, t]);
4751
+ if (find.step()) {
4752
+ const row = find.getAsObject();
4753
+ db.run(`UPDATE test_history SET failed = 1, runs = runs + 1, updated_at = strftime('%s','now') WHERE id = ?`, [row.id]);
4754
+ find.free();
4755
+ } else {
4756
+ find.free();
4757
+ db.run(`INSERT INTO test_history (file_sig, test_path, failed) VALUES (?, ?, 1)`, [sig, t]);
4758
+ }
4759
+ recorded++;
4760
+ }
4761
+ if (failedTests.length === 0) {
4762
+ db.run(
4763
+ `INSERT INTO test_history (file_sig, test_path, failed) VALUES (?, ?, 0)
4764
+ ON CONFLICT DO NOTHING`,
4765
+ [sig, "__green__"]
4766
+ );
4767
+ recorded++;
4768
+ }
4769
+ }
4770
+ void recorded;
4771
+ saveDb();
4772
+ return { recorded };
4773
+ } catch {
4774
+ return { recorded: 0 };
4775
+ }
4776
+ }
4777
+ async function markTestFlaky(changedFiles, testPath) {
4778
+ try {
4779
+ await ensureTestHistorySchema();
4780
+ const db = await getDb();
4781
+ for (const f of changedFiles.slice(0, 10)) {
4782
+ db.run(`UPDATE test_history SET flaky = 1 WHERE file_sig = ? AND test_path = ?`, [fileSig(f), testPath]);
4783
+ }
4784
+ saveDb();
4785
+ } catch {
4786
+ }
4787
+ }
4788
+ async function rankTestsByHistory(changedFile, candidates) {
4789
+ let history = [];
4790
+ try {
4791
+ await ensureTestHistorySchema();
4792
+ const db = await getDb();
4793
+ const stmt = db.prepare(`SELECT test_path, failed, flaky, runs FROM test_history WHERE file_sig = ?`);
4794
+ stmt.bind([fileSig(changedFile)]);
4795
+ while (stmt.step()) history.push(stmt.getAsObject());
4796
+ stmt.free();
4797
+ } catch {
4798
+ }
4799
+ const byPath = new Map(history.map((h) => [h.test_path, h]));
4800
+ return candidates.map((c, i) => {
4801
+ const h = byPath.get(c);
4802
+ if (h && h.failed === 1 && h.flaky === 0) {
4803
+ return { path: c, score: 100 - i * 0.01 + Math.min(h.runs, 10), reason: `\u{1F525} failed for this area before (${h.runs}x)` };
4804
+ }
4805
+ if (h && h.flaky === 1) {
4806
+ return { path: c, score: -1, reason: "\u26AA flaky \u2014 down-weighted, run last" };
4807
+ }
4808
+ return { path: c, score: 10 - i * 0.01, reason: "static match" };
4809
+ }).sort((a, b) => b.score - a.score);
4810
+ }
4811
+ var init_testHistory = __esm({
4812
+ "src/engine/testHistory.ts"() {
4813
+ "use strict";
4814
+ init_kumaDb();
4815
+ }
4816
+ });
4817
+
4546
4818
  // src/engine/impactAnalysis.ts
4547
4819
  var impactAnalysis_exports = {};
4548
4820
  __export(impactAnalysis_exports, {
4549
- calculateBlastRadius: () => calculateBlastRadius
4821
+ calculateBlastRadius: () => calculateBlastRadius,
4822
+ classifyDiffShape: () => classifyDiffShape,
4823
+ getIndexerStatus: () => getIndexerStatus
4550
4824
  });
4551
- import fs9 from "fs";
4552
- import path11 from "path";
4825
+ import fs10 from "fs";
4826
+ import path12 from "path";
4827
+ import { execSync as execSync5 } from "child_process";
4553
4828
  import fastGlob3 from "fast-glob";
4829
+ function extOf(target) {
4830
+ const base = path12.basename(target);
4831
+ const dot = base.lastIndexOf(".");
4832
+ return dot > 0 ? base.slice(dot).toLowerCase() : "";
4833
+ }
4834
+ function isExampleOrDocName(target) {
4835
+ const base = path12.basename(target).toLowerCase();
4836
+ return base.endsWith(".example") || base.startsWith(".env") || base.endsWith(".md") || base.endsWith(".txt") || base.endsWith(".rst") || base === "license" || base.startsWith("license.");
4837
+ }
4838
+ function classifyDiffShape(target, root) {
4839
+ if (isExampleOrDocName(target)) {
4840
+ return { shape: "config-or-docs", detail: "non-runtime file (docs/config/example)" };
4841
+ }
4842
+ const ext = extOf(target);
4843
+ if (ext && !RUNTIME_CODE_EXTS.has(ext)) {
4844
+ return { shape: "config-or-docs", detail: `non-runtime extension (${ext})` };
4845
+ }
4846
+ try {
4847
+ const cwd = root || getProjectRoot();
4848
+ const diff = execSync5(`git diff --unified=0 -- "${target}"`, {
4849
+ cwd,
4850
+ encoding: "utf-8",
4851
+ timeout: 4e3,
4852
+ stdio: ["pipe", "pipe", "pipe"]
4853
+ }).trim();
4854
+ if (!diff) return { shape: "runtime-code", detail: "no working-tree diff; analyzed by file kind" };
4855
+ const prefixes = COMMENT_PREFIXES[ext] || ["//", "#", "*", "/*", "--"];
4856
+ const changed = diff.split("\n").filter((l) => (l.startsWith("+") || l.startsWith("-")) && !l.startsWith("+++") && !l.startsWith("---"));
4857
+ if (changed.length === 0) return { shape: "runtime-code", detail: "no working-tree diff; analyzed by file kind" };
4858
+ const allTrivial = changed.every((l) => {
4859
+ const body = l.slice(1).trim();
4860
+ return body === "" || prefixes.some((p) => body.startsWith(p));
4861
+ });
4862
+ if (allTrivial) return { shape: "comment-only", detail: `${changed.length} changed line(s), all comments/blank` };
4863
+ } catch {
4864
+ }
4865
+ return { shape: "runtime-code", detail: "runtime code change" };
4866
+ }
4554
4867
  async function findImportingFiles(targetFile, root) {
4555
- const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
4868
+ const normTarget = normalizeScope(path12.normalize(targetFile)) || path12.normalize(targetFile);
4556
4869
  try {
4557
4870
  const db = await getDb();
4558
4871
  const targetFileId = `file::${normTarget}`;
@@ -4587,12 +4900,12 @@ async function findImportingFiles(targetFile, root) {
4587
4900
  }
4588
4901
  callStmt.free();
4589
4902
  if (dependents.length > 0) {
4590
- return dependents;
4903
+ return { dependents, evidence: dependents.map((d) => `${d} (graph edge)`) };
4591
4904
  }
4592
4905
  } catch {
4593
4906
  }
4594
- const baseName = path11.basename(normTarget, path11.extname(normTarget));
4595
- const relTarget = path11.isAbsolute(normTarget) ? path11.relative(root, normTarget) : normTarget;
4907
+ const baseName = path12.basename(normTarget, path12.extname(normTarget));
4908
+ const relTarget = path12.isAbsolute(normTarget) ? path12.relative(root, normTarget) : normTarget;
4596
4909
  try {
4597
4910
  const files = await fastGlob3(
4598
4911
  ["**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs}"],
@@ -4603,28 +4916,32 @@ async function findImportingFiles(targetFile, root) {
4603
4916
  }
4604
4917
  );
4605
4918
  const dependents = [];
4919
+ const evidence = [];
4606
4920
  const importRegex = new RegExp(
4607
- `(?:import|from|require\\s*\\(|export\\s+.*from)\\s*['"][^'"]*\\b${baseName}(?:\\.[a-zA-Z0-9]+)?['"]`,
4921
+ `(?:import|from|require\\s*\\()[\\w{}*,.\\s]*['"][^'"]*\\b${baseName}(?:\\.[a-zA-Z0-9]+)?['"]`,
4608
4922
  "i"
4609
4923
  );
4610
4924
  for (const f of files) {
4611
- if (path11.normalize(f) === path11.normalize(relTarget)) continue;
4612
- const fullPath = path11.join(root, f);
4925
+ if (path12.normalize(f) === path12.normalize(relTarget)) continue;
4926
+ const fullPath = path12.join(root, f);
4613
4927
  try {
4614
- const content = fs9.readFileSync(fullPath, "utf-8");
4615
- if (importRegex.test(content)) {
4928
+ const content = fs10.readFileSync(fullPath, "utf-8");
4929
+ const lines = content.split("\n");
4930
+ const hitIdx = lines.findIndex((l) => importRegex.test(l));
4931
+ if (hitIdx >= 0) {
4616
4932
  dependents.push(f);
4933
+ evidence.push(`${f}:${hitIdx + 1} (import match)`);
4617
4934
  }
4618
4935
  } catch {
4619
4936
  }
4620
4937
  }
4621
- return dependents;
4938
+ return { dependents, evidence };
4622
4939
  } catch {
4623
- return [];
4940
+ return { dependents: [], evidence: [] };
4624
4941
  }
4625
4942
  }
4626
- async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = []) {
4627
- const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
4943
+ async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = [], exactOnly = false) {
4944
+ const normTarget = normalizeScope(path12.normalize(targetFile)) || path12.normalize(targetFile);
4628
4945
  try {
4629
4946
  const db = await getDb();
4630
4947
  const targetFileId = `file::${normTarget}`;
@@ -4648,18 +4965,14 @@ async function findRelatedTests(targetFile, root, owningPackage, additionalDepen
4648
4965
  }
4649
4966
  } catch {
4650
4967
  }
4651
- const baseName = path11.basename(normTarget, path11.extname(normTarget));
4968
+ const baseName = path12.basename(normTarget, path12.extname(normTarget));
4652
4969
  const testFiles = [];
4653
4970
  const stems = /* @__PURE__ */ new Set();
4654
4971
  if (baseName) stems.add(baseName);
4655
- for (const part of baseName.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
4656
- stems.add(part);
4657
- }
4658
- for (const dep of additionalDependents.slice(0, 3)) {
4659
- const depBase = path11.basename(dep, path11.extname(dep));
4660
- if (depBase) stems.add(depBase);
4661
- for (const part of depBase.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
4662
- stems.add(part);
4972
+ if (!exactOnly) {
4973
+ for (const dep of additionalDependents.slice(0, 3)) {
4974
+ const depBase = path12.basename(dep, path12.extname(dep));
4975
+ if (depBase) stems.add(depBase);
4663
4976
  }
4664
4977
  }
4665
4978
  const testPatterns = [];
@@ -4694,12 +5007,12 @@ async function findRelatedTests(targetFile, root, owningPackage, additionalDepen
4694
5007
  );
4695
5008
  }
4696
5009
  const pkgTests = await fastGlob3(pkgPatterns, {
4697
- cwd: path11.join(root, owningPackage.path),
5010
+ cwd: path12.join(root, owningPackage.path),
4698
5011
  ignore: ["**/node_modules/**", "**/dist/**"],
4699
5012
  onlyFiles: true
4700
5013
  });
4701
5014
  for (const pt of pkgTests) {
4702
- const fullRel = path11.join(owningPackage.path, pt);
5015
+ const fullRel = path12.join(owningPackage.path, pt);
4703
5016
  if (!testFiles.includes(fullRel)) {
4704
5017
  testFiles.push(fullRel);
4705
5018
  }
@@ -4748,6 +5061,22 @@ function assessRisk(target, targetType, directDependents, downstreamPackages, af
4748
5061
  }
4749
5062
  return { risk, riskFlags };
4750
5063
  }
5064
+ async function getIndexerStatus() {
5065
+ let graphEdges = false;
5066
+ try {
5067
+ const db = await getDb();
5068
+ const stmt = db.prepare(`SELECT COUNT(*) as cnt FROM edges WHERE type IN ('imports','depends_on','calls','tests')`);
5069
+ if (stmt.step()) graphEdges = Number(stmt.getAsObject().cnt) > 0;
5070
+ stmt.free();
5071
+ } catch {
5072
+ }
5073
+ return {
5074
+ tier: graphEdges ? 1 : 2,
5075
+ graphEdges,
5076
+ languages: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "rs"],
5077
+ note: graphEdges ? "Tier 1: graph edges grounded the verdict (Tier 2 ripgrep fallback available)" : "Tier 2: ripgrep import-regex fallback (no graph edges; run a scan to reach Tier 1; LSP indexers are future work per #35)"
5078
+ };
5079
+ }
4751
5080
  async function calculateBlastRadius(target, options = {}) {
4752
5081
  const root = options.root || getProjectRoot();
4753
5082
  const wsInfo = await getWorkspaceInfo(root);
@@ -4772,66 +5101,65 @@ async function calculateBlastRadius(target, options = {}) {
4772
5101
  downstreamPackages.push(...rev.map((p) => p.name));
4773
5102
  }
4774
5103
  let directDependents = [];
5104
+ let evidence = [];
4775
5105
  if (targetType === "file" || targetType === "symbol") {
4776
- directDependents = await findImportingFiles(target, root);
4777
- }
4778
- const normLower = target.toLowerCase();
4779
- const isUserDestructive = /(user|account|member|customer|profile).*(delet|destro|remov|wipe|terminat)|(delet|destro|remov|wipe|terminat).*(user|account|member|customer|profile)/i.test(target);
4780
- const isSecurityTopic = /(auth|security|session|permission|jwt|token|middleware)/i.test(target);
4781
- if (isUserDestructive || isSecurityTopic || directDependents.length === 0) {
5106
+ const found = await findImportingFiles(target, root);
5107
+ directDependents = found.dependents;
5108
+ evidence = found.evidence;
5109
+ }
5110
+ const { shape: diffShape, detail: shapeDetail } = classifyDiffShape(target, root);
5111
+ const nonRuntime = diffShape !== "runtime-code";
5112
+ const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents, nonRuntime);
5113
+ let testReasons = /* @__PURE__ */ new Map();
5114
+ if (targetType === "symbol") {
4782
5115
  try {
4783
- const db = await getDb();
4784
- const tokens = normLower.split(/[\s_-]+/).filter((t) => t.length >= 3);
4785
- for (const token of tokens) {
4786
- const stmt = db.prepare(`SELECT DISTINCT file_path, name FROM nodes WHERE (name LIKE ? OR file_path LIKE ?) LIMIT 5`);
4787
- stmt.bind([`%${token}%`, `%${token}%`]);
4788
- while (stmt.step()) {
4789
- const row = stmt.getAsObject();
4790
- const fp = row.file_path || row.name;
4791
- if (fp && !directDependents.includes(fp) && fp !== target) {
4792
- directDependents.push(fp);
4793
- }
4794
- }
4795
- stmt.free();
4796
- }
4797
- const authMiddlewareMatches = await fastGlob3(
4798
- [
4799
- "**/*auth-middleware*.*",
4800
- "**/*auth_middleware*.*",
4801
- "**/*auth.middleware*.*",
4802
- "**/middleware/*auth*.*",
4803
- "**/middlewares/*auth*.*",
4804
- "**/guards/*auth*.*",
4805
- "**/security/*auth*.*",
4806
- "**/lib/*auth*.*"
4807
- ],
4808
- {
4809
- cwd: root,
4810
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kuma/**"],
4811
- onlyFiles: true
4812
- }
4813
- );
4814
- for (const am of authMiddlewareMatches) {
4815
- if (!directDependents.includes(am) && am !== target) {
4816
- directDependents.push(am);
5116
+ const { symbolReachability: symbolReachability2 } = await Promise.resolve().then(() => (init_symbolReach(), symbolReach_exports));
5117
+ const reach = await symbolReachability2(target, 10, root);
5118
+ for (const r of reach.rows) {
5119
+ if (!affectedTests.includes(r.test)) {
5120
+ affectedTests.push(r.test);
5121
+ testReasons.set(r.test, `reachability tier ${reach.tier}: ${r.evidence.substring(0, 80)}`);
4817
5122
  }
5123
+ evidence.push(r.evidence);
4818
5124
  }
4819
5125
  } catch {
4820
5126
  }
4821
5127
  }
4822
- const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents);
4823
- const { risk, riskFlags } = assessRisk(
4824
- target,
4825
- targetType,
4826
- directDependents,
4827
- downstreamPackages,
4828
- affectedTests,
4829
- owningPackage
4830
- );
5128
+ try {
5129
+ const { rankTestsByHistory: rankTestsByHistory2 } = await Promise.resolve().then(() => (init_testHistory(), testHistory_exports));
5130
+ const ranked = await rankTestsByHistory2(target, affectedTests);
5131
+ affectedTests.length = 0;
5132
+ for (const r of ranked) {
5133
+ affectedTests.push(r.path);
5134
+ if (r.reason !== "static match") testReasons.set(r.path, r.reason);
5135
+ }
5136
+ } catch {
5137
+ }
5138
+ let risk;
5139
+ let riskFlags;
5140
+ if (nonRuntime) {
5141
+ risk = "low";
5142
+ riskFlags = [
5143
+ `${diffShape === "comment-only" ? "Comment-only change" : "Non-runtime file"} (${shapeDetail}) \u2014 risk capped at LOW`,
5144
+ ...directDependents.length > 0 || affectedTests.length > 0 ? [`Grounded refs kept for reference: ${directDependents.length} importer(s), ${affectedTests.length} test(s)`] : []
5145
+ ];
5146
+ directDependents = [];
5147
+ affectedTests.length = 0;
5148
+ } else {
5149
+ ({ risk, riskFlags } = assessRisk(
5150
+ target,
5151
+ targetType,
5152
+ directDependents,
5153
+ downstreamPackages,
5154
+ affectedTests,
5155
+ owningPackage
5156
+ ));
5157
+ }
4831
5158
  const summaryLines = [
4832
5159
  `\u{1F3AF} **Impact / Blast Radius: \`${target}\`**`,
4833
5160
  `\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`,
4834
5161
  `\u26A0\uFE0F **Risk Level: ${risk.toUpperCase()}**`,
5162
+ `\u{1F4D0} Diff shape: \`${diffShape}\` (${shapeDetail})`,
4835
5163
  owningPackage ? `\u{1F4E6} Owning package: \`${owningPackage.name}\` (${owningPackage.path})` : ""
4836
5164
  ];
4837
5165
  if (riskFlags.length > 0) {
@@ -4858,11 +5186,22 @@ async function calculateBlastRadius(target, options = {}) {
4858
5186
  summaryLines.push(` ... and ${directDependents.length - 6} more`);
4859
5187
  }
4860
5188
  }
5189
+ if (evidence.length > 0) {
5190
+ summaryLines.push("");
5191
+ summaryLines.push(`\u{1F50E} **Evidence (${evidence.length}):**`);
5192
+ for (const e of evidence.slice(0, 3)) {
5193
+ summaryLines.push(` \u2022 \`${e}\``);
5194
+ }
5195
+ if (evidence.length > 3) {
5196
+ summaryLines.push(` ... and ${evidence.length - 3} more`);
5197
+ }
5198
+ }
4861
5199
  if (affectedTests.length > 0) {
4862
5200
  summaryLines.push("");
4863
5201
  summaryLines.push(`\u{1F9EA} **Affected Test Suites (${affectedTests.length}):**`);
4864
5202
  for (const t of affectedTests.slice(0, 5)) {
4865
- summaryLines.push(` \u2022 \`${t}\``);
5203
+ const why = testReasons.get(t) ? ` \u2014 ${testReasons.get(t)}` : "";
5204
+ summaryLines.push(` \u2022 \`${t}\`${why}`);
4866
5205
  }
4867
5206
  if (affectedTests.length > 5) {
4868
5207
  summaryLines.push(` ... and ${affectedTests.length - 5} more`);
@@ -4881,15 +5220,52 @@ async function calculateBlastRadius(target, options = {}) {
4881
5220
  risk,
4882
5221
  riskFlags,
4883
5222
  summary: summaryLines.filter(Boolean).join("\n"),
4884
- confidence: 0.85
5223
+ confidence: nonRuntime ? 0.95 : 0.85,
5224
+ diffShape,
5225
+ evidence
4885
5226
  };
4886
5227
  }
5228
+ var RUNTIME_CODE_EXTS, COMMENT_PREFIXES;
4887
5229
  var init_impactAnalysis = __esm({
4888
5230
  "src/engine/impactAnalysis.ts"() {
4889
5231
  "use strict";
4890
5232
  init_pathValidator();
4891
5233
  init_kumaDb();
4892
5234
  init_workspaceIntelligence();
5235
+ RUNTIME_CODE_EXTS = /* @__PURE__ */ new Set([
5236
+ ".ts",
5237
+ ".tsx",
5238
+ ".js",
5239
+ ".jsx",
5240
+ ".mjs",
5241
+ ".cjs",
5242
+ ".py",
5243
+ ".go",
5244
+ ".rs",
5245
+ ".java",
5246
+ ".rb",
5247
+ ".php",
5248
+ ".swift",
5249
+ ".kt",
5250
+ ".cs",
5251
+ ".scala",
5252
+ ".vue",
5253
+ ".svelte"
5254
+ ]);
5255
+ COMMENT_PREFIXES = {
5256
+ ".ts": ["//", "*", "/*"],
5257
+ ".tsx": ["//", "*", "/*"],
5258
+ ".js": ["//", "*", "/*"],
5259
+ ".jsx": ["//", "*", "/*"],
5260
+ ".mjs": ["//", "*", "/*"],
5261
+ ".cjs": ["//", "*", "/*"],
5262
+ ".py": ["#"],
5263
+ ".go": ["//"],
5264
+ ".rs": ["//"],
5265
+ ".java": ["//", "*", "/*"],
5266
+ ".rb": ["#"],
5267
+ ".php": ["//", "#", "*"]
5268
+ };
4893
5269
  }
4894
5270
  });
4895
5271
 
@@ -5963,27 +6339,27 @@ async function codebaseSearchFallback(query, limit = 20) {
5963
6339
  if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
5964
6340
  let entries = [];
5965
6341
  try {
5966
- entries = fs27.readdirSync(dir);
6342
+ entries = fs34.readdirSync(dir);
5967
6343
  } catch {
5968
6344
  return;
5969
6345
  }
5970
6346
  for (const entry of entries) {
5971
6347
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
5972
- const full = path30.join(dir, entry);
6348
+ const full = path37.join(dir, entry);
5973
6349
  try {
5974
- const stat = fs27.statSync(full);
6350
+ const stat = fs34.statSync(full);
5975
6351
  if (stat.isDirectory()) {
5976
6352
  walk2(full, depth + 1);
5977
6353
  } else if (entry.toLowerCase().includes(query.toLowerCase())) {
5978
- results.push(path30.relative(root, full));
6354
+ results.push(path37.relative(root, full));
5979
6355
  }
5980
6356
  } catch {
5981
6357
  }
5982
6358
  }
5983
6359
  };
5984
6360
  var walk = walk2;
5985
- const fs27 = await import("fs");
5986
- const path30 = await import("path");
6361
+ const fs34 = await import("fs");
6362
+ const path37 = await import("path");
5987
6363
  const root = process.cwd();
5988
6364
  const results = [];
5989
6365
  const MAX_WALK_DEPTH = 8;
@@ -6116,9 +6492,9 @@ ${summary}`;
6116
6492
  }
6117
6493
  async function codebaseImpactFallback(target) {
6118
6494
  try {
6119
- const { execSync: execSync7 } = await import("child_process");
6495
+ const { execSync: execSync10 } = await import("child_process");
6120
6496
  const root = process.cwd();
6121
- const grepResult = execSync7(
6497
+ const grepResult = execSync10(
6122
6498
  `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`,
6123
6499
  { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
6124
6500
  ).trim();
@@ -6245,7 +6621,28 @@ var init_kumaGraph = __esm({
6245
6621
  });
6246
6622
 
6247
6623
  // src/utils/kumaShared.ts
6248
- import { execSync as execSync4 } from "child_process";
6624
+ var kumaShared_exports = {};
6625
+ __export(kumaShared_exports, {
6626
+ buildDriftMessages: () => buildDriftMessages,
6627
+ checkLadderViolations: () => checkLadderViolations,
6628
+ countEditCalls: () => countEditCalls,
6629
+ detectTestWeakening: () => detectTestWeakening,
6630
+ getBranchCommittedFiles: () => getBranchCommittedFiles,
6631
+ getGitDiffStat: () => getGitDiffStat,
6632
+ getPrioritySuggestion: () => getPrioritySuggestion,
6633
+ getSessionStats: () => getSessionStats,
6634
+ getUncommittedFiles: () => getUncommittedFiles,
6635
+ getUnresolvedCount: () => getUnresolvedCount,
6636
+ getUnresolvedDetails: () => getUnresolvedDetails,
6637
+ hasVerificationEvidence: () => hasVerificationEvidence,
6638
+ isBashTool: () => isBashTool,
6639
+ isEditTool: () => isEditTool,
6640
+ isReadTool: () => isReadTool,
6641
+ isTestTool: () => isTestTool
6642
+ });
6643
+ import { execSync as execSync6 } from "child_process";
6644
+ import fs11 from "fs";
6645
+ import path13 from "path";
6249
6646
  function isEditTool(toolName) {
6250
6647
  const n = (toolName || "").toLowerCase();
6251
6648
  return n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply") || n.includes("replace");
@@ -6283,7 +6680,7 @@ function getSessionStats(inputGoal) {
6283
6680
  function getGitDiffStat(timeout = 3e3) {
6284
6681
  try {
6285
6682
  const root = getProjectRoot();
6286
- return execSync4("git diff --stat", {
6683
+ return execSync6("git diff --stat", {
6287
6684
  cwd: root,
6288
6685
  encoding: "utf-8",
6289
6686
  timeout
@@ -6292,6 +6689,83 @@ function getGitDiffStat(timeout = 3e3) {
6292
6689
  return "";
6293
6690
  }
6294
6691
  }
6692
+ function getUncommittedFiles(timeout = 3e3) {
6693
+ const files = /* @__PURE__ */ new Set();
6694
+ try {
6695
+ const root = getProjectRoot();
6696
+ const porcelain = execSync6("git status --porcelain", {
6697
+ cwd: root,
6698
+ encoding: "utf-8",
6699
+ timeout,
6700
+ stdio: ["pipe", "pipe", "pipe"]
6701
+ }).trim();
6702
+ for (const line of porcelain.split("\n")) {
6703
+ const m = line.match(/^.{3}(.+?)(?:\s+->\s+.+)?$/);
6704
+ if (m && m[1]) files.add(m[1].trim().replace(/^"|"$/g, ""));
6705
+ }
6706
+ } catch {
6707
+ }
6708
+ return files;
6709
+ }
6710
+ function getBranchCommittedFiles(timeout = 5e3) {
6711
+ const files = /* @__PURE__ */ new Set();
6712
+ let base = "";
6713
+ try {
6714
+ const root = getProjectRoot();
6715
+ const branch = execSync6("git rev-parse --abbrev-ref HEAD", {
6716
+ cwd: root,
6717
+ encoding: "utf-8",
6718
+ timeout,
6719
+ stdio: ["pipe", "pipe", "pipe"]
6720
+ }).trim();
6721
+ const baseRef = execSync6(
6722
+ `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo origin/${branch === "main" || branch === "master" ? "HEAD" : "main"}`,
6723
+ { cwd: root, encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
6724
+ ).trim().replace("refs/remotes/", "");
6725
+ const mergeBase = execSync6(`git merge-base HEAD ${baseRef} 2>/dev/null || git rev-list --max-parents=0 HEAD`, {
6726
+ cwd: root,
6727
+ encoding: "utf-8",
6728
+ timeout,
6729
+ stdio: ["pipe", "pipe", "pipe"]
6730
+ }).trim().split("\n")[0];
6731
+ base = baseRef;
6732
+ if (mergeBase) {
6733
+ const out = execSync6(`git diff --name-only ${mergeBase} HEAD`, {
6734
+ cwd: root,
6735
+ encoding: "utf-8",
6736
+ timeout,
6737
+ stdio: ["pipe", "pipe", "pipe"]
6738
+ }).trim();
6739
+ for (const f of out.split("\n")) if (f.trim()) files.add(f.trim());
6740
+ }
6741
+ } catch {
6742
+ }
6743
+ return { base, files };
6744
+ }
6745
+ function hasVerificationEvidence(toolCalls) {
6746
+ if (toolCalls.some((c) => isTestTool(c.toolName))) return true;
6747
+ return hasVerificationInHookLog();
6748
+ }
6749
+ function hasVerificationInHookLog(hours = 24) {
6750
+ try {
6751
+ const fp = path13.join(getProjectRoot(), ".kuma", "injections.jsonl");
6752
+ if (!fs11.existsSync(fp)) return false;
6753
+ const cutoff = Date.now() - hours * 60 * 60 * 1e3;
6754
+ const lines = fs11.readFileSync(fp, "utf-8").split("\n");
6755
+ for (const line of lines) {
6756
+ if (!line.trim()) continue;
6757
+ try {
6758
+ const entry = JSON.parse(line);
6759
+ if ((entry.at || 0) < cutoff) continue;
6760
+ const cmd = String(entry.command || "");
6761
+ if (cmd && TEST_COMMAND_PATTERN.test(cmd)) return true;
6762
+ } catch {
6763
+ }
6764
+ }
6765
+ } catch {
6766
+ }
6767
+ return false;
6768
+ }
6295
6769
  function getUnresolvedCount(failedFiles) {
6296
6770
  let count = 0;
6297
6771
  for (const f of failedFiles) {
@@ -6301,6 +6775,28 @@ function getUnresolvedCount(failedFiles) {
6301
6775
  }
6302
6776
  return count;
6303
6777
  }
6778
+ function getUnresolvedDetails(failedFiles) {
6779
+ const result = [];
6780
+ for (const f of failedFiles) {
6781
+ for (const ff of f.failures) {
6782
+ if (!ff.resolved) {
6783
+ result.push({ task: f.task, error: ff.error.substring(0, 200) });
6784
+ }
6785
+ }
6786
+ }
6787
+ return result;
6788
+ }
6789
+ function checkLadderViolations(toolCalls, modifiedFiles, hasRunTests) {
6790
+ const violations = [];
6791
+ const editCalls = toolCalls.filter((c) => isEditTool(c.toolName)).length;
6792
+ if (editCalls > 5) {
6793
+ violations.push(`${editCalls} file ops in a row \u2014 consider if all are needed`);
6794
+ }
6795
+ if (modifiedFiles.length > 5 && !hasRunTests) {
6796
+ violations.push(`${modifiedFiles.length} files modified without verification`);
6797
+ }
6798
+ return violations;
6799
+ }
6304
6800
  function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
6305
6801
  const drifts = [];
6306
6802
  if (modifiedFiles > 0 && !hasRunTests) {
@@ -6339,7 +6835,7 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
6339
6835
  if (!goal) {
6340
6836
  return "No goal set \u2014 use goal parameter or setGoal to track intent";
6341
6837
  }
6342
- if (modifiedFiles === 0) {
6838
+ if (modifiedFiles === 0 && !hasRunTests) {
6343
6839
  return "Start by exploring what exists before writing code";
6344
6840
  }
6345
6841
  return "On track \u2014 continue with current approach";
@@ -6347,11 +6843,43 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
6347
6843
  function countEditCalls(toolCalls) {
6348
6844
  return toolCalls.filter((c) => isEditTool(c.toolName)).length;
6349
6845
  }
6846
+ function detectTestWeakening(files, root) {
6847
+ const weakened = [];
6848
+ let cwd = "";
6849
+ try {
6850
+ cwd = root || getProjectRoot();
6851
+ } catch {
6852
+ return [];
6853
+ }
6854
+ for (const f of files) {
6855
+ if (!TEST_FILE_PATTERN.test(f)) continue;
6856
+ try {
6857
+ const diff = execSync6(`git diff --unified=0 -- "${f}"`, {
6858
+ cwd,
6859
+ encoding: "utf-8",
6860
+ timeout: 4e3,
6861
+ stdio: ["pipe", "pipe", "pipe"]
6862
+ }).trim();
6863
+ if (!diff) continue;
6864
+ const removed = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---"));
6865
+ const removedAssertions = removed.filter((l) => ASSERTION_PATTERN.test(l)).length;
6866
+ if (removedAssertions > 0 && removed.length > 0 && removedAssertions / removed.length >= 0.5) {
6867
+ weakened.push(`${f} (\u2212${removedAssertions} assertion(s))`);
6868
+ }
6869
+ } catch {
6870
+ }
6871
+ }
6872
+ return weakened;
6873
+ }
6874
+ var TEST_COMMAND_PATTERN, TEST_FILE_PATTERN, ASSERTION_PATTERN;
6350
6875
  var init_kumaShared = __esm({
6351
6876
  "src/utils/kumaShared.ts"() {
6352
6877
  "use strict";
6353
6878
  init_sessionMemory();
6354
6879
  init_pathValidator();
6880
+ TEST_COMMAND_PATTERN = /(npm|pnpm|yarn|bun)\s+(test|run\s+(test|typecheck|check|lint))|pytest|go\s+test|cargo\s+test|jest|vitest|typecheck|tsc\s+--noEmit/i;
6881
+ TEST_FILE_PATTERN = /(\.test\.|\.spec\.|_test\.|__tests__|\/tests?\/)/i;
6882
+ ASSERTION_PATTERN = /\b(expect|assert|toBe|toEqual|toContain|toMatch|ok\(|equal\(|deepEqual\()\b/;
6355
6883
  }
6356
6884
  });
6357
6885
 
@@ -6365,19 +6893,19 @@ __export(kumaMemory_exports, {
6365
6893
  scoreMemoryRelevance: () => scoreMemoryRelevance,
6366
6894
  shouldRecordDecision: () => shouldRecordDecision
6367
6895
  });
6368
- import fs10 from "fs";
6369
- import path12 from "path";
6896
+ import fs12 from "fs";
6897
+ import path14 from "path";
6370
6898
  function scoreMemoryRelevance(context, limit = 5) {
6371
6899
  const results = [];
6372
- const kumaDir = path12.join(getProjectRoot(), MEMORY_DIR);
6373
- if (!fs10.existsSync(kumaDir)) return [];
6900
+ const kumaDir = path14.join(getProjectRoot(), MEMORY_DIR);
6901
+ if (!fs12.existsSync(kumaDir)) return [];
6374
6902
  const terms = Array.from(new Set(context.toLowerCase().split(/[\s/._-]+/).filter((w) => w.length > 2)));
6375
6903
  if (terms.length === 0) return [];
6376
6904
  try {
6377
- const files = fs10.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
6905
+ const files = fs12.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
6378
6906
  for (const file of files) {
6379
6907
  try {
6380
- const content = fs10.readFileSync(path12.join(kumaDir, file), "utf-8");
6908
+ const content = fs12.readFileSync(path14.join(kumaDir, file), "utf-8");
6381
6909
  const lower = content.toLowerCase();
6382
6910
  let matchCount = 0;
6383
6911
  for (const term of terms) {
@@ -6548,13 +7076,86 @@ var init_kumaMemory = __esm({
6548
7076
  }
6549
7077
  });
6550
7078
 
7079
+ // src/engine/cacheFreshness.ts
7080
+ var cacheFreshness_exports = {};
7081
+ __export(cacheFreshness_exports, {
7082
+ MAP_TTL_MS: () => MAP_TTL_MS,
7083
+ RESEARCH_TTL_MS: () => RESEARCH_TTL_MS,
7084
+ clearDirty: () => clearDirty,
7085
+ formatAge: () => formatAge,
7086
+ getDirtyFiles: () => getDirtyFiles,
7087
+ isStale: () => isStale,
7088
+ markDirty: () => markDirty
7089
+ });
7090
+ import fs13 from "fs";
7091
+ import path15 from "path";
7092
+ function isStale(savedAtMs, ttlMs, nowMs = Date.now()) {
7093
+ if (!savedAtMs) return true;
7094
+ return nowMs - savedAtMs > ttlMs;
7095
+ }
7096
+ function formatAge(savedAtMs, nowMs = Date.now()) {
7097
+ const s = Math.max(0, Math.floor((nowMs - savedAtMs) / 1e3));
7098
+ if (s < 60) return `${s}s`;
7099
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
7100
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
7101
+ return `${Math.floor(s / 86400)}d`;
7102
+ }
7103
+ function dirtyPath() {
7104
+ return path15.join(getProjectRoot(), DIRTY_FILE);
7105
+ }
7106
+ function loadDirty() {
7107
+ try {
7108
+ const fp = dirtyPath();
7109
+ if (fs13.existsSync(fp)) {
7110
+ const parsed = JSON.parse(fs13.readFileSync(fp, "utf-8"));
7111
+ if (parsed && typeof parsed === "object") return parsed;
7112
+ }
7113
+ } catch {
7114
+ }
7115
+ return {};
7116
+ }
7117
+ function markDirty(filePath) {
7118
+ try {
7119
+ const fp = dirtyPath();
7120
+ const dir = path15.dirname(fp);
7121
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7122
+ const dirty = loadDirty();
7123
+ dirty[filePath] = Date.now();
7124
+ const keys = Object.keys(dirty).sort((a, b) => dirty[b] - dirty[a]).slice(0, 200);
7125
+ const bounded = {};
7126
+ for (const k of keys) bounded[k] = dirty[k];
7127
+ fs13.writeFileSync(fp, JSON.stringify(bounded), "utf-8");
7128
+ } catch {
7129
+ }
7130
+ }
7131
+ function getDirtyFiles() {
7132
+ return Object.keys(loadDirty());
7133
+ }
7134
+ function clearDirty() {
7135
+ try {
7136
+ const fp = dirtyPath();
7137
+ if (fs13.existsSync(fp)) fs13.rmSync(fp, { force: true });
7138
+ } catch {
7139
+ }
7140
+ }
7141
+ var RESEARCH_TTL_MS, MAP_TTL_MS, DIRTY_FILE;
7142
+ var init_cacheFreshness = __esm({
7143
+ "src/engine/cacheFreshness.ts"() {
7144
+ "use strict";
7145
+ init_pathValidator();
7146
+ RESEARCH_TTL_MS = 7 * 24 * 3600 * 1e3;
7147
+ MAP_TTL_MS = 24 * 3600 * 1e3;
7148
+ DIRTY_FILE = ".kuma/dirty.json";
7149
+ }
7150
+ });
7151
+
6551
7152
  // src/engine/codeReuseEngine.ts
6552
7153
  var codeReuseEngine_exports = {};
6553
7154
  __export(codeReuseEngine_exports, {
6554
7155
  findReusableSymbols: () => findReusableSymbols
6555
7156
  });
6556
- import path13 from "path";
6557
- import fs11 from "fs";
7157
+ import path16 from "path";
7158
+ import fs14 from "fs";
6558
7159
  async function findReusableSymbols(query, scope) {
6559
7160
  const cleanQuery = query.trim().toLowerCase();
6560
7161
  const searchTokens = cleanQuery.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((t) => t.length >= 2);
@@ -6677,7 +7278,7 @@ function detectRecommendedUtilDirectory() {
6677
7278
  "utils"
6678
7279
  ];
6679
7280
  for (const c of candidates) {
6680
- if (fs11.existsSync(path13.resolve(root, c))) {
7281
+ if (fs14.existsSync(path16.resolve(root, c))) {
6681
7282
  return c;
6682
7283
  }
6683
7284
  }
@@ -6696,7 +7297,7 @@ var domainClusterEngine_exports = {};
6696
7297
  __export(domainClusterEngine_exports, {
6697
7298
  buildSubsystemClusters: () => buildSubsystemClusters
6698
7299
  });
6699
- import path14 from "path";
7300
+ import path17 from "path";
6700
7301
  async function buildSubsystemClusters() {
6701
7302
  const db = await getDb();
6702
7303
  const fileStmt = db.prepare(`
@@ -6773,7 +7374,7 @@ async function buildSubsystemClusters() {
6773
7374
  for (const [cKey, files] of clusterFileMap.entries()) {
6774
7375
  for (const f of files) {
6775
7376
  fileToCluster.set(f, cKey);
6776
- fileToCluster.set(path14.normalize(f), cKey);
7377
+ fileToCluster.set(path17.normalize(f), cKey);
6777
7378
  }
6778
7379
  }
6779
7380
  const clusterInteractions = /* @__PURE__ */ new Map();
@@ -6781,8 +7382,8 @@ async function buildSubsystemClusters() {
6781
7382
  const srcFile = extractFilePathFromNodeId(e.source);
6782
7383
  const tgtFile = extractFilePathFromNodeId(e.target);
6783
7384
  if (!srcFile || !tgtFile) continue;
6784
- const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(path14.normalize(srcFile));
6785
- const tgtCluster = fileToCluster.get(tgtFile) || fileToCluster.get(path14.normalize(tgtFile));
7385
+ const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(path17.normalize(srcFile));
7386
+ const tgtCluster = fileToCluster.get(tgtFile) || fileToCluster.get(path17.normalize(tgtFile));
6786
7387
  if (srcCluster && tgtCluster && srcCluster !== tgtCluster) {
6787
7388
  const pairKey = `${srcCluster}->${tgtCluster}`;
6788
7389
  const stats = clusterInteractions.get(pairKey) || { calls: 0, imports: 0 };
@@ -6804,13 +7405,13 @@ async function buildSubsystemClusters() {
6804
7405
  const clusterId = nodeId("feature_domain", cKey);
6805
7406
  const name = formatClusterName(cKey);
6806
7407
  const role = inferSubsystemRole(cKey, files);
6807
- const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path14.basename(f))).slice(0, 3);
7408
+ const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path17.basename(f))).slice(0, 3);
6808
7409
  if (entrypoints.length === 0 && files.length > 0) {
6809
7410
  entrypoints.push(files[0]);
6810
7411
  }
6811
7412
  const allSyms = [];
6812
7413
  for (const f of files) {
6813
- const syms = fileSymbols.get(f) || fileSymbols.get(path14.normalize(f)) || [];
7414
+ const syms = fileSymbols.get(f) || fileSymbols.get(path17.normalize(f)) || [];
6814
7415
  for (const s of syms) {
6815
7416
  if (!allSyms.includes(s)) allSyms.push(s);
6816
7417
  }
@@ -6989,7 +7590,7 @@ function inferSubsystemRole(key, files) {
6989
7590
  if (k.includes("util") || k.includes("common")) {
6990
7591
  return "Shared path validation, configuration loading, and platform detection helpers.";
6991
7592
  }
6992
- return `Core subsystem handling ${files.slice(0, 3).map((f) => path14.basename(f)).join(", ")}.`;
7593
+ return `Core subsystem handling ${files.slice(0, 3).map((f) => path17.basename(f)).join(", ")}.`;
6993
7594
  }
6994
7595
  function inferTypedVerb(sourceKey, targetKey, _stats) {
6995
7596
  const src = sourceKey.toLowerCase();
@@ -7068,18 +7669,18 @@ __export(domainRules_exports, {
7068
7669
  readLayer: () => readLayer,
7069
7670
  writeLayer: () => writeLayer
7070
7671
  });
7071
- import fs12 from "fs";
7072
- import path15 from "path";
7672
+ import fs15 from "fs";
7673
+ import path18 from "path";
7073
7674
  function layerFilePath(layer) {
7074
- return path15.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
7675
+ return path18.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
7075
7676
  }
7076
7677
  function ensureLayerFile(layer) {
7077
7678
  const fp = layerFilePath(layer);
7078
- if (!fs12.existsSync(fp)) {
7679
+ if (!fs15.existsSync(fp)) {
7079
7680
  const template = layer === "arch_flow" ? ARCH_FLOW_TEMPLATE : GOTCHAS_TEMPLATE;
7080
- const dir = path15.dirname(fp);
7081
- if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
7082
- fs12.writeFileSync(fp, template, "utf-8");
7681
+ const dir = path18.dirname(fp);
7682
+ if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
7683
+ fs15.writeFileSync(fp, template, "utf-8");
7083
7684
  console.error(`[DomainRules] Auto-created ${LAYER_FILES[layer].filename}`);
7084
7685
  }
7085
7686
  return fp;
@@ -7087,7 +7688,7 @@ function ensureLayerFile(layer) {
7087
7688
  function readLayer(layer) {
7088
7689
  try {
7089
7690
  const fp = ensureLayerFile(layer);
7090
- return fs12.readFileSync(fp, "utf-8");
7691
+ return fs15.readFileSync(fp, "utf-8");
7091
7692
  } catch (err) {
7092
7693
  return `Error reading ${layer}: ${err}`;
7093
7694
  }
@@ -7095,7 +7696,7 @@ function readLayer(layer) {
7095
7696
  function writeLayer(layer, content) {
7096
7697
  try {
7097
7698
  const fp = ensureLayerFile(layer);
7098
- fs12.writeFileSync(fp, content, "utf-8");
7699
+ fs15.writeFileSync(fp, content, "utf-8");
7099
7700
  const label = LAYER_FILES[layer].label;
7100
7701
  sessionMemory.recordToolCall("kuma_memory", { action: `write_${layer}` });
7101
7702
  return `\u2705 **${label}** updated successfully.`;
@@ -7106,9 +7707,9 @@ function writeLayer(layer, content) {
7106
7707
  function appendToLayer(layer, entry) {
7107
7708
  try {
7108
7709
  const fp = ensureLayerFile(layer);
7109
- const existing = fs12.readFileSync(fp, "utf-8");
7710
+ const existing = fs15.readFileSync(fp, "utf-8");
7110
7711
  const newContent = existing.trimEnd() + "\n\n" + entry.trim() + "\n";
7111
- fs12.writeFileSync(fp, newContent, "utf-8");
7712
+ fs15.writeFileSync(fp, newContent, "utf-8");
7112
7713
  return `\u2705 Entry added to **${LAYER_FILES[layer].label}**.`;
7113
7714
  } catch (err) {
7114
7715
  return `\u274C Failed to append: ${err}`;
@@ -7118,7 +7719,7 @@ function getActiveGotchas() {
7118
7719
  const results = [];
7119
7720
  try {
7120
7721
  const fp = ensureLayerFile("gotcha");
7121
- const content = fs12.readFileSync(fp, "utf-8");
7722
+ const content = fs15.readFileSync(fp, "utf-8");
7122
7723
  const lines = content.split("\n");
7123
7724
  let currentSection = "";
7124
7725
  let currentFilePath = "";
@@ -7154,9 +7755,9 @@ function getActiveGotchas() {
7154
7755
  function checkFileGotchas(filePath) {
7155
7756
  const gotchas = getActiveGotchas();
7156
7757
  const warnings = [];
7157
- const base = path15.basename(filePath);
7758
+ const base = path18.basename(filePath);
7158
7759
  for (const g of gotchas) {
7159
- const gBase = path15.basename(g.filePath);
7760
+ const gBase = path18.basename(g.filePath);
7160
7761
  if (filePath.includes(g.filePath) || g.filePath.includes(filePath) || base && gBase && base === gBase) {
7161
7762
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
7162
7763
  warnings.push(`${icon} **Gotcha**: ${g.description} (${g.severity})`);
@@ -7256,8 +7857,8 @@ __export(kumaInject_exports, {
7256
7857
  parseHookInput: () => parseHookInput,
7257
7858
  trackFileEditLoop: () => trackFileEditLoop
7258
7859
  });
7259
- import fs13 from "fs";
7260
- import path16 from "path";
7860
+ import fs16 from "fs";
7861
+ import path19 from "path";
7261
7862
  function hashFileContent(filePath) {
7262
7863
  return hashFile(filePath);
7263
7864
  }
@@ -7268,16 +7869,17 @@ function isGotchaFresh(storedHash, currentHash) {
7268
7869
  }
7269
7870
  async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false) {
7270
7871
  const root = getProjectRoot();
7271
- const base = path16.basename(filePath);
7872
+ const base = path19.basename(filePath);
7272
7873
  try {
7273
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7274
- if (fs13.existsSync(dbFile)) {
7874
+ const dbFile = path19.join(root, ".kuma", "kuma.db");
7875
+ if (fs16.existsSync(dbFile)) {
7275
7876
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7276
7877
  const db = await getDb2();
7277
7878
  const stmt = db.prepare(`
7278
7879
  SELECT id, file_path, description, severity, workaround, content_hash
7279
7880
  FROM known_gotchas
7280
7881
  WHERE status IN ('active', 'verified')
7882
+ AND (quarantined IS NULL OR quarantined = 0)
7281
7883
  AND (
7282
7884
  file_path = ?
7283
7885
  OR file_path LIKE ?
@@ -7311,11 +7913,11 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
7311
7913
  } catch {
7312
7914
  }
7313
7915
  try {
7314
- if (!fs13.existsSync(path16.join(root, ".kuma"))) return [];
7916
+ if (!fs16.existsSync(path19.join(root, ".kuma"))) return [];
7315
7917
  const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
7316
7918
  const allGotchas = getActiveGotchas2();
7317
7919
  const matched = allGotchas.filter(
7318
- (g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path16.basename(g.filePath) === base
7920
+ (g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path19.basename(g.filePath) === base
7319
7921
  );
7320
7922
  if (matched.length > 0) {
7321
7923
  return matched.slice(0, limit).map((g) => ({
@@ -7334,8 +7936,8 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
7334
7936
  async function getFileTrace(filePath, limit = 8) {
7335
7937
  try {
7336
7938
  const root = getProjectRoot();
7337
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7338
- if (!fs13.existsSync(dbFile)) return [];
7939
+ const dbFile = path19.join(root, ".kuma", "kuma.db");
7940
+ if (!fs16.existsSync(dbFile)) return [];
7339
7941
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7340
7942
  const db = await getDb2();
7341
7943
  const stmt = db.prepare(`
@@ -7345,7 +7947,7 @@ async function getFileTrace(filePath, limit = 8) {
7345
7947
  ORDER BY cl.created_at DESC
7346
7948
  LIMIT ?
7347
7949
  `);
7348
- stmt.bind([`%${path16.basename(filePath)}%`, limit]);
7950
+ stmt.bind([`%${path19.basename(filePath)}%`, limit]);
7349
7951
  const rows = [];
7350
7952
  while (stmt.step()) rows.push(stmt.getAsObject());
7351
7953
  stmt.free();
@@ -7363,7 +7965,7 @@ async function getFileTrace(filePath, limit = 8) {
7363
7965
  function formatFileTrace(entries, filePath) {
7364
7966
  if (entries.length === 0) return "";
7365
7967
  const lines = [
7366
- `\u{1F570}\uFE0F **Why is ${path16.basename(filePath)} written this way?** \u2014 ${entries.length} change(s) recorded:`
7968
+ `\u{1F570}\uFE0F **Why is ${path19.basename(filePath)} written this way?** \u2014 ${entries.length} change(s) recorded:`
7367
7969
  ];
7368
7970
  for (const e of entries) {
7369
7971
  const date = new Date((e.createdAt || 0) * 1e3).toLocaleDateString("en-US", {
@@ -7379,12 +7981,12 @@ function formatFileTrace(entries, filePath) {
7379
7981
  async function getDecisionsForFile(filePath, limit = 3) {
7380
7982
  const decisions = [];
7381
7983
  const root = getProjectRoot();
7382
- const base = path16.basename(filePath);
7984
+ const base = path19.basename(filePath);
7383
7985
  const baseWithoutExt = base.replace(/\.[^.]+$/, "");
7384
7986
  try {
7385
- const decisionsMdPath = path16.join(root, ".kuma", "memories", "decisions.md");
7386
- if (fs13.existsSync(decisionsMdPath)) {
7387
- const content = fs13.readFileSync(decisionsMdPath, "utf-8");
7987
+ const decisionsMdPath = path19.join(root, ".kuma", "memories", "decisions.md");
7988
+ if (fs16.existsSync(decisionsMdPath)) {
7989
+ const content = fs16.readFileSync(decisionsMdPath, "utf-8");
7388
7990
  const sections = content.split(/^##\s+/m);
7389
7991
  for (const section of sections) {
7390
7992
  if (!section.trim() || section.startsWith("# ")) continue;
@@ -7455,7 +8057,17 @@ async function getDecisionsForFile(filePath, limit = 3) {
7455
8057
  }
7456
8058
  async function getRelevantContext(filePath, goal) {
7457
8059
  const parts = [];
7458
- const gotchas = await getFreshGotchasForFile(filePath, 4);
8060
+ const { sessionMemory: sessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
8061
+ const gotchaPool = await getFreshGotchasForFile(filePath, 4);
8062
+ const gotchas = gotchaPool.filter((g) => {
8063
+ const key = `${g.filePath}::${g.description.substring(0, 60)}`;
8064
+ if (sessionMemory2.wasShown("gotcha", key)) return false;
8065
+ return true;
8066
+ });
8067
+ sessionMemory2.markShown(
8068
+ "gotcha",
8069
+ gotchaPool.map((g) => `${g.filePath}::${g.description.substring(0, 60)}`)
8070
+ );
7459
8071
  if (gotchas.length > 0) {
7460
8072
  const lines = ["\u26A0\uFE0F ACTIVE GOTCHAS (fresh):"];
7461
8073
  for (const g of gotchas) {
@@ -7465,7 +8077,12 @@ async function getRelevantContext(filePath, goal) {
7465
8077
  }
7466
8078
  parts.push(lines.join("\n"));
7467
8079
  }
7468
- const decisions = await getDecisionsForFile(filePath, goal ? 3 : 2);
8080
+ const decisionPool = await getDecisionsForFile(filePath, goal ? 3 : 2);
8081
+ const decisions = decisionPool.filter((d) => {
8082
+ if (sessionMemory2.wasShown("decision", d.substring(0, 80))) return false;
8083
+ return true;
8084
+ });
8085
+ sessionMemory2.markShown("decision", decisionPool.map((d) => d.substring(0, 80)));
7469
8086
  if (decisions.length > 0) {
7470
8087
  parts.push("\u{1F4CC} RELEVANT DECISIONS:\n " + decisions.join("\n "));
7471
8088
  }
@@ -7474,13 +8091,13 @@ async function getRelevantContext(filePath, goal) {
7474
8091
  if (traceStr) parts.push(traceStr);
7475
8092
  try {
7476
8093
  const root = getProjectRoot();
7477
- if (fs13.existsSync(path16.join(root, ".kuma", "kuma.db"))) {
8094
+ if (fs16.existsSync(path19.join(root, ".kuma", "kuma.db"))) {
7478
8095
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7479
8096
  const db = await getDb2();
7480
8097
  const stmt = db.prepare(
7481
8098
  `SELECT COUNT(*) FROM known_gotchas WHERE status = 'resolved' AND file_path LIKE ?`
7482
8099
  );
7483
- stmt.bind([`%${path16.basename(filePath)}%`]);
8100
+ stmt.bind([`%${path19.basename(filePath)}%`]);
7484
8101
  const found = stmt.step();
7485
8102
  const count = found ? Number(stmt.getAsObject()["COUNT(*)"]) : 0;
7486
8103
  stmt.free();
@@ -7491,7 +8108,7 @@ async function getRelevantContext(filePath, goal) {
7491
8108
  } catch {
7492
8109
  }
7493
8110
  if (gotchas.length > 0) {
7494
- const scopeHint = path16.basename(path16.dirname(filePath)) || "current change";
8111
+ const scopeHint = path19.basename(path19.dirname(filePath)) || "current change";
7495
8112
  parts.push(
7496
8113
  `\u2705 AFTER editing, verify: kuma_safety({ action: "verify", scope: "${scopeHint}" })`
7497
8114
  );
@@ -7507,11 +8124,11 @@ async function getRelevantContext(filePath, goal) {
7507
8124
  async function getFileSummary2(filePath) {
7508
8125
  try {
7509
8126
  const root = getProjectRoot();
7510
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7511
- if (!fs13.existsSync(dbFile)) return null;
8127
+ const dbFile = path19.join(root, ".kuma", "kuma.db");
8128
+ if (!fs16.existsSync(dbFile)) return null;
7512
8129
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7513
8130
  const db = await getDb2();
7514
- const base = path16.basename(filePath);
8131
+ const base = path19.basename(filePath);
7515
8132
  const stmt = db.prepare(`
7516
8133
  SELECT
7517
8134
  (SELECT COUNT(*) FROM known_gotchas WHERE status = 'active' AND (file_path = ? OR file_path LIKE ?)) as active_gotchas,
@@ -7545,9 +8162,9 @@ function parseHookInput(raw) {
7545
8162
  const root = data && typeof data === "object" ? data : {};
7546
8163
  const ti = root.tool_input && typeof root.tool_input === "object" ? root.tool_input : {};
7547
8164
  const args = root.args && typeof root.args === "object" ? root.args : {};
7548
- const paths = /* @__PURE__ */ new Set();
8165
+ const paths2 = /* @__PURE__ */ new Set();
7549
8166
  const add = (p) => {
7550
- if (typeof p === "string" && p.trim()) paths.add(p.trim());
8167
+ if (typeof p === "string" && p.trim()) paths2.add(p.trim());
7551
8168
  };
7552
8169
  add(ti.file_path);
7553
8170
  add(ti.new_file_path);
@@ -7565,7 +8182,7 @@ function parseHookInput(raw) {
7565
8182
  add(args.file);
7566
8183
  const cmd = (typeof ti.command === "string" ? ti.command : void 0) ?? (typeof root.command === "string" ? root.command : void 0) ?? (typeof root.commandline === "string" ? root.commandline : void 0) ?? (typeof args.command === "string" ? args.command : void 0);
7567
8184
  const goal = (typeof ti.goal === "string" ? ti.goal : void 0) ?? (typeof root.goal === "string" ? root.goal : void 0);
7568
- const out = { filePaths: [...paths] };
8185
+ const out = { filePaths: [...paths2] };
7569
8186
  if (goal) out.goal = goal;
7570
8187
  if (cmd) out.command = cmd;
7571
8188
  return out;
@@ -7603,9 +8220,9 @@ async function getCommandContext(command) {
7603
8220
  }
7604
8221
  function loadHookState(root) {
7605
8222
  try {
7606
- const fp = path16.join(root, HOOK_STATE_FILE);
7607
- if (fs13.existsSync(fp)) {
7608
- return JSON.parse(fs13.readFileSync(fp, "utf-8"));
8223
+ const fp = path19.join(root, HOOK_STATE_FILE);
8224
+ if (fs16.existsSync(fp)) {
8225
+ return JSON.parse(fs16.readFileSync(fp, "utf-8"));
7609
8226
  }
7610
8227
  } catch {
7611
8228
  }
@@ -7613,10 +8230,10 @@ function loadHookState(root) {
7613
8230
  }
7614
8231
  function saveHookState(root, state) {
7615
8232
  try {
7616
- const fp = path16.join(root, HOOK_STATE_FILE);
7617
- const dir = path16.dirname(fp);
7618
- if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7619
- fs13.writeFileSync(fp, JSON.stringify(state), "utf-8");
8233
+ const fp = path19.join(root, HOOK_STATE_FILE);
8234
+ const dir = path19.dirname(fp);
8235
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
8236
+ fs16.writeFileSync(fp, JSON.stringify(state), "utf-8");
7620
8237
  } catch {
7621
8238
  }
7622
8239
  }
@@ -7650,7 +8267,7 @@ async function trackFileEditLoop(filePath) {
7650
8267
  saveHookState(root, state);
7651
8268
  if (recorded) {
7652
8269
  try {
7653
- if (!fs13.existsSync(path16.join(root, ".kuma", "kuma.db"))) return entry.count;
8270
+ if (!fs16.existsSync(path19.join(root, ".kuma", "kuma.db"))) return entry.count;
7654
8271
  const { addGotcha: addGotcha2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
7655
8272
  await addGotcha2({
7656
8273
  filePath,
@@ -7688,20 +8305,24 @@ __export(kumaGotchas_exports, {
7688
8305
  addGotcha: () => addGotcha,
7689
8306
  checkGotchasForFile: () => checkGotchasForFile,
7690
8307
  deprecateGotcha: () => deprecateGotcha,
8308
+ findSimilarGotchaId: () => findSimilarGotchaId,
7691
8309
  getActiveGotchasForCommand: () => getActiveGotchasForCommand,
7692
8310
  getGotchasByStatus: () => getGotchasByStatus,
7693
8311
  getInjectionStats: () => getInjectionStats,
8312
+ jaccardSimilarity: () => jaccardSimilarity,
7694
8313
  listGotchas: () => listGotchas,
7695
8314
  recordInjection: () => recordInjection,
7696
8315
  resolveGotcha: () => resolveGotcha,
7697
8316
  resolveGotchasForScope: () => resolveGotchasForScope,
8317
+ screenForInjection: () => screenForInjection,
7698
8318
  setGotchaStatus: () => setGotchaStatus,
7699
8319
  syncGotchaStatusToMarkdown: () => syncGotchaStatusToMarkdown,
7700
8320
  syncGotchasGraph: () => syncGotchasGraph,
7701
- syncGotchasToDb: () => syncGotchasToDb
8321
+ syncGotchasToDb: () => syncGotchasToDb,
8322
+ tokenizeForSimilarity: () => tokenizeForSimilarity
7702
8323
  });
7703
- import fs14 from "fs";
7704
- import path17 from "path";
8324
+ import fs17 from "fs";
8325
+ import path20 from "path";
7705
8326
  async function ensureGotchasSchema() {
7706
8327
  const db = await getDb();
7707
8328
  db.exec(`
@@ -7721,7 +8342,11 @@ async function ensureGotchasSchema() {
7721
8342
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
7722
8343
  last_verified_at INTEGER,
7723
8344
  scope_package TEXT,
7724
- verified_by TEXT
8345
+ verified_by TEXT,
8346
+ provenance TEXT,
8347
+ tier TEXT NOT NULL DEFAULT 'single'
8348
+ CHECK(tier IN ('human-confirmed','corroborated','single','external')),
8349
+ quarantined INTEGER NOT NULL DEFAULT 0
7725
8350
  );
7726
8351
  CREATE INDEX IF NOT EXISTS idx_gotchas_file ON known_gotchas(file_path);
7727
8352
  CREATE INDEX IF NOT EXISTS idx_gotchas_severity ON known_gotchas(severity);
@@ -7747,6 +8372,15 @@ async function ensureGotchasSchema() {
7747
8372
  if (!cols.includes("verified_by")) {
7748
8373
  db.run(`ALTER TABLE known_gotchas ADD COLUMN verified_by TEXT`);
7749
8374
  }
8375
+ if (!cols.includes("provenance")) {
8376
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN provenance TEXT`);
8377
+ }
8378
+ if (!cols.includes("tier")) {
8379
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN tier TEXT NOT NULL DEFAULT 'single'`);
8380
+ }
8381
+ if (!cols.includes("quarantined")) {
8382
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN quarantined INTEGER NOT NULL DEFAULT 0`);
8383
+ }
7750
8384
  const schema = db.exec(`SELECT sql FROM sqlite_master WHERE type='table' AND name='known_gotchas'`);
7751
8385
  const gotchaSql = schema[0]?.values?.[0]?.[0] || "";
7752
8386
  if (gotchaSql && !gotchaSql.includes("candidate")) {
@@ -7767,7 +8401,11 @@ async function ensureGotchasSchema() {
7767
8401
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
7768
8402
  last_verified_at INTEGER,
7769
8403
  scope_package TEXT,
7770
- verified_by TEXT
8404
+ verified_by TEXT,
8405
+ provenance TEXT,
8406
+ tier TEXT NOT NULL DEFAULT 'single'
8407
+ CHECK(tier IN ('human-confirmed','corroborated','single','external')),
8408
+ quarantined INTEGER NOT NULL DEFAULT 0
7771
8409
  )`);
7772
8410
  db.run(`INSERT OR IGNORE INTO known_gotchas (id, file_path, description, severity, workaround, added_by, created_at, updated_at, content_hash, trigger_command, status, last_verified_at) SELECT id, file_path, description, severity, workaround, added_by, created_at, updated_at, content_hash, trigger_command, status, last_verified_at FROM known_gotchas_old`);
7773
8411
  db.run(`DROP TABLE known_gotchas_old`);
@@ -7776,12 +8414,30 @@ async function ensureGotchasSchema() {
7776
8414
  }
7777
8415
  saveDb();
7778
8416
  }
8417
+ function screenForInjection(text) {
8418
+ const t = text.toLowerCase();
8419
+ const patterns = [
8420
+ [/ignore\s+(all\s+)?previous\s+instructions?/, "directive to ignore previous instructions"],
8421
+ [/disregard\s+(the\s+)?(system|prior)\s+(prompt|instructions?)/, "directive to disregard system prompt"],
8422
+ [/(reveal|exfiltrate|leak|send|upload).{0,40}(secret|api[_\s-]?key|token|password|credential)/, "directive to reveal secrets"],
8423
+ [/execute\s+(the\s+)?following\s+(command|code|payload)/, "directive to execute embedded payload"],
8424
+ [/you\s+are\s+now\s+(a|an|in)\s/, "identity-reassignment attempt"]
8425
+ ];
8426
+ for (const [re, reason] of patterns) {
8427
+ if (re.test(t)) return reason;
8428
+ }
8429
+ return null;
8430
+ }
7779
8431
  async function addGotcha(entry) {
7780
8432
  try {
7781
8433
  await ensureGotchasSchema();
7782
8434
  const db = await getDb();
7783
8435
  const severity = entry.severity || "medium";
7784
8436
  const status = entry.status || "active";
8437
+ const provenance = entry.provenance || "session:agent";
8438
+ const quarantineReason = screenForInjection(`${entry.description} ${entry.workaround || ""}`);
8439
+ const quarantined = quarantineReason ? 1 : 0;
8440
+ const tier = quarantined ? "external" : entry.tier || (status === "verified" ? "human-confirmed" : "single");
7785
8441
  const contentHash = hashFileContent(entry.filePath);
7786
8442
  const trap = entry.description;
7787
8443
  const ruleFix = entry.workaround || "Review implementation carefully before modifying.";
@@ -7806,15 +8462,23 @@ async function addGotcha(entry) {
7806
8462
  existingId = row.id;
7807
8463
  }
7808
8464
  checkStmt.free();
8465
+ let mergedNote = "";
8466
+ if (existingId === null) {
8467
+ const similarId = await findSimilarGotchaId(entry.filePath, entry.description);
8468
+ if (similarId !== null) {
8469
+ existingId = similarId;
8470
+ mergedNote = " (merged: near-duplicate of an existing gotcha \u2014 refreshed instead of appended)";
8471
+ }
8472
+ }
7809
8473
  if (existingId !== null) {
7810
8474
  db.run(
7811
- `UPDATE known_gotchas SET severity = ?, workaround = ?, content_hash = ?, trigger_command = ?, status = ?, scope_package = coalesce(?, scope_package), verified_by = coalesce(?, verified_by), updated_at = strftime('%s','now') WHERE id = ?`,
7812
- [severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, existingId]
8475
+ `UPDATE known_gotchas SET severity = ?, workaround = ?, content_hash = ?, trigger_command = ?, status = ?, scope_package = coalesce(?, scope_package), verified_by = coalesce(?, verified_by), provenance = coalesce(?, provenance), tier = CASE WHEN quarantined = 1 THEN tier WHEN ? = 'human-confirmed' THEN 'human-confirmed' ELSE 'corroborated' END, updated_at = strftime('%s','now') WHERE id = ?`,
8476
+ [severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, provenance, tier, existingId]
7813
8477
  );
7814
8478
  } else {
7815
8479
  db.run(
7816
- `INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7817
- [entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null]
8480
+ `INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by, provenance, tier, quarantined) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
8481
+ [entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, provenance, tier, quarantined]
7818
8482
  );
7819
8483
  }
7820
8484
  const mdResult = appendToLayer("gotcha", formatted);
@@ -7846,12 +8510,50 @@ async function addGotcha(entry) {
7846
8510
  } catch {
7847
8511
  }
7848
8512
  const action = existingId !== null ? "updated" : "recorded";
7849
- return `\u2705 **Gotcha ${action}**: ${entry.filePath} \u2014 ${entry.description}${linkedCount > 0 ? ` (linked to ${linkedCount} flow(s))` : ""}
8513
+ const quarantineNote = quarantined ? `
8514
+ \u{1F512} **Quarantined** (${quarantineReason}) \u2014 excluded from auto-inject until human-confirmed. Promote via verify + resolve/re-record.` : "";
8515
+ return `\u2705 **Gotcha ${action}**: ${entry.filePath} \u2014 ${entry.description}${mergedNote}${quarantineNote}${linkedCount > 0 ? ` (linked to ${linkedCount} flow(s))` : ""} [tier: ${tier}]
7850
8516
  ${mdResult}`;
7851
8517
  } catch (err) {
7852
8518
  return `\u274C Failed to add gotcha: ${err}`;
7853
8519
  }
7854
8520
  }
8521
+ function tokenizeForSimilarity(text) {
8522
+ return new Set(
8523
+ text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)
8524
+ );
8525
+ }
8526
+ function jaccardSimilarity(a, b) {
8527
+ if (a.size === 0 || b.size === 0) return 0;
8528
+ let intersection = 0;
8529
+ for (const t of a) if (b.has(t)) intersection++;
8530
+ return intersection / (a.size + b.size - intersection);
8531
+ }
8532
+ async function findSimilarGotchaId(filePath, description) {
8533
+ try {
8534
+ const db = await getDb();
8535
+ const stmt = db.prepare(
8536
+ `SELECT id, description FROM known_gotchas WHERE file_path = ? AND status IN ('active','verified','candidate') LIMIT 50`
8537
+ );
8538
+ stmt.bind([filePath]);
8539
+ const rows = [];
8540
+ while (stmt.step()) rows.push(stmt.getAsObject());
8541
+ stmt.free();
8542
+ const incoming = tokenizeForSimilarity(description);
8543
+ let best = null;
8544
+ let bestScore = 0;
8545
+ for (const r of rows) {
8546
+ const score = jaccardSimilarity(incoming, tokenizeForSimilarity(r.description));
8547
+ if (score >= GOTCHA_MERGE_THRESHOLD && score > bestScore) {
8548
+ bestScore = score;
8549
+ best = r.id;
8550
+ }
8551
+ }
8552
+ return best;
8553
+ } catch {
8554
+ return null;
8555
+ }
8556
+ }
7855
8557
  async function listGotchas(params) {
7856
8558
  try {
7857
8559
  await ensureGotchasSchema();
@@ -7917,8 +8619,12 @@ async function listGotchas(params) {
7917
8619
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
7918
8620
  const ageSec = nowSec - (Number(g.created_at) || nowSec);
7919
8621
  const isNew = ageSec < 24 * 3600 ? " \u{1F195}" : "";
8622
+ const ageDays = Math.floor(ageSec / 86400);
8623
+ const expired = ageDays > 90 ? " \u23F3expired" : "";
8624
+ const tier = String(g.tier || "single");
8625
+ const quar = Number(g.quarantined) === 1 ? " \u{1F512}quarantined" : "";
7920
8626
  const updated = g.updated_at ? ` \xB7 lastSeen ${new Date(Number(g.updated_at) * 1e3).toISOString().split("T")[0]}` : "";
7921
- lines.push(`${icon} [${g.severity}]${isNew} ${g.file_path}${updated}`);
8627
+ lines.push(`${icon} [${g.severity}]${isNew}${expired}${quar} ${g.file_path}${updated} (${ageDays}d old, tier: ${tier})`);
7922
8628
  lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
7923
8629
  if (g.trigger_command) lines.push(` \u2328\uFE0F when running: \`${g.trigger_command.substring(0, 80)}\``);
7924
8630
  if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
@@ -8013,7 +8719,7 @@ async function getActiveGotchasForCommand(command) {
8013
8719
  const stmt = db.prepare(
8014
8720
  `SELECT file_path, description, severity, workaround, trigger_command
8015
8721
  FROM known_gotchas
8016
- WHERE status IN ('active', 'verified') AND trigger_command IS NOT NULL AND length(trigger_command) > 0
8722
+ WHERE status IN ('active', 'verified') AND (quarantined IS NULL OR quarantined = 0) AND trigger_command IS NOT NULL AND length(trigger_command) > 0
8017
8723
  ORDER BY
8018
8724
  CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END
8019
8725
  LIMIT 30`
@@ -8038,8 +8744,8 @@ async function getActiveGotchasForCommand(command) {
8038
8744
  }
8039
8745
  async function recordInjection(params) {
8040
8746
  try {
8041
- const fp = path17.join(getProjectRoot(), ".kuma", "injections.jsonl");
8042
- if (!fs14.existsSync(path17.dirname(fp))) fs14.mkdirSync(path17.dirname(fp), { recursive: true });
8747
+ const fp = path20.join(getProjectRoot(), ".kuma", "injections.jsonl");
8748
+ if (!fs17.existsSync(path20.dirname(fp))) fs17.mkdirSync(path20.dirname(fp), { recursive: true });
8043
8749
  const line = JSON.stringify({
8044
8750
  filePath: params.filePath || null,
8045
8751
  command: params.command || null,
@@ -8048,16 +8754,16 @@ async function recordInjection(params) {
8048
8754
  // conservative estimate: avoided re-research / re-discovery
8049
8755
  at: Date.now()
8050
8756
  });
8051
- fs14.appendFileSync(fp, line + "\n", "utf-8");
8757
+ fs17.appendFileSync(fp, line + "\n", "utf-8");
8052
8758
  } catch {
8053
8759
  }
8054
8760
  }
8055
8761
  function getInjectionStats(hours = 24) {
8056
8762
  try {
8057
- const fp = path17.join(getProjectRoot(), ".kuma", "injections.jsonl");
8058
- if (!fs14.existsSync(fp)) return { count: 0, savedMs: 0 };
8763
+ const fp = path20.join(getProjectRoot(), ".kuma", "injections.jsonl");
8764
+ if (!fs17.existsSync(fp)) return { count: 0, savedMs: 0 };
8059
8765
  const cutoff = Date.now() - hours * 60 * 60 * 1e3;
8060
- const lines = fs14.readFileSync(fp, "utf-8").split("\n");
8766
+ const lines = fs17.readFileSync(fp, "utf-8").split("\n");
8061
8767
  let count = 0;
8062
8768
  let savedMs = 0;
8063
8769
  for (const line of lines) {
@@ -8079,10 +8785,17 @@ function getInjectionStats(hours = 24) {
8079
8785
  async function setGotchaStatus(id, status, verifiedBy) {
8080
8786
  try {
8081
8787
  const db = await getDb();
8082
- db.run(
8083
- `UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
8084
- [status, verifiedBy || null, id]
8085
- );
8788
+ if (status === "verified") {
8789
+ db.run(
8790
+ `UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), tier = 'human-confirmed', quarantined = 0, last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
8791
+ [status, verifiedBy || null, id]
8792
+ );
8793
+ } else {
8794
+ db.run(
8795
+ `UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
8796
+ [status, verifiedBy || null, id]
8797
+ );
8798
+ }
8086
8799
  saveDb();
8087
8800
  return true;
8088
8801
  } catch {
@@ -8108,9 +8821,9 @@ async function getGotchasByStatus(status, limit = 20) {
8108
8821
  }
8109
8822
  function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8110
8823
  try {
8111
- const mdPath = path17.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
8112
- if (!fs14.existsSync(mdPath)) return false;
8113
- const content = fs14.readFileSync(mdPath, "utf-8");
8824
+ const mdPath = path20.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
8825
+ if (!fs17.existsSync(mdPath)) return false;
8826
+ const content = fs17.readFileSync(mdPath, "utf-8");
8114
8827
  const lines = content.split("\n");
8115
8828
  const normalizedTargets = targetPaths.map((p) => p.trim().toLowerCase()).filter(Boolean);
8116
8829
  if (normalizedTargets.length === 0) return false;
@@ -8127,9 +8840,9 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8127
8840
  modified = true;
8128
8841
  }
8129
8842
  const headingFile = headingMatch[1].trim().toLowerCase();
8130
- const headingBase = path17.basename(headingFile);
8843
+ const headingBase = path20.basename(headingFile);
8131
8844
  inMatchingSection = normalizedTargets.some(
8132
- (t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path17.basename(t)
8845
+ (t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path20.basename(t)
8133
8846
  );
8134
8847
  sectionHasStatus = false;
8135
8848
  outputLines.push(line);
@@ -8151,7 +8864,7 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8151
8864
  modified = true;
8152
8865
  }
8153
8866
  if (modified) {
8154
- fs14.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
8867
+ fs17.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
8155
8868
  }
8156
8869
  return modified;
8157
8870
  } catch {
@@ -8274,6 +8987,7 @@ async function deprecateGotcha(idOrScope, reason) {
8274
8987
  return { deprecated: 0, message: `\u274C Failed to deprecate gotcha: ${err}` };
8275
8988
  }
8276
8989
  }
8990
+ var GOTCHA_MERGE_THRESHOLD;
8277
8991
  var init_kumaGotchas = __esm({
8278
8992
  "src/engine/kumaGotchas.ts"() {
8279
8993
  "use strict";
@@ -8282,6 +8996,7 @@ var init_kumaGotchas = __esm({
8282
8996
  init_pathValidator();
8283
8997
  init_domainRules();
8284
8998
  init_kumaInject();
8999
+ GOTCHA_MERGE_THRESHOLD = 0.6;
8285
9000
  }
8286
9001
  });
8287
9002
 
@@ -8290,24 +9005,24 @@ var kumaMaintenance_exports = {};
8290
9005
  __export(kumaMaintenance_exports, {
8291
9006
  maybeRunAutoCleanup: () => maybeRunAutoCleanup
8292
9007
  });
8293
- import fs15 from "fs";
8294
- import path18 from "path";
9008
+ import fs18 from "fs";
9009
+ import path21 from "path";
8295
9010
  async function maybeRunAutoCleanup() {
8296
9011
  try {
8297
9012
  const root = getProjectRoot();
8298
- const stateFile = path18.join(root, GC_STATE_FILE);
9013
+ const stateFile = path21.join(root, GC_STATE_FILE);
8299
9014
  const now = Date.now();
8300
9015
  let lastRun = 0;
8301
- if (fs15.existsSync(stateFile)) {
9016
+ if (fs18.existsSync(stateFile)) {
8302
9017
  try {
8303
- lastRun = JSON.parse(fs15.readFileSync(stateFile, "utf-8")).lastRun || 0;
9018
+ lastRun = JSON.parse(fs18.readFileSync(stateFile, "utf-8")).lastRun || 0;
8304
9019
  } catch {
8305
9020
  lastRun = 0;
8306
9021
  }
8307
9022
  }
8308
9023
  if (now - lastRun < GC_INTERVAL_MS) return "";
8309
- fs15.mkdirSync(path18.dirname(stateFile), { recursive: true });
8310
- fs15.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
9024
+ fs18.mkdirSync(path21.dirname(stateFile), { recursive: true });
9025
+ fs18.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
8311
9026
  const { syncGotchasToDb: syncGotchasToDb2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
8312
9027
  await syncGotchasToDb2();
8313
9028
  const { runGarbageCollection: runGarbageCollection2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
@@ -8343,22 +9058,22 @@ __export(kumaPathRules_exports, {
8343
9058
  loadPathRules: () => loadPathRules,
8344
9059
  matchRulePath: () => matchRulePath
8345
9060
  });
8346
- import fs16 from "fs";
8347
- import path19 from "path";
9061
+ import fs19 from "fs";
9062
+ import path22 from "path";
8348
9063
  function rulesDir() {
8349
- return path19.join(getProjectRoot(), RULES_DIR);
9064
+ return path22.join(getProjectRoot(), RULES_DIR);
8350
9065
  }
8351
9066
  function ensureRulesDir() {
8352
9067
  try {
8353
9068
  const dir = rulesDir();
8354
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
9069
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
8355
9070
  } catch {
8356
9071
  }
8357
9072
  }
8358
9073
  function parseRuleFile(filePath) {
8359
9074
  try {
8360
- const raw = fs16.readFileSync(filePath, "utf-8");
8361
- const id = path19.basename(filePath, ".md");
9075
+ const raw = fs19.readFileSync(filePath, "utf-8");
9076
+ const id = path22.basename(filePath, ".md");
8362
9077
  const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
8363
9078
  if (!fm) {
8364
9079
  return { id, description: id, paths: ["*"], content: raw.trim(), filePath };
@@ -8366,15 +9081,15 @@ function parseRuleFile(filePath) {
8366
9081
  const meta = fm[1];
8367
9082
  const body = fm[2].trim();
8368
9083
  const description = meta.match(/description:\s*(.+)/)?.[1]?.trim() || id;
8369
- const paths = [];
9084
+ const paths2 = [];
8370
9085
  const pathSection = meta.match(/paths:\s*\r?\n([\s\S]*?)(?=\r?\n\S|$)/);
8371
9086
  if (pathSection) {
8372
9087
  for (const line of pathSection[1].split("\n")) {
8373
9088
  const m = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
8374
- if (m) paths.push(m[1].trim());
9089
+ if (m) paths2.push(m[1].trim());
8375
9090
  }
8376
9091
  }
8377
- return { id, description, paths: paths.length ? paths : ["*"], content: body, filePath };
9092
+ return { id, description, paths: paths2.length ? paths2 : ["*"], content: body, filePath };
8378
9093
  } catch {
8379
9094
  return null;
8380
9095
  }
@@ -8383,7 +9098,7 @@ function loadPathRules() {
8383
9098
  ensureRulesDir();
8384
9099
  try {
8385
9100
  const dir = rulesDir();
8386
- return fs16.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseRuleFile(path19.join(dir, f))).filter((r) => r !== null);
9101
+ return fs19.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseRuleFile(path22.join(dir, f))).filter((r) => r !== null);
8387
9102
  } catch {
8388
9103
  return [];
8389
9104
  }
@@ -8471,8 +9186,8 @@ __export(kumaFlowCache_exports, {
8471
9186
  getFreshDomainFlow: () => getFreshDomainFlow,
8472
9187
  getHopsForDomain: () => getHopsForDomain
8473
9188
  });
8474
- import fs17 from "fs";
8475
- import path20 from "path";
9189
+ import fs20 from "fs";
9190
+ import path23 from "path";
8476
9191
  async function getFlowFreshness(domain) {
8477
9192
  try {
8478
9193
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
@@ -8507,26 +9222,26 @@ async function getFlowFreshness(domain) {
8507
9222
  }
8508
9223
  function resolveImport(fromFile, spec) {
8509
9224
  const root = getProjectRoot();
8510
- const fromDir = path20.dirname(fromFile);
9225
+ const fromDir = path23.dirname(fromFile);
8511
9226
  const candidates = [];
8512
9227
  if (spec.startsWith(".")) {
8513
- const abs = path20.resolve(fromDir, spec);
9228
+ const abs = path23.resolve(fromDir, spec);
8514
9229
  candidates.push(abs);
8515
9230
  } else {
8516
- candidates.push(path20.join(root, spec));
9231
+ candidates.push(path23.join(root, spec));
8517
9232
  }
8518
9233
  for (const c of candidates) {
8519
9234
  for (const ext of SOURCE_EXTENSIONS) {
8520
9235
  const withExt = `${c}${ext}`;
8521
- if (fs17.existsSync(withExt)) return path20.relative(root, withExt);
8522
- if (fs17.existsSync(c) && fs17.statSync(c).isDirectory()) {
8523
- const idx = path20.join(c, "index") + ext;
8524
- if (fs17.existsSync(idx)) return path20.relative(root, idx);
9236
+ if (fs20.existsSync(withExt)) return path23.relative(root, withExt);
9237
+ if (fs20.existsSync(c) && fs20.statSync(c).isDirectory()) {
9238
+ const idx = path23.join(c, "index") + ext;
9239
+ if (fs20.existsSync(idx)) return path23.relative(root, idx);
8525
9240
  }
8526
9241
  }
8527
- if (fs17.existsSync(c)) {
9242
+ if (fs20.existsSync(c)) {
8528
9243
  try {
8529
- if (fs17.statSync(c).isFile()) return path20.relative(root, c);
9244
+ if (fs20.statSync(c).isFile()) return path23.relative(root, c);
8530
9245
  } catch {
8531
9246
  }
8532
9247
  }
@@ -8535,13 +9250,13 @@ function resolveImport(fromFile, spec) {
8535
9250
  }
8536
9251
  function deriveHopsFromImports(entryFile, maxHops = 6) {
8537
9252
  const root = getProjectRoot();
8538
- const absEntry = path20.isAbsolute(entryFile) ? entryFile : path20.resolve(root, entryFile);
8539
- if (!fs17.existsSync(absEntry)) return [];
9253
+ const absEntry = path23.isAbsolute(entryFile) ? entryFile : path23.resolve(root, entryFile);
9254
+ if (!fs20.existsSync(absEntry)) return [];
8540
9255
  const hops = [];
8541
9256
  const seen = /* @__PURE__ */ new Set([entryFile]);
8542
- const fromName = path20.basename(absEntry);
9257
+ const fromName = path23.basename(absEntry);
8543
9258
  try {
8544
- const lines = fs17.readFileSync(absEntry, "utf-8").split("\n");
9259
+ const lines = fs20.readFileSync(absEntry, "utf-8").split("\n");
8545
9260
  for (const line of lines) {
8546
9261
  if (hops.length >= maxHops) break;
8547
9262
  const spec = matchImportPath(line);
@@ -8551,9 +9266,9 @@ function deriveHopsFromImports(entryFile, maxHops = 6) {
8551
9266
  seen.add(resolved);
8552
9267
  hops.push({
8553
9268
  from: fromName,
8554
- to: path20.basename(resolved),
9269
+ to: path23.basename(resolved),
8555
9270
  relation: "imports",
8556
- description: `${fromName} imports ${path20.basename(resolved)}`
9271
+ description: `${fromName} imports ${path23.basename(resolved)}`
8557
9272
  });
8558
9273
  }
8559
9274
  } catch {
@@ -8616,9 +9331,9 @@ async function getHopsForDomain(domain) {
8616
9331
  }
8617
9332
  try {
8618
9333
  const root = getProjectRoot();
8619
- const archPath = path20.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
8620
- if (fs17.existsSync(archPath)) {
8621
- const content = fs17.readFileSync(archPath, "utf-8");
9334
+ const archPath = path23.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
9335
+ if (fs20.existsSync(archPath)) {
9336
+ const content = fs20.readFileSync(archPath, "utf-8");
8622
9337
  const lines = content.split("\n");
8623
9338
  for (const line of lines) {
8624
9339
  const domMatch = line.match(new RegExp(`domain:\\s*${escapeRegex(domain)}\\s*\\|\\s*hops:\\s*(.+)`, "i"));
@@ -8663,7 +9378,7 @@ Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} |
8663
9378
  hops: derivedHops,
8664
9379
  filePaths: freshness.filePaths
8665
9380
  });
8666
- lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path20.basename(entry)} \u2014 cache refreshed.`);
9381
+ lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path23.basename(entry)} \u2014 cache refreshed.`);
8667
9382
  hops.splice(0, hops.length, ...derivedHops);
8668
9383
  } else {
8669
9384
  lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
@@ -8707,6 +9422,127 @@ var init_kumaFlowCache = __esm({
8707
9422
  }
8708
9423
  });
8709
9424
 
9425
+ // src/engine/packageBriefs.ts
9426
+ var packageBriefs_exports = {};
9427
+ __export(packageBriefs_exports, {
9428
+ BRIEFS_DIR: () => BRIEFS_DIR,
9429
+ briefFreshness: () => briefFreshness,
9430
+ buildPackageBrief: () => buildPackageBrief,
9431
+ readBrief: () => readBrief,
9432
+ refreshPackageBriefs: () => refreshPackageBriefs
9433
+ });
9434
+ import fs21 from "fs";
9435
+ import path24 from "path";
9436
+ function briefsDir(root) {
9437
+ return path24.join(root || getProjectRoot(), BRIEFS_DIR);
9438
+ }
9439
+ function slug(name) {
9440
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60) || "root";
9441
+ }
9442
+ async function buildPackageBrief(pkgName, pkgPath, _root) {
9443
+ const lines = [`# \u{1F4E6} ${pkgName}`, "", `_Auto-generated by Kuma \u2014 do not edit. Regenerate via map refresh._`, ""];
9444
+ if (pkgPath && pkgPath !== ".") lines.push(`path: \`${pkgPath}\``, "");
9445
+ try {
9446
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
9447
+ const db = await getDb2();
9448
+ const like = pkgPath && pkgPath !== "." ? `%${pkgPath}%` : `%`;
9449
+ const stmt = db.prepare(`
9450
+ SELECT n.file_path as fp, COUNT(*) as cnt FROM edges e
9451
+ JOIN nodes n ON n.id = e.target_id
9452
+ WHERE n.file_path LIKE ? AND n.file_path NOT LIKE '%test%'
9453
+ GROUP BY n.file_path ORDER BY cnt DESC LIMIT 8
9454
+ `);
9455
+ stmt.bind([like]);
9456
+ const hubs = [];
9457
+ while (stmt.step()) {
9458
+ const r = stmt.getAsObject();
9459
+ if (r.fp) hubs.push(`- \`${r.fp}\` (${r.cnt} refs)`);
9460
+ }
9461
+ stmt.free();
9462
+ lines.push(`## \u{1F3AF} Key files`, ...hubs.length > 0 ? hubs : ["- (no graph data yet \u2014 run a scan)"], "");
9463
+ try {
9464
+ const g = db.prepare(
9465
+ `SELECT file_path, description, severity FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0) AND file_path LIKE ? ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 ELSE 2 END LIMIT 5`
9466
+ );
9467
+ g.bind([like]);
9468
+ const rows = [];
9469
+ while (g.step()) {
9470
+ const r = g.getAsObject();
9471
+ rows.push(`- [${r.severity}] \`${r.file_path}\` \u2014 ${String(r.description).substring(0, 100)}`);
9472
+ }
9473
+ g.free();
9474
+ lines.push(`## \u26A0\uFE0F Gotchas`, ...rows.length > 0 ? rows : ["- none"], "");
9475
+ } catch {
9476
+ }
9477
+ try {
9478
+ const t = db.prepare(
9479
+ `SELECT DISTINCT file_path FROM nodes WHERE type = 'test' AND file_path LIKE ? LIMIT 10`
9480
+ );
9481
+ t.bind([like]);
9482
+ const tests = [];
9483
+ while (t.step()) {
9484
+ const r = t.getAsObject();
9485
+ if (r.file_path) tests.push(`- \`${r.file_path}\``);
9486
+ }
9487
+ t.free();
9488
+ lines.push(`## \u{1F9EA} Tests`, ...tests.length > 0 ? tests : ["- (run a scan to map tests)"], "");
9489
+ } catch {
9490
+ }
9491
+ } catch {
9492
+ lines.push("_(graph unavailable \u2014 brief will fill in after a scan)_", "");
9493
+ }
9494
+ return lines.join("\n");
9495
+ }
9496
+ async function refreshPackageBriefs() {
9497
+ const root = getProjectRoot();
9498
+ const dir = briefsDir(root);
9499
+ try {
9500
+ const ws = await getWorkspaceInfo(root);
9501
+ if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
9502
+ let count = 0;
9503
+ const pkgs = ws.isWorkspace ? ws.packages : [{ name: "root", path: ".", isRoot: true }];
9504
+ for (const p of pkgs.slice(0, 20)) {
9505
+ const pkg = p;
9506
+ const content = await buildPackageBrief(pkg.name, pkg.path, root);
9507
+ fs21.writeFileSync(path24.join(dir, `${slug(pkg.name)}.md`), content, "utf-8");
9508
+ count++;
9509
+ }
9510
+ return { count, dir };
9511
+ } catch {
9512
+ return { count: 0, dir };
9513
+ }
9514
+ }
9515
+ function briefFreshness(pkgName) {
9516
+ try {
9517
+ const fp = path24.join(briefsDir(), `${slug(pkgName)}.md`);
9518
+ if (!fs21.existsSync(fp)) return { exists: false, fresh: false, age: "missing" };
9519
+ const ageMs = Date.now() - fs21.statSync(fp).mtimeMs;
9520
+ return { exists: true, fresh: !isStale(Date.now() - ageMs, MAP_TTL_MS), age: formatAge(Date.now() - ageMs) };
9521
+ } catch {
9522
+ return { exists: false, fresh: false, age: "missing" };
9523
+ }
9524
+ }
9525
+ function readBrief(pkgName, maxChars = 1200) {
9526
+ try {
9527
+ const fp = path24.join(briefsDir(), `${slug(pkgName)}.md`);
9528
+ if (!fs21.existsSync(fp)) return null;
9529
+ const content = fs21.readFileSync(fp, "utf-8");
9530
+ return content.length > maxChars ? content.slice(0, maxChars) + "\n\u2026(truncated)" : content;
9531
+ } catch {
9532
+ return null;
9533
+ }
9534
+ }
9535
+ var BRIEFS_DIR;
9536
+ var init_packageBriefs = __esm({
9537
+ "src/engine/packageBriefs.ts"() {
9538
+ "use strict";
9539
+ init_pathValidator();
9540
+ init_workspaceIntelligence();
9541
+ init_cacheFreshness();
9542
+ BRIEFS_DIR = ".kuma/briefs";
9543
+ }
9544
+ });
9545
+
8710
9546
  // src/engine/kumaSearch.ts
8711
9547
  var kumaSearch_exports = {};
8712
9548
  __export(kumaSearch_exports, {
@@ -8719,8 +9555,8 @@ __export(kumaSearch_exports, {
8719
9555
  formatHybridResults: () => formatHybridResults,
8720
9556
  hybridSearch: () => hybridSearch
8721
9557
  });
8722
- import fs19 from "fs";
8723
- import path22 from "path";
9558
+ import fs23 from "fs";
9559
+ import path26 from "path";
8724
9560
  function expandQueryTerms(query) {
8725
9561
  const terms = query.toLowerCase().split(/[\s_-]+/).filter((t) => t.length > 2);
8726
9562
  const expanded = /* @__PURE__ */ new Map();
@@ -8783,13 +9619,13 @@ async function buildSearchVectors() {
8783
9619
  }
8784
9620
  try {
8785
9621
  const root = getProjectRoot();
8786
- const memDir = path22.join(root, ".kuma", "memories");
8787
- if (fs19.existsSync(memDir)) {
8788
- const files = fs19.readdirSync(memDir).filter((f) => f.endsWith(".md")).slice(0, SCALE_LIMITS.maxMemoryFiles);
9622
+ const memDir = path26.join(root, ".kuma", "memories");
9623
+ if (fs23.existsSync(memDir)) {
9624
+ const files = fs23.readdirSync(memDir).filter((f) => f.endsWith(".md")).slice(0, SCALE_LIMITS.maxMemoryFiles);
8789
9625
  for (const file of files) {
8790
9626
  try {
8791
- const fullPath = path22.join(memDir, file);
8792
- const content = fs19.readFileSync(fullPath, "utf-8");
9627
+ const fullPath = path26.join(memDir, file);
9628
+ const content = fs23.readFileSync(fullPath, "utf-8");
8793
9629
  const text = content.toLowerCase();
8794
9630
  const rawTerms = extractTerms(text);
8795
9631
  const termCounts = /* @__PURE__ */ new Map();
@@ -8800,7 +9636,7 @@ async function buildSearchVectors() {
8800
9636
  terms: termCounts,
8801
9637
  source: file.replace(/\.md$/, ""),
8802
9638
  sourceType: "memory",
8803
- recency: recencyScore(Math.floor(fs19.statSync(fullPath).mtimeMs / 1e3))
9639
+ recency: recencyScore(Math.floor(fs23.statSync(fullPath).mtimeMs / 1e3))
8804
9640
  });
8805
9641
  } catch {
8806
9642
  }
@@ -9117,14 +9953,14 @@ __export(architectureGuard_exports, {
9117
9953
  detectWorkspaceCircularDeps: () => detectWorkspaceCircularDeps,
9118
9954
  loadArchitectureRules: () => loadArchitectureRules
9119
9955
  });
9120
- import fs23 from "fs";
9121
- import path26 from "path";
9956
+ import fs27 from "fs";
9957
+ import path30 from "path";
9122
9958
  import fastGlob4 from "fast-glob";
9123
9959
  function loadArchitectureRules(root) {
9124
- const rulesPath = path26.join(root, ".kuma", "architecture.json");
9125
- if (fs23.existsSync(rulesPath)) {
9960
+ const rulesPath = path30.join(root, ".kuma", "architecture.json");
9961
+ if (fs27.existsSync(rulesPath)) {
9126
9962
  try {
9127
- const data = JSON.parse(fs23.readFileSync(rulesPath, "utf-8"));
9963
+ const data = JSON.parse(fs27.readFileSync(rulesPath, "utf-8"));
9128
9964
  if (Array.isArray(data.rules)) {
9129
9965
  return data.rules;
9130
9966
  }
@@ -9186,11 +10022,11 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
9186
10022
  const customRules = loadArchitectureRules(root);
9187
10023
  const importRegex = /(?:import|export\s+.*from|require\s*\()\s*['"]([^'"]+)['"]/g;
9188
10024
  for (const file of targetFiles) {
9189
- const fullPath = path26.isAbsolute(file) ? file : path26.join(root, file);
9190
- if (!fs23.existsSync(fullPath)) continue;
10025
+ const fullPath = path30.isAbsolute(file) ? file : path30.join(root, file);
10026
+ if (!fs27.existsSync(fullPath)) continue;
9191
10027
  let content = "";
9192
10028
  try {
9193
- content = fs23.readFileSync(fullPath, "utf-8");
10029
+ content = fs27.readFileSync(fullPath, "utf-8");
9194
10030
  } catch {
9195
10031
  continue;
9196
10032
  }
@@ -9200,7 +10036,7 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
9200
10036
  const importPath = match[1];
9201
10037
  if (wsInfo.isWorkspace && currentPkg && !currentPkg.isRoot) {
9202
10038
  if (importPath.startsWith("../") && (importPath.includes("/src/") || importPath.includes("/dist/"))) {
9203
- const resolvedTarget = path26.normalize(path26.resolve(path26.dirname(fullPath), importPath));
10039
+ const resolvedTarget = path30.normalize(path30.resolve(path30.dirname(fullPath), importPath));
9204
10040
  const targetPkg = findPackageForFile(resolvedTarget, wsInfo);
9205
10041
  if (targetPkg && targetPkg.name !== currentPkg.name) {
9206
10042
  violations.push({
@@ -9241,6 +10077,256 @@ var init_architectureGuard = __esm({
9241
10077
  }
9242
10078
  });
9243
10079
 
10080
+ // src/engine/docDrift.ts
10081
+ var docDrift_exports = {};
10082
+ __export(docDrift_exports, {
10083
+ collectDocFiles: () => collectDocFiles,
10084
+ extractClaims: () => extractClaims,
10085
+ formatDocDrift: () => formatDocDrift,
10086
+ scanDocDrift: () => scanDocDrift
10087
+ });
10088
+ import fs28 from "fs";
10089
+ import path31 from "path";
10090
+ import { execSync as execSync9 } from "child_process";
10091
+ function collectDocFiles(root) {
10092
+ const cwd = root || getProjectRoot();
10093
+ const out = [];
10094
+ const pushIfMd = (dir, rel) => {
10095
+ try {
10096
+ for (const f of fs28.readdirSync(dir)) {
10097
+ if (out.length >= MAX_DOCS) return;
10098
+ const full = path31.join(dir, f);
10099
+ try {
10100
+ if (fs28.statSync(full).isFile() && /\.md$/i.test(f)) out.push(rel(f));
10101
+ } catch {
10102
+ }
10103
+ }
10104
+ } catch {
10105
+ }
10106
+ };
10107
+ pushIfMd(path31.join(cwd, "docs"), (f) => path31.join("docs", f));
10108
+ pushIfMd(cwd, (f) => f);
10109
+ return out;
10110
+ }
10111
+ function isExemptDoc(relPath, content) {
10112
+ if (EXEMPT_PATTERN.test(relPath)) return true;
10113
+ const head = content.split("\n").slice(0, 15).join("\n");
10114
+ return EXEMPT_PATTERN.test(head);
10115
+ }
10116
+ function extractClaims(relPath, content) {
10117
+ const claims = [];
10118
+ const lines = content.split("\n");
10119
+ lines.forEach((line, idx) => {
10120
+ const lineNo = idx + 1;
10121
+ for (const m of line.matchAll(ENV_PATTERN)) {
10122
+ const name = m[1];
10123
+ if (ENV_ALLOWLIST_HINTS.some((h) => name.toLowerCase().includes(h))) {
10124
+ claims.push({ kind: "env", text: name, docFile: relPath, docLine: lineNo });
10125
+ }
10126
+ }
10127
+ for (const m of line.matchAll(ENDPOINT_PATTERN)) {
10128
+ const ep = m[1];
10129
+ if (ep.length > 60) continue;
10130
+ claims.push({ kind: "endpoint", text: ep, docFile: relPath, docLine: lineNo });
10131
+ }
10132
+ for (const m of line.matchAll(ABSENCE_PATTERN)) {
10133
+ const thing = (m[1] || "").trim();
10134
+ if (thing.length >= 3 && thing.length <= 40) {
10135
+ claims.push({ kind: "absence", text: thing, docFile: relPath, docLine: lineNo });
10136
+ }
10137
+ }
10138
+ });
10139
+ return claims;
10140
+ }
10141
+ function codeContainsBatch(root, needles) {
10142
+ const found = /* @__PURE__ */ new Set();
10143
+ const clean = [...new Set(needles.map((n) => n.replace(/"/g, "").trim()).filter((n) => n.length >= 3))];
10144
+ if (clean.length === 0) return found;
10145
+ for (let i = 0; i < clean.length; i += 50) {
10146
+ const chunk = clean.slice(i, i + 50);
10147
+ try {
10148
+ const args = chunk.map((n) => `-e "${n}"`).join(" ");
10149
+ const out = execSync9(
10150
+ `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`,
10151
+ { cwd: root, encoding: "utf-8", timeout: 8e3, maxBuffer: 256 * 1024 }
10152
+ ).trim();
10153
+ for (const n of chunk) {
10154
+ const isIdent = /^[A-Za-z0-9_]+$/.test(n);
10155
+ const hit = isIdent ? new RegExp(`\\b${n}\\b`).test(out) : out.includes(n);
10156
+ if (hit) found.add(n);
10157
+ }
10158
+ } catch {
10159
+ }
10160
+ }
10161
+ return found;
10162
+ }
10163
+ function scanDocDrift(root) {
10164
+ const cwd = root || getProjectRoot();
10165
+ const skipped = [];
10166
+ let claims = [];
10167
+ for (const rel of collectDocFiles(cwd)) {
10168
+ try {
10169
+ const content = fs28.readFileSync(path31.join(cwd, rel), "utf-8");
10170
+ if (isExemptDoc(rel, content)) {
10171
+ skipped.push(rel);
10172
+ continue;
10173
+ }
10174
+ claims.push(...extractClaims(rel, content));
10175
+ if (claims.length >= MAX_CLAIMS) break;
10176
+ } catch {
10177
+ }
10178
+ }
10179
+ claims = claims.slice(0, MAX_CLAIMS);
10180
+ const present = codeContainsBatch(cwd, claims.map((c) => c.text));
10181
+ const drifted = [];
10182
+ for (const c of claims) {
10183
+ if (c.kind === "absence") {
10184
+ if (present.has(c.text)) {
10185
+ drifted.push({ ...c, codeHint: `code references "${c.text}" but doc claims absence` });
10186
+ }
10187
+ } else {
10188
+ if (!present.has(c.text)) {
10189
+ drifted.push({ ...c, codeHint: `"${c.text}" found in no code/config file` });
10190
+ }
10191
+ }
10192
+ if (drifted.length >= 10) break;
10193
+ }
10194
+ return { checked: claims.length, drifted, skipped };
10195
+ }
10196
+ function formatDocDrift(checked, drifted, skipped) {
10197
+ if (drifted.length === 0) {
10198
+ return `\u{1F4C4} **Doc drift**: ${checked} claim(s) checked, no drift detected.`;
10199
+ }
10200
+ const lines = [
10201
+ `\u{1F4C4} **Doc drift detected** \u2014 ${drifted.length} stale claim(s) of ${checked} checked:`
10202
+ ];
10203
+ for (const d of drifted) {
10204
+ lines.push(` \u26A0\uFE0F [${d.kind}] "${d.text}" \u2014 ${d.docFile}:${d.docLine} vs ${d.codeHint}`);
10205
+ }
10206
+ if (skipped.length > 0) lines.push(` \u2139\uFE0F exempt (RFC/aspirational): ${skipped.slice(0, 3).join(", ")}`);
10207
+ lines.push(` \u{1F4A1} Confirm with a human, then patch the doc or the code.`);
10208
+ return lines.join("\n");
10209
+ }
10210
+ var MAX_DOCS, MAX_CLAIMS, EXEMPT_PATTERN, ENV_PATTERN, ENDPOINT_PATTERN, ABSENCE_PATTERN, ENV_ALLOWLIST_HINTS;
10211
+ var init_docDrift = __esm({
10212
+ "src/engine/docDrift.ts"() {
10213
+ "use strict";
10214
+ init_pathValidator();
10215
+ MAX_DOCS = 20;
10216
+ MAX_CLAIMS = 200;
10217
+ EXEMPT_PATTERN = /(rfc|proposal|draft|roadmap|aspirational|vision|wishlist)/i;
10218
+ ENV_PATTERN = /\b([A-Z][A-Z0-9_]{3,})\b/g;
10219
+ ENDPOINT_PATTERN = /(https?:\/\/[^\s)"']+|\/[a-z][a-z0-9/_.-]{2,})/g;
10220
+ ABSENCE_PATTERN = /\b(?:no|without|does not use|doesn't use|never)\s+([A-Za-z][A-Za-z0-9_. -]{2,40}?)(?:\.|,|;|$)/gi;
10221
+ ENV_ALLOWLIST_HINTS = ["key", "secret", "token", "url", "dsn", "endpoint", "host", "port", "id", "name", "path", "dir", "debug"];
10222
+ }
10223
+ });
10224
+
10225
+ // src/engine/guardLedger.ts
10226
+ var guardLedger_exports = {};
10227
+ __export(guardLedger_exports, {
10228
+ GUARD_FP_BUDGET_RATE: () => GUARD_FP_BUDGET_RATE,
10229
+ GUARD_FP_MIN_FEEDBACK: () => GUARD_FP_MIN_FEEDBACK,
10230
+ demoteSeverity: () => demoteSeverity,
10231
+ formatPrecisionLedger: () => formatPrecisionLedger,
10232
+ getDowntieredRules: () => getDowntieredRules,
10233
+ getRulePrecision: () => getRulePrecision,
10234
+ recordGuardFeedback: () => recordGuardFeedback,
10235
+ recordGuardFlag: () => recordGuardFlag
10236
+ });
10237
+ async function ensureLedgerSchema() {
10238
+ const db = await getDb();
10239
+ db.exec(`
10240
+ CREATE TABLE IF NOT EXISTS guard_ledger (
10241
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10242
+ rule TEXT NOT NULL,
10243
+ verdict TEXT NOT NULL DEFAULT 'flag',
10244
+ false_positive INTEGER NOT NULL DEFAULT 0,
10245
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
10246
+ );
10247
+ CREATE INDEX IF NOT EXISTS idx_ledger_rule ON guard_ledger(rule);
10248
+ `);
10249
+ saveDb();
10250
+ }
10251
+ async function recordGuardFlag(rule) {
10252
+ try {
10253
+ await ensureLedgerSchema();
10254
+ const db = await getDb();
10255
+ db.run(`INSERT INTO guard_ledger (rule, verdict) VALUES (?, 'flag')`, [rule]);
10256
+ saveDb();
10257
+ } catch {
10258
+ }
10259
+ }
10260
+ async function recordGuardFeedback(rule, wasFalsePositive) {
10261
+ try {
10262
+ await ensureLedgerSchema();
10263
+ const db = await getDb();
10264
+ db.run(
10265
+ `INSERT INTO guard_ledger (rule, verdict, false_positive) VALUES (?, 'feedback', ?)`,
10266
+ [rule, wasFalsePositive ? 1 : 0]
10267
+ );
10268
+ saveDb();
10269
+ } catch {
10270
+ }
10271
+ }
10272
+ async function getRulePrecision() {
10273
+ try {
10274
+ await ensureLedgerSchema();
10275
+ const db = await getDb();
10276
+ const stmt = db.prepare(`
10277
+ SELECT rule,
10278
+ SUM(CASE WHEN verdict = 'flag' THEN 1 ELSE 0 END) as flags,
10279
+ SUM(CASE WHEN verdict = 'feedback' THEN 1 ELSE 0 END) as feedbacks,
10280
+ SUM(CASE WHEN verdict = 'feedback' AND false_positive = 1 THEN 1 ELSE 0 END) as fps
10281
+ FROM guard_ledger GROUP BY rule
10282
+ `);
10283
+ const out = [];
10284
+ while (stmt.step()) {
10285
+ const r = stmt.getAsObject();
10286
+ const fpRate = r.feedbacks >= GUARD_FP_MIN_FEEDBACK ? r.fps / r.feedbacks : null;
10287
+ out.push({
10288
+ rule: r.rule,
10289
+ flags: Number(r.flags) || 0,
10290
+ feedbacks: Number(r.feedbacks) || 0,
10291
+ falsePositives: Number(r.fps) || 0,
10292
+ fpRate,
10293
+ overBudget: fpRate !== null && fpRate > GUARD_FP_BUDGET_RATE
10294
+ });
10295
+ }
10296
+ stmt.free();
10297
+ return out;
10298
+ } catch {
10299
+ return [];
10300
+ }
10301
+ }
10302
+ async function getDowntieredRules() {
10303
+ const precision = await getRulePrecision();
10304
+ return new Set(precision.filter((p) => p.overBudget).map((p) => p.rule));
10305
+ }
10306
+ function demoteSeverity(sev) {
10307
+ if (sev === "high") return "medium";
10308
+ if (sev === "medium") return "low";
10309
+ return "low";
10310
+ }
10311
+ function formatPrecisionLedger(rows) {
10312
+ if (rows.length === 0) return "\u{1F4CF} **Guard precision ledger** \u2014 no flags recorded yet.";
10313
+ const lines = ["\u{1F4CF} **Guard precision ledger** (per-rule, FP budget 50% / \u22655 feedbacks)", ""];
10314
+ for (const r of rows) {
10315
+ const fp = r.fpRate === null ? "n/a" : `${Math.round(r.fpRate * 100)}%`;
10316
+ lines.push(` ${r.overBudget ? "\u{1F53B}" : "\u2705"} \`${r.rule}\` \u2014 ${r.flags} flag(s), FP ${fp}${r.overBudget ? " OVER BUDGET (auto-downtiered)" : ""}`);
10317
+ }
10318
+ return lines.join("\n");
10319
+ }
10320
+ var GUARD_FP_BUDGET_RATE, GUARD_FP_MIN_FEEDBACK;
10321
+ var init_guardLedger = __esm({
10322
+ "src/engine/guardLedger.ts"() {
10323
+ "use strict";
10324
+ init_kumaDb();
10325
+ GUARD_FP_BUDGET_RATE = 0.5;
10326
+ GUARD_FP_MIN_FEEDBACK = 5;
10327
+ }
10328
+ });
10329
+
9244
10330
  // src/engine/kumaCheckpoint.ts
9245
10331
  var kumaCheckpoint_exports = {};
9246
10332
  __export(kumaCheckpoint_exports, {
@@ -9251,27 +10337,27 @@ __export(kumaCheckpoint_exports, {
9251
10337
  pruneCheckpoints: () => pruneCheckpoints,
9252
10338
  rollbackToCheckpoint: () => rollbackToCheckpoint
9253
10339
  });
9254
- import fs24 from "fs";
9255
- import path27 from "path";
10340
+ import fs29 from "fs";
10341
+ import path32 from "path";
9256
10342
  async function createCheckpoint(label, description) {
9257
10343
  try {
9258
10344
  const root = getProjectRoot();
9259
- const cpDir = path27.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
9260
- if (fs24.existsSync(cpDir)) {
10345
+ const cpDir = path32.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
10346
+ if (fs29.existsSync(cpDir)) {
9261
10347
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
9262
10348
  }
9263
- fs24.mkdirSync(cpDir, { recursive: true });
10349
+ fs29.mkdirSync(cpDir, { recursive: true });
9264
10350
  let gitHead;
9265
10351
  let isGitClean = false;
9266
10352
  try {
9267
- const { execSync: execSync7 } = await import("child_process");
9268
- gitHead = execSync7("git rev-parse HEAD", {
10353
+ const { execSync: execSync10 } = await import("child_process");
10354
+ gitHead = execSync10("git rev-parse HEAD", {
9269
10355
  cwd: root,
9270
10356
  encoding: "utf-8",
9271
10357
  timeout: 3e3,
9272
10358
  stdio: ["pipe", "pipe", "pipe"]
9273
10359
  }).trim();
9274
- const statusOut = execSync7("git status --porcelain", {
10360
+ const statusOut = execSync10("git status --porcelain", {
9275
10361
  cwd: root,
9276
10362
  encoding: "utf-8",
9277
10363
  timeout: 3e3,
@@ -9282,8 +10368,8 @@ async function createCheckpoint(label, description) {
9282
10368
  }
9283
10369
  const candidatePaths = /* @__PURE__ */ new Set();
9284
10370
  try {
9285
- const { execSync: execSync7 } = await import("child_process");
9286
- const gitOut = execSync7("git status --porcelain", {
10371
+ const { execSync: execSync10 } = await import("child_process");
10372
+ const gitOut = execSync10("git status --porcelain", {
9287
10373
  cwd: root,
9288
10374
  encoding: "utf-8",
9289
10375
  timeout: 4e3,
@@ -9326,15 +10412,15 @@ async function createCheckpoint(label, description) {
9326
10412
  }
9327
10413
  const files = [];
9328
10414
  for (const fp of candidatePaths) {
9329
- const fullPath = path27.resolve(root, fp);
10415
+ const fullPath = path32.resolve(root, fp);
9330
10416
  try {
9331
- if (fs24.existsSync(fullPath) && fs24.statSync(fullPath).isFile()) {
9332
- const content = fs24.readFileSync(fullPath, "utf-8");
10417
+ if (fs29.existsSync(fullPath) && fs29.statSync(fullPath).isFile()) {
10418
+ const content = fs29.readFileSync(fullPath, "utf-8");
9333
10419
  const hash = simpleHash2(content);
9334
- const snapFile = path27.join(cpDir, "files", `${fp}.kuma-snap`);
9335
- const fileDir = path27.dirname(snapFile);
9336
- if (!fs24.existsSync(fileDir)) fs24.mkdirSync(fileDir, { recursive: true });
9337
- fs24.writeFileSync(snapFile, content, "utf-8");
10420
+ const snapFile = path32.join(cpDir, "files", `${fp}.kuma-snap`);
10421
+ const fileDir = path32.dirname(snapFile);
10422
+ if (!fs29.existsSync(fileDir)) fs29.mkdirSync(fileDir, { recursive: true });
10423
+ fs29.writeFileSync(snapFile, content, "utf-8");
9338
10424
  files.push({ path: fp, hash });
9339
10425
  }
9340
10426
  } catch {
@@ -9342,7 +10428,7 @@ async function createCheckpoint(label, description) {
9342
10428
  }
9343
10429
  const db = await getDb();
9344
10430
  const dbData = db.export();
9345
- fs24.writeFileSync(path27.join(cpDir, "kuma.db"), Buffer.from(dbData));
10431
+ fs29.writeFileSync(path32.join(cpDir, "kuma.db"), Buffer.from(dbData));
9346
10432
  const manifest = {
9347
10433
  label,
9348
10434
  timestamp: Date.now(),
@@ -9352,8 +10438,8 @@ async function createCheckpoint(label, description) {
9352
10438
  dbSnapshot: true,
9353
10439
  description
9354
10440
  };
9355
- fs24.writeFileSync(
9356
- path27.join(cpDir, "manifest.json"),
10441
+ fs29.writeFileSync(
10442
+ path32.join(cpDir, "manifest.json"),
9357
10443
  JSON.stringify(manifest, null, 2),
9358
10444
  "utf-8"
9359
10445
  );
@@ -9381,29 +10467,29 @@ async function createCheckpoint(label, description) {
9381
10467
  async function rollbackToCheckpoint(label) {
9382
10468
  try {
9383
10469
  const root = getProjectRoot();
9384
- const cpDir = path27.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
9385
- const manifestPath = path27.join(cpDir, "manifest.json");
9386
- if (!fs24.existsSync(manifestPath)) {
10470
+ const cpDir = path32.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
10471
+ const manifestPath = path32.join(cpDir, "manifest.json");
10472
+ if (!fs29.existsSync(manifestPath)) {
9387
10473
  return `\u274C Checkpoint "${label}" not found. Available checkpoints:
9388
10474
 
9389
10475
  ${listCheckpoints()}`;
9390
10476
  }
9391
10477
  const manifest = JSON.parse(
9392
- fs24.readFileSync(manifestPath, "utf-8")
10478
+ fs29.readFileSync(manifestPath, "utf-8")
9393
10479
  );
9394
10480
  let restored = 0;
9395
10481
  let failed = 0;
9396
10482
  for (const f of manifest.files) {
9397
- const snapPath = path27.join(cpDir, "files", `${f.path}.kuma-snap`);
9398
- const legacyPath = path27.join(cpDir, "files", f.path);
9399
- const sourcePath = fs24.existsSync(snapPath) ? snapPath : legacyPath;
9400
- if (fs24.existsSync(sourcePath)) {
10483
+ const snapPath = path32.join(cpDir, "files", `${f.path}.kuma-snap`);
10484
+ const legacyPath = path32.join(cpDir, "files", f.path);
10485
+ const sourcePath = fs29.existsSync(snapPath) ? snapPath : legacyPath;
10486
+ if (fs29.existsSync(sourcePath)) {
9401
10487
  try {
9402
- const content = fs24.readFileSync(sourcePath, "utf-8");
9403
- const targetPath = path27.resolve(root, f.path);
9404
- const targetDir = path27.dirname(targetPath);
9405
- if (!fs24.existsSync(targetDir)) fs24.mkdirSync(targetDir, { recursive: true });
9406
- fs24.writeFileSync(targetPath, content, "utf-8");
10488
+ const content = fs29.readFileSync(sourcePath, "utf-8");
10489
+ const targetPath = path32.resolve(root, f.path);
10490
+ const targetDir = path32.dirname(targetPath);
10491
+ if (!fs29.existsSync(targetDir)) fs29.mkdirSync(targetDir, { recursive: true });
10492
+ fs29.writeFileSync(targetPath, content, "utf-8");
9407
10493
  restored++;
9408
10494
  } catch {
9409
10495
  failed++;
@@ -9412,8 +10498,8 @@ ${listCheckpoints()}`;
9412
10498
  }
9413
10499
  if (manifest.files.length === 0 && manifest.gitHead) {
9414
10500
  try {
9415
- const { execSync: execSync7 } = await import("child_process");
9416
- execSync7(`git checkout ${manifest.gitHead} -- .`, {
10501
+ const { execSync: execSync10 } = await import("child_process");
10502
+ execSync10(`git checkout ${manifest.gitHead} -- .`, {
9417
10503
  cwd: root,
9418
10504
  encoding: "utf-8",
9419
10505
  timeout: 8e3,
@@ -9424,12 +10510,12 @@ ${listCheckpoints()}`;
9424
10510
  failed++;
9425
10511
  }
9426
10512
  }
9427
- const dbSnapshotPath = path27.join(cpDir, "kuma.db");
9428
- if (fs24.existsSync(dbSnapshotPath)) {
9429
- const kumaDir = path27.join(root, ".kuma");
9430
- const dbPath = path27.join(kumaDir, "kuma.db");
9431
- const snapshotData = fs24.readFileSync(dbSnapshotPath);
9432
- fs24.writeFileSync(dbPath, snapshotData);
10513
+ const dbSnapshotPath = path32.join(cpDir, "kuma.db");
10514
+ if (fs29.existsSync(dbSnapshotPath)) {
10515
+ const kumaDir = path32.join(root, ".kuma");
10516
+ const dbPath = path32.join(kumaDir, "kuma.db");
10517
+ const snapshotData = fs29.readFileSync(dbSnapshotPath);
10518
+ fs29.writeFileSync(dbPath, snapshotData);
9433
10519
  resetDbInstance();
9434
10520
  }
9435
10521
  sessionMemory.recordToolCall("kuma_checkpoint_rollback", {
@@ -9454,18 +10540,18 @@ ${listCheckpoints()}`;
9454
10540
  function listCheckpoints() {
9455
10541
  try {
9456
10542
  const root = getProjectRoot();
9457
- const cpDir = path27.join(root, CHECKPOINT_DIR);
9458
- if (!fs24.existsSync(cpDir)) {
10543
+ const cpDir = path32.join(root, CHECKPOINT_DIR);
10544
+ if (!fs29.existsSync(cpDir)) {
9459
10545
  return "\u{1F4ED} No checkpoints found. Use kuma_safety({ action: 'checkpoint', label: 'pre-feature-x' }) to create one.";
9460
10546
  }
9461
- const entries = fs24.readdirSync(cpDir);
10547
+ const entries = fs29.readdirSync(cpDir);
9462
10548
  const checkpoints = [];
9463
10549
  for (const entry of entries) {
9464
- const manifestPath = path27.join(cpDir, entry, "manifest.json");
9465
- if (fs24.existsSync(manifestPath)) {
10550
+ const manifestPath = path32.join(cpDir, entry, "manifest.json");
10551
+ if (fs29.existsSync(manifestPath)) {
9466
10552
  try {
9467
10553
  const manifest = JSON.parse(
9468
- fs24.readFileSync(manifestPath, "utf-8")
10554
+ fs29.readFileSync(manifestPath, "utf-8")
9469
10555
  );
9470
10556
  checkpoints.push({ label: entry, manifest });
9471
10557
  } catch {
@@ -9497,12 +10583,12 @@ function listCheckpoints() {
9497
10583
  function pruneCheckpoints(keep = 5) {
9498
10584
  try {
9499
10585
  const root = getProjectRoot();
9500
- const cpDir = path27.join(root, CHECKPOINT_DIR);
9501
- if (!fs24.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
9502
- const entries = fs24.readdirSync(cpDir).map((e) => ({ name: e, time: fs24.statSync(path27.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
10586
+ const cpDir = path32.join(root, CHECKPOINT_DIR);
10587
+ if (!fs29.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
10588
+ const entries = fs29.readdirSync(cpDir).map((e) => ({ name: e, time: fs29.statSync(path32.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
9503
10589
  let removed = 0;
9504
10590
  for (let i = keep; i < entries.length; i++) {
9505
- fs24.rmSync(path27.join(cpDir, entries[i].name), { recursive: true, force: true });
10591
+ fs29.rmSync(path32.join(cpDir, entries[i].name), { recursive: true, force: true });
9506
10592
  removed++;
9507
10593
  }
9508
10594
  return removed > 0 ? `\u{1F9F9} Pruned **${removed} old checkpoint(s)**. Kept ${Math.min(keep, entries.length)} most recent.` : "\u2705 No checkpoints needed pruning.";
@@ -9527,9 +10613,9 @@ function isRiskyEditPath(filePath) {
9527
10613
  }
9528
10614
  function loadAutoCpDone(root) {
9529
10615
  try {
9530
- const fp = path27.join(root, AUTO_CP_STATE);
9531
- if (fs24.existsSync(fp)) {
9532
- const parsed = JSON.parse(fs24.readFileSync(fp, "utf-8"));
10616
+ const fp = path32.join(root, AUTO_CP_STATE);
10617
+ if (fs29.existsSync(fp)) {
10618
+ const parsed = JSON.parse(fs29.readFileSync(fp, "utf-8"));
9533
10619
  return parsed?.autoCheckpoints ?? {};
9534
10620
  }
9535
10621
  } catch {
@@ -9538,10 +10624,10 @@ function loadAutoCpDone(root) {
9538
10624
  }
9539
10625
  function saveAutoCpDone(root, done) {
9540
10626
  try {
9541
- const fp = path27.join(root, AUTO_CP_STATE);
9542
- const parsed = fs24.existsSync(fp) ? JSON.parse(fs24.readFileSync(fp, "utf-8")) : {};
10627
+ const fp = path32.join(root, AUTO_CP_STATE);
10628
+ const parsed = fs29.existsSync(fp) ? JSON.parse(fs29.readFileSync(fp, "utf-8")) : {};
9543
10629
  parsed.autoCheckpoints = done;
9544
- fs24.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
10630
+ fs29.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
9545
10631
  } catch {
9546
10632
  }
9547
10633
  }
@@ -9551,14 +10637,14 @@ function maybeAutoCheckpoint(filePath) {
9551
10637
  const root = getProjectRoot();
9552
10638
  const done = loadAutoCpDone(root);
9553
10639
  if (done[filePath]) return null;
9554
- const abs = path27.isAbsolute(filePath) ? filePath : path27.join(root, filePath);
9555
- if (!fs24.existsSync(abs) || !fs24.statSync(abs).isFile()) return null;
9556
- const label = `auto-pre-${sanitizeLabel(path27.basename(filePath))}`;
9557
- const cpDir = path27.join(root, CHECKPOINT_DIR, label);
9558
- if (!fs24.existsSync(cpDir)) fs24.mkdirSync(cpDir, { recursive: true });
9559
- fs24.copyFileSync(abs, path27.join(cpDir, path27.basename(abs)));
9560
- fs24.writeFileSync(
9561
- path27.join(cpDir, "manifest.json"),
10640
+ const abs = path32.isAbsolute(filePath) ? filePath : path32.join(root, filePath);
10641
+ if (!fs29.existsSync(abs) || !fs29.statSync(abs).isFile()) return null;
10642
+ const label = `auto-pre-${sanitizeLabel(path32.basename(filePath))}`;
10643
+ const cpDir = path32.join(root, CHECKPOINT_DIR, label);
10644
+ if (!fs29.existsSync(cpDir)) fs29.mkdirSync(cpDir, { recursive: true });
10645
+ fs29.copyFileSync(abs, path32.join(cpDir, path32.basename(abs)));
10646
+ fs29.writeFileSync(
10647
+ path32.join(cpDir, "manifest.json"),
9562
10648
  JSON.stringify({
9563
10649
  label,
9564
10650
  timestamp: Date.now(),
@@ -9598,8 +10684,8 @@ __export(init_exports, {
9598
10684
  generateInitMdContent: () => generateInitMdContent,
9599
10685
  runInit: () => runInit
9600
10686
  });
9601
- import fs25 from "fs";
9602
- import path28 from "path";
10687
+ import fs30 from "fs";
10688
+ import path33 from "path";
9603
10689
  function configFilePath(type) {
9604
10690
  switch (type) {
9605
10691
  case "claude":
@@ -9701,11 +10787,10 @@ function opencodeAgentsMdTemplate() {
9701
10787
  "Record what matters, skip what doesn't.",
9702
10788
  "",
9703
10789
  "<kuma_obedience>",
9704
- "## \u{1F512} Obedience (non-negotiable)",
9705
- '- MUST call `kuma_kuma_context({ action: "init" })` (or `kuma_context({ action: "init" })`) at session start.',
9706
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
9707
- '- MUST call `kuma_kuma_safety({ action: "verify" })` (or `kuma_safety({ action: "verify" })`) after edits.',
9708
- "- MUST NOT call actions outside the core actions.",
10790
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
10791
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
10792
+ '- AGENT-EXPECTED (~70%): call `kuma_kuma_context({ action: "init" })` (or `kuma_context`) first; record gotchas IMMEDIATELY; verify after edits.',
10793
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
9709
10794
  "</kuma_obedience>",
9710
10795
  "",
9711
10796
  "<kuma_workflow>",
@@ -9869,6 +10954,7 @@ function geminiTemplate() {
9869
10954
  '- **Gotchas:** Call `kuma_memory({ action: "gotcha", scope: "<file>", content: "<description>" })` IMMEDIATELY when any quirk/bug is discovered.',
9870
10955
  '- **Decisions:** Call `kuma_memory({ action: "decision", title: "...", rationale: "..." })` when choosing between options.',
9871
10956
  '- **Post-Edits:** Call `kuma_safety({ action: "verify" })` after modifying code to run affected package/unit tests.',
10957
+ "- **Enforcement (honest model, issue #39):** schema + hooks enforce what they can; the rest is best-effort \u2014 see `.kuma/init.md` for mechanisms and fallbacks.",
9872
10958
  "</kuma_obedience>",
9873
10959
  "",
9874
10960
  "### \u{1F375} Matcha & Kuma Harmony",
@@ -9948,6 +11034,16 @@ function opencodePluginTemplate() {
9948
11034
  ' const fp = input.filePath || input.path || (input.file && input.file.path) || "";',
9949
11035
  ' if (fp) console.warn(`\u{1F43B} [Kuma] ${fp} edited \u2014 consider kuma_safety({ action: "verify", scope: "${fp}" })`);',
9950
11036
  " },",
11037
+ ' "experimental.session.compacting": async (input, output) => {',
11038
+ " // Compaction survival set via the native channel (docs: plugins#compaction).",
11039
+ " try {",
11040
+ ' const { execSync } = await import("node:child_process");',
11041
+ ' const ctx = execSync("kuma hook pre-compact", { encoding: "utf-8", timeout: 8000 });',
11042
+ ' const parsed = JSON.parse(ctx || "{}");',
11043
+ ' const add = parsed?.hookSpecificOutput?.hookSpecificOutput?.additionalContext || "";',
11044
+ " if (add && output && Array.isArray(output.context)) output.context.push(add);",
11045
+ " } catch { /* compacting must never fail the session */ }",
11046
+ " },",
9951
11047
  " };",
9952
11048
  "};",
9953
11049
  ""
@@ -10007,15 +11103,26 @@ function generateInitMdContent() {
10007
11103
  "---",
10008
11104
  "",
10009
11105
  "<kuma_obedience>",
10010
- "## \u{1F512} OBEDIENCE (non-negotiable)",
11106
+ "## \u{1F512} OBEDIENCE \u2014 honest model (issue #39)",
10011
11107
  "",
10012
- '1. **MUST call `kuma_context({ action: "init" })`** at session start \u2014 before any code edit.',
10013
- '2. **MUST record gotchas IMMEDIATELY** when a bug/quirk is found (`kuma_memory({ action: "gotcha" })`).',
10014
- '3. **MUST call `kuma_safety({ action: "verify" })`** after code edits.',
10015
- "4. **MUST NOT call actions outside the core actions** in this file \u2014 anything else is rejected by the MCP schema.",
10016
- "5. If a Kuma call fails or returns an error, **re-read this file and retry** \u2014 do not silently continue.",
11108
+ "Rules are split by what actually guarantees them. A rule written as",
11109
+ "mandatory but silently skippable is worse than an explicit best-effort \u2014",
11110
+ "so every rule below names its mechanism or its fallback.",
11111
+ "",
11112
+ "### HARNESS-ENFORCED (guaranteed by mechanism, zero agent choice)",
11113
+ "",
11114
+ "- **Unknown actions rejected** \u2014 enforced by the MCP schema (invalid action = error, not silence).",
11115
+ '- **Pre-edit gotcha inject** \u2014 enforced by hooks: Claude `PreToolUse` (`kuma hook pre-edit`), OpenCode `tool.execute.before`, Cursor/Windsurf/Copilot/Codex native hooks from `kuma init`. Fallback if hooks missing: call `kuma_context({ action: "history", target: "<file>" })` before editing.',
11116
+ "- **Session brief** \u2014 enforced by `SessionStart` hook (`kuma hook session-start`). Fallback if skipped: the brief lazy-loads on the first unfamiliar edit (history/research include it).",
11117
+ '- **Post-edit map patch** \u2014 enforced by dirty-flags: the pre-edit hook marks files dirty, the next context call syncs + clears. Fallback: `kuma_context({ action: "map" })` rebuilds on demand.',
10017
11118
  "",
10018
- "> These rules are enforced: the MCP schema rejects unknown actions, and Claude Code hooks inject gotchas before every edit.",
11119
+ "### AGENT-EXPECTED (best-effort, designed for ~70% compliance)",
11120
+ "",
11121
+ '1. **Call `kuma_context({ action: "init" })` at session start.** Nudged (non-blocking warning) on any other call first. Fallback if skipped: lazy brief on first unfamiliar edit.',
11122
+ '2. **Record gotchas IMMEDIATELY** (`kuma_memory({ action: "gotcha" })`). Nudged by guard (`error-without-gotcha`). Fallback if skipped: auto-gotcha records after repeated verify failures; CI/evals catch the rest.',
11123
+ '3. **Call `kuma_safety({ action: "verify" })` after edits.** Nudged post-edit by guard + hooks. Fallback if skipped: CI gate is the backstop \u2014 never treat skipped verify as verified.',
11124
+ "4. **Record decisions/arch_flows/research_saves as you go.** No enforcement; value compounds with compliance. Fallback: grep/glob still work \u2014 memory is acceleration, not a gate.",
11125
+ "5. If a Kuma call fails or returns an error, **re-read this file and retry** \u2014 do not silently continue.",
10019
11126
  "</kuma_obedience>",
10020
11127
  "",
10021
11128
  "---",
@@ -10192,11 +11299,10 @@ function handleOpencodeSecondary(root, results) {
10192
11299
  "Record what matters, skip what doesn't.",
10193
11300
  "",
10194
11301
  "<kuma_obedience>",
10195
- "## \u{1F512} Obedience (non-negotiable)",
10196
- '- MUST call `kuma_kuma_context({ action: "init" })` at session start.',
10197
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
10198
- '- MUST call `kuma_kuma_safety({ action: "verify" })` after edits.',
10199
- "- MUST NOT call actions outside the core actions.",
11302
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
11303
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
11304
+ '- AGENT-EXPECTED (~70%): call `kuma_kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; verify after edits.',
11305
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
10200
11306
  "</kuma_obedience>",
10201
11307
  "",
10202
11308
  "<kuma_workflow>",
@@ -10213,21 +11319,21 @@ function handleOpencodeSecondary(root, results) {
10213
11319
  "",
10214
11320
  "\u{1F4D6} Full rules: `.kuma/init.md`"
10215
11321
  ].join("\n");
10216
- const skillPath = path28.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
11322
+ const skillPath = path33.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
10217
11323
  if (!results.some((r) => r.filePath === ".agents/skills/kuma/SKILL.md")) {
10218
11324
  try {
10219
- const dir = path28.dirname(skillPath);
10220
- if (fs25.existsSync(skillPath)) {
10221
- const existing = fs25.readFileSync(skillPath, "utf-8");
11325
+ const dir = path33.dirname(skillPath);
11326
+ if (fs30.existsSync(skillPath)) {
11327
+ const existing = fs30.readFileSync(skillPath, "utf-8");
10222
11328
  if (existing.includes("kuma-mcp")) {
10223
11329
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "skipped" });
10224
11330
  } else {
10225
- fs25.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
11331
+ fs30.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
10226
11332
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "appended" });
10227
11333
  }
10228
11334
  } else {
10229
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10230
- fs25.writeFileSync(skillPath, opencodeContent, "utf-8");
11335
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11336
+ fs30.writeFileSync(skillPath, opencodeContent, "utf-8");
10231
11337
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "created" });
10232
11338
  }
10233
11339
  } catch (err) {
@@ -10239,21 +11345,21 @@ function handleOpencodeSecondary(root, results) {
10239
11345
  });
10240
11346
  }
10241
11347
  }
10242
- const opencodeSkillPath = path28.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
11348
+ const opencodeSkillPath = path33.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
10243
11349
  if (!results.some((r) => r.filePath === ".opencode/skills/kuma/SKILL.md")) {
10244
11350
  try {
10245
- const dir = path28.dirname(opencodeSkillPath);
10246
- if (fs25.existsSync(opencodeSkillPath)) {
10247
- const existing = fs25.readFileSync(opencodeSkillPath, "utf-8");
11351
+ const dir = path33.dirname(opencodeSkillPath);
11352
+ if (fs30.existsSync(opencodeSkillPath)) {
11353
+ const existing = fs30.readFileSync(opencodeSkillPath, "utf-8");
10248
11354
  if (existing.includes("kuma-mcp")) {
10249
11355
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "skipped" });
10250
11356
  } else {
10251
- fs25.writeFileSync(opencodeSkillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
11357
+ fs30.writeFileSync(opencodeSkillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
10252
11358
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "appended" });
10253
11359
  }
10254
11360
  } else {
10255
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10256
- fs25.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
11361
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11362
+ fs30.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
10257
11363
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "created" });
10258
11364
  }
10259
11365
  } catch (err) {
@@ -10265,21 +11371,21 @@ function handleOpencodeSecondary(root, results) {
10265
11371
  });
10266
11372
  }
10267
11373
  }
10268
- const pluginPath = path28.resolve(root, ".opencode", "plugins", "kuma.js");
11374
+ const pluginPath = path33.resolve(root, ".opencode", "plugins", "kuma.js");
10269
11375
  if (!results.some((r) => r.filePath === ".opencode/plugins/kuma.js")) {
10270
11376
  try {
10271
- const dir = path28.dirname(pluginPath);
10272
- if (fs25.existsSync(pluginPath)) {
10273
- const existing = fs25.readFileSync(pluginPath, "utf-8");
11377
+ const dir = path33.dirname(pluginPath);
11378
+ if (fs30.existsSync(pluginPath)) {
11379
+ const existing = fs30.readFileSync(pluginPath, "utf-8");
10274
11380
  if (existing.includes("KumaPlugin") || existing.includes("kuma")) {
10275
11381
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "skipped" });
10276
11382
  } else {
10277
- fs25.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
11383
+ fs30.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
10278
11384
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "appended" });
10279
11385
  }
10280
11386
  } else {
10281
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10282
- fs25.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
11387
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11388
+ fs30.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
10283
11389
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "created" });
10284
11390
  }
10285
11391
  } catch (err) {
@@ -10293,21 +11399,21 @@ function handleOpencodeSecondary(root, results) {
10293
11399
  }
10294
11400
  }
10295
11401
  function handleCodexSecondary(root, results) {
10296
- const tomlPath = path28.resolve(root, ".codex/config.toml");
11402
+ const tomlPath = path33.resolve(root, ".codex/config.toml");
10297
11403
  if (results.some((r) => r.filePath === ".codex/config.toml")) return;
10298
11404
  try {
10299
- const dir = path28.dirname(tomlPath);
10300
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10301
- if (fs25.existsSync(tomlPath)) {
10302
- const existingContent = fs25.readFileSync(tomlPath, "utf-8");
11405
+ const dir = path33.dirname(tomlPath);
11406
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11407
+ if (fs30.existsSync(tomlPath)) {
11408
+ const existingContent = fs30.readFileSync(tomlPath, "utf-8");
10303
11409
  if (existingContent.includes("kuma")) {
10304
11410
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
10305
11411
  return;
10306
11412
  }
10307
- fs25.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
11413
+ fs30.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
10308
11414
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
10309
11415
  } else {
10310
- fs25.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
11416
+ fs30.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
10311
11417
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
10312
11418
  }
10313
11419
  } catch (err) {
@@ -10320,11 +11426,11 @@ function handleCodexSecondary(root, results) {
10320
11426
  }
10321
11427
  }
10322
11428
  function handleQwenSecondary(root, results) {
10323
- const settingsPath = path28.resolve(root, "settings.json");
11429
+ const settingsPath = path33.resolve(root, "settings.json");
10324
11430
  if (results.some((r) => r.filePath === "settings.json")) return;
10325
11431
  try {
10326
- if (fs25.existsSync(settingsPath)) {
10327
- const existingContent = fs25.readFileSync(settingsPath, "utf-8");
11432
+ if (fs30.existsSync(settingsPath)) {
11433
+ const existingContent = fs30.readFileSync(settingsPath, "utf-8");
10328
11434
  if (existingContent.includes("kuma")) {
10329
11435
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
10330
11436
  try {
@@ -10332,7 +11438,7 @@ function handleQwenSecondary(root, results) {
10332
11438
  parsed.mcpServers = parsed.mcpServers || {};
10333
11439
  if (!parsed.mcpServers.kuma) {
10334
11440
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10335
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11441
+ fs30.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10336
11442
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
10337
11443
  return;
10338
11444
  }
@@ -10347,13 +11453,13 @@ function handleQwenSecondary(root, results) {
10347
11453
  parsed.mcpServers = parsed.mcpServers || {};
10348
11454
  if (!parsed.mcpServers.kuma) {
10349
11455
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10350
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11456
+ fs30.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10351
11457
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
10352
11458
  }
10353
11459
  } catch {
10354
11460
  }
10355
11461
  } else {
10356
- fs25.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
11462
+ fs30.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
10357
11463
  results.push({ type: "qwen", filePath: "settings.json", action: "created" });
10358
11464
  }
10359
11465
  } catch (err) {
@@ -10376,11 +11482,10 @@ function getCombinedAgentsMd(selectedTypes) {
10376
11482
  "## SESSION WORKFLOW",
10377
11483
  "",
10378
11484
  "<kuma_obedience>",
10379
- "## \u{1F512} Obedience (non-negotiable)",
10380
- '- MUST call `[kuma_]context({ action: "init" })` at session start.',
10381
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
10382
- '- MUST call `[kuma_]safety({ action: "verify" })` after edits.',
10383
- "- MUST NOT call actions outside the 13 core actions.",
11485
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
11486
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
11487
+ '- AGENT-EXPECTED (~70%): call `[kuma_]context({ action: "init" })` first; record gotchas IMMEDIATELY; verify after edits.',
11488
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
10384
11489
  "</kuma_obedience>",
10385
11490
  "",
10386
11491
  "<kuma_workflow>",
@@ -10456,12 +11561,12 @@ function getCombinedAgentsMd(selectedTypes) {
10456
11561
  return lines.join("\n");
10457
11562
  }
10458
11563
  function handleAntigravityMcpConfig(root, results) {
10459
- const mcpPath = path28.resolve(root, ".agents/mcp_config.json");
11564
+ const mcpPath = path33.resolve(root, ".agents/mcp_config.json");
10460
11565
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
10461
11566
  try {
10462
- const mcpDir = path28.dirname(mcpPath);
10463
- if (fs25.existsSync(mcpPath)) {
10464
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
11567
+ const mcpDir = path33.dirname(mcpPath);
11568
+ if (fs30.existsSync(mcpPath)) {
11569
+ const existingContent = fs30.readFileSync(mcpPath, "utf-8");
10465
11570
  if (existingContent.includes("kuma")) {
10466
11571
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "skipped" });
10467
11572
  return;
@@ -10470,14 +11575,14 @@ function handleAntigravityMcpConfig(root, results) {
10470
11575
  const parsed = JSON.parse(existingContent);
10471
11576
  parsed.mcpServers = parsed.mcpServers || {};
10472
11577
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10473
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11578
+ fs30.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10474
11579
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
10475
11580
  } catch {
10476
11581
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "error", error: "Invalid JSON in existing mcp_config.json" });
10477
11582
  }
10478
11583
  } else {
10479
- if (!fs25.existsSync(mcpDir)) fs25.mkdirSync(mcpDir, { recursive: true });
10480
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
11584
+ if (!fs30.existsSync(mcpDir)) fs30.mkdirSync(mcpDir, { recursive: true });
11585
+ fs30.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
10481
11586
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
10482
11587
  }
10483
11588
  } catch (err) {
@@ -10490,21 +11595,21 @@ function handleAntigravityMcpConfig(root, results) {
10490
11595
  }
10491
11596
  }
10492
11597
  function handleAntigravityRules(root, results) {
10493
- const rulePath = path28.resolve(root, ".agents", "rules", "kuma.md");
11598
+ const rulePath = path33.resolve(root, ".agents", "rules", "kuma.md");
10494
11599
  if (results.some((r) => r.filePath === ".agents/rules/kuma.md")) return;
10495
11600
  try {
10496
- const dir = path28.dirname(rulePath);
10497
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10498
- if (fs25.existsSync(rulePath)) {
10499
- const existing = fs25.readFileSync(rulePath, "utf-8");
11601
+ const dir = path33.dirname(rulePath);
11602
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11603
+ if (fs30.existsSync(rulePath)) {
11604
+ const existing = fs30.readFileSync(rulePath, "utf-8");
10500
11605
  if (existing.includes("Kuma MCP") || existing.includes("kuma-mcp")) {
10501
11606
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "skipped" });
10502
11607
  return;
10503
11608
  }
10504
- fs25.writeFileSync(rulePath, existing.trimEnd() + "\n\n---\n\n" + antigravityRuleTemplate(), "utf-8");
11609
+ fs30.writeFileSync(rulePath, existing.trimEnd() + "\n\n---\n\n" + antigravityRuleTemplate(), "utf-8");
10505
11610
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "appended" });
10506
11611
  } else {
10507
- fs25.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
11612
+ fs30.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
10508
11613
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "created" });
10509
11614
  }
10510
11615
  } catch (err) {
@@ -10517,19 +11622,19 @@ function handleAntigravityRules(root, results) {
10517
11622
  }
10518
11623
  }
10519
11624
  function handleGeminiMd(root, results) {
10520
- const geminiPath = path28.resolve(root, "GEMINI.md");
11625
+ const geminiPath = path33.resolve(root, "GEMINI.md");
10521
11626
  if (results.some((r) => r.filePath === "GEMINI.md")) return;
10522
11627
  try {
10523
- if (fs25.existsSync(geminiPath)) {
10524
- const existing = fs25.readFileSync(geminiPath, "utf-8");
11628
+ if (fs30.existsSync(geminiPath)) {
11629
+ const existing = fs30.readFileSync(geminiPath, "utf-8");
10525
11630
  if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
10526
11631
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "skipped" });
10527
11632
  return;
10528
11633
  }
10529
- fs25.writeFileSync(geminiPath, existing.trimEnd() + "\n\n---\n\n" + geminiTemplate(), "utf-8");
11634
+ fs30.writeFileSync(geminiPath, existing.trimEnd() + "\n\n---\n\n" + geminiTemplate(), "utf-8");
10530
11635
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "appended" });
10531
11636
  } else {
10532
- fs25.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
11637
+ fs30.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
10533
11638
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "created" });
10534
11639
  }
10535
11640
  } catch (err) {
@@ -10547,19 +11652,19 @@ function handleAntigravitySecondary(root, results) {
10547
11652
  handleGeminiMd(root, results);
10548
11653
  }
10549
11654
  function handleWindsurfSecondary(root, results) {
10550
- const rulesPath = path28.resolve(root, ".windsurfrules");
11655
+ const rulesPath = path33.resolve(root, ".windsurfrules");
10551
11656
  if (results.some((r) => r.filePath === ".windsurfrules")) return;
10552
11657
  try {
10553
- if (fs25.existsSync(rulesPath)) {
10554
- const existing = fs25.readFileSync(rulesPath, "utf-8");
11658
+ if (fs30.existsSync(rulesPath)) {
11659
+ const existing = fs30.readFileSync(rulesPath, "utf-8");
10555
11660
  if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
10556
11661
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "skipped" });
10557
11662
  return;
10558
11663
  }
10559
- fs25.writeFileSync(rulesPath, existing.trimEnd() + "\n\n---\n\n" + windsurfrulesTemplate(), "utf-8");
11664
+ fs30.writeFileSync(rulesPath, existing.trimEnd() + "\n\n---\n\n" + windsurfrulesTemplate(), "utf-8");
10560
11665
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "appended" });
10561
11666
  } else {
10562
- fs25.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
11667
+ fs30.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
10563
11668
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "created" });
10564
11669
  }
10565
11670
  } catch (err) {
@@ -10572,21 +11677,21 @@ function handleWindsurfSecondary(root, results) {
10572
11677
  }
10573
11678
  }
10574
11679
  function handleClineSecondary(root, results) {
10575
- const rooPath = path28.resolve(root, ".roo", "rules", "kuma.md");
11680
+ const rooPath = path33.resolve(root, ".roo", "rules", "kuma.md");
10576
11681
  if (!results.some((r) => r.filePath === ".roo/rules/kuma.md")) {
10577
11682
  try {
10578
- const dir = path28.dirname(rooPath);
10579
- if (fs25.existsSync(rooPath)) {
10580
- const existing = fs25.readFileSync(rooPath, "utf-8");
11683
+ const dir = path33.dirname(rooPath);
11684
+ if (fs30.existsSync(rooPath)) {
11685
+ const existing = fs30.readFileSync(rooPath, "utf-8");
10581
11686
  if (existing.includes("kuma-mcp") || existing.includes("Kuma MCP")) {
10582
11687
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "skipped" });
10583
11688
  } else {
10584
- fs25.writeFileSync(rooPath, existing.trimEnd() + "\n\n---\n\n" + rooRulesTemplate(), "utf-8");
11689
+ fs30.writeFileSync(rooPath, existing.trimEnd() + "\n\n---\n\n" + rooRulesTemplate(), "utf-8");
10585
11690
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "appended" });
10586
11691
  }
10587
11692
  } else {
10588
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10589
- fs25.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
11693
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11694
+ fs30.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
10590
11695
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "created" });
10591
11696
  }
10592
11697
  } catch (err) {
@@ -10598,21 +11703,21 @@ function handleClineSecondary(root, results) {
10598
11703
  });
10599
11704
  }
10600
11705
  }
10601
- const traePath = path28.resolve(root, ".trae", "rules", "kuma.md");
11706
+ const traePath = path33.resolve(root, ".trae", "rules", "kuma.md");
10602
11707
  if (!results.some((r) => r.filePath === ".trae/rules/kuma.md")) {
10603
11708
  try {
10604
- const dir = path28.dirname(traePath);
10605
- if (fs25.existsSync(traePath)) {
10606
- const existing = fs25.readFileSync(traePath, "utf-8");
11709
+ const dir = path33.dirname(traePath);
11710
+ if (fs30.existsSync(traePath)) {
11711
+ const existing = fs30.readFileSync(traePath, "utf-8");
10607
11712
  if (existing.includes("Kuma MCP")) {
10608
11713
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "skipped" });
10609
11714
  } else {
10610
- fs25.writeFileSync(traePath, existing.trimEnd() + "\n\n---\n\n" + traeRulesTemplate(), "utf-8");
11715
+ fs30.writeFileSync(traePath, existing.trimEnd() + "\n\n---\n\n" + traeRulesTemplate(), "utf-8");
10611
11716
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "appended" });
10612
11717
  }
10613
11718
  } else {
10614
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10615
- fs25.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
11719
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11720
+ fs30.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
10616
11721
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "created" });
10617
11722
  }
10618
11723
  } catch (err) {
@@ -10624,21 +11729,21 @@ function handleClineSecondary(root, results) {
10624
11729
  });
10625
11730
  }
10626
11731
  }
10627
- const qoderPath = path28.resolve(root, ".qoder", "rules", "kuma.md");
11732
+ const qoderPath = path33.resolve(root, ".qoder", "rules", "kuma.md");
10628
11733
  if (!results.some((r) => r.filePath === ".qoder/rules/kuma.md")) {
10629
11734
  try {
10630
- const dir = path28.dirname(qoderPath);
10631
- if (fs25.existsSync(qoderPath)) {
10632
- const existing = fs25.readFileSync(qoderPath, "utf-8");
11735
+ const dir = path33.dirname(qoderPath);
11736
+ if (fs30.existsSync(qoderPath)) {
11737
+ const existing = fs30.readFileSync(qoderPath, "utf-8");
10633
11738
  if (existing.includes("Kuma MCP")) {
10634
11739
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "skipped" });
10635
11740
  } else {
10636
- fs25.writeFileSync(qoderPath, existing.trimEnd() + "\n\n---\n\n" + qoderRulesTemplate(), "utf-8");
11741
+ fs30.writeFileSync(qoderPath, existing.trimEnd() + "\n\n---\n\n" + qoderRulesTemplate(), "utf-8");
10637
11742
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "appended" });
10638
11743
  }
10639
11744
  } else {
10640
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10641
- fs25.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
11745
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11746
+ fs30.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
10642
11747
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "created" });
10643
11748
  }
10644
11749
  } catch (err) {
@@ -10652,18 +11757,18 @@ function handleClineSecondary(root, results) {
10652
11757
  }
10653
11758
  }
10654
11759
  function handleAiderSecondary(root, results) {
10655
- const ymlPath = path28.resolve(root, ".aider.conf.yml");
11760
+ const ymlPath = path33.resolve(root, ".aider.conf.yml");
10656
11761
  if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
10657
11762
  try {
10658
11763
  const conventionsRef = "read: CONVENTIONS.md";
10659
- if (fs25.existsSync(ymlPath)) {
10660
- const existingContent = fs25.readFileSync(ymlPath, "utf-8");
11764
+ if (fs30.existsSync(ymlPath)) {
11765
+ const existingContent = fs30.readFileSync(ymlPath, "utf-8");
10661
11766
  if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
10662
11767
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
10663
11768
  return;
10664
11769
  }
10665
11770
  const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
10666
- fs25.writeFileSync(ymlPath, newContent, "utf-8");
11771
+ fs30.writeFileSync(ymlPath, newContent, "utf-8");
10667
11772
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
10668
11773
  } else {
10669
11774
  const content = [
@@ -10673,7 +11778,7 @@ function handleAiderSecondary(root, results) {
10673
11778
  conventionsRef,
10674
11779
  ""
10675
11780
  ].join("\n");
10676
- fs25.writeFileSync(ymlPath, content, "utf-8");
11781
+ fs30.writeFileSync(ymlPath, content, "utf-8");
10677
11782
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
10678
11783
  }
10679
11784
  } catch (err) {
@@ -10686,10 +11791,10 @@ function handleAiderSecondary(root, results) {
10686
11791
  }
10687
11792
  }
10688
11793
  function handleCopilotSecondary(root, results) {
10689
- const skillPath = path28.resolve(root, ".github/skills/kuma/SKILL.md");
11794
+ const skillPath = path33.resolve(root, ".github/skills/kuma/SKILL.md");
10690
11795
  if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
10691
11796
  try {
10692
- const dir = path28.dirname(skillPath);
11797
+ const dir = path33.dirname(skillPath);
10693
11798
  const content = [
10694
11799
  "---",
10695
11800
  "name: kuma-mcp",
@@ -10700,17 +11805,17 @@ function handleCopilotSecondary(root, results) {
10700
11805
  "",
10701
11806
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules."
10702
11807
  ].join("\n");
10703
- if (fs25.existsSync(skillPath)) {
10704
- const existingContent = fs25.readFileSync(skillPath, "utf-8");
11808
+ if (fs30.existsSync(skillPath)) {
11809
+ const existingContent = fs30.readFileSync(skillPath, "utf-8");
10705
11810
  if (existingContent.includes("kuma")) {
10706
11811
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
10707
11812
  return;
10708
11813
  }
10709
- fs25.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
11814
+ fs30.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
10710
11815
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
10711
11816
  } else {
10712
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10713
- fs25.writeFileSync(skillPath, content, "utf-8");
11817
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11818
+ fs30.writeFileSync(skillPath, content, "utf-8");
10714
11819
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
10715
11820
  }
10716
11821
  } catch (err) {
@@ -10723,21 +11828,21 @@ function handleCopilotSecondary(root, results) {
10723
11828
  }
10724
11829
  }
10725
11830
  function handleOpenclawSecondary(root, results) {
10726
- const mcpPath = path28.resolve(root, ".agents/mcp_config.json");
11831
+ const mcpPath = path33.resolve(root, ".agents/mcp_config.json");
10727
11832
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
10728
11833
  try {
10729
- const dir = path28.dirname(mcpPath);
10730
- if (fs25.existsSync(mcpPath)) {
10731
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
11834
+ const dir = path33.dirname(mcpPath);
11835
+ if (fs30.existsSync(mcpPath)) {
11836
+ const existingContent = fs30.readFileSync(mcpPath, "utf-8");
10732
11837
  if (existingContent.includes("kuma")) return;
10733
11838
  const parsed = JSON.parse(existingContent);
10734
11839
  parsed.mcpServers = parsed.mcpServers || {};
10735
11840
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10736
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11841
+ fs30.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10737
11842
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
10738
11843
  } else {
10739
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10740
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
11844
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11845
+ fs30.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
10741
11846
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
10742
11847
  }
10743
11848
  } catch (err) {
@@ -10750,20 +11855,20 @@ function handleOpenclawSecondary(root, results) {
10750
11855
  }
10751
11856
  }
10752
11857
  function handleInitMdGeneration(root, results) {
10753
- const initMdPath = path28.resolve(root, ".kuma/init.md");
11858
+ const initMdPath = path33.resolve(root, ".kuma/init.md");
10754
11859
  try {
10755
- const kumaDir = path28.dirname(initMdPath);
10756
- if (!fs25.existsSync(kumaDir)) fs25.mkdirSync(kumaDir, { recursive: true });
10757
- if (fs25.existsSync(initMdPath)) {
10758
- const existing = fs25.readFileSync(initMdPath, "utf-8");
11860
+ const kumaDir = path33.dirname(initMdPath);
11861
+ if (!fs30.existsSync(kumaDir)) fs30.mkdirSync(kumaDir, { recursive: true });
11862
+ if (fs30.existsSync(initMdPath)) {
11863
+ const existing = fs30.readFileSync(initMdPath, "utf-8");
10759
11864
  if (existing.includes("_Generated by Kuma MCP_")) {
10760
11865
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "skipped" });
10761
11866
  return;
10762
11867
  }
10763
- fs25.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
11868
+ fs30.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
10764
11869
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "appended" });
10765
11870
  } else {
10766
- fs25.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
11871
+ fs30.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
10767
11872
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "created" });
10768
11873
  }
10769
11874
  } catch (err) {
@@ -10776,20 +11881,20 @@ function handleInitMdGeneration(root, results) {
10776
11881
  }
10777
11882
  }
10778
11883
  function handleCodewhaleSecondary(root, results) {
10779
- const mcpPath = path28.resolve(root, ".codewhale/mcp.json");
11884
+ const mcpPath = path33.resolve(root, ".codewhale/mcp.json");
10780
11885
  if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
10781
11886
  try {
10782
- const dir = path28.dirname(mcpPath);
10783
- if (fs25.existsSync(mcpPath)) {
10784
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
11887
+ const dir = path33.dirname(mcpPath);
11888
+ if (fs30.existsSync(mcpPath)) {
11889
+ const existingContent = fs30.readFileSync(mcpPath, "utf-8");
10785
11890
  if (existingContent.includes("kuma")) return;
10786
11891
  const parsed = JSON.parse(existingContent);
10787
11892
  parsed.mcpServers = parsed.mcpServers || {};
10788
11893
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10789
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11894
+ fs30.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10790
11895
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
10791
11896
  } else {
10792
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11897
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
10793
11898
  const config = {
10794
11899
  mcpServers: {
10795
11900
  kuma: {
@@ -10799,7 +11904,7 @@ function handleCodewhaleSecondary(root, results) {
10799
11904
  }
10800
11905
  }
10801
11906
  };
10802
- fs25.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
11907
+ fs30.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
10803
11908
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
10804
11909
  }
10805
11910
  } catch (err) {
@@ -10812,7 +11917,7 @@ function handleCodewhaleSecondary(root, results) {
10812
11917
  }
10813
11918
  }
10814
11919
  function handleClaudeSecondary(root, results) {
10815
- const settingsPath = path28.resolve(root, ".claude", "settings.json");
11920
+ const settingsPath = path33.resolve(root, ".claude", "settings.json");
10816
11921
  const hookBlock = {
10817
11922
  hooks: {
10818
11923
  PreToolUse: [
@@ -10824,32 +11929,56 @@ function handleClaudeSecondary(root, results) {
10824
11929
  matcher: "Bash",
10825
11930
  hooks: [{ type: "command", command: "kuma hook pre-bash" }]
10826
11931
  }
11932
+ ],
11933
+ // Issue #38: SessionStart serves the cached repo brief before the
11934
+ // first agent turn — zero agent choice involved.
11935
+ SessionStart: [
11936
+ {
11937
+ matcher: "startup",
11938
+ hooks: [{ type: "command", command: "kuma hook session-start" }]
11939
+ }
11940
+ ],
11941
+ // Compaction survival set (goal + critical gotchas + dirty files).
11942
+ PreCompact: [
11943
+ {
11944
+ matcher: "",
11945
+ hooks: [{ type: "command", command: "kuma hook pre-compact" }]
11946
+ }
10827
11947
  ]
10828
11948
  }
10829
11949
  };
10830
11950
  try {
10831
- if (fs25.existsSync(settingsPath)) {
10832
- const existing = JSON.parse(fs25.readFileSync(settingsPath, "utf-8"));
11951
+ if (fs30.existsSync(settingsPath)) {
11952
+ const existing = JSON.parse(fs30.readFileSync(settingsPath, "utf-8"));
11953
+ const existingHooks = existing.hooks || {};
11954
+ const mergedPre = [...existingHooks.PreToolUse || []];
11955
+ for (const entry of hookBlock.hooks.PreToolUse) {
11956
+ if (!mergedPre.some((h) => h.matcher === entry.matcher)) mergedPre.push(entry);
11957
+ }
11958
+ const mergedStart = [...existingHooks.SessionStart || []];
11959
+ for (const entry of hookBlock.hooks.SessionStart) {
11960
+ const hasKuma = mergedStart.some(
11961
+ (h) => JSON.stringify(h.hooks || []).includes("kuma hook session-start")
11962
+ );
11963
+ if (!hasKuma) mergedStart.push(entry);
11964
+ }
11965
+ const mergedCompact = [...existingHooks.PreCompact || []];
11966
+ const hasPreCompact = mergedCompact.some(
11967
+ (h) => JSON.stringify(h.hooks || []).includes("kuma hook pre-compact")
11968
+ );
11969
+ if (!hasPreCompact) {
11970
+ mergedCompact.push(...hookBlock.hooks.PreCompact);
11971
+ }
10833
11972
  const merged = {
10834
11973
  ...existing,
10835
- hooks: {
10836
- ...existing.hooks || {},
10837
- ...hookBlock.hooks
10838
- }
11974
+ hooks: { ...existingHooks, PreToolUse: mergedPre, SessionStart: mergedStart, PreCompact: mergedCompact }
10839
11975
  };
10840
- const matchers = new Set((merged.hooks.PreToolUse || []).map((h) => h.matcher));
10841
- for (const entry of hookBlock.hooks.PreToolUse) {
10842
- if (!matchers.has(entry.matcher)) {
10843
- merged.hooks.PreToolUse = [...merged.hooks.PreToolUse || [], entry];
10844
- matchers.add(entry.matcher);
10845
- }
10846
- }
10847
- fs25.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
11976
+ fs30.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
10848
11977
  results.push({ type: "claude", filePath: ".claude/settings.json", action: "appended" });
10849
11978
  } else {
10850
- const dir = path28.dirname(settingsPath);
10851
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10852
- fs25.writeFileSync(settingsPath, JSON.stringify(hookBlock, null, 2) + "\n", "utf-8");
11979
+ const dir = path33.dirname(settingsPath);
11980
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
11981
+ fs30.writeFileSync(settingsPath, JSON.stringify(hookBlock, null, 2) + "\n", "utf-8");
10853
11982
  results.push({ type: "claude", filePath: ".claude/settings.json", action: "created" });
10854
11983
  }
10855
11984
  } catch (err) {
@@ -10860,10 +11989,10 @@ function handleClaudeSecondary(root, results) {
10860
11989
  error: err instanceof Error ? err.message : String(err)
10861
11990
  });
10862
11991
  }
10863
- const skillPath = path28.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
11992
+ const skillPath = path33.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
10864
11993
  if (!results.some((r) => r.filePath === ".claude/skills/kuma/SKILL.md")) {
10865
11994
  try {
10866
- const dir = path28.dirname(skillPath);
11995
+ const dir = path33.dirname(skillPath);
10867
11996
  const content = [
10868
11997
  "---",
10869
11998
  "name: kuma-mcp",
@@ -10875,17 +12004,17 @@ function handleClaudeSecondary(root, results) {
10875
12004
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules.",
10876
12005
  "\u{1F4C2} Readable state: `.kuma/KNOWN_GOTCHAS.md` (gotchas) \xB7 `.kuma/memories/decisions.md` (decisions)"
10877
12006
  ].join("\n");
10878
- if (fs25.existsSync(skillPath)) {
10879
- const existing = fs25.readFileSync(skillPath, "utf-8");
12007
+ if (fs30.existsSync(skillPath)) {
12008
+ const existing = fs30.readFileSync(skillPath, "utf-8");
10880
12009
  if (existing.includes("kuma-mcp")) {
10881
12010
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "skipped" });
10882
12011
  } else {
10883
- fs25.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + content, "utf-8");
12012
+ fs30.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + content, "utf-8");
10884
12013
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "appended" });
10885
12014
  }
10886
12015
  } else {
10887
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10888
- fs25.writeFileSync(skillPath, content, "utf-8");
12016
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
12017
+ fs30.writeFileSync(skillPath, content, "utf-8");
10889
12018
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "created" });
10890
12019
  }
10891
12020
  } catch (err) {
@@ -10899,27 +12028,27 @@ function handleClaudeSecondary(root, results) {
10899
12028
  }
10900
12029
  }
10901
12030
  function writeHooksFile(root, relPath, type, results, build) {
10902
- const fullPath = path28.resolve(root, relPath);
12031
+ const fullPath = path33.resolve(root, relPath);
10903
12032
  try {
10904
12033
  let existing = {};
10905
12034
  let existed = false;
10906
- if (fs25.existsSync(fullPath)) {
12035
+ if (fs30.existsSync(fullPath)) {
10907
12036
  existed = true;
10908
12037
  try {
10909
- existing = JSON.parse(fs25.readFileSync(fullPath, "utf-8"));
12038
+ existing = JSON.parse(fs30.readFileSync(fullPath, "utf-8"));
10910
12039
  } catch {
10911
12040
  existing = {};
10912
12041
  }
10913
12042
  } else {
10914
- const dir = path28.dirname(fullPath);
10915
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
12043
+ const dir = path33.dirname(fullPath);
12044
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
10916
12045
  }
10917
12046
  const { doc, changed } = build(existing);
10918
12047
  if (!changed) {
10919
12048
  results.push({ type, filePath: relPath, action: "skipped" });
10920
12049
  return;
10921
12050
  }
10922
- fs25.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
12051
+ fs30.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
10923
12052
  results.push({ type, filePath: relPath, action: existed ? "appended" : "created" });
10924
12053
  } catch (err) {
10925
12054
  results.push({ type, filePath: relPath, action: "error", error: err instanceof Error ? err.message : String(err) });
@@ -11000,18 +12129,18 @@ function handleCodexHooks(root, results) {
11000
12129
  });
11001
12130
  }
11002
12131
  function handleCursorGotchaRules(root, results) {
11003
- const rulesDir2 = path28.resolve(root, ".cursor", "rules", "kuma-gotchas");
12132
+ const rulesDir2 = path33.resolve(root, ".cursor", "rules", "kuma-gotchas");
11004
12133
  try {
11005
12134
  let gotchas = [];
11006
12135
  try {
11007
12136
  gotchas = getActiveGotchas().filter((g) => g.severity === "high" || g.severity === "critical");
11008
12137
  } catch {
11009
12138
  }
11010
- if (fs25.existsSync(rulesDir2)) {
11011
- for (const f of fs25.readdirSync(rulesDir2)) {
12139
+ if (fs30.existsSync(rulesDir2)) {
12140
+ for (const f of fs30.readdirSync(rulesDir2)) {
11012
12141
  if (f.endsWith(".mdc")) {
11013
12142
  try {
11014
- fs25.rmSync(path28.join(rulesDir2, f), { force: true });
12143
+ fs30.rmSync(path33.join(rulesDir2, f), { force: true });
11015
12144
  } catch {
11016
12145
  }
11017
12146
  }
@@ -11021,13 +12150,13 @@ function handleCursorGotchaRules(root, results) {
11021
12150
  results.push({ type: "cursor", filePath: ".cursor/rules/kuma-gotchas/", action: "skipped" });
11022
12151
  return;
11023
12152
  }
11024
- fs25.mkdirSync(rulesDir2, { recursive: true });
12153
+ fs30.mkdirSync(rulesDir2, { recursive: true });
11025
12154
  const seen = /* @__PURE__ */ new Set();
11026
12155
  let created = 0;
11027
12156
  for (const g of gotchas) {
11028
- const slug = g.filePath.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.(ts|js|tsx|jsx|py|go|rs|java|rb|php)$/i, "").slice(0, 60);
11029
- if (!slug || seen.has(slug)) continue;
11030
- seen.add(slug);
12157
+ const slug2 = g.filePath.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.(ts|js|tsx|jsx|py|go|rs|java|rb|php)$/i, "").slice(0, 60);
12158
+ if (!slug2 || seen.has(slug2)) continue;
12159
+ seen.add(slug2);
11031
12160
  const md = [
11032
12161
  "---",
11033
12162
  `description: "KUMA gotcha \u2014 ${g.description.substring(0, 80)}"`,
@@ -11042,7 +12171,7 @@ function handleCursorGotchaRules(root, results) {
11042
12171
  "`kuma_context({ action: 'history', target: '<file>' })`",
11043
12172
  ""
11044
12173
  ].join("\n");
11045
- fs25.writeFileSync(path28.join(rulesDir2, `${slug}.mdc`), md, "utf-8");
12174
+ fs30.writeFileSync(path33.join(rulesDir2, `${slug2}.mdc`), md, "utf-8");
11046
12175
  created++;
11047
12176
  }
11048
12177
  results.push({
@@ -11069,28 +12198,28 @@ function runInit(options) {
11069
12198
  let agentsMdHandled = false;
11070
12199
  for (const type of selected) {
11071
12200
  const relativePath = configFilePath(type);
11072
- const fullPath = path28.resolve(root, relativePath);
12201
+ const fullPath = path33.resolve(root, relativePath);
11073
12202
  const getTemplate = TEMPLATES[type];
11074
12203
  try {
11075
12204
  if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
11076
12205
  agentsMdHandled = true;
11077
12206
  const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
11078
- if (fs25.existsSync(fullPath)) {
12207
+ if (fs30.existsSync(fullPath)) {
11079
12208
  if (options.skipExisting) {
11080
12209
  results.push({ type, filePath: relativePath, action: "skipped" });
11081
12210
  } else {
11082
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
12211
+ const existingContent = fs30.readFileSync(fullPath, "utf-8");
11083
12212
  if (existingContent.includes("_Generated by Kuma MCP_")) {
11084
12213
  results.push({ type, filePath: relativePath, action: "skipped" });
11085
12214
  } else {
11086
- fs25.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
12215
+ fs30.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
11087
12216
  results.push({ type, filePath: relativePath, action: "appended" });
11088
12217
  }
11089
12218
  }
11090
12219
  } else {
11091
- const dir = path28.dirname(fullPath);
11092
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11093
- fs25.writeFileSync(fullPath, combinedContent, "utf-8");
12220
+ const dir = path33.dirname(fullPath);
12221
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
12222
+ fs30.writeFileSync(fullPath, combinedContent, "utf-8");
11094
12223
  results.push({ type, filePath: relativePath, action: "created" });
11095
12224
  }
11096
12225
  if (selectedSet.has("opencode")) handleOpencodeSecondary(root, results);
@@ -11102,12 +12231,12 @@ function runInit(options) {
11102
12231
  continue;
11103
12232
  } else {
11104
12233
  const template = getTemplate();
11105
- if (fs25.existsSync(fullPath)) {
12234
+ if (fs30.existsSync(fullPath)) {
11106
12235
  if (options.skipExisting) {
11107
12236
  results.push({ type, filePath: relativePath, action: "skipped" });
11108
12237
  continue;
11109
12238
  }
11110
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
12239
+ const existingContent = fs30.readFileSync(fullPath, "utf-8");
11111
12240
  if (existingContent.includes("_Generated by Kuma MCP_")) {
11112
12241
  if (type === "antigravity") {
11113
12242
  handleAntigravitySecondary(root, results);
@@ -11124,14 +12253,14 @@ function runInit(options) {
11124
12253
  continue;
11125
12254
  }
11126
12255
  const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
11127
- fs25.writeFileSync(fullPath, newContent, "utf-8");
12256
+ fs30.writeFileSync(fullPath, newContent, "utf-8");
11128
12257
  results.push({ type, filePath: relativePath, action: "appended" });
11129
12258
  } else {
11130
- const dir = path28.dirname(fullPath);
11131
- if (!fs25.existsSync(dir)) {
11132
- fs25.mkdirSync(dir, { recursive: true });
12259
+ const dir = path33.dirname(fullPath);
12260
+ if (!fs30.existsSync(dir)) {
12261
+ fs30.mkdirSync(dir, { recursive: true });
11133
12262
  }
11134
- fs25.writeFileSync(fullPath, template, "utf-8");
12263
+ fs30.writeFileSync(fullPath, template, "utf-8");
11135
12264
  results.push({ type, filePath: relativePath, action: "created" });
11136
12265
  }
11137
12266
  if (type === "antigravity") {
@@ -11256,11 +12385,10 @@ var init_init = __esm({
11256
12385
  "Record what matters, skip what doesn't.",
11257
12386
  "",
11258
12387
  "<kuma_obedience>",
11259
- "## \u{1F512} Obedience (non-negotiable)",
11260
- '- MUST call `kuma_context({ action: "init" })` at session start.',
11261
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
11262
- '- MUST call `kuma_safety({ action: "verify" })` after edits.',
11263
- "- MUST NOT call actions outside the core actions.",
12388
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
12389
+ "- HARNESS-ENFORCED (no choice): MCP schema rejects unknown actions; hooks inject gotchas pre-edit (`kuma hook pre-edit`); SessionStart serves the brief; dirty-flags patch the map post-edit.",
12390
+ '- AGENT-EXPECTED (best-effort ~70%): call `kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; `kuma_safety({ action: "verify" })` after edits.',
12391
+ "- FALLBACKS: init skipped \u2192 lazy brief on first unfamiliar edit; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop (never treat as verified).",
11264
12392
  "</kuma_obedience>",
11265
12393
  "",
11266
12394
  "<kuma_workflow>",
@@ -11299,27 +12427,277 @@ var init_init = __esm({
11299
12427
  }
11300
12428
  });
11301
12429
 
11302
- // src/utils/agentDetector.ts
11303
- var agentDetector_exports = {};
11304
- __export(agentDetector_exports, {
11305
- detectAgent: () => detectAgent,
11306
- getAgentLabel: () => getAgentLabel,
11307
- getSkillPath: () => getSkillPath,
11308
- skillExists: () => skillExists
12430
+ // src/engine/compactionContext.ts
12431
+ var compactionContext_exports = {};
12432
+ __export(compactionContext_exports, {
12433
+ getCompactionContext: () => getCompactionContext
11309
12434
  });
11310
- import fs26 from "fs";
11311
- import path29 from "path";
11312
- function checkFile(root, filePath) {
12435
+ import fs31 from "fs";
12436
+ import path34 from "path";
12437
+ async function getCompactionContext() {
12438
+ const parts = [];
11313
12439
  try {
11314
- return fs26.existsSync(path29.join(root, filePath));
12440
+ const { sessionMemory: sessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
12441
+ const summary = sessionMemory2.getSummary();
12442
+ const goal = String(summary.currentGoal || "");
12443
+ if (goal) parts.push(`Goal: ${goal.substring(0, 120)}`);
12444
+ const modified = (summary.modifiedFiles || []).length;
12445
+ if (modified > 0) parts.push(`Modified: ${modified} file(s)`);
11315
12446
  } catch {
11316
- return false;
11317
12447
  }
11318
- }
12448
+ try {
12449
+ const dbFile = path34.join(getProjectRoot(), ".kuma", "kuma.db");
12450
+ if (fs31.existsSync(dbFile)) {
12451
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
12452
+ const db = await getDb2();
12453
+ const stmt = db.prepare(
12454
+ `SELECT file_path, description FROM known_gotchas
12455
+ WHERE status IN ('active','verified') AND severity IN ('critical','high')
12456
+ AND (quarantined IS NULL OR quarantined = 0)
12457
+ ORDER BY updated_at DESC LIMIT 3`
12458
+ );
12459
+ const rows = [];
12460
+ while (stmt.step()) {
12461
+ const r = stmt.getAsObject();
12462
+ rows.push(`[${r.file_path}] ${String(r.description).substring(0, 80)}`);
12463
+ }
12464
+ stmt.free();
12465
+ if (rows.length > 0) parts.push(`Gotchas: ${rows.join(" | ")}`);
12466
+ }
12467
+ } catch {
12468
+ }
12469
+ try {
12470
+ const { getDirtyFiles: getDirtyFiles2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12471
+ const dirty = getDirtyFiles2().slice(0, 5);
12472
+ if (dirty.length > 0) parts.push(`Dirty: ${dirty.join(", ")}`);
12473
+ } catch {
12474
+ }
12475
+ try {
12476
+ const cpDir = path34.join(getProjectRoot(), ".kuma", "checkpoints");
12477
+ if (fs31.existsSync(cpDir)) {
12478
+ const cps = fs31.readdirSync(cpDir).slice(0, 3);
12479
+ if (cps.length > 0) parts.push(`Checkpoints: ${cps.join(", ")}`);
12480
+ }
12481
+ } catch {
12482
+ }
12483
+ let out = parts.join("\n");
12484
+ if (out.length > BUDGET) out = out.slice(0, BUDGET) + "\u2026";
12485
+ return out;
12486
+ }
12487
+ var BUDGET;
12488
+ var init_compactionContext = __esm({
12489
+ "src/engine/compactionContext.ts"() {
12490
+ "use strict";
12491
+ init_pathValidator();
12492
+ BUDGET = 600;
12493
+ }
12494
+ });
12495
+
12496
+ // src/engine/kumaDaemon.ts
12497
+ var kumaDaemon_exports = {};
12498
+ __export(kumaDaemon_exports, {
12499
+ DEFAULT_INTERVAL_MS: () => DEFAULT_INTERVAL_MS,
12500
+ daemonRunChild: () => daemonRunChild,
12501
+ daemonStart: () => daemonStart,
12502
+ daemonStatus: () => daemonStatus,
12503
+ daemonStop: () => daemonStop,
12504
+ daemonTick: () => daemonTick
12505
+ });
12506
+ import fs32 from "fs";
12507
+ import path35 from "path";
12508
+ import { spawn } from "child_process";
12509
+ function paths(root) {
12510
+ const r = root || getProjectRoot();
12511
+ return {
12512
+ pid: path35.join(r, PID_FILE),
12513
+ status: path35.join(r, STATUS_FILE),
12514
+ log: path35.join(r, LOG_FILE)
12515
+ };
12516
+ }
12517
+ function readPid(root) {
12518
+ try {
12519
+ const fp = paths(root).pid;
12520
+ if (!fs32.existsSync(fp)) return null;
12521
+ const pid = Number(fs32.readFileSync(fp, "utf-8").trim());
12522
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
12523
+ } catch {
12524
+ return null;
12525
+ }
12526
+ }
12527
+ function isAlive(pid) {
12528
+ try {
12529
+ process.kill(pid, 0);
12530
+ return true;
12531
+ } catch {
12532
+ return false;
12533
+ }
12534
+ }
12535
+ async function daemonTick(root) {
12536
+ const r = root || getProjectRoot();
12537
+ const p = paths(r);
12538
+ const notes = [];
12539
+ try {
12540
+ const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
12541
+ const scan = await syncModifiedFiles2(50);
12542
+ notes.push(`map: ${scan.filesScanned}f/${scan.nodeCount}n/${scan.edgeCount}e`);
12543
+ } catch (err) {
12544
+ notes.push(`map: skipped (${err})`);
12545
+ }
12546
+ try {
12547
+ const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
12548
+ const { count } = await refreshPackageBriefs2();
12549
+ notes.push(`briefs: ${count}`);
12550
+ } catch {
12551
+ notes.push("briefs: skipped");
12552
+ }
12553
+ try {
12554
+ const { detectDrift: detectDrift2 } = await Promise.resolve().then(() => (init_kumaDriftDetector(), kumaDriftDetector_exports));
12555
+ const records = await detectDrift2();
12556
+ const stale = records.filter((x) => x.severity === "stale" || x.severity === "missing").length;
12557
+ notes.push(`drift: ${stale}/${records.length} stale`);
12558
+ } catch {
12559
+ notes.push("drift: skipped");
12560
+ }
12561
+ try {
12562
+ const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12563
+ clearDirty2();
12564
+ } catch {
12565
+ }
12566
+ const status = {
12567
+ lastRunAt: Date.now(),
12568
+ lastResult: notes.join(" \xB7 ")
12569
+ };
12570
+ try {
12571
+ let prev = {};
12572
+ if (fs32.existsSync(p.status)) prev = JSON.parse(fs32.readFileSync(p.status, "utf-8"));
12573
+ fs32.writeFileSync(p.status, JSON.stringify({ ...prev, ...status }, null, 2), "utf-8");
12574
+ } catch {
12575
+ }
12576
+ return notes.join(" \xB7 ");
12577
+ }
12578
+ async function runLoop(intervalMs) {
12579
+ const r = getProjectRoot();
12580
+ const p = paths(r);
12581
+ const log = (msg) => {
12582
+ try {
12583
+ fs32.appendFileSync(p.log, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
12584
+ `, "utf-8");
12585
+ } catch {
12586
+ }
12587
+ };
12588
+ log(`daemon started (interval ${intervalMs}ms)`);
12589
+ for (; ; ) {
12590
+ try {
12591
+ const res = await daemonTick(r);
12592
+ log(`tick: ${res}`);
12593
+ } catch (err) {
12594
+ log(`tick failed: ${err}`);
12595
+ }
12596
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
12597
+ }
12598
+ }
12599
+ async function daemonStart(intervalMs = DEFAULT_INTERVAL_MS, root) {
12600
+ const r = root || getProjectRoot();
12601
+ const p = paths(r);
12602
+ const existing = readPid(r);
12603
+ if (existing && isAlive(existing)) {
12604
+ return `\u{1F43B} Daemon already running (pid ${existing}). Use \`kuma daemon stop\` first.`;
12605
+ }
12606
+ const entry = process.argv[1];
12607
+ const child = spawn(process.execPath, [entry, "daemon", "run", String(intervalMs)], {
12608
+ cwd: r,
12609
+ detached: true,
12610
+ stdio: "ignore"
12611
+ });
12612
+ child.unref();
12613
+ try {
12614
+ fs32.writeFileSync(p.pid, String(child.pid), "utf-8");
12615
+ fs32.writeFileSync(
12616
+ p.status,
12617
+ JSON.stringify({ running: true, pid: child.pid, startedAt: Date.now(), lastRunAt: null, lastResult: null, intervalMs }, null, 2),
12618
+ "utf-8"
12619
+ );
12620
+ } catch {
12621
+ }
12622
+ return `\u{1F43B} Daemon started (pid ${child.pid}, every ${Math.round(intervalMs / 6e4)}m). Status: .kuma/daemon-status.json`;
12623
+ }
12624
+ async function daemonStop(root) {
12625
+ const r = root || getProjectRoot();
12626
+ const p = paths(r);
12627
+ const pid = readPid(r);
12628
+ if (!pid) return "\u{1F43B} Daemon not running (no pid file).";
12629
+ if (!isAlive(pid)) {
12630
+ try {
12631
+ fs32.rmSync(p.pid, { force: true });
12632
+ } catch {
12633
+ }
12634
+ return "\u{1F43B} Daemon pid file was stale \u2014 cleaned up (process already gone).";
12635
+ }
12636
+ try {
12637
+ process.kill(pid, "SIGTERM");
12638
+ try {
12639
+ fs32.rmSync(p.pid, { force: true });
12640
+ } catch {
12641
+ }
12642
+ return `\u{1F43B} Daemon stopped (was pid ${pid}).`;
12643
+ } catch (err) {
12644
+ return `\u{1F43B} Could not stop daemon pid ${pid}: ${err}`;
12645
+ }
12646
+ }
12647
+ async function daemonStatus(root) {
12648
+ const r = root || getProjectRoot();
12649
+ const p = paths(r);
12650
+ const pid = readPid(r);
12651
+ let status = { running: false, pid: null, startedAt: null, lastRunAt: null, lastResult: null, intervalMs: DEFAULT_INTERVAL_MS };
12652
+ try {
12653
+ if (fs32.existsSync(p.status)) status = { ...status, ...JSON.parse(fs32.readFileSync(p.status, "utf-8")) };
12654
+ } catch {
12655
+ }
12656
+ const alive = pid !== null && isAlive(pid);
12657
+ status.running = alive;
12658
+ status.pid = alive ? pid : null;
12659
+ if (!alive) {
12660
+ return `\u{1F43B} Daemon: stopped.${status.lastRunAt ? ` Last tick ${new Date(status.lastRunAt).toISOString()}: ${status.lastResult || "n/a"}` : " Never ran."} Start with \`kuma daemon start\`.`;
12661
+ }
12662
+ return `\u{1F43B} Daemon: running (pid ${pid}).${status.lastRunAt ? ` Last tick ${new Date(status.lastRunAt).toISOString()}: ${status.lastResult || "n/a"}` : " Warming up\u2026"}`;
12663
+ }
12664
+ async function daemonRunChild(intervalMs) {
12665
+ const ms = Number.isFinite(intervalMs) && intervalMs >= 3e4 ? intervalMs : DEFAULT_INTERVAL_MS;
12666
+ await runLoop(ms);
12667
+ }
12668
+ var PID_FILE, STATUS_FILE, LOG_FILE, DEFAULT_INTERVAL_MS;
12669
+ var init_kumaDaemon = __esm({
12670
+ "src/engine/kumaDaemon.ts"() {
12671
+ "use strict";
12672
+ init_pathValidator();
12673
+ PID_FILE = ".kuma/daemon.pid";
12674
+ STATUS_FILE = ".kuma/daemon-status.json";
12675
+ LOG_FILE = ".kuma/daemon.log";
12676
+ DEFAULT_INTERVAL_MS = 5 * 60 * 1e3;
12677
+ }
12678
+ });
12679
+
12680
+ // src/utils/agentDetector.ts
12681
+ var agentDetector_exports = {};
12682
+ __export(agentDetector_exports, {
12683
+ detectAgent: () => detectAgent,
12684
+ getAgentLabel: () => getAgentLabel,
12685
+ getSkillPath: () => getSkillPath,
12686
+ skillExists: () => skillExists
12687
+ });
12688
+ import fs33 from "fs";
12689
+ import path36 from "path";
12690
+ function checkFile(root, filePath) {
12691
+ try {
12692
+ return fs33.existsSync(path36.join(root, filePath));
12693
+ } catch {
12694
+ return false;
12695
+ }
12696
+ }
11319
12697
  function checkDir(root, dirPath) {
11320
12698
  try {
11321
- const fullPath = path29.join(root, dirPath);
11322
- return fs26.existsSync(fullPath) && fs26.statSync(fullPath).isDirectory();
12699
+ const fullPath = path36.join(root, dirPath);
12700
+ return fs33.existsSync(fullPath) && fs33.statSync(fullPath).isDirectory();
11323
12701
  } catch {
11324
12702
  return false;
11325
12703
  }
@@ -11387,7 +12765,7 @@ function getAgentLabel(type) {
11387
12765
  function skillExists(type, projectRoot) {
11388
12766
  const root = projectRoot ?? getProjectRoot();
11389
12767
  const skillPath = getSkillPath(type);
11390
- return fs26.existsSync(path29.join(root, skillPath));
12768
+ return fs33.existsSync(path36.join(root, skillPath));
11391
12769
  }
11392
12770
  var AGENT_DETECTORS;
11393
12771
  var init_agentDetector = __esm({
@@ -11928,11 +13306,10 @@ var init_skillGenerator = __esm({
11928
13306
  "use strict";
11929
13307
  CORE_ACTIONS = [
11930
13308
  "<kuma_obedience>",
11931
- "## \u{1F512} Obedience (non-negotiable)",
11932
- '- MUST call `kuma_context({ action: "init" })` at session start.',
11933
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
11934
- '- MUST call `kuma_safety({ action: "verify" })` after edits to resolve affected tests.',
11935
- "- MUST NOT call actions outside the core actions.",
13309
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
13310
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
13311
+ '- AGENT-EXPECTED (~70%): `kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; `kuma_safety({ action: "verify" })` after edits.',
13312
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
11936
13313
  "</kuma_obedience>",
11937
13314
  "",
11938
13315
  "<kuma_workflow>",
@@ -11984,8 +13361,8 @@ init_kumaGraph();
11984
13361
  init_kumaMemory();
11985
13362
  init_kumaDriftDetector();
11986
13363
  init_pathValidator();
11987
- import fs18 from "fs";
11988
- import path21 from "path";
13364
+ import fs22 from "fs";
13365
+ import path25 from "path";
11989
13366
  var CONTEXT_ALIASES = {
11990
13367
  "research": "research",
11991
13368
  "search": "research",
@@ -12031,9 +13408,13 @@ var CONTEXT_ALIASES = {
12031
13408
  "anti-duplication": "reuse"
12032
13409
  };
12033
13410
  async function handleContext(params) {
13411
+ let dirtySynced = 0;
12034
13412
  try {
13413
+ const { getDirtyFiles: getDirtyFiles2, clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12035
13414
  const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
13415
+ dirtySynced = getDirtyFiles2().length;
12036
13416
  await syncModifiedFiles2(30);
13417
+ if (dirtySynced > 0) clearDirty2();
12037
13418
  } catch {
12038
13419
  }
12039
13420
  const rawAction = params.action || "init";
@@ -12049,7 +13430,7 @@ async function handleContext(params) {
12049
13430
  case "init":
12050
13431
  return handleInit(params);
12051
13432
  case "research":
12052
- return obediencePrefix + await handleResearch(params);
13433
+ return obediencePrefix + await handleResearch(params, dirtySynced);
12053
13434
  case "history":
12054
13435
  return obediencePrefix + await handleHistory(params);
12055
13436
  case "flow":
@@ -12256,11 +13637,15 @@ function sessionFocusAdvice(focus) {
12256
13637
  return "Record gotchas & decisions as they happen \u2014 skip what isn't worth it.";
12257
13638
  }
12258
13639
  }
12259
- async function handleResearch(params) {
13640
+ async function handleResearch(params, dirtySynced = 0) {
12260
13641
  const scope = params.scope || "project";
12261
13642
  sessionMemory.setGoal(`Researching: ${scope}`);
12262
13643
  sessionMemory.recordToolCall("kuma_context_research", { scope });
12263
13644
  const lines = [`\u{1F52C} **Research: ${scope}**`, `\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`, ""];
13645
+ if (dirtySynced > 0) {
13646
+ lines.push(`\u{1F5FA}\uFE0F Map patched post-edit: ${dirtySynced} dirty file(s) synced before this research (no rebuild asked).`);
13647
+ lines.push("");
13648
+ }
12264
13649
  lines.push("**Step 1/5: Loading Research Cache**");
12265
13650
  const cached = await getResearchCache(scope);
12266
13651
  let record = null;
@@ -12274,7 +13659,18 @@ async function handleResearch(params) {
12274
13659
  const ageSeconds = Math.floor((Date.now() - (record.validatedAt ? new Date(record.validatedAt).getTime() : 0)) / 1e3);
12275
13660
  const ageStr = ageSeconds > 86400 ? `${Math.floor(ageSeconds / 86400)}d` : ageSeconds > 3600 ? `${Math.floor(ageSeconds / 3600)}h` : `${Math.floor(ageSeconds / 60)}m`;
12276
13661
  lines.push(` \u2705 Found cached research (${ageStr} old)`);
12277
- if (ageSeconds > 86400) lines.push(` ${ageSeconds > 604800 ? "\u{1F534}" : "\u{1F7E1}"} **Staleness:** Cache is ${ageStr} old \u2014 may be stale`);
13662
+ try {
13663
+ const { isStale: isStale2, RESEARCH_TTL_MS: RESEARCH_TTL_MS2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
13664
+ const validatedMs = record.validatedAt ? new Date(record.validatedAt).getTime() : 0;
13665
+ if (isStale2(validatedMs, RESEARCH_TTL_MS2)) {
13666
+ lines.push(` \u23F3 **STALE**: older than TTL 7d \u2014 re-researching fresh below`);
13667
+ record = null;
13668
+ } else {
13669
+ lines.push(` \u2705 Fresh (TTL 7d)`);
13670
+ }
13671
+ } catch {
13672
+ }
13673
+ if (ageSeconds > 86400 && record) lines.push(` ${ageSeconds > 604800 ? "\u{1F534}" : "\u{1F7E1}"} **Staleness:** Cache is ${ageStr} old \u2014 may be stale`);
12278
13674
  } else lines.push(" \u23F3 No cached research \u2014 starting fresh");
12279
13675
  lines.push("");
12280
13676
  lines.push("**Step 2/5: Staleness Check**");
@@ -12383,10 +13779,10 @@ async function handleHistory(params) {
12383
13779
  lines.push(traceFormatted);
12384
13780
  } else {
12385
13781
  try {
12386
- const { execSync: execSync7 } = await import("child_process");
13782
+ const { execSync: execSync10 } = await import("child_process");
12387
13783
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
12388
13784
  const root = getProjectRoot2();
12389
- const gitHistory = execSync7(
13785
+ const gitHistory = execSync10(
12390
13786
  `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
12391
13787
  { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
12392
13788
  ).trim();
@@ -12418,7 +13814,7 @@ ${gitHistory}`);
12418
13814
  try {
12419
13815
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
12420
13816
  const db = await getDb2();
12421
- const base = path21.basename(target);
13817
+ const base = path25.basename(target);
12422
13818
  const stmt = db.prepare(`
12423
13819
  SELECT description, last_verified_at FROM known_gotchas
12424
13820
  WHERE status = 'resolved' AND (file_path LIKE ? OR file_path LIKE ?)
@@ -12431,9 +13827,9 @@ ${gitHistory}`);
12431
13827
  if (resolved.length === 0) {
12432
13828
  try {
12433
13829
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
12434
- const mdPath = path21.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
12435
- if (fs18.existsSync(mdPath)) {
12436
- const mdContent = fs18.readFileSync(mdPath, "utf-8");
13830
+ const mdPath = path25.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
13831
+ if (fs22.existsSync(mdPath)) {
13832
+ const mdContent = fs22.readFileSync(mdPath, "utf-8");
12437
13833
  const sections = mdContent.split(/^###\s+/m);
12438
13834
  for (const sec of sections) {
12439
13835
  const secLower = sec.toLowerCase();
@@ -12478,7 +13874,18 @@ async function handleMap(_params) {
12478
13874
  sessionMemory.recordToolCall("kuma_context_map", {});
12479
13875
  const { getWorkspaceInfo: getWorkspaceInfo2, formatWorkspaceMap: formatWorkspaceMap2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
12480
13876
  const wsInfo = await getWorkspaceInfo2();
12481
- return formatWorkspaceMap2(wsInfo);
13877
+ const base = formatWorkspaceMap2(wsInfo);
13878
+ try {
13879
+ const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
13880
+ const { count } = await refreshPackageBriefs2();
13881
+ if (count > 0) {
13882
+ return base + `
13883
+
13884
+ \u{1F4DA} **Pre-computed briefs**: ${count} package brief(s) refreshed in \`.kuma/briefs/\` \u2014 read instead of grep-walking (saves ~5x discovery tokens).`;
13885
+ }
13886
+ } catch {
13887
+ }
13888
+ return base;
12482
13889
  }
12483
13890
  async function handleImpact(params) {
12484
13891
  const target = params.target || params.scope || "project";
@@ -12494,8 +13901,8 @@ init_kumaDb();
12494
13901
  init_kumaMemory();
12495
13902
  init_pathValidator();
12496
13903
  init_pathValidator();
12497
- import fs20 from "fs";
12498
- import path23 from "path";
13904
+ import fs24 from "fs";
13905
+ import path27 from "path";
12499
13906
  var MEMORY_ALIASES = {
12500
13907
  "decision": "decision",
12501
13908
  "adr": "decision",
@@ -12519,7 +13926,11 @@ var MEMORY_ALIASES = {
12519
13926
  "gotcha-resolve": "gotcha",
12520
13927
  "deprecate_gotcha": "gotcha",
12521
13928
  "deprecate-gotcha": "gotcha",
12522
- "deprecate": "gotcha"
13929
+ "deprecate": "gotcha",
13930
+ "test_outcome": "test_outcome",
13931
+ "test-outcome": "test_outcome",
13932
+ "testoutcome": "test_outcome",
13933
+ "test-record": "test_outcome"
12523
13934
  };
12524
13935
  async function handleMemory(params) {
12525
13936
  const rawAction = params.action || "search";
@@ -12537,9 +13948,23 @@ async function handleMemory(params) {
12537
13948
  return handleArchFlow(params);
12538
13949
  case "gotcha":
12539
13950
  return handleGotchaAction(params);
13951
+ case "test_outcome":
13952
+ return handleTestOutcome(params);
12540
13953
  default:
12541
- return `Unknown action "${action}". Use: gotcha, decision, arch_flow, research_save, search`;
13954
+ return `Unknown action "${action}". Use: gotcha, decision, arch_flow, research_save, search, test_outcome`;
13955
+ }
13956
+ }
13957
+ async function handleTestOutcome(params) {
13958
+ const changedRaw = params.scope || params.target || "";
13959
+ const failedRaw = params.content || params.description || "";
13960
+ if (!changedRaw.trim()) {
13961
+ return '\u274C **Test outcome NOT saved** \u2014 missing `scope` (changed file(s), comma-separated).\n\u2705 Use: kuma_memory({ action: "test_outcome", scope: "<changed files>", content: "<failed tests, empty when green>" })\nFuture impact queries rank history-backed tests first (\u{1F525}) and flaky ones last.';
12542
13962
  }
13963
+ const changed = changedRaw.split(",").map((s) => s.trim()).filter(Boolean);
13964
+ const failed = failedRaw.split(",").map((s) => s.trim()).filter(Boolean);
13965
+ const { recordTestOutcome: recordTestOutcome2 } = await Promise.resolve().then(() => (init_testHistory(), testHistory_exports));
13966
+ const { recorded } = await recordTestOutcome2(changed, failed);
13967
+ return failed.length === 0 ? `\u2705 Green run recorded for ${changed.length} file(s) (${recorded} row(s)). Future impact queries trust these areas more.` : `\u2705 Recorded ${failed.length} failing test(s) for ${changed.length} file(s). Future impact queries rank them first (\u{1F525}). If a retry flips one to green, re-record with it absent from content and it stays signal; use flaky marking via retry instead.`;
12543
13968
  }
12544
13969
  async function handleDecision(params) {
12545
13970
  const title = params.title || params.content || (params.target ? `Decision regarding ${params.target}` : params.scope ? `Decision for ${params.scope}` : "");
@@ -12588,9 +14013,9 @@ async function handleResearchSave(params) {
12588
14013
  } catch {
12589
14014
  }
12590
14015
  try {
12591
- const researchDir = path23.join(getProjectRoot(), ".kuma", "research");
12592
- if (!fs20.existsSync(researchDir)) fs20.mkdirSync(researchDir, { recursive: true });
12593
- fs20.writeFileSync(path23.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
14016
+ const researchDir = path27.join(getProjectRoot(), ".kuma", "research");
14017
+ if (!fs24.existsSync(researchDir)) fs24.mkdirSync(researchDir, { recursive: true });
14018
+ fs24.writeFileSync(path27.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
12594
14019
  } catch {
12595
14020
  }
12596
14021
  sessionMemory.recordMemoryAction("research_save");
@@ -12786,9 +14211,9 @@ init_sessionMemory();
12786
14211
  init_pathValidator();
12787
14212
  init_sessionMemory();
12788
14213
  init_kumaShared();
12789
- import fs21 from "fs";
12790
- import path24 from "path";
12791
- import { execSync as execSync5 } from "child_process";
14214
+ import fs25 from "fs";
14215
+ import path28 from "path";
14216
+ import { execSync as execSync7 } from "child_process";
12792
14217
  var SCRIPT_PATCH_PATTERNS = [
12793
14218
  "writeFileSync",
12794
14219
  "writeFile",
@@ -12814,20 +14239,20 @@ function findPatchScripts(projectRoot) {
12814
14239
  const warnings = [];
12815
14240
  const recentFiles = scanRecentFiles(projectRoot);
12816
14241
  for (const file of recentFiles) {
12817
- const ext = path24.extname(file).toLowerCase();
14242
+ const ext = path28.extname(file).toLowerCase();
12818
14243
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
12819
- const relativePath = path24.relative(projectRoot, file);
14244
+ const relativePath = path28.relative(projectRoot, file);
12820
14245
  if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".") || relativePath.startsWith(".kuma/scratch")) {
12821
14246
  continue;
12822
14247
  }
12823
14248
  try {
12824
- const content = fs21.readFileSync(file, "utf-8").toLowerCase();
14249
+ const content = fs25.readFileSync(file, "utf-8").toLowerCase();
12825
14250
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
12826
14251
  if (matchedPattern) {
12827
14252
  warnings.push({
12828
14253
  severity: "high",
12829
14254
  pattern: "script-patching",
12830
- message: `Created script file that modifies other files: ${path24.basename(file)}`,
14255
+ message: `Created script file that modifies other files: ${path28.basename(file)}`,
12831
14256
  suggestion: "Use your native edit tools instead \u2014 targeted edits with auto-backup and rollback",
12832
14257
  evidence: `File: ${relativePath} contains '${matchedPattern}'`,
12833
14258
  filePath: relativePath
@@ -12861,15 +14286,15 @@ function detectBashGrepUsage() {
12861
14286
  function scanRecentFiles(projectRoot) {
12862
14287
  const recent = [];
12863
14288
  try {
12864
- const entries = fs21.readdirSync(projectRoot, { withFileTypes: true });
14289
+ const entries = fs25.readdirSync(projectRoot, { withFileTypes: true });
12865
14290
  const now = Date.now();
12866
14291
  const recentThreshold = 30 * 60 * 1e3;
12867
14292
  for (const entry of entries) {
12868
14293
  if (!entry.isFile()) continue;
12869
14294
  try {
12870
- const stat = fs21.statSync(path24.join(projectRoot, entry.name));
14295
+ const stat = fs25.statSync(path28.join(projectRoot, entry.name));
12871
14296
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
12872
- recent.push(path24.join(projectRoot, entry.name));
14297
+ recent.push(path28.join(projectRoot, entry.name));
12873
14298
  }
12874
14299
  } catch {
12875
14300
  continue;
@@ -12882,7 +14307,7 @@ function scanRecentFiles(projectRoot) {
12882
14307
  function detectGitPatchScripts(projectRoot) {
12883
14308
  const warnings = [];
12884
14309
  try {
12885
- const statusStdout = execSync5("git status --porcelain", {
14310
+ const statusStdout = execSync7("git status --porcelain", {
12886
14311
  cwd: projectRoot,
12887
14312
  encoding: "utf-8",
12888
14313
  timeout: 3e3
@@ -12893,14 +14318,14 @@ function detectGitPatchScripts(projectRoot) {
12893
14318
  const prefix = line.substring(0, 2);
12894
14319
  if (prefix !== "??" && prefix !== "A ") continue;
12895
14320
  const file = line.substring(3).trim();
12896
- const ext = path24.extname(file).toLowerCase();
14321
+ const ext = path28.extname(file).toLowerCase();
12897
14322
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
12898
14323
  const isRootLevel = !file.includes("/");
12899
14324
  const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
12900
14325
  if (!isRootLevel && !isScriptsDir) continue;
12901
- const fullPath = path24.join(projectRoot, file);
12902
- if (fs21.existsSync(fullPath)) {
12903
- const content = fs21.readFileSync(fullPath, "utf-8").toLowerCase();
14326
+ const fullPath = path28.join(projectRoot, file);
14327
+ if (fs25.existsSync(fullPath)) {
14328
+ const content = fs25.readFileSync(fullPath, "utf-8").toLowerCase();
12904
14329
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
12905
14330
  if (matchedPattern) {
12906
14331
  warnings.push({
@@ -12915,7 +14340,7 @@ function detectGitPatchScripts(projectRoot) {
12915
14340
  }
12916
14341
  }
12917
14342
  }
12918
- const deletedStdout = execSync5("git diff --name-only --diff-filter=D HEAD", {
14343
+ const deletedStdout = execSync7("git diff --name-only --diff-filter=D HEAD", {
12919
14344
  cwd: projectRoot,
12920
14345
  encoding: "utf-8",
12921
14346
  timeout: 3e3
@@ -12923,7 +14348,7 @@ function detectGitPatchScripts(projectRoot) {
12923
14348
  if (deletedStdout) {
12924
14349
  const deletedFiles = deletedStdout.split("\n").filter(Boolean);
12925
14350
  for (const file of deletedFiles) {
12926
- const ext = path24.extname(file).toLowerCase();
14351
+ const ext = path28.extname(file).toLowerCase();
12927
14352
  if (SCRIPT_EXTENSIONS.includes(ext)) {
12928
14353
  warnings.push({
12929
14354
  severity: "high",
@@ -12985,21 +14410,21 @@ function detectAllAntiPatterns() {
12985
14410
  // src/engine/contextSnapshot.ts
12986
14411
  init_sessionMemory();
12987
14412
  init_pathValidator();
12988
- import fs22 from "fs";
12989
- import path25 from "path";
12990
- import { execSync as execSync6 } from "child_process";
14413
+ import fs26 from "fs";
14414
+ import path29 from "path";
14415
+ import { execSync as execSync8 } from "child_process";
12991
14416
  var SNAPSHOT_DIR = "context-snapshots";
12992
14417
  function snapshotDir() {
12993
- return path25.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
14418
+ return path29.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
12994
14419
  }
12995
14420
  function ensureSnapshotDir() {
12996
14421
  const dir = snapshotDir();
12997
- if (!fs22.existsSync(dir)) {
12998
- fs22.mkdirSync(dir, { recursive: true });
14422
+ if (!fs26.existsSync(dir)) {
14423
+ fs26.mkdirSync(dir, { recursive: true });
12999
14424
  }
13000
14425
  }
13001
14426
  function snapshotFilePath(timestamp) {
13002
- return path25.join(snapshotDir(), `${timestamp}.json`);
14427
+ return path29.join(snapshotDir(), `${timestamp}.json`);
13003
14428
  }
13004
14429
  function saveSnapshot(goal) {
13005
14430
  try {
@@ -13020,7 +14445,7 @@ function saveSnapshot(goal) {
13020
14445
  hasConventions: !!summary.hasConventions
13021
14446
  };
13022
14447
  try {
13023
- fs22.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
14448
+ fs26.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
13024
14449
  } catch {
13025
14450
  }
13026
14451
  sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
@@ -13059,7 +14484,7 @@ function formatSnapshot(snapshot) {
13059
14484
  function getGitDiffStat2() {
13060
14485
  try {
13061
14486
  const root = getProjectRoot();
13062
- const stdout = execSync6("git diff --stat", {
14487
+ const stdout = execSync8("git diff --stat", {
13063
14488
  cwd: root,
13064
14489
  encoding: "utf-8",
13065
14490
  timeout: 3e3
@@ -13106,24 +14531,62 @@ async function handleKumaGuard(params) {
13106
14531
  });
13107
14532
  }
13108
14533
  const drifts = [];
14534
+ const info = [];
14535
+ let verified = stats.hasRunTests;
14536
+ let uncommittedModifiedCount = stats.modifiedFiles.length;
13109
14537
  if (check === "all" || check === "drift") {
13110
14538
  const unresolvedCount = getUnresolvedCount(stats.failedFiles);
13111
14539
  const gitStat = getGitDiffStat();
13112
14540
  const editCalls = stats.toolCalls.filter((c) => isEditTool(c.toolName)).length;
14541
+ verified = stats.hasRunTests || hasVerificationEvidence(stats.toolCalls);
14542
+ const uncommitted = getUncommittedFiles();
14543
+ const modifiedPaths = stats.modifiedFiles.map((f) => String(f.filePath || f.path || ""));
14544
+ const uncommittedModified = modifiedPaths.filter(
14545
+ (p) => [...uncommitted].some((u) => u === p || u.endsWith(p) || p.endsWith(u))
14546
+ );
14547
+ const committedOnly = modifiedPaths.length - uncommittedModified.length;
14548
+ uncommittedModifiedCount = uncommittedModified.length;
13113
14549
  drifts.push(...buildDriftMessages(
13114
- stats.modifiedFiles.length,
13115
- stats.hasRunTests,
14550
+ uncommittedModified.length,
14551
+ verified,
13116
14552
  unresolvedCount,
13117
14553
  gitStat
13118
14554
  ));
13119
- if (stats.modifiedFiles.length > 0 && !stats.hasRunTests) {
14555
+ if (committedOnly > 0 && uncommittedModified.length === 0) {
14556
+ info.push(`${committedOnly} file(s) differ from base branch but are committed \u2014 legitimate feature state, not drift (no action needed)`);
14557
+ }
14558
+ if (verified && uncommittedModified.length > 0) {
14559
+ info.push(`Verification already ran in-session (test/typecheck evidence found) \u2014 no re-run demanded`);
14560
+ }
14561
+ if (uncommittedModified.length > 0 && !verified) {
13120
14562
  warnings.push({
13121
14563
  severity: "medium",
13122
14564
  pattern: "no-test-after-edit",
13123
- message: `${stats.modifiedFiles.length} file(s) modified without running tests`,
14565
+ message: `${uncommittedModified.length} uncommitted file(s) modified without running tests`,
13124
14566
  suggestion: "Run the project's test/typecheck command to verify changes"
13125
14567
  });
13126
14568
  }
14569
+ try {
14570
+ const { detectTestWeakening: detectTestWeakening2 } = await Promise.resolve().then(() => (init_kumaShared(), kumaShared_exports));
14571
+ const weakened = detectTestWeakening2(modifiedPaths);
14572
+ for (const w of weakened) {
14573
+ warnings.push({
14574
+ severity: "high",
14575
+ pattern: "test-weakening",
14576
+ message: `Test-weakening diff detected: ${w} \u2014 assertions removed without replacement`,
14577
+ suggestion: "Restore removed assertions or justify why the weakened test still guards the behavior"
14578
+ });
14579
+ }
14580
+ } catch {
14581
+ }
14582
+ try {
14583
+ const { scanDocDrift: scanDocDrift2, formatDocDrift: formatDocDrift2 } = await Promise.resolve().then(() => (init_docDrift(), docDrift_exports));
14584
+ const { checked, drifted, skipped } = scanDocDrift2();
14585
+ if (drifted.length > 0) {
14586
+ info.push(formatDocDrift2(checked, drifted.slice(0, 5), skipped));
14587
+ }
14588
+ } catch {
14589
+ }
13127
14590
  if (editCalls > 5) {
13128
14591
  warnings.push({
13129
14592
  severity: "low",
@@ -13195,6 +14658,25 @@ async function handleKumaGuard(params) {
13195
14658
  }
13196
14659
  return formatSnapshot(snapshot);
13197
14660
  }
14661
+ try {
14662
+ const { recordGuardFlag: recordGuardFlag2, getDowntieredRules: getDowntieredRules2, demoteSeverity: demoteSeverity2 } = await Promise.resolve().then(() => (init_guardLedger(), guardLedger_exports));
14663
+ for (const w of warnings) {
14664
+ try {
14665
+ await recordGuardFlag2(w.pattern);
14666
+ } catch {
14667
+ }
14668
+ }
14669
+ const downtiered = await getDowntieredRules2();
14670
+ if (downtiered.size > 0) {
14671
+ for (const w of warnings) {
14672
+ if (downtiered.has(w.pattern) && (w.severity === "high" || w.severity === "medium")) {
14673
+ w.severity = demoteSeverity2(w.severity);
14674
+ w.message = `\u{1F53B} [auto-downtiered: rule over FP budget] ${w.message}`;
14675
+ }
14676
+ }
14677
+ }
14678
+ } catch {
14679
+ }
13198
14680
  const hasWarnings = warnings.length > 0;
13199
14681
  const hasDrifts = drifts.length > 0;
13200
14682
  const onTrack = !hasWarnings && !hasDrifts;
@@ -13203,8 +14685,8 @@ async function handleKumaGuard(params) {
13203
14685
  warnings,
13204
14686
  loop.isLooping,
13205
14687
  getUnresolvedCount(stats.failedFiles),
13206
- stats.modifiedFiles.length,
13207
- stats.hasRunTests,
14688
+ uncommittedModifiedCount,
14689
+ verified,
13208
14690
  countEditCalls(stats.toolCalls)
13209
14691
  );
13210
14692
  const report = {
@@ -13212,6 +14694,7 @@ async function handleKumaGuard(params) {
13212
14694
  onTrack,
13213
14695
  warnings,
13214
14696
  drifts,
14697
+ info,
13215
14698
  suggestion,
13216
14699
  stats: {
13217
14700
  goal: stats.goal,
@@ -13242,6 +14725,90 @@ async function handleKumaGuard(params) {
13242
14725
  return JSON.stringify(reportWithRecordings, null, 2);
13243
14726
  }
13244
14727
 
14728
+ // src/engine/costLedger.ts
14729
+ init_kumaDb();
14730
+ async function ensureCostSchema() {
14731
+ const db = await getDb();
14732
+ db.exec(`
14733
+ CREATE TABLE IF NOT EXISTS cost_ledger (
14734
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14735
+ tool TEXT NOT NULL,
14736
+ action TEXT,
14737
+ duration_ms INTEGER NOT NULL DEFAULT 0,
14738
+ output_chars INTEGER NOT NULL DEFAULT 0,
14739
+ outcome TEXT NOT NULL DEFAULT 'ok',
14740
+ tokens INTEGER,
14741
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
14742
+ );
14743
+ CREATE INDEX IF NOT EXISTS idx_cost_tool ON cost_ledger(tool);
14744
+ `);
14745
+ saveDb();
14746
+ }
14747
+ async function recordCost(r) {
14748
+ try {
14749
+ await ensureCostSchema();
14750
+ const db = await getDb();
14751
+ db.run(
14752
+ `INSERT INTO cost_ledger (tool, action, duration_ms, output_chars, outcome, tokens) VALUES (?, ?, ?, ?, ?, ?)`,
14753
+ [r.tool, r.action || null, Math.round(r.durationMs), r.outputChars, r.outcome, r.tokens ?? null]
14754
+ );
14755
+ saveDb();
14756
+ } catch {
14757
+ }
14758
+ }
14759
+ async function getCostStats(limit = 15) {
14760
+ try {
14761
+ await ensureCostSchema();
14762
+ const db = await getDb();
14763
+ const stmt = db.prepare(`
14764
+ SELECT tool, COUNT(*) as calls,
14765
+ SUM(CASE WHEN outcome = 'error' THEN 1 ELSE 0 END) as errors,
14766
+ AVG(duration_ms) as avgMs, AVG(output_chars) as avgChars,
14767
+ SUM(tokens) as totalTokens
14768
+ FROM cost_ledger GROUP BY tool ORDER BY calls DESC LIMIT ?
14769
+ `);
14770
+ stmt.bind([limit]);
14771
+ const out = [];
14772
+ while (stmt.step()) {
14773
+ const r = stmt.getAsObject();
14774
+ out.push({
14775
+ tool: String(r.tool),
14776
+ calls: Number(r.calls) || 0,
14777
+ errors: Number(r.errors) || 0,
14778
+ avgMs: Math.round(Number(r.avgMs) || 0),
14779
+ avgChars: Math.round(Number(r.avgChars) || 0),
14780
+ totalTokens: r.totalTokens === null ? null : Number(r.totalTokens)
14781
+ });
14782
+ }
14783
+ stmt.free();
14784
+ return out;
14785
+ } catch {
14786
+ return [];
14787
+ }
14788
+ }
14789
+ function formatCostStats(rows) {
14790
+ if (rows.length === 0) return "\u{1F4B0} **Cost ledger** \u2014 no tool calls recorded yet.";
14791
+ const lines = ["\u{1F4B0} **Cost per tool** (duration \xD7 output size \u2192 cost-per-success signal)", ""];
14792
+ for (const r of rows) {
14793
+ const err = r.errors > 0 ? `, \u274C${r.errors}` : "";
14794
+ const tok = r.totalTokens !== null ? `, ${r.totalTokens} tokens` : "";
14795
+ lines.push(` \u2022 \`${r.tool}\` \u2014 ${r.calls} call(s), ~${r.avgMs}ms, ~${r.avgChars} chars${err}${tok}`);
14796
+ }
14797
+ return lines.join("\n");
14798
+ }
14799
+ async function withCost(tool, action, fn) {
14800
+ const start = Date.now();
14801
+ try {
14802
+ const result = await fn();
14803
+ const chars = typeof result === "string" ? result.length : JSON.stringify(result ?? "").length;
14804
+ await recordCost({ tool, action, durationMs: Date.now() - start, outputChars: chars, outcome: "ok" });
14805
+ return { result, outcome: "ok" };
14806
+ } catch (err) {
14807
+ await recordCost({ tool, action, durationMs: Date.now() - start, outputChars: String(err).length, outcome: "error" });
14808
+ throw err;
14809
+ }
14810
+ }
14811
+
13245
14812
  // src/tools/kumaSafetyTool.ts
13246
14813
  async function handleSafety(params) {
13247
14814
  const { action } = params;
@@ -13261,10 +14828,17 @@ async function handleSafety(params) {
13261
14828
  return prefix + await handleCheckpoint(params);
13262
14829
  case "rollback_label":
13263
14830
  return prefix + await handleRollbackLabel(params);
14831
+ case "cost":
14832
+ return prefix + await handleCost();
13264
14833
  default:
13265
- return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label`;
14834
+ return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label, cost`;
13266
14835
  }
13267
14836
  }
14837
+ async function handleCost() {
14838
+ sessionMemory.recordToolCall("kuma_safety_cost", {});
14839
+ const stats = await getCostStats();
14840
+ return formatCostStats(stats);
14841
+ }
13268
14842
  async function handleGuard(params) {
13269
14843
  const base = await handleKumaGuard({
13270
14844
  goal: params.guardGoal,
@@ -13437,12 +15011,12 @@ function registerAllTools(server) {
13437
15011
  const contextHandler = async (params) => {
13438
15012
  try {
13439
15013
  await ensureInitialized();
13440
- const text = await handleContext({
15014
+ const { result: text } = await withCost("kuma_context", params.action, () => handleContext({
13441
15015
  action: params.action,
13442
15016
  scope: params.scope,
13443
15017
  target: params.target,
13444
15018
  goal: params.goal
13445
- });
15019
+ }));
13446
15020
  return { content: [{ type: "text", text }] };
13447
15021
  } catch (err) {
13448
15022
  return { content: [{ type: "text", text: `Error in kuma_context: ${err}` }], isError: true };
@@ -13450,10 +15024,10 @@ function registerAllTools(server) {
13450
15024
  };
13451
15025
  server.tool("kuma_context", contextDesc, contextSchema, contextHandler);
13452
15026
  server.tool("kuma_kuma_context", contextDesc, contextSchema, contextHandler);
13453
- const memoryDesc = "Persistent knowledge. Record what matters, skip what doesn't. This is what saves time in future sessions. Don't record what grep/glob answers faster (functions, imports, types, components)." + CORE_NOTE + "`gotcha` (IMMEDIATELY when you find a bug/quirk), `decision` (when choosing between options), `arch_flow` (after tracing a complete flow, max 5 core files), `research_save` (after exploring an area), `search` (quick lookup of memory + knowledge graph), `gotcha_resolve` (archive fixed bugs).";
15027
+ const memoryDesc = "Persistent knowledge. Record what matters, skip what doesn't. This is what saves time in future sessions. Don't record what grep/glob answers faster (functions, imports, types, components)." + CORE_NOTE + "`gotcha` (IMMEDIATELY when you find a bug/quirk), `decision` (when choosing between options), `arch_flow` (after tracing a complete flow, max 5 core files), `research_save` (after exploring an area), `search` (quick lookup of memory + knowledge graph), `gotcha_resolve` (archive fixed bugs), `test_outcome` (record changed files + failed tests to train test selection).";
13454
15028
  const memorySchema = {
13455
- action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve"]).describe(
13456
- "gotcha=record bug/quirk, decision=ADR, arch_flow=record architecture flow, research_save=save findings, search=quick memory+graph lookup, gotcha_resolve=archive fixed bug"
15029
+ action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve", "test_outcome"]).describe(
15030
+ "gotcha=record bug/quirk, decision=ADR, arch_flow=record architecture flow, research_save=save findings, search=quick memory+graph lookup, gotcha_resolve=archive fixed bug, test_outcome=record test results for predictive selection"
13457
15031
  ),
13458
15032
  scope: z.string().optional().describe("File path for gotcha \u2014 or scope for research_save/search"),
13459
15033
  target: z.string().optional().describe("Target file, gotcha ID, or component"),
@@ -13475,7 +15049,7 @@ function registerAllTools(server) {
13475
15049
  const memoryHandler = async (params) => {
13476
15050
  try {
13477
15051
  await ensureInitialized();
13478
- const text = await handleMemory({
15052
+ const { result: text } = await withCost("kuma_memory", params.action, () => handleMemory({
13479
15053
  action: params.action,
13480
15054
  scope: params.scope,
13481
15055
  target: params.target,
@@ -13493,7 +15067,7 @@ function registerAllTools(server) {
13493
15067
  description: params.description,
13494
15068
  trigger_command: params.trigger_command,
13495
15069
  id: params.id
13496
- });
15070
+ }));
13497
15071
  return { content: [{ type: "text", text }] };
13498
15072
  } catch (err) {
13499
15073
  return { content: [{ type: "text", text: `Error in kuma_memory: ${err}` }], isError: true };
@@ -13501,10 +15075,10 @@ function registerAllTools(server) {
13501
15075
  };
13502
15076
  server.tool("kuma_memory", memoryDesc, memorySchema, memoryHandler);
13503
15077
  server.tool("kuma_kuma_memory", memoryDesc, memorySchema, memoryHandler);
13504
- const safetyDesc = "Safety & verification. Use at task boundaries, not on every edit." + CORE_NOTE + "`guard` (before risky work: anti-patterns, drift, loops), `verify` (after edits: scoped tests + validation), `checkpoint` + `rollback_label` (the one rollback mechanism: snapshot before risky work, restore after).";
15078
+ const safetyDesc = "Safety & verification. Use at task boundaries, not on every edit." + CORE_NOTE + "`guard` (before risky work: anti-patterns, drift, loops), `verify` (after edits: scoped tests + validation), `checkpoint` + `rollback_label` (the one rollback mechanism: snapshot before risky work, restore after), `cost` (cost-per-tool ledger).";
13505
15079
  const safetySchema = {
13506
- action: z.enum(["guard", "verify", "checkpoint", "rollback_label"]).describe(
13507
- "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"
15080
+ action: z.enum(["guard", "verify", "checkpoint", "rollback_label", "cost"]).describe(
15081
+ "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, cost=cost-per-tool ledger"
13508
15082
  ),
13509
15083
  scope: z.string().optional().describe("Scope for verify (e.g. 'auth', file path)"),
13510
15084
  target: z.string().optional().describe("Target test file or scope for verify"),
@@ -13520,7 +15094,7 @@ function registerAllTools(server) {
13520
15094
  const safetyHandler = async (params) => {
13521
15095
  try {
13522
15096
  await ensureInitialized();
13523
- const text = await handleSafety({
15097
+ const { result: text } = await withCost("kuma_safety", params.action, () => handleSafety({
13524
15098
  action: params.action,
13525
15099
  guardGoal: params.guardGoal,
13526
15100
  guardCheck: params.guardCheck,
@@ -13532,7 +15106,7 @@ function registerAllTools(server) {
13532
15106
  force: params.force,
13533
15107
  label: params.label,
13534
15108
  description: params.description
13535
- });
15109
+ }));
13536
15110
  return { content: [{ type: "text", text }] };
13537
15111
  } catch (err) {
13538
15112
  return { content: [{ type: "text", text: `Error in kuma_safety: ${err}` }], isError: true };
@@ -13567,6 +15141,11 @@ Usage:
13567
15141
  npx @plumpslabs/kuma init --help Show this help
13568
15142
  npx @plumpslabs/kuma hook pre-edit Claude Code PreToolUse hook \u2014 inject gotcha/decision/history before edits
13569
15143
  npx @plumpslabs/kuma hook pre-bash Claude Code PreToolUse hook \u2014 inject command-triggered gotchas before Bash
15144
+ npx @plumpslabs/kuma hook session-start SessionStart hook \u2014 cached repo brief before the first turn
15145
+ npx @plumpslabs/kuma hook pre-compact PreCompact hook \u2014 survival set (goal + critical gotchas + dirty)
15146
+ npx @plumpslabs/kuma hook post-commit Git post-commit \u2014 periodic incremental map sync (no rebuild)
15147
+ npx @plumpslabs/kuma hook install-git Install the git post-commit hook (idempotent)
15148
+ npx @plumpslabs/kuma daemon start|stop|status Background sensors (map + briefs + drift) every 5m
13570
15149
 
13571
15150
  Available config files:
13572
15151
  --claude CLAUDE.md (Claude Code)
@@ -13659,14 +15238,14 @@ async function main() {
13659
15238
  console.error("");
13660
15239
  let killedCount = 0;
13661
15240
  try {
13662
- const path30 = await import("path");
13663
- const fs27 = await import("fs");
13664
- const lockDir = path30.resolve(process.cwd(), ".kuma/verifier.lock");
13665
- if (fs27.existsSync(lockDir)) {
13666
- const pidFile = path30.join(lockDir, "pid");
13667
- if (fs27.existsSync(pidFile)) {
15241
+ const path37 = await import("path");
15242
+ const fs34 = await import("fs");
15243
+ const lockDir = path37.resolve(process.cwd(), ".kuma/verifier.lock");
15244
+ if (fs34.existsSync(lockDir)) {
15245
+ const pidFile = path37.join(lockDir, "pid");
15246
+ if (fs34.existsSync(pidFile)) {
13668
15247
  try {
13669
- const pid = parseInt(fs27.readFileSync(pidFile, "utf-8"), 10);
15248
+ const pid = parseInt(fs34.readFileSync(pidFile, "utf-8"), 10);
13670
15249
  if (pid && pid !== process.pid) {
13671
15250
  try {
13672
15251
  process.kill(-pid, "SIGKILL");
@@ -13682,7 +15261,7 @@ async function main() {
13682
15261
  } catch {
13683
15262
  }
13684
15263
  }
13685
- fs27.rmSync(lockDir, { recursive: true, force: true });
15264
+ fs34.rmSync(lockDir, { recursive: true, force: true });
13686
15265
  }
13687
15266
  if (killedCount === 0) {
13688
15267
  console.error("\u2705 No running verifications found to kill.");
@@ -13690,25 +15269,25 @@ async function main() {
13690
15269
  } catch {
13691
15270
  }
13692
15271
  try {
13693
- const { execSync: execSync7 } = await import("child_process");
15272
+ const { execSync: execSync10 } = await import("child_process");
13694
15273
  try {
13695
- execSync7("pkill -f 'jest' 2>/dev/null || true");
15274
+ execSync10("pkill -f 'jest' 2>/dev/null || true");
13696
15275
  } catch {
13697
15276
  }
13698
15277
  try {
13699
- execSync7("pkill -f 'jest-worker' 2>/dev/null || true");
15278
+ execSync10("pkill -f 'jest-worker' 2>/dev/null || true");
13700
15279
  } catch {
13701
15280
  }
13702
15281
  try {
13703
- execSync7("pkill -f 'pnpm test' 2>/dev/null || true");
15282
+ execSync10("pkill -f 'pnpm test' 2>/dev/null || true");
13704
15283
  } catch {
13705
15284
  }
13706
15285
  try {
13707
- execSync7("pkill -f 'npm test' 2>/dev/null || true");
15286
+ execSync10("pkill -f 'npm test' 2>/dev/null || true");
13708
15287
  } catch {
13709
15288
  }
13710
15289
  try {
13711
- execSync7("pkill -f 'yarn test' 2>/dev/null || true");
15290
+ execSync10("pkill -f 'yarn test' 2>/dev/null || true");
13712
15291
  } catch {
13713
15292
  }
13714
15293
  console.error("\u2705 Killed orphaned test processes");
@@ -13737,7 +15316,7 @@ async function main() {
13737
15316
  if (args[0] === "studio") {
13738
15317
  const { fileURLToPath } = await import("url");
13739
15318
  const { dirname, join } = await import("path");
13740
- const { spawn } = await import("child_process");
15319
+ const { spawn: spawn2 } = await import("child_process");
13741
15320
  const { existsSync } = await import("fs");
13742
15321
  const distDir = dirname(fileURLToPath(import.meta.url));
13743
15322
  const studioPath = join(distDir, "..", "packages", "ide", "studio", "dist", "index.js");
@@ -13748,7 +15327,7 @@ async function main() {
13748
15327
  process.exit(1);
13749
15328
  }
13750
15329
  const studioArgs = args.slice(1);
13751
- const child = spawn("node", [studioPath, ...studioArgs], {
15330
+ const child = spawn2("node", [studioPath, ...studioArgs], {
13752
15331
  stdio: "inherit",
13753
15332
  cwd: process.cwd()
13754
15333
  });
@@ -13776,6 +15355,11 @@ async function main() {
13776
15355
  }
13777
15356
  const target = hook.filePaths[0];
13778
15357
  await trackFileEditLoop2(target);
15358
+ try {
15359
+ const { markDirty: markDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
15360
+ markDirty2(target);
15361
+ } catch {
15362
+ }
13779
15363
  try {
13780
15364
  const { maybeAutoCheckpoint: maybeAutoCheckpoint2 } = await Promise.resolve().then(() => (init_kumaCheckpoint(), kumaCheckpoint_exports));
13781
15365
  const autoCp = maybeAutoCheckpoint2(target);
@@ -13830,9 +15414,158 @@ async function main() {
13830
15414
  }
13831
15415
  process.exit(0);
13832
15416
  }
15417
+ if (args[0] === "hook" && args[1] === "pre-compact") {
15418
+ try {
15419
+ const { getCompactionContext: getCompactionContext2 } = await Promise.resolve().then(() => (init_compactionContext(), compactionContext_exports));
15420
+ const { buildHookResponse: buildHookResponse2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
15421
+ const context = await getCompactionContext2();
15422
+ const body = context.trim() ? `\u{1F4E6} [KUMA survival set \u2014 preserve across compaction]
15423
+ ${context}` : "";
15424
+ process.stdout.write(buildHookResponse2(body));
15425
+ } catch {
15426
+ process.stdout.write("{}");
15427
+ }
15428
+ process.exit(0);
15429
+ }
15430
+ if (args[0] === "hook" && args[1] === "post-commit") {
15431
+ try {
15432
+ const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
15433
+ const result = await syncModifiedFiles2(50);
15434
+ try {
15435
+ const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
15436
+ await refreshPackageBriefs2();
15437
+ } catch {
15438
+ }
15439
+ try {
15440
+ const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
15441
+ clearDirty2();
15442
+ } catch {
15443
+ }
15444
+ process.stdout.write(`\u{1F43B} [Kuma] post-commit map sync: ${result.filesScanned} file(s), ${result.nodeCount} nodes, ${result.edgeCount} edges.`);
15445
+ } catch (err) {
15446
+ process.stdout.write(`\u{1F43B} [Kuma] post-commit sync skipped: ${err}`);
15447
+ }
15448
+ process.exit(0);
15449
+ }
15450
+ if (args[0] === "hook" && args[1] === "install-git") {
15451
+ try {
15452
+ const fsMod = await import("fs");
15453
+ const pathMod = await import("path");
15454
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
15455
+ const hookPath = pathMod.join(getProjectRoot2(), ".git", "hooks", "post-commit");
15456
+ const line = "kuma hook post-commit >/dev/null 2>&1 || true";
15457
+ if (fsMod.existsSync(hookPath)) {
15458
+ const existing = fsMod.readFileSync(hookPath, "utf-8");
15459
+ if (existing.includes("kuma hook post-commit")) {
15460
+ process.stdout.write("\u{1F43B} [Kuma] git post-commit hook already installed.");
15461
+ } else {
15462
+ fsMod.appendFileSync(hookPath, `
15463
+ # Kuma periodic mapping
15464
+ ${line}
15465
+ `, "utf-8");
15466
+ process.stdout.write("\u{1F43B} [Kuma] appended to existing git post-commit hook.");
15467
+ }
15468
+ } else {
15469
+ fsMod.writeFileSync(hookPath, `#!/bin/sh
15470
+ # Kuma periodic mapping
15471
+ ${line}
15472
+ `, { mode: 493 });
15473
+ process.stdout.write("\u{1F43B} [Kuma] git post-commit hook installed.");
15474
+ }
15475
+ } catch (err) {
15476
+ process.stdout.write(`\u{1F43B} [Kuma] install-git failed: ${err}`);
15477
+ }
15478
+ process.exit(0);
15479
+ }
15480
+ if (args[0] === "daemon") {
15481
+ const sub = args[1] || "status";
15482
+ try {
15483
+ const { daemonStart: daemonStart2, daemonStop: daemonStop2, daemonStatus: daemonStatus2, daemonRunChild: daemonRunChild2, DEFAULT_INTERVAL_MS: DEFAULT_INTERVAL_MS2 } = await Promise.resolve().then(() => (init_kumaDaemon(), kumaDaemon_exports));
15484
+ if (sub === "start") {
15485
+ const ms = Number(args[2]) > 0 ? Number(args[2]) : DEFAULT_INTERVAL_MS2;
15486
+ process.stdout.write(await daemonStart2(ms));
15487
+ } else if (sub === "stop") {
15488
+ process.stdout.write(await daemonStop2());
15489
+ } else if (sub === "run") {
15490
+ await daemonRunChild2(Number(args[2]));
15491
+ return;
15492
+ } else {
15493
+ process.stdout.write(await daemonStatus2());
15494
+ }
15495
+ } catch (err) {
15496
+ process.stdout.write(`\u{1F43B} Daemon error: ${err}`);
15497
+ }
15498
+ process.exit(0);
15499
+ }
13833
15500
  if (args[0] === "--hook") {
13834
15501
  process.exit(0);
13835
15502
  }
15503
+ if (args[0] === "hook" && args[1] === "session-start") {
15504
+ try {
15505
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
15506
+ const root = getProjectRoot2();
15507
+ const proj = root.split("/").pop() || "unknown";
15508
+ const lines = [`\u{1F43B} [Kuma session brief] ${proj}`];
15509
+ try {
15510
+ const { execSync: execSync10 } = await import("child_process");
15511
+ const branch = execSync10("git rev-parse --abbrev-ref HEAD", {
15512
+ cwd: root,
15513
+ encoding: "utf-8",
15514
+ timeout: 2e3,
15515
+ stdio: ["pipe", "pipe", "pipe"]
15516
+ }).trim();
15517
+ if (branch) lines.push(`git: ${branch}`);
15518
+ } catch {
15519
+ }
15520
+ try {
15521
+ const fsMod = await import("fs");
15522
+ const pathMod = await import("path");
15523
+ const dbFile = pathMod.join(root, ".kuma", "kuma.db");
15524
+ if (fsMod.existsSync(dbFile)) {
15525
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
15526
+ const db = await getDb2();
15527
+ const stmt = db.prepare(
15528
+ `SELECT COUNT(*) as cnt FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0)`
15529
+ );
15530
+ if (stmt.step()) lines.push(`active gotchas: ${stmt.getAsObject().cnt}`);
15531
+ stmt.free();
15532
+ const top = db.prepare(
15533
+ `SELECT file_path, description, severity FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0) ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 ELSE 2 END LIMIT 3`
15534
+ );
15535
+ while (top.step()) {
15536
+ const r = top.getAsObject();
15537
+ lines.push(` [${r.severity}] ${r.file_path} \u2014 ${String(r.description).substring(0, 100)}`);
15538
+ }
15539
+ top.free();
15540
+ } else {
15541
+ const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
15542
+ const gotchas = getActiveGotchas2().filter((g) => g.severity === "critical" || g.severity === "high").slice(0, 3);
15543
+ lines.push(`active gotchas: ${getActiveGotchas2().length} (markdown layer)`);
15544
+ for (const g of gotchas) lines.push(` [${g.severity}] ${g.filePath} \u2014 ${g.description.substring(0, 100)}`);
15545
+ }
15546
+ } catch {
15547
+ }
15548
+ lines.push(`MUST: kuma_context({ action: "init" }) first; record gotchas immediately; verify after edits.`);
15549
+ try {
15550
+ const { readBrief: readBrief2, briefFreshness: briefFreshness2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
15551
+ const { getWorkspaceInfo: getWorkspaceInfo2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
15552
+ const ws = await getWorkspaceInfo2(root);
15553
+ const firstPkg = ws.isWorkspace && ws.packages.length > 0 ? ws.packages[0].name : "root";
15554
+ const fresh = briefFreshness2(firstPkg);
15555
+ if (fresh.exists && fresh.fresh) {
15556
+ const brief = readBrief2(firstPkg, 800);
15557
+ if (brief) lines.push("", "\u{1F4DA} [cached brief \u2014 read, don't re-discover]", brief);
15558
+ } else {
15559
+ lines.push(`\u{1F4DA} No fresh brief (age: ${fresh.age}) \u2014 run kuma_context({ action: "map" }) once to pre-compute.`);
15560
+ }
15561
+ } catch {
15562
+ }
15563
+ process.stdout.write(lines.join("\n"));
15564
+ } catch {
15565
+ process.stdout.write('\u{1F43B} [Kuma session brief] unavailable \u2014 run kuma_context({ action: "init" }).');
15566
+ }
15567
+ process.exit(0);
15568
+ }
13836
15569
  if (args[0] === "init") {
13837
15570
  const flags = args.slice(1);
13838
15571
  if (flags.includes("--help") || flags.includes("-h")) {
@@ -13893,23 +15626,23 @@ async function main() {
13893
15626
  });
13894
15627
  const output = formatInitResults(results);
13895
15628
  console.log(output);
13896
- const fs27 = await import("fs");
13897
- const path30 = await import("path");
13898
- const matchaSkills = path30.resolve(process.cwd(), "skills/matcha/SKILL.md");
13899
- const matchaAgents = path30.resolve(
15629
+ const fs34 = await import("fs");
15630
+ const path37 = await import("path");
15631
+ const matchaSkills = path37.resolve(process.cwd(), "skills/matcha/SKILL.md");
15632
+ const matchaAgents = path37.resolve(
13900
15633
  process.cwd(),
13901
15634
  ".agents/skills/matcha/SKILL.md"
13902
15635
  );
13903
- const matchaRootSkills = path30.resolve(
15636
+ const matchaRootSkills = path37.resolve(
13904
15637
  process.cwd(),
13905
15638
  "skills/matcha/SKILL.md"
13906
15639
  );
13907
- const matchaAgentsMd = path30.resolve(process.cwd(), "AGENTS.md");
13908
- const matchaWindsurfRules = path30.resolve(
15640
+ const matchaAgentsMd = path37.resolve(process.cwd(), "AGENTS.md");
15641
+ const matchaWindsurfRules = path37.resolve(
13909
15642
  process.cwd(),
13910
15643
  ".windsurf"
13911
15644
  );
13912
- if (fs27.existsSync(matchaSkills) || fs27.existsSync(matchaAgents) || fs27.existsSync(matchaRootSkills) || fs27.existsSync(matchaAgentsMd) || fs27.existsSync(matchaWindsurfRules)) {
15645
+ if (fs34.existsSync(matchaSkills) || fs34.existsSync(matchaAgents) || fs34.existsSync(matchaRootSkills) || fs34.existsSync(matchaAgentsMd) || fs34.existsSync(matchaWindsurfRules)) {
13913
15646
  console.error(
13914
15647
  "\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
13915
15648
  );
@@ -13923,13 +15656,13 @@ async function main() {
13923
15656
  (async () => {
13924
15657
  try {
13925
15658
  const { generateInitMdContent: generateInitMdContent2 } = await Promise.resolve().then(() => (init_init(), init_exports));
13926
- const fs27 = await import("fs");
13927
- const path30 = await import("path");
13928
- const initMdPath = path30.resolve(process.cwd(), ".kuma/init.md");
13929
- if (!fs27.existsSync(initMdPath)) {
13930
- const kumaDir = path30.dirname(initMdPath);
13931
- if (!fs27.existsSync(kumaDir)) fs27.mkdirSync(kumaDir, { recursive: true });
13932
- fs27.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
15659
+ const fs34 = await import("fs");
15660
+ const path37 = await import("path");
15661
+ const initMdPath = path37.resolve(process.cwd(), ".kuma/init.md");
15662
+ if (!fs34.existsSync(initMdPath)) {
15663
+ const kumaDir = path37.dirname(initMdPath);
15664
+ if (!fs34.existsSync(kumaDir)) fs34.mkdirSync(kumaDir, { recursive: true });
15665
+ fs34.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
13933
15666
  console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
13934
15667
  }
13935
15668
  } catch (err) {
@@ -13940,8 +15673,8 @@ async function main() {
13940
15673
  try {
13941
15674
  const { detectAgent: detectAgent2, getSkillPath: getSkillPath2, getAgentLabel: getAgentLabel2 } = await Promise.resolve().then(() => (init_agentDetector(), agentDetector_exports));
13942
15675
  const { generateSkill: generateSkill2, getSecondaryFiles: getSecondaryFiles2 } = await Promise.resolve().then(() => (init_skillGenerator(), skillGenerator_exports));
13943
- const fs27 = await import("fs");
13944
- const path30 = await import("path");
15676
+ const fs34 = await import("fs");
15677
+ const path37 = await import("path");
13945
15678
  const detection = detectAgent2();
13946
15679
  if (!detection.primary) {
13947
15680
  console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
@@ -13949,22 +15682,22 @@ async function main() {
13949
15682
  }
13950
15683
  const agentType = detection.primary;
13951
15684
  const skillPath = getSkillPath2(agentType);
13952
- const fullPath = path30.resolve(process.cwd(), skillPath);
13953
- if (fs27.existsSync(fullPath)) {
15685
+ const fullPath = path37.resolve(process.cwd(), skillPath);
15686
+ if (fs34.existsSync(fullPath)) {
13954
15687
  console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel2(agentType)} \u2014 skipping`);
13955
15688
  return;
13956
15689
  }
13957
- const dir = path30.dirname(fullPath);
13958
- if (!fs27.existsSync(dir)) fs27.mkdirSync(dir, { recursive: true });
13959
- fs27.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
15690
+ const dir = path37.dirname(fullPath);
15691
+ if (!fs34.existsSync(dir)) fs34.mkdirSync(dir, { recursive: true });
15692
+ fs34.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
13960
15693
  console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel2(agentType)}`);
13961
15694
  const secondaryFiles = getSecondaryFiles2(agentType);
13962
15695
  for (const sf of secondaryFiles) {
13963
- const sfPath = path30.resolve(process.cwd(), sf.path);
13964
- const sfDir = path30.dirname(sfPath);
13965
- if (!fs27.existsSync(sfPath)) {
13966
- if (!fs27.existsSync(sfDir)) fs27.mkdirSync(sfDir, { recursive: true });
13967
- fs27.writeFileSync(sfPath, sf.content, "utf-8");
15696
+ const sfPath = path37.resolve(process.cwd(), sf.path);
15697
+ const sfDir = path37.dirname(sfPath);
15698
+ if (!fs34.existsSync(sfPath)) {
15699
+ if (!fs34.existsSync(sfDir)) fs34.mkdirSync(sfDir, { recursive: true });
15700
+ fs34.writeFileSync(sfPath, sf.content, "utf-8");
13968
15701
  console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
13969
15702
  }
13970
15703
  }