@plumpslabs/kuma 2.4.12 → 2.4.14

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: execSync9 } = await import("child_process");
1224
+ const psOutput = execSync9(
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();
@@ -1735,6 +1752,19 @@ No unresolved issues.`;
1735
1752
  this.autoTrackToDb(toolName, params, timestamp).catch(() => {
1736
1753
  });
1737
1754
  }
1755
+ /**
1756
+ * Issue #32 P1 — obedience enforcement helper (non-blocking).
1757
+ * Returns true when a tool call with the given prefix was recorded
1758
+ * in this session (checks in-memory toolCalls, last 100).
1759
+ */
1760
+ hasToolCall(prefix) {
1761
+ try {
1762
+ this.ensureInit();
1763
+ return this.state.toolCalls.some((t) => t.toolName.startsWith(prefix));
1764
+ } catch {
1765
+ return false;
1766
+ }
1767
+ }
1738
1768
  /**
1739
1769
  * Auto-track tool calls to the database sessions + tool_calls + experiences tables.
1740
1770
  * This is the passive observability pipeline — every tool call gets recorded
@@ -2121,6 +2151,23 @@ No unresolved issues.`;
2121
2151
  this.state.metrics.filesEdited++;
2122
2152
  this.save();
2123
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
+ }
2124
2171
  /**
2125
2172
  * Get metrics summary for session.
2126
2173
  */
@@ -4530,14 +4577,163 @@ var init_workspaceIntelligence = __esm({
4530
4577
  }
4531
4578
  });
4532
4579
 
4580
+ // src/engine/testHistory.ts
4581
+ var testHistory_exports = {};
4582
+ __export(testHistory_exports, {
4583
+ fileSig: () => fileSig,
4584
+ markTestFlaky: () => markTestFlaky,
4585
+ rankTestsByHistory: () => rankTestsByHistory,
4586
+ recordTestOutcome: () => recordTestOutcome
4587
+ });
4588
+ async function ensureTestHistorySchema() {
4589
+ const db = await getDb();
4590
+ db.exec(`
4591
+ CREATE TABLE IF NOT EXISTS test_history (
4592
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
4593
+ file_sig TEXT NOT NULL,
4594
+ test_path TEXT NOT NULL,
4595
+ failed INTEGER NOT NULL DEFAULT 0,
4596
+ flaky INTEGER NOT NULL DEFAULT 0,
4597
+ runs INTEGER NOT NULL DEFAULT 1,
4598
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
4599
+ );
4600
+ CREATE INDEX IF NOT EXISTS idx_th_sig ON test_history(file_sig);
4601
+ CREATE INDEX IF NOT EXISTS idx_th_test ON test_history(test_path);
4602
+ `);
4603
+ saveDb();
4604
+ }
4605
+ function fileSig(filePath) {
4606
+ const parts = filePath.replace(/\\/g, "/").split("/");
4607
+ const base = (parts.pop() || "").split(".")[0];
4608
+ const dir = parts.slice(-2).join("/");
4609
+ return `${dir}/${base}`.toLowerCase();
4610
+ }
4611
+ async function recordTestOutcome(changedFiles, failedTests) {
4612
+ try {
4613
+ await ensureTestHistorySchema();
4614
+ const db = await getDb();
4615
+ let recorded = 0;
4616
+ for (const f of changedFiles.slice(0, 10)) {
4617
+ const sig = fileSig(f);
4618
+ for (const t of failedTests.slice(0, 10)) {
4619
+ const find = db.prepare(`SELECT id, failed, flaky, runs FROM test_history WHERE file_sig = ? AND test_path = ?`);
4620
+ find.bind([sig, t]);
4621
+ if (find.step()) {
4622
+ const row = find.getAsObject();
4623
+ db.run(`UPDATE test_history SET failed = 1, runs = runs + 1, updated_at = strftime('%s','now') WHERE id = ?`, [row.id]);
4624
+ find.free();
4625
+ } else {
4626
+ find.free();
4627
+ db.run(`INSERT INTO test_history (file_sig, test_path, failed) VALUES (?, ?, 1)`, [sig, t]);
4628
+ }
4629
+ recorded++;
4630
+ }
4631
+ if (failedTests.length === 0) {
4632
+ db.run(
4633
+ `INSERT INTO test_history (file_sig, test_path, failed) VALUES (?, ?, 0)
4634
+ ON CONFLICT DO NOTHING`,
4635
+ [sig, "__green__"]
4636
+ );
4637
+ recorded++;
4638
+ }
4639
+ }
4640
+ void recorded;
4641
+ saveDb();
4642
+ return { recorded };
4643
+ } catch {
4644
+ return { recorded: 0 };
4645
+ }
4646
+ }
4647
+ async function markTestFlaky(changedFiles, testPath) {
4648
+ try {
4649
+ await ensureTestHistorySchema();
4650
+ const db = await getDb();
4651
+ for (const f of changedFiles.slice(0, 10)) {
4652
+ db.run(`UPDATE test_history SET flaky = 1 WHERE file_sig = ? AND test_path = ?`, [fileSig(f), testPath]);
4653
+ }
4654
+ saveDb();
4655
+ } catch {
4656
+ }
4657
+ }
4658
+ async function rankTestsByHistory(changedFile, candidates) {
4659
+ let history = [];
4660
+ try {
4661
+ await ensureTestHistorySchema();
4662
+ const db = await getDb();
4663
+ const stmt = db.prepare(`SELECT test_path, failed, flaky, runs FROM test_history WHERE file_sig = ?`);
4664
+ stmt.bind([fileSig(changedFile)]);
4665
+ while (stmt.step()) history.push(stmt.getAsObject());
4666
+ stmt.free();
4667
+ } catch {
4668
+ }
4669
+ const byPath = new Map(history.map((h) => [h.test_path, h]));
4670
+ return candidates.map((c, i) => {
4671
+ const h = byPath.get(c);
4672
+ if (h && h.failed === 1 && h.flaky === 0) {
4673
+ return { path: c, score: 100 - i * 0.01 + Math.min(h.runs, 10), reason: `\u{1F525} failed for this area before (${h.runs}x)` };
4674
+ }
4675
+ if (h && h.flaky === 1) {
4676
+ return { path: c, score: -1, reason: "\u26AA flaky \u2014 down-weighted, run last" };
4677
+ }
4678
+ return { path: c, score: 10 - i * 0.01, reason: "static match" };
4679
+ }).sort((a, b) => b.score - a.score);
4680
+ }
4681
+ var init_testHistory = __esm({
4682
+ "src/engine/testHistory.ts"() {
4683
+ "use strict";
4684
+ init_kumaDb();
4685
+ }
4686
+ });
4687
+
4533
4688
  // src/engine/impactAnalysis.ts
4534
4689
  var impactAnalysis_exports = {};
4535
4690
  __export(impactAnalysis_exports, {
4536
- calculateBlastRadius: () => calculateBlastRadius
4691
+ calculateBlastRadius: () => calculateBlastRadius,
4692
+ classifyDiffShape: () => classifyDiffShape,
4693
+ getIndexerStatus: () => getIndexerStatus
4537
4694
  });
4538
4695
  import fs9 from "fs";
4539
4696
  import path11 from "path";
4697
+ import { execSync as execSync4 } from "child_process";
4540
4698
  import fastGlob3 from "fast-glob";
4699
+ function extOf(target) {
4700
+ const base = path11.basename(target);
4701
+ const dot = base.lastIndexOf(".");
4702
+ return dot > 0 ? base.slice(dot).toLowerCase() : "";
4703
+ }
4704
+ function isExampleOrDocName(target) {
4705
+ const base = path11.basename(target).toLowerCase();
4706
+ return base.endsWith(".example") || base.startsWith(".env") || base.endsWith(".md") || base.endsWith(".txt") || base.endsWith(".rst") || base === "license" || base.startsWith("license.");
4707
+ }
4708
+ function classifyDiffShape(target, root) {
4709
+ if (isExampleOrDocName(target)) {
4710
+ return { shape: "config-or-docs", detail: "non-runtime file (docs/config/example)" };
4711
+ }
4712
+ const ext = extOf(target);
4713
+ if (ext && !RUNTIME_CODE_EXTS.has(ext)) {
4714
+ return { shape: "config-or-docs", detail: `non-runtime extension (${ext})` };
4715
+ }
4716
+ try {
4717
+ const cwd = root || getProjectRoot();
4718
+ const diff = execSync4(`git diff --unified=0 -- "${target}"`, {
4719
+ cwd,
4720
+ encoding: "utf-8",
4721
+ timeout: 4e3,
4722
+ stdio: ["pipe", "pipe", "pipe"]
4723
+ }).trim();
4724
+ if (!diff) return { shape: "runtime-code", detail: "no working-tree diff; analyzed by file kind" };
4725
+ const prefixes = COMMENT_PREFIXES[ext] || ["//", "#", "*", "/*", "--"];
4726
+ const changed = diff.split("\n").filter((l) => (l.startsWith("+") || l.startsWith("-")) && !l.startsWith("+++") && !l.startsWith("---"));
4727
+ if (changed.length === 0) return { shape: "runtime-code", detail: "no working-tree diff; analyzed by file kind" };
4728
+ const allTrivial = changed.every((l) => {
4729
+ const body = l.slice(1).trim();
4730
+ return body === "" || prefixes.some((p) => body.startsWith(p));
4731
+ });
4732
+ if (allTrivial) return { shape: "comment-only", detail: `${changed.length} changed line(s), all comments/blank` };
4733
+ } catch {
4734
+ }
4735
+ return { shape: "runtime-code", detail: "runtime code change" };
4736
+ }
4541
4737
  async function findImportingFiles(targetFile, root) {
4542
4738
  const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
4543
4739
  try {
@@ -4574,7 +4770,7 @@ async function findImportingFiles(targetFile, root) {
4574
4770
  }
4575
4771
  callStmt.free();
4576
4772
  if (dependents.length > 0) {
4577
- return dependents;
4773
+ return { dependents, evidence: dependents.map((d) => `${d} (graph edge)`) };
4578
4774
  }
4579
4775
  } catch {
4580
4776
  }
@@ -4590,6 +4786,7 @@ async function findImportingFiles(targetFile, root) {
4590
4786
  }
4591
4787
  );
4592
4788
  const dependents = [];
4789
+ const evidence = [];
4593
4790
  const importRegex = new RegExp(
4594
4791
  `(?:import|from|require\\s*\\(|export\\s+.*from)\\s*['"][^'"]*\\b${baseName}(?:\\.[a-zA-Z0-9]+)?['"]`,
4595
4792
  "i"
@@ -4599,18 +4796,21 @@ async function findImportingFiles(targetFile, root) {
4599
4796
  const fullPath = path11.join(root, f);
4600
4797
  try {
4601
4798
  const content = fs9.readFileSync(fullPath, "utf-8");
4602
- if (importRegex.test(content)) {
4799
+ const lines = content.split("\n");
4800
+ const hitIdx = lines.findIndex((l) => importRegex.test(l));
4801
+ if (hitIdx >= 0) {
4603
4802
  dependents.push(f);
4803
+ evidence.push(`${f}:${hitIdx + 1} (import match)`);
4604
4804
  }
4605
4805
  } catch {
4606
4806
  }
4607
4807
  }
4608
- return dependents;
4808
+ return { dependents, evidence };
4609
4809
  } catch {
4610
- return [];
4810
+ return { dependents: [], evidence: [] };
4611
4811
  }
4612
4812
  }
4613
- async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = []) {
4813
+ async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = [], exactOnly = false) {
4614
4814
  const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
4615
4815
  try {
4616
4816
  const db = await getDb();
@@ -4639,14 +4839,10 @@ async function findRelatedTests(targetFile, root, owningPackage, additionalDepen
4639
4839
  const testFiles = [];
4640
4840
  const stems = /* @__PURE__ */ new Set();
4641
4841
  if (baseName) stems.add(baseName);
4642
- for (const part of baseName.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
4643
- stems.add(part);
4644
- }
4645
- for (const dep of additionalDependents.slice(0, 3)) {
4646
- const depBase = path11.basename(dep, path11.extname(dep));
4647
- if (depBase) stems.add(depBase);
4648
- for (const part of depBase.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
4649
- stems.add(part);
4842
+ if (!exactOnly) {
4843
+ for (const dep of additionalDependents.slice(0, 3)) {
4844
+ const depBase = path11.basename(dep, path11.extname(dep));
4845
+ if (depBase) stems.add(depBase);
4650
4846
  }
4651
4847
  }
4652
4848
  const testPatterns = [];
@@ -4735,6 +4931,22 @@ function assessRisk(target, targetType, directDependents, downstreamPackages, af
4735
4931
  }
4736
4932
  return { risk, riskFlags };
4737
4933
  }
4934
+ async function getIndexerStatus() {
4935
+ let graphEdges = false;
4936
+ try {
4937
+ const db = await getDb();
4938
+ const stmt = db.prepare(`SELECT COUNT(*) as cnt FROM edges WHERE type IN ('imports','depends_on','calls','tests')`);
4939
+ if (stmt.step()) graphEdges = Number(stmt.getAsObject().cnt) > 0;
4940
+ stmt.free();
4941
+ } catch {
4942
+ }
4943
+ return {
4944
+ tier: graphEdges ? 1 : 2,
4945
+ graphEdges,
4946
+ languages: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "rs"],
4947
+ 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)"
4948
+ };
4949
+ }
4738
4950
  async function calculateBlastRadius(target, options = {}) {
4739
4951
  const root = options.root || getProjectRoot();
4740
4952
  const wsInfo = await getWorkspaceInfo(root);
@@ -4759,66 +4971,51 @@ async function calculateBlastRadius(target, options = {}) {
4759
4971
  downstreamPackages.push(...rev.map((p) => p.name));
4760
4972
  }
4761
4973
  let directDependents = [];
4974
+ let evidence = [];
4762
4975
  if (targetType === "file" || targetType === "symbol") {
4763
- directDependents = await findImportingFiles(target, root);
4764
- }
4765
- const normLower = target.toLowerCase();
4766
- 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);
4767
- const isSecurityTopic = /(auth|security|session|permission|jwt|token|middleware)/i.test(target);
4768
- if (isUserDestructive || isSecurityTopic || directDependents.length === 0) {
4769
- try {
4770
- const db = await getDb();
4771
- const tokens = normLower.split(/[\s_-]+/).filter((t) => t.length >= 3);
4772
- for (const token of tokens) {
4773
- const stmt = db.prepare(`SELECT DISTINCT file_path, name FROM nodes WHERE (name LIKE ? OR file_path LIKE ?) LIMIT 5`);
4774
- stmt.bind([`%${token}%`, `%${token}%`]);
4775
- while (stmt.step()) {
4776
- const row = stmt.getAsObject();
4777
- const fp = row.file_path || row.name;
4778
- if (fp && !directDependents.includes(fp) && fp !== target) {
4779
- directDependents.push(fp);
4780
- }
4781
- }
4782
- stmt.free();
4783
- }
4784
- const authMiddlewareMatches = await fastGlob3(
4785
- [
4786
- "**/*auth-middleware*.*",
4787
- "**/*auth_middleware*.*",
4788
- "**/*auth.middleware*.*",
4789
- "**/middleware/*auth*.*",
4790
- "**/middlewares/*auth*.*",
4791
- "**/guards/*auth*.*",
4792
- "**/security/*auth*.*",
4793
- "**/lib/*auth*.*"
4794
- ],
4795
- {
4796
- cwd: root,
4797
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kuma/**"],
4798
- onlyFiles: true
4799
- }
4800
- );
4801
- for (const am of authMiddlewareMatches) {
4802
- if (!directDependents.includes(am) && am !== target) {
4803
- directDependents.push(am);
4804
- }
4805
- }
4806
- } catch {
4976
+ const found = await findImportingFiles(target, root);
4977
+ directDependents = found.dependents;
4978
+ evidence = found.evidence;
4979
+ }
4980
+ const { shape: diffShape, detail: shapeDetail } = classifyDiffShape(target, root);
4981
+ const nonRuntime = diffShape !== "runtime-code";
4982
+ const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents, nonRuntime);
4983
+ let testReasons = /* @__PURE__ */ new Map();
4984
+ try {
4985
+ const { rankTestsByHistory: rankTestsByHistory2 } = await Promise.resolve().then(() => (init_testHistory(), testHistory_exports));
4986
+ const ranked = await rankTestsByHistory2(target, affectedTests);
4987
+ affectedTests.length = 0;
4988
+ for (const r of ranked) {
4989
+ affectedTests.push(r.path);
4990
+ if (r.reason !== "static match") testReasons.set(r.path, r.reason);
4807
4991
  }
4992
+ } catch {
4993
+ }
4994
+ let risk;
4995
+ let riskFlags;
4996
+ if (nonRuntime) {
4997
+ risk = "low";
4998
+ riskFlags = [
4999
+ `${diffShape === "comment-only" ? "Comment-only change" : "Non-runtime file"} (${shapeDetail}) \u2014 risk capped at LOW`,
5000
+ ...directDependents.length > 0 || affectedTests.length > 0 ? [`Grounded refs kept for reference: ${directDependents.length} importer(s), ${affectedTests.length} test(s)`] : []
5001
+ ];
5002
+ directDependents = [];
5003
+ affectedTests.length = 0;
5004
+ } else {
5005
+ ({ risk, riskFlags } = assessRisk(
5006
+ target,
5007
+ targetType,
5008
+ directDependents,
5009
+ downstreamPackages,
5010
+ affectedTests,
5011
+ owningPackage
5012
+ ));
4808
5013
  }
4809
- const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents);
4810
- const { risk, riskFlags } = assessRisk(
4811
- target,
4812
- targetType,
4813
- directDependents,
4814
- downstreamPackages,
4815
- affectedTests,
4816
- owningPackage
4817
- );
4818
5014
  const summaryLines = [
4819
5015
  `\u{1F3AF} **Impact / Blast Radius: \`${target}\`**`,
4820
5016
  `\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`,
4821
5017
  `\u26A0\uFE0F **Risk Level: ${risk.toUpperCase()}**`,
5018
+ `\u{1F4D0} Diff shape: \`${diffShape}\` (${shapeDetail})`,
4822
5019
  owningPackage ? `\u{1F4E6} Owning package: \`${owningPackage.name}\` (${owningPackage.path})` : ""
4823
5020
  ];
4824
5021
  if (riskFlags.length > 0) {
@@ -4845,11 +5042,22 @@ async function calculateBlastRadius(target, options = {}) {
4845
5042
  summaryLines.push(` ... and ${directDependents.length - 6} more`);
4846
5043
  }
4847
5044
  }
5045
+ if (evidence.length > 0) {
5046
+ summaryLines.push("");
5047
+ summaryLines.push(`\u{1F50E} **Evidence (${evidence.length}):**`);
5048
+ for (const e of evidence.slice(0, 3)) {
5049
+ summaryLines.push(` \u2022 \`${e}\``);
5050
+ }
5051
+ if (evidence.length > 3) {
5052
+ summaryLines.push(` ... and ${evidence.length - 3} more`);
5053
+ }
5054
+ }
4848
5055
  if (affectedTests.length > 0) {
4849
5056
  summaryLines.push("");
4850
5057
  summaryLines.push(`\u{1F9EA} **Affected Test Suites (${affectedTests.length}):**`);
4851
5058
  for (const t of affectedTests.slice(0, 5)) {
4852
- summaryLines.push(` \u2022 \`${t}\``);
5059
+ const why = testReasons.get(t) ? ` \u2014 ${testReasons.get(t)}` : "";
5060
+ summaryLines.push(` \u2022 \`${t}\`${why}`);
4853
5061
  }
4854
5062
  if (affectedTests.length > 5) {
4855
5063
  summaryLines.push(` ... and ${affectedTests.length - 5} more`);
@@ -4868,15 +5076,52 @@ async function calculateBlastRadius(target, options = {}) {
4868
5076
  risk,
4869
5077
  riskFlags,
4870
5078
  summary: summaryLines.filter(Boolean).join("\n"),
4871
- confidence: 0.85
5079
+ confidence: nonRuntime ? 0.95 : 0.85,
5080
+ diffShape,
5081
+ evidence
4872
5082
  };
4873
5083
  }
5084
+ var RUNTIME_CODE_EXTS, COMMENT_PREFIXES;
4874
5085
  var init_impactAnalysis = __esm({
4875
5086
  "src/engine/impactAnalysis.ts"() {
4876
5087
  "use strict";
4877
5088
  init_pathValidator();
4878
5089
  init_kumaDb();
4879
5090
  init_workspaceIntelligence();
5091
+ RUNTIME_CODE_EXTS = /* @__PURE__ */ new Set([
5092
+ ".ts",
5093
+ ".tsx",
5094
+ ".js",
5095
+ ".jsx",
5096
+ ".mjs",
5097
+ ".cjs",
5098
+ ".py",
5099
+ ".go",
5100
+ ".rs",
5101
+ ".java",
5102
+ ".rb",
5103
+ ".php",
5104
+ ".swift",
5105
+ ".kt",
5106
+ ".cs",
5107
+ ".scala",
5108
+ ".vue",
5109
+ ".svelte"
5110
+ ]);
5111
+ COMMENT_PREFIXES = {
5112
+ ".ts": ["//", "*", "/*"],
5113
+ ".tsx": ["//", "*", "/*"],
5114
+ ".js": ["//", "*", "/*"],
5115
+ ".jsx": ["//", "*", "/*"],
5116
+ ".mjs": ["//", "*", "/*"],
5117
+ ".cjs": ["//", "*", "/*"],
5118
+ ".py": ["#"],
5119
+ ".go": ["//"],
5120
+ ".rs": ["//"],
5121
+ ".java": ["//", "*", "/*"],
5122
+ ".rb": ["#"],
5123
+ ".php": ["//", "#", "*"]
5124
+ };
4880
5125
  }
4881
5126
  });
4882
5127
 
@@ -5950,27 +6195,27 @@ async function codebaseSearchFallback(query, limit = 20) {
5950
6195
  if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
5951
6196
  let entries = [];
5952
6197
  try {
5953
- entries = fs27.readdirSync(dir);
6198
+ entries = fs32.readdirSync(dir);
5954
6199
  } catch {
5955
6200
  return;
5956
6201
  }
5957
6202
  for (const entry of entries) {
5958
6203
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
5959
- const full = path30.join(dir, entry);
6204
+ const full = path35.join(dir, entry);
5960
6205
  try {
5961
- const stat = fs27.statSync(full);
6206
+ const stat = fs32.statSync(full);
5962
6207
  if (stat.isDirectory()) {
5963
6208
  walk2(full, depth + 1);
5964
6209
  } else if (entry.toLowerCase().includes(query.toLowerCase())) {
5965
- results.push(path30.relative(root, full));
6210
+ results.push(path35.relative(root, full));
5966
6211
  }
5967
6212
  } catch {
5968
6213
  }
5969
6214
  }
5970
6215
  };
5971
6216
  var walk = walk2;
5972
- const fs27 = await import("fs");
5973
- const path30 = await import("path");
6217
+ const fs32 = await import("fs");
6218
+ const path35 = await import("path");
5974
6219
  const root = process.cwd();
5975
6220
  const results = [];
5976
6221
  const MAX_WALK_DEPTH = 8;
@@ -6103,9 +6348,9 @@ ${summary}`;
6103
6348
  }
6104
6349
  async function codebaseImpactFallback(target) {
6105
6350
  try {
6106
- const { execSync: execSync7 } = await import("child_process");
6351
+ const { execSync: execSync9 } = await import("child_process");
6107
6352
  const root = process.cwd();
6108
- const grepResult = execSync7(
6353
+ const grepResult = execSync9(
6109
6354
  `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`,
6110
6355
  { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
6111
6356
  ).trim();
@@ -6232,7 +6477,28 @@ var init_kumaGraph = __esm({
6232
6477
  });
6233
6478
 
6234
6479
  // src/utils/kumaShared.ts
6235
- import { execSync as execSync4 } from "child_process";
6480
+ var kumaShared_exports = {};
6481
+ __export(kumaShared_exports, {
6482
+ buildDriftMessages: () => buildDriftMessages,
6483
+ checkLadderViolations: () => checkLadderViolations,
6484
+ countEditCalls: () => countEditCalls,
6485
+ detectTestWeakening: () => detectTestWeakening,
6486
+ getBranchCommittedFiles: () => getBranchCommittedFiles,
6487
+ getGitDiffStat: () => getGitDiffStat,
6488
+ getPrioritySuggestion: () => getPrioritySuggestion,
6489
+ getSessionStats: () => getSessionStats,
6490
+ getUncommittedFiles: () => getUncommittedFiles,
6491
+ getUnresolvedCount: () => getUnresolvedCount,
6492
+ getUnresolvedDetails: () => getUnresolvedDetails,
6493
+ hasVerificationEvidence: () => hasVerificationEvidence,
6494
+ isBashTool: () => isBashTool,
6495
+ isEditTool: () => isEditTool,
6496
+ isReadTool: () => isReadTool,
6497
+ isTestTool: () => isTestTool
6498
+ });
6499
+ import { execSync as execSync5 } from "child_process";
6500
+ import fs10 from "fs";
6501
+ import path12 from "path";
6236
6502
  function isEditTool(toolName) {
6237
6503
  const n = (toolName || "").toLowerCase();
6238
6504
  return n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply") || n.includes("replace");
@@ -6270,7 +6536,7 @@ function getSessionStats(inputGoal) {
6270
6536
  function getGitDiffStat(timeout = 3e3) {
6271
6537
  try {
6272
6538
  const root = getProjectRoot();
6273
- return execSync4("git diff --stat", {
6539
+ return execSync5("git diff --stat", {
6274
6540
  cwd: root,
6275
6541
  encoding: "utf-8",
6276
6542
  timeout
@@ -6279,6 +6545,83 @@ function getGitDiffStat(timeout = 3e3) {
6279
6545
  return "";
6280
6546
  }
6281
6547
  }
6548
+ function getUncommittedFiles(timeout = 3e3) {
6549
+ const files = /* @__PURE__ */ new Set();
6550
+ try {
6551
+ const root = getProjectRoot();
6552
+ const porcelain = execSync5("git status --porcelain", {
6553
+ cwd: root,
6554
+ encoding: "utf-8",
6555
+ timeout,
6556
+ stdio: ["pipe", "pipe", "pipe"]
6557
+ }).trim();
6558
+ for (const line of porcelain.split("\n")) {
6559
+ const m = line.match(/^.{3}(.+?)(?:\s+->\s+.+)?$/);
6560
+ if (m && m[1]) files.add(m[1].trim().replace(/^"|"$/g, ""));
6561
+ }
6562
+ } catch {
6563
+ }
6564
+ return files;
6565
+ }
6566
+ function getBranchCommittedFiles(timeout = 5e3) {
6567
+ const files = /* @__PURE__ */ new Set();
6568
+ let base = "";
6569
+ try {
6570
+ const root = getProjectRoot();
6571
+ const branch = execSync5("git rev-parse --abbrev-ref HEAD", {
6572
+ cwd: root,
6573
+ encoding: "utf-8",
6574
+ timeout,
6575
+ stdio: ["pipe", "pipe", "pipe"]
6576
+ }).trim();
6577
+ const baseRef = execSync5(
6578
+ `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo origin/${branch === "main" || branch === "master" ? "HEAD" : "main"}`,
6579
+ { cwd: root, encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
6580
+ ).trim().replace("refs/remotes/", "");
6581
+ const mergeBase = execSync5(`git merge-base HEAD ${baseRef} 2>/dev/null || git rev-list --max-parents=0 HEAD`, {
6582
+ cwd: root,
6583
+ encoding: "utf-8",
6584
+ timeout,
6585
+ stdio: ["pipe", "pipe", "pipe"]
6586
+ }).trim().split("\n")[0];
6587
+ base = baseRef;
6588
+ if (mergeBase) {
6589
+ const out = execSync5(`git diff --name-only ${mergeBase} HEAD`, {
6590
+ cwd: root,
6591
+ encoding: "utf-8",
6592
+ timeout,
6593
+ stdio: ["pipe", "pipe", "pipe"]
6594
+ }).trim();
6595
+ for (const f of out.split("\n")) if (f.trim()) files.add(f.trim());
6596
+ }
6597
+ } catch {
6598
+ }
6599
+ return { base, files };
6600
+ }
6601
+ function hasVerificationEvidence(toolCalls) {
6602
+ if (toolCalls.some((c) => isTestTool(c.toolName))) return true;
6603
+ return hasVerificationInHookLog();
6604
+ }
6605
+ function hasVerificationInHookLog(hours = 24) {
6606
+ try {
6607
+ const fp = path12.join(getProjectRoot(), ".kuma", "injections.jsonl");
6608
+ if (!fs10.existsSync(fp)) return false;
6609
+ const cutoff = Date.now() - hours * 60 * 60 * 1e3;
6610
+ const lines = fs10.readFileSync(fp, "utf-8").split("\n");
6611
+ for (const line of lines) {
6612
+ if (!line.trim()) continue;
6613
+ try {
6614
+ const entry = JSON.parse(line);
6615
+ if ((entry.at || 0) < cutoff) continue;
6616
+ const cmd = String(entry.command || "");
6617
+ if (cmd && TEST_COMMAND_PATTERN.test(cmd)) return true;
6618
+ } catch {
6619
+ }
6620
+ }
6621
+ } catch {
6622
+ }
6623
+ return false;
6624
+ }
6282
6625
  function getUnresolvedCount(failedFiles) {
6283
6626
  let count = 0;
6284
6627
  for (const f of failedFiles) {
@@ -6288,6 +6631,28 @@ function getUnresolvedCount(failedFiles) {
6288
6631
  }
6289
6632
  return count;
6290
6633
  }
6634
+ function getUnresolvedDetails(failedFiles) {
6635
+ const result = [];
6636
+ for (const f of failedFiles) {
6637
+ for (const ff of f.failures) {
6638
+ if (!ff.resolved) {
6639
+ result.push({ task: f.task, error: ff.error.substring(0, 200) });
6640
+ }
6641
+ }
6642
+ }
6643
+ return result;
6644
+ }
6645
+ function checkLadderViolations(toolCalls, modifiedFiles, hasRunTests) {
6646
+ const violations = [];
6647
+ const editCalls = toolCalls.filter((c) => isEditTool(c.toolName)).length;
6648
+ if (editCalls > 5) {
6649
+ violations.push(`${editCalls} file ops in a row \u2014 consider if all are needed`);
6650
+ }
6651
+ if (modifiedFiles.length > 5 && !hasRunTests) {
6652
+ violations.push(`${modifiedFiles.length} files modified without verification`);
6653
+ }
6654
+ return violations;
6655
+ }
6291
6656
  function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
6292
6657
  const drifts = [];
6293
6658
  if (modifiedFiles > 0 && !hasRunTests) {
@@ -6326,7 +6691,7 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
6326
6691
  if (!goal) {
6327
6692
  return "No goal set \u2014 use goal parameter or setGoal to track intent";
6328
6693
  }
6329
- if (modifiedFiles === 0) {
6694
+ if (modifiedFiles === 0 && !hasRunTests) {
6330
6695
  return "Start by exploring what exists before writing code";
6331
6696
  }
6332
6697
  return "On track \u2014 continue with current approach";
@@ -6334,11 +6699,43 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
6334
6699
  function countEditCalls(toolCalls) {
6335
6700
  return toolCalls.filter((c) => isEditTool(c.toolName)).length;
6336
6701
  }
6702
+ function detectTestWeakening(files, root) {
6703
+ const weakened = [];
6704
+ let cwd = "";
6705
+ try {
6706
+ cwd = root || getProjectRoot();
6707
+ } catch {
6708
+ return [];
6709
+ }
6710
+ for (const f of files) {
6711
+ if (!TEST_FILE_PATTERN.test(f)) continue;
6712
+ try {
6713
+ const diff = execSync5(`git diff --unified=0 -- "${f}"`, {
6714
+ cwd,
6715
+ encoding: "utf-8",
6716
+ timeout: 4e3,
6717
+ stdio: ["pipe", "pipe", "pipe"]
6718
+ }).trim();
6719
+ if (!diff) continue;
6720
+ const removed = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---"));
6721
+ const removedAssertions = removed.filter((l) => ASSERTION_PATTERN.test(l)).length;
6722
+ if (removedAssertions > 0 && removed.length > 0 && removedAssertions / removed.length >= 0.5) {
6723
+ weakened.push(`${f} (\u2212${removedAssertions} assertion(s))`);
6724
+ }
6725
+ } catch {
6726
+ }
6727
+ }
6728
+ return weakened;
6729
+ }
6730
+ var TEST_COMMAND_PATTERN, TEST_FILE_PATTERN, ASSERTION_PATTERN;
6337
6731
  var init_kumaShared = __esm({
6338
6732
  "src/utils/kumaShared.ts"() {
6339
6733
  "use strict";
6340
6734
  init_sessionMemory();
6341
6735
  init_pathValidator();
6736
+ 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;
6737
+ TEST_FILE_PATTERN = /(\.test\.|\.spec\.|_test\.|__tests__|\/tests?\/)/i;
6738
+ ASSERTION_PATTERN = /\b(expect|assert|toBe|toEqual|toContain|toMatch|ok\(|equal\(|deepEqual\()\b/;
6342
6739
  }
6343
6740
  });
6344
6741
 
@@ -6352,19 +6749,19 @@ __export(kumaMemory_exports, {
6352
6749
  scoreMemoryRelevance: () => scoreMemoryRelevance,
6353
6750
  shouldRecordDecision: () => shouldRecordDecision
6354
6751
  });
6355
- import fs10 from "fs";
6356
- import path12 from "path";
6752
+ import fs11 from "fs";
6753
+ import path13 from "path";
6357
6754
  function scoreMemoryRelevance(context, limit = 5) {
6358
6755
  const results = [];
6359
- const kumaDir = path12.join(getProjectRoot(), MEMORY_DIR);
6360
- if (!fs10.existsSync(kumaDir)) return [];
6756
+ const kumaDir = path13.join(getProjectRoot(), MEMORY_DIR);
6757
+ if (!fs11.existsSync(kumaDir)) return [];
6361
6758
  const terms = Array.from(new Set(context.toLowerCase().split(/[\s/._-]+/).filter((w) => w.length > 2)));
6362
6759
  if (terms.length === 0) return [];
6363
6760
  try {
6364
- const files = fs10.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
6761
+ const files = fs11.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
6365
6762
  for (const file of files) {
6366
6763
  try {
6367
- const content = fs10.readFileSync(path12.join(kumaDir, file), "utf-8");
6764
+ const content = fs11.readFileSync(path13.join(kumaDir, file), "utf-8");
6368
6765
  const lower = content.toLowerCase();
6369
6766
  let matchCount = 0;
6370
6767
  for (const term of terms) {
@@ -6535,13 +6932,86 @@ var init_kumaMemory = __esm({
6535
6932
  }
6536
6933
  });
6537
6934
 
6935
+ // src/engine/cacheFreshness.ts
6936
+ var cacheFreshness_exports = {};
6937
+ __export(cacheFreshness_exports, {
6938
+ MAP_TTL_MS: () => MAP_TTL_MS,
6939
+ RESEARCH_TTL_MS: () => RESEARCH_TTL_MS,
6940
+ clearDirty: () => clearDirty,
6941
+ formatAge: () => formatAge,
6942
+ getDirtyFiles: () => getDirtyFiles,
6943
+ isStale: () => isStale,
6944
+ markDirty: () => markDirty
6945
+ });
6946
+ import fs12 from "fs";
6947
+ import path14 from "path";
6948
+ function isStale(savedAtMs, ttlMs, nowMs = Date.now()) {
6949
+ if (!savedAtMs) return true;
6950
+ return nowMs - savedAtMs > ttlMs;
6951
+ }
6952
+ function formatAge(savedAtMs, nowMs = Date.now()) {
6953
+ const s = Math.max(0, Math.floor((nowMs - savedAtMs) / 1e3));
6954
+ if (s < 60) return `${s}s`;
6955
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
6956
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
6957
+ return `${Math.floor(s / 86400)}d`;
6958
+ }
6959
+ function dirtyPath() {
6960
+ return path14.join(getProjectRoot(), DIRTY_FILE);
6961
+ }
6962
+ function loadDirty() {
6963
+ try {
6964
+ const fp = dirtyPath();
6965
+ if (fs12.existsSync(fp)) {
6966
+ const parsed = JSON.parse(fs12.readFileSync(fp, "utf-8"));
6967
+ if (parsed && typeof parsed === "object") return parsed;
6968
+ }
6969
+ } catch {
6970
+ }
6971
+ return {};
6972
+ }
6973
+ function markDirty(filePath) {
6974
+ try {
6975
+ const fp = dirtyPath();
6976
+ const dir = path14.dirname(fp);
6977
+ if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
6978
+ const dirty = loadDirty();
6979
+ dirty[filePath] = Date.now();
6980
+ const keys = Object.keys(dirty).sort((a, b) => dirty[b] - dirty[a]).slice(0, 200);
6981
+ const bounded = {};
6982
+ for (const k of keys) bounded[k] = dirty[k];
6983
+ fs12.writeFileSync(fp, JSON.stringify(bounded), "utf-8");
6984
+ } catch {
6985
+ }
6986
+ }
6987
+ function getDirtyFiles() {
6988
+ return Object.keys(loadDirty());
6989
+ }
6990
+ function clearDirty() {
6991
+ try {
6992
+ const fp = dirtyPath();
6993
+ if (fs12.existsSync(fp)) fs12.rmSync(fp, { force: true });
6994
+ } catch {
6995
+ }
6996
+ }
6997
+ var RESEARCH_TTL_MS, MAP_TTL_MS, DIRTY_FILE;
6998
+ var init_cacheFreshness = __esm({
6999
+ "src/engine/cacheFreshness.ts"() {
7000
+ "use strict";
7001
+ init_pathValidator();
7002
+ RESEARCH_TTL_MS = 7 * 24 * 3600 * 1e3;
7003
+ MAP_TTL_MS = 24 * 3600 * 1e3;
7004
+ DIRTY_FILE = ".kuma/dirty.json";
7005
+ }
7006
+ });
7007
+
6538
7008
  // src/engine/codeReuseEngine.ts
6539
7009
  var codeReuseEngine_exports = {};
6540
7010
  __export(codeReuseEngine_exports, {
6541
7011
  findReusableSymbols: () => findReusableSymbols
6542
7012
  });
6543
- import path13 from "path";
6544
- import fs11 from "fs";
7013
+ import path15 from "path";
7014
+ import fs13 from "fs";
6545
7015
  async function findReusableSymbols(query, scope) {
6546
7016
  const cleanQuery = query.trim().toLowerCase();
6547
7017
  const searchTokens = cleanQuery.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((t) => t.length >= 2);
@@ -6664,7 +7134,7 @@ function detectRecommendedUtilDirectory() {
6664
7134
  "utils"
6665
7135
  ];
6666
7136
  for (const c of candidates) {
6667
- if (fs11.existsSync(path13.resolve(root, c))) {
7137
+ if (fs13.existsSync(path15.resolve(root, c))) {
6668
7138
  return c;
6669
7139
  }
6670
7140
  }
@@ -6683,7 +7153,7 @@ var domainClusterEngine_exports = {};
6683
7153
  __export(domainClusterEngine_exports, {
6684
7154
  buildSubsystemClusters: () => buildSubsystemClusters
6685
7155
  });
6686
- import path14 from "path";
7156
+ import path16 from "path";
6687
7157
  async function buildSubsystemClusters() {
6688
7158
  const db = await getDb();
6689
7159
  const fileStmt = db.prepare(`
@@ -6760,7 +7230,7 @@ async function buildSubsystemClusters() {
6760
7230
  for (const [cKey, files] of clusterFileMap.entries()) {
6761
7231
  for (const f of files) {
6762
7232
  fileToCluster.set(f, cKey);
6763
- fileToCluster.set(path14.normalize(f), cKey);
7233
+ fileToCluster.set(path16.normalize(f), cKey);
6764
7234
  }
6765
7235
  }
6766
7236
  const clusterInteractions = /* @__PURE__ */ new Map();
@@ -6768,8 +7238,8 @@ async function buildSubsystemClusters() {
6768
7238
  const srcFile = extractFilePathFromNodeId(e.source);
6769
7239
  const tgtFile = extractFilePathFromNodeId(e.target);
6770
7240
  if (!srcFile || !tgtFile) continue;
6771
- const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(path14.normalize(srcFile));
6772
- const tgtCluster = fileToCluster.get(tgtFile) || fileToCluster.get(path14.normalize(tgtFile));
7241
+ const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(path16.normalize(srcFile));
7242
+ const tgtCluster = fileToCluster.get(tgtFile) || fileToCluster.get(path16.normalize(tgtFile));
6773
7243
  if (srcCluster && tgtCluster && srcCluster !== tgtCluster) {
6774
7244
  const pairKey = `${srcCluster}->${tgtCluster}`;
6775
7245
  const stats = clusterInteractions.get(pairKey) || { calls: 0, imports: 0 };
@@ -6791,13 +7261,13 @@ async function buildSubsystemClusters() {
6791
7261
  const clusterId = nodeId("feature_domain", cKey);
6792
7262
  const name = formatClusterName(cKey);
6793
7263
  const role = inferSubsystemRole(cKey, files);
6794
- const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path14.basename(f))).slice(0, 3);
7264
+ const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path16.basename(f))).slice(0, 3);
6795
7265
  if (entrypoints.length === 0 && files.length > 0) {
6796
7266
  entrypoints.push(files[0]);
6797
7267
  }
6798
7268
  const allSyms = [];
6799
7269
  for (const f of files) {
6800
- const syms = fileSymbols.get(f) || fileSymbols.get(path14.normalize(f)) || [];
7270
+ const syms = fileSymbols.get(f) || fileSymbols.get(path16.normalize(f)) || [];
6801
7271
  for (const s of syms) {
6802
7272
  if (!allSyms.includes(s)) allSyms.push(s);
6803
7273
  }
@@ -6976,7 +7446,7 @@ function inferSubsystemRole(key, files) {
6976
7446
  if (k.includes("util") || k.includes("common")) {
6977
7447
  return "Shared path validation, configuration loading, and platform detection helpers.";
6978
7448
  }
6979
- return `Core subsystem handling ${files.slice(0, 3).map((f) => path14.basename(f)).join(", ")}.`;
7449
+ return `Core subsystem handling ${files.slice(0, 3).map((f) => path16.basename(f)).join(", ")}.`;
6980
7450
  }
6981
7451
  function inferTypedVerb(sourceKey, targetKey, _stats) {
6982
7452
  const src = sourceKey.toLowerCase();
@@ -7055,18 +7525,18 @@ __export(domainRules_exports, {
7055
7525
  readLayer: () => readLayer,
7056
7526
  writeLayer: () => writeLayer
7057
7527
  });
7058
- import fs12 from "fs";
7059
- import path15 from "path";
7528
+ import fs14 from "fs";
7529
+ import path17 from "path";
7060
7530
  function layerFilePath(layer) {
7061
- return path15.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
7531
+ return path17.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
7062
7532
  }
7063
7533
  function ensureLayerFile(layer) {
7064
7534
  const fp = layerFilePath(layer);
7065
- if (!fs12.existsSync(fp)) {
7535
+ if (!fs14.existsSync(fp)) {
7066
7536
  const template = layer === "arch_flow" ? ARCH_FLOW_TEMPLATE : GOTCHAS_TEMPLATE;
7067
- const dir = path15.dirname(fp);
7068
- if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
7069
- fs12.writeFileSync(fp, template, "utf-8");
7537
+ const dir = path17.dirname(fp);
7538
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
7539
+ fs14.writeFileSync(fp, template, "utf-8");
7070
7540
  console.error(`[DomainRules] Auto-created ${LAYER_FILES[layer].filename}`);
7071
7541
  }
7072
7542
  return fp;
@@ -7074,7 +7544,7 @@ function ensureLayerFile(layer) {
7074
7544
  function readLayer(layer) {
7075
7545
  try {
7076
7546
  const fp = ensureLayerFile(layer);
7077
- return fs12.readFileSync(fp, "utf-8");
7547
+ return fs14.readFileSync(fp, "utf-8");
7078
7548
  } catch (err) {
7079
7549
  return `Error reading ${layer}: ${err}`;
7080
7550
  }
@@ -7082,7 +7552,7 @@ function readLayer(layer) {
7082
7552
  function writeLayer(layer, content) {
7083
7553
  try {
7084
7554
  const fp = ensureLayerFile(layer);
7085
- fs12.writeFileSync(fp, content, "utf-8");
7555
+ fs14.writeFileSync(fp, content, "utf-8");
7086
7556
  const label = LAYER_FILES[layer].label;
7087
7557
  sessionMemory.recordToolCall("kuma_memory", { action: `write_${layer}` });
7088
7558
  return `\u2705 **${label}** updated successfully.`;
@@ -7093,9 +7563,9 @@ function writeLayer(layer, content) {
7093
7563
  function appendToLayer(layer, entry) {
7094
7564
  try {
7095
7565
  const fp = ensureLayerFile(layer);
7096
- const existing = fs12.readFileSync(fp, "utf-8");
7566
+ const existing = fs14.readFileSync(fp, "utf-8");
7097
7567
  const newContent = existing.trimEnd() + "\n\n" + entry.trim() + "\n";
7098
- fs12.writeFileSync(fp, newContent, "utf-8");
7568
+ fs14.writeFileSync(fp, newContent, "utf-8");
7099
7569
  return `\u2705 Entry added to **${LAYER_FILES[layer].label}**.`;
7100
7570
  } catch (err) {
7101
7571
  return `\u274C Failed to append: ${err}`;
@@ -7105,7 +7575,7 @@ function getActiveGotchas() {
7105
7575
  const results = [];
7106
7576
  try {
7107
7577
  const fp = ensureLayerFile("gotcha");
7108
- const content = fs12.readFileSync(fp, "utf-8");
7578
+ const content = fs14.readFileSync(fp, "utf-8");
7109
7579
  const lines = content.split("\n");
7110
7580
  let currentSection = "";
7111
7581
  let currentFilePath = "";
@@ -7141,9 +7611,9 @@ function getActiveGotchas() {
7141
7611
  function checkFileGotchas(filePath) {
7142
7612
  const gotchas = getActiveGotchas();
7143
7613
  const warnings = [];
7144
- const base = path15.basename(filePath);
7614
+ const base = path17.basename(filePath);
7145
7615
  for (const g of gotchas) {
7146
- const gBase = path15.basename(g.filePath);
7616
+ const gBase = path17.basename(g.filePath);
7147
7617
  if (filePath.includes(g.filePath) || g.filePath.includes(filePath) || base && gBase && base === gBase) {
7148
7618
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
7149
7619
  warnings.push(`${icon} **Gotcha**: ${g.description} (${g.severity})`);
@@ -7243,8 +7713,8 @@ __export(kumaInject_exports, {
7243
7713
  parseHookInput: () => parseHookInput,
7244
7714
  trackFileEditLoop: () => trackFileEditLoop
7245
7715
  });
7246
- import fs13 from "fs";
7247
- import path16 from "path";
7716
+ import fs15 from "fs";
7717
+ import path18 from "path";
7248
7718
  function hashFileContent(filePath) {
7249
7719
  return hashFile(filePath);
7250
7720
  }
@@ -7255,16 +7725,17 @@ function isGotchaFresh(storedHash, currentHash) {
7255
7725
  }
7256
7726
  async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false) {
7257
7727
  const root = getProjectRoot();
7258
- const base = path16.basename(filePath);
7728
+ const base = path18.basename(filePath);
7259
7729
  try {
7260
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7261
- if (fs13.existsSync(dbFile)) {
7730
+ const dbFile = path18.join(root, ".kuma", "kuma.db");
7731
+ if (fs15.existsSync(dbFile)) {
7262
7732
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7263
7733
  const db = await getDb2();
7264
7734
  const stmt = db.prepare(`
7265
7735
  SELECT id, file_path, description, severity, workaround, content_hash
7266
7736
  FROM known_gotchas
7267
7737
  WHERE status IN ('active', 'verified')
7738
+ AND (quarantined IS NULL OR quarantined = 0)
7268
7739
  AND (
7269
7740
  file_path = ?
7270
7741
  OR file_path LIKE ?
@@ -7298,11 +7769,11 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
7298
7769
  } catch {
7299
7770
  }
7300
7771
  try {
7301
- if (!fs13.existsSync(path16.join(root, ".kuma"))) return [];
7772
+ if (!fs15.existsSync(path18.join(root, ".kuma"))) return [];
7302
7773
  const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
7303
7774
  const allGotchas = getActiveGotchas2();
7304
7775
  const matched = allGotchas.filter(
7305
- (g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path16.basename(g.filePath) === base
7776
+ (g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path18.basename(g.filePath) === base
7306
7777
  );
7307
7778
  if (matched.length > 0) {
7308
7779
  return matched.slice(0, limit).map((g) => ({
@@ -7321,8 +7792,8 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
7321
7792
  async function getFileTrace(filePath, limit = 8) {
7322
7793
  try {
7323
7794
  const root = getProjectRoot();
7324
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7325
- if (!fs13.existsSync(dbFile)) return [];
7795
+ const dbFile = path18.join(root, ".kuma", "kuma.db");
7796
+ if (!fs15.existsSync(dbFile)) return [];
7326
7797
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7327
7798
  const db = await getDb2();
7328
7799
  const stmt = db.prepare(`
@@ -7332,7 +7803,7 @@ async function getFileTrace(filePath, limit = 8) {
7332
7803
  ORDER BY cl.created_at DESC
7333
7804
  LIMIT ?
7334
7805
  `);
7335
- stmt.bind([`%${path16.basename(filePath)}%`, limit]);
7806
+ stmt.bind([`%${path18.basename(filePath)}%`, limit]);
7336
7807
  const rows = [];
7337
7808
  while (stmt.step()) rows.push(stmt.getAsObject());
7338
7809
  stmt.free();
@@ -7350,7 +7821,7 @@ async function getFileTrace(filePath, limit = 8) {
7350
7821
  function formatFileTrace(entries, filePath) {
7351
7822
  if (entries.length === 0) return "";
7352
7823
  const lines = [
7353
- `\u{1F570}\uFE0F **Why is ${path16.basename(filePath)} written this way?** \u2014 ${entries.length} change(s) recorded:`
7824
+ `\u{1F570}\uFE0F **Why is ${path18.basename(filePath)} written this way?** \u2014 ${entries.length} change(s) recorded:`
7354
7825
  ];
7355
7826
  for (const e of entries) {
7356
7827
  const date = new Date((e.createdAt || 0) * 1e3).toLocaleDateString("en-US", {
@@ -7366,12 +7837,12 @@ function formatFileTrace(entries, filePath) {
7366
7837
  async function getDecisionsForFile(filePath, limit = 3) {
7367
7838
  const decisions = [];
7368
7839
  const root = getProjectRoot();
7369
- const base = path16.basename(filePath);
7840
+ const base = path18.basename(filePath);
7370
7841
  const baseWithoutExt = base.replace(/\.[^.]+$/, "");
7371
7842
  try {
7372
- const decisionsMdPath = path16.join(root, ".kuma", "memories", "decisions.md");
7373
- if (fs13.existsSync(decisionsMdPath)) {
7374
- const content = fs13.readFileSync(decisionsMdPath, "utf-8");
7843
+ const decisionsMdPath = path18.join(root, ".kuma", "memories", "decisions.md");
7844
+ if (fs15.existsSync(decisionsMdPath)) {
7845
+ const content = fs15.readFileSync(decisionsMdPath, "utf-8");
7375
7846
  const sections = content.split(/^##\s+/m);
7376
7847
  for (const section of sections) {
7377
7848
  if (!section.trim() || section.startsWith("# ")) continue;
@@ -7442,7 +7913,17 @@ async function getDecisionsForFile(filePath, limit = 3) {
7442
7913
  }
7443
7914
  async function getRelevantContext(filePath, goal) {
7444
7915
  const parts = [];
7445
- const gotchas = await getFreshGotchasForFile(filePath, 4);
7916
+ const { sessionMemory: sessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
7917
+ const gotchaPool = await getFreshGotchasForFile(filePath, 4);
7918
+ const gotchas = gotchaPool.filter((g) => {
7919
+ const key = `${g.filePath}::${g.description.substring(0, 60)}`;
7920
+ if (sessionMemory2.wasShown("gotcha", key)) return false;
7921
+ return true;
7922
+ });
7923
+ sessionMemory2.markShown(
7924
+ "gotcha",
7925
+ gotchaPool.map((g) => `${g.filePath}::${g.description.substring(0, 60)}`)
7926
+ );
7446
7927
  if (gotchas.length > 0) {
7447
7928
  const lines = ["\u26A0\uFE0F ACTIVE GOTCHAS (fresh):"];
7448
7929
  for (const g of gotchas) {
@@ -7452,7 +7933,12 @@ async function getRelevantContext(filePath, goal) {
7452
7933
  }
7453
7934
  parts.push(lines.join("\n"));
7454
7935
  }
7455
- const decisions = await getDecisionsForFile(filePath, goal ? 3 : 2);
7936
+ const decisionPool = await getDecisionsForFile(filePath, goal ? 3 : 2);
7937
+ const decisions = decisionPool.filter((d) => {
7938
+ if (sessionMemory2.wasShown("decision", d.substring(0, 80))) return false;
7939
+ return true;
7940
+ });
7941
+ sessionMemory2.markShown("decision", decisionPool.map((d) => d.substring(0, 80)));
7456
7942
  if (decisions.length > 0) {
7457
7943
  parts.push("\u{1F4CC} RELEVANT DECISIONS:\n " + decisions.join("\n "));
7458
7944
  }
@@ -7461,13 +7947,13 @@ async function getRelevantContext(filePath, goal) {
7461
7947
  if (traceStr) parts.push(traceStr);
7462
7948
  try {
7463
7949
  const root = getProjectRoot();
7464
- if (fs13.existsSync(path16.join(root, ".kuma", "kuma.db"))) {
7950
+ if (fs15.existsSync(path18.join(root, ".kuma", "kuma.db"))) {
7465
7951
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7466
7952
  const db = await getDb2();
7467
7953
  const stmt = db.prepare(
7468
7954
  `SELECT COUNT(*) FROM known_gotchas WHERE status = 'resolved' AND file_path LIKE ?`
7469
7955
  );
7470
- stmt.bind([`%${path16.basename(filePath)}%`]);
7956
+ stmt.bind([`%${path18.basename(filePath)}%`]);
7471
7957
  const found = stmt.step();
7472
7958
  const count = found ? Number(stmt.getAsObject()["COUNT(*)"]) : 0;
7473
7959
  stmt.free();
@@ -7478,7 +7964,7 @@ async function getRelevantContext(filePath, goal) {
7478
7964
  } catch {
7479
7965
  }
7480
7966
  if (gotchas.length > 0) {
7481
- const scopeHint = path16.basename(path16.dirname(filePath)) || "current change";
7967
+ const scopeHint = path18.basename(path18.dirname(filePath)) || "current change";
7482
7968
  parts.push(
7483
7969
  `\u2705 AFTER editing, verify: kuma_safety({ action: "verify", scope: "${scopeHint}" })`
7484
7970
  );
@@ -7494,11 +7980,11 @@ async function getRelevantContext(filePath, goal) {
7494
7980
  async function getFileSummary2(filePath) {
7495
7981
  try {
7496
7982
  const root = getProjectRoot();
7497
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7498
- if (!fs13.existsSync(dbFile)) return null;
7983
+ const dbFile = path18.join(root, ".kuma", "kuma.db");
7984
+ if (!fs15.existsSync(dbFile)) return null;
7499
7985
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7500
7986
  const db = await getDb2();
7501
- const base = path16.basename(filePath);
7987
+ const base = path18.basename(filePath);
7502
7988
  const stmt = db.prepare(`
7503
7989
  SELECT
7504
7990
  (SELECT COUNT(*) FROM known_gotchas WHERE status = 'active' AND (file_path = ? OR file_path LIKE ?)) as active_gotchas,
@@ -7529,7 +8015,9 @@ async function getFileSummary2(filePath) {
7529
8015
  function parseHookInput(raw) {
7530
8016
  try {
7531
8017
  const data = JSON.parse(raw);
7532
- const ti = (data && typeof data === "object" ? data.tool_input : {}) || {};
8018
+ const root = data && typeof data === "object" ? data : {};
8019
+ const ti = root.tool_input && typeof root.tool_input === "object" ? root.tool_input : {};
8020
+ const args = root.args && typeof root.args === "object" ? root.args : {};
7533
8021
  const paths = /* @__PURE__ */ new Set();
7534
8022
  const add = (p) => {
7535
8023
  if (typeof p === "string" && p.trim()) paths.add(p.trim());
@@ -7538,10 +8026,22 @@ function parseHookInput(raw) {
7538
8026
  add(ti.new_file_path);
7539
8027
  add(ti.notebook_path);
7540
8028
  if (Array.isArray(ti.file_paths)) ti.file_paths.forEach(add);
7541
- return {
7542
- filePaths: [...paths],
7543
- goal: typeof ti.goal === "string" ? ti.goal : void 0
7544
- };
8029
+ add(root.file_path);
8030
+ add(root.file);
8031
+ add(root.path);
8032
+ add(root.targetFile);
8033
+ if (Array.isArray(root.file_paths)) root.file_paths.forEach(add);
8034
+ if (Array.isArray(root.files)) root.files.forEach(add);
8035
+ add(args.filePath);
8036
+ add(args.targetFile);
8037
+ add(args.path);
8038
+ add(args.file);
8039
+ 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);
8040
+ const goal = (typeof ti.goal === "string" ? ti.goal : void 0) ?? (typeof root.goal === "string" ? root.goal : void 0);
8041
+ const out = { filePaths: [...paths] };
8042
+ if (goal) out.goal = goal;
8043
+ if (cmd) out.command = cmd;
8044
+ return out;
7545
8045
  } catch {
7546
8046
  return { filePaths: [] };
7547
8047
  }
@@ -7576,9 +8076,9 @@ async function getCommandContext(command) {
7576
8076
  }
7577
8077
  function loadHookState(root) {
7578
8078
  try {
7579
- const fp = path16.join(root, HOOK_STATE_FILE);
7580
- if (fs13.existsSync(fp)) {
7581
- return JSON.parse(fs13.readFileSync(fp, "utf-8"));
8079
+ const fp = path18.join(root, HOOK_STATE_FILE);
8080
+ if (fs15.existsSync(fp)) {
8081
+ return JSON.parse(fs15.readFileSync(fp, "utf-8"));
7582
8082
  }
7583
8083
  } catch {
7584
8084
  }
@@ -7586,10 +8086,10 @@ function loadHookState(root) {
7586
8086
  }
7587
8087
  function saveHookState(root, state) {
7588
8088
  try {
7589
- const fp = path16.join(root, HOOK_STATE_FILE);
7590
- const dir = path16.dirname(fp);
7591
- if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7592
- fs13.writeFileSync(fp, JSON.stringify(state), "utf-8");
8089
+ const fp = path18.join(root, HOOK_STATE_FILE);
8090
+ const dir = path18.dirname(fp);
8091
+ if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
8092
+ fs15.writeFileSync(fp, JSON.stringify(state), "utf-8");
7593
8093
  } catch {
7594
8094
  }
7595
8095
  }
@@ -7623,7 +8123,7 @@ async function trackFileEditLoop(filePath) {
7623
8123
  saveHookState(root, state);
7624
8124
  if (recorded) {
7625
8125
  try {
7626
- if (!fs13.existsSync(path16.join(root, ".kuma", "kuma.db"))) return entry.count;
8126
+ if (!fs15.existsSync(path18.join(root, ".kuma", "kuma.db"))) return entry.count;
7627
8127
  const { addGotcha: addGotcha2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
7628
8128
  await addGotcha2({
7629
8129
  filePath,
@@ -7661,20 +8161,24 @@ __export(kumaGotchas_exports, {
7661
8161
  addGotcha: () => addGotcha,
7662
8162
  checkGotchasForFile: () => checkGotchasForFile,
7663
8163
  deprecateGotcha: () => deprecateGotcha,
8164
+ findSimilarGotchaId: () => findSimilarGotchaId,
7664
8165
  getActiveGotchasForCommand: () => getActiveGotchasForCommand,
7665
8166
  getGotchasByStatus: () => getGotchasByStatus,
7666
8167
  getInjectionStats: () => getInjectionStats,
8168
+ jaccardSimilarity: () => jaccardSimilarity,
7667
8169
  listGotchas: () => listGotchas,
7668
8170
  recordInjection: () => recordInjection,
7669
8171
  resolveGotcha: () => resolveGotcha,
7670
8172
  resolveGotchasForScope: () => resolveGotchasForScope,
8173
+ screenForInjection: () => screenForInjection,
7671
8174
  setGotchaStatus: () => setGotchaStatus,
7672
8175
  syncGotchaStatusToMarkdown: () => syncGotchaStatusToMarkdown,
7673
8176
  syncGotchasGraph: () => syncGotchasGraph,
7674
- syncGotchasToDb: () => syncGotchasToDb
8177
+ syncGotchasToDb: () => syncGotchasToDb,
8178
+ tokenizeForSimilarity: () => tokenizeForSimilarity
7675
8179
  });
7676
- import fs14 from "fs";
7677
- import path17 from "path";
8180
+ import fs16 from "fs";
8181
+ import path19 from "path";
7678
8182
  async function ensureGotchasSchema() {
7679
8183
  const db = await getDb();
7680
8184
  db.exec(`
@@ -7694,7 +8198,11 @@ async function ensureGotchasSchema() {
7694
8198
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
7695
8199
  last_verified_at INTEGER,
7696
8200
  scope_package TEXT,
7697
- verified_by TEXT
8201
+ verified_by TEXT,
8202
+ provenance TEXT,
8203
+ tier TEXT NOT NULL DEFAULT 'single'
8204
+ CHECK(tier IN ('human-confirmed','corroborated','single','external')),
8205
+ quarantined INTEGER NOT NULL DEFAULT 0
7698
8206
  );
7699
8207
  CREATE INDEX IF NOT EXISTS idx_gotchas_file ON known_gotchas(file_path);
7700
8208
  CREATE INDEX IF NOT EXISTS idx_gotchas_severity ON known_gotchas(severity);
@@ -7720,6 +8228,15 @@ async function ensureGotchasSchema() {
7720
8228
  if (!cols.includes("verified_by")) {
7721
8229
  db.run(`ALTER TABLE known_gotchas ADD COLUMN verified_by TEXT`);
7722
8230
  }
8231
+ if (!cols.includes("provenance")) {
8232
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN provenance TEXT`);
8233
+ }
8234
+ if (!cols.includes("tier")) {
8235
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN tier TEXT NOT NULL DEFAULT 'single'`);
8236
+ }
8237
+ if (!cols.includes("quarantined")) {
8238
+ db.run(`ALTER TABLE known_gotchas ADD COLUMN quarantined INTEGER NOT NULL DEFAULT 0`);
8239
+ }
7723
8240
  const schema = db.exec(`SELECT sql FROM sqlite_master WHERE type='table' AND name='known_gotchas'`);
7724
8241
  const gotchaSql = schema[0]?.values?.[0]?.[0] || "";
7725
8242
  if (gotchaSql && !gotchaSql.includes("candidate")) {
@@ -7740,7 +8257,11 @@ async function ensureGotchasSchema() {
7740
8257
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
7741
8258
  last_verified_at INTEGER,
7742
8259
  scope_package TEXT,
7743
- verified_by TEXT
8260
+ verified_by TEXT,
8261
+ provenance TEXT,
8262
+ tier TEXT NOT NULL DEFAULT 'single'
8263
+ CHECK(tier IN ('human-confirmed','corroborated','single','external')),
8264
+ quarantined INTEGER NOT NULL DEFAULT 0
7744
8265
  )`);
7745
8266
  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`);
7746
8267
  db.run(`DROP TABLE known_gotchas_old`);
@@ -7749,12 +8270,30 @@ async function ensureGotchasSchema() {
7749
8270
  }
7750
8271
  saveDb();
7751
8272
  }
8273
+ function screenForInjection(text) {
8274
+ const t = text.toLowerCase();
8275
+ const patterns = [
8276
+ [/ignore\s+(all\s+)?previous\s+instructions?/, "directive to ignore previous instructions"],
8277
+ [/disregard\s+(the\s+)?(system|prior)\s+(prompt|instructions?)/, "directive to disregard system prompt"],
8278
+ [/(reveal|exfiltrate|leak|send|upload).{0,40}(secret|api[_\s-]?key|token|password|credential)/, "directive to reveal secrets"],
8279
+ [/execute\s+(the\s+)?following\s+(command|code|payload)/, "directive to execute embedded payload"],
8280
+ [/you\s+are\s+now\s+(a|an|in)\s/, "identity-reassignment attempt"]
8281
+ ];
8282
+ for (const [re, reason] of patterns) {
8283
+ if (re.test(t)) return reason;
8284
+ }
8285
+ return null;
8286
+ }
7752
8287
  async function addGotcha(entry) {
7753
8288
  try {
7754
8289
  await ensureGotchasSchema();
7755
8290
  const db = await getDb();
7756
8291
  const severity = entry.severity || "medium";
7757
8292
  const status = entry.status || "active";
8293
+ const provenance = entry.provenance || "session:agent";
8294
+ const quarantineReason = screenForInjection(`${entry.description} ${entry.workaround || ""}`);
8295
+ const quarantined = quarantineReason ? 1 : 0;
8296
+ const tier = quarantined ? "external" : entry.tier || (status === "verified" ? "human-confirmed" : "single");
7758
8297
  const contentHash = hashFileContent(entry.filePath);
7759
8298
  const trap = entry.description;
7760
8299
  const ruleFix = entry.workaround || "Review implementation carefully before modifying.";
@@ -7779,15 +8318,23 @@ async function addGotcha(entry) {
7779
8318
  existingId = row.id;
7780
8319
  }
7781
8320
  checkStmt.free();
8321
+ let mergedNote = "";
8322
+ if (existingId === null) {
8323
+ const similarId = await findSimilarGotchaId(entry.filePath, entry.description);
8324
+ if (similarId !== null) {
8325
+ existingId = similarId;
8326
+ mergedNote = " (merged: near-duplicate of an existing gotcha \u2014 refreshed instead of appended)";
8327
+ }
8328
+ }
7782
8329
  if (existingId !== null) {
7783
8330
  db.run(
7784
- `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 = ?`,
7785
- [severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, existingId]
8331
+ `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 = ?`,
8332
+ [severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, provenance, tier, existingId]
7786
8333
  );
7787
8334
  } else {
7788
8335
  db.run(
7789
- `INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7790
- [entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null]
8336
+ `INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by, provenance, tier, quarantined) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
8337
+ [entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, provenance, tier, quarantined]
7791
8338
  );
7792
8339
  }
7793
8340
  const mdResult = appendToLayer("gotcha", formatted);
@@ -7819,23 +8366,61 @@ async function addGotcha(entry) {
7819
8366
  } catch {
7820
8367
  }
7821
8368
  const action = existingId !== null ? "updated" : "recorded";
7822
- return `\u2705 **Gotcha ${action}**: ${entry.filePath} \u2014 ${entry.description}${linkedCount > 0 ? ` (linked to ${linkedCount} flow(s))` : ""}
8369
+ const quarantineNote = quarantined ? `
8370
+ \u{1F512} **Quarantined** (${quarantineReason}) \u2014 excluded from auto-inject until human-confirmed. Promote via verify + resolve/re-record.` : "";
8371
+ return `\u2705 **Gotcha ${action}**: ${entry.filePath} \u2014 ${entry.description}${mergedNote}${quarantineNote}${linkedCount > 0 ? ` (linked to ${linkedCount} flow(s))` : ""} [tier: ${tier}]
7823
8372
  ${mdResult}`;
7824
8373
  } catch (err) {
7825
8374
  return `\u274C Failed to add gotcha: ${err}`;
7826
8375
  }
7827
8376
  }
7828
- async function listGotchas(params) {
8377
+ function tokenizeForSimilarity(text) {
8378
+ return new Set(
8379
+ text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)
8380
+ );
8381
+ }
8382
+ function jaccardSimilarity(a, b) {
8383
+ if (a.size === 0 || b.size === 0) return 0;
8384
+ let intersection = 0;
8385
+ for (const t of a) if (b.has(t)) intersection++;
8386
+ return intersection / (a.size + b.size - intersection);
8387
+ }
8388
+ async function findSimilarGotchaId(filePath, description) {
7829
8389
  try {
7830
- await ensureGotchasSchema();
7831
8390
  const db = await getDb();
7832
- let sql = "SELECT * FROM known_gotchas WHERE 1=1";
7833
- const bind = [];
7834
- if (params.filePath) {
7835
- sql += " AND file_path LIKE ?";
7836
- bind.push(`%${params.filePath}%`);
7837
- }
7838
- if (params.severity) {
8391
+ const stmt = db.prepare(
8392
+ `SELECT id, description FROM known_gotchas WHERE file_path = ? AND status IN ('active','verified','candidate') LIMIT 50`
8393
+ );
8394
+ stmt.bind([filePath]);
8395
+ const rows = [];
8396
+ while (stmt.step()) rows.push(stmt.getAsObject());
8397
+ stmt.free();
8398
+ const incoming = tokenizeForSimilarity(description);
8399
+ let best = null;
8400
+ let bestScore = 0;
8401
+ for (const r of rows) {
8402
+ const score = jaccardSimilarity(incoming, tokenizeForSimilarity(r.description));
8403
+ if (score >= GOTCHA_MERGE_THRESHOLD && score > bestScore) {
8404
+ bestScore = score;
8405
+ best = r.id;
8406
+ }
8407
+ }
8408
+ return best;
8409
+ } catch {
8410
+ return null;
8411
+ }
8412
+ }
8413
+ async function listGotchas(params) {
8414
+ try {
8415
+ await ensureGotchasSchema();
8416
+ const db = await getDb();
8417
+ let sql = "SELECT * FROM known_gotchas WHERE 1=1";
8418
+ const bind = [];
8419
+ if (params.filePath) {
8420
+ sql += " AND file_path LIKE ?";
8421
+ bind.push(`%${params.filePath}%`);
8422
+ }
8423
+ if (params.severity) {
7839
8424
  sql += " AND severity = ?";
7840
8425
  bind.push(params.severity);
7841
8426
  }
@@ -7845,7 +8430,7 @@ async function listGotchas(params) {
7845
8430
  } else if (!params.status) {
7846
8431
  sql += " AND status IN ('active', 'verified')";
7847
8432
  }
7848
- sql += " ORDER BY severity DESC, created_at DESC LIMIT 50";
8433
+ sql += " ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END, updated_at DESC LIMIT 51";
7849
8434
  const stmt = db.prepare(sql);
7850
8435
  stmt.bind(bind);
7851
8436
  const results = [];
@@ -7854,14 +8439,48 @@ async function listGotchas(params) {
7854
8439
  if (results.length === 0) {
7855
8440
  return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
7856
8441
  }
8442
+ let total = results.length;
8443
+ try {
8444
+ let countSql = "SELECT COUNT(*) as cnt FROM known_gotchas WHERE 1=1";
8445
+ const countBind = [];
8446
+ if (params.filePath) {
8447
+ countSql += " AND file_path LIKE ?";
8448
+ countBind.push(`%${params.filePath}%`);
8449
+ }
8450
+ if (params.severity) {
8451
+ countSql += " AND severity = ?";
8452
+ countBind.push(params.severity);
8453
+ }
8454
+ if (params.status && params.status !== "all") {
8455
+ countSql += " AND status = ?";
8456
+ countBind.push(params.status);
8457
+ } else if (!params.status) {
8458
+ countSql += " AND status IN ('active', 'verified')";
8459
+ }
8460
+ const cs = db.prepare(countSql);
8461
+ cs.bind(countBind);
8462
+ if (cs.step()) total = cs.getAsObject().cnt || results.length;
8463
+ cs.free();
8464
+ } catch {
8465
+ }
8466
+ const truncated = total > results.length;
8467
+ const shown = truncated ? results.slice(0, 50) : results;
8468
+ const nowSec = Math.floor(Date.now() / 1e3);
7857
8469
  const lines = [
7858
- `\u26A0\uFE0F **Known Gotchas** \u2014 ${results.length} recorded`,
8470
+ `\u26A0\uFE0F **Known Gotchas** \u2014 showing ${shown.length} of ${total}${truncated ? " (truncated: true \u2014 narrow with filePath/severity)" : ""}`,
7859
8471
  "\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",
7860
8472
  ""
7861
8473
  ];
7862
- for (const g of results) {
8474
+ for (const g of shown) {
7863
8475
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
7864
- lines.push(`${icon} [${g.severity}] ${g.file_path}`);
8476
+ const ageSec = nowSec - (Number(g.created_at) || nowSec);
8477
+ const isNew = ageSec < 24 * 3600 ? " \u{1F195}" : "";
8478
+ const ageDays = Math.floor(ageSec / 86400);
8479
+ const expired = ageDays > 90 ? " \u23F3expired" : "";
8480
+ const tier = String(g.tier || "single");
8481
+ const quar = Number(g.quarantined) === 1 ? " \u{1F512}quarantined" : "";
8482
+ const updated = g.updated_at ? ` \xB7 lastSeen ${new Date(Number(g.updated_at) * 1e3).toISOString().split("T")[0]}` : "";
8483
+ lines.push(`${icon} [${g.severity}]${isNew}${expired}${quar} ${g.file_path}${updated} (${ageDays}d old, tier: ${tier})`);
7865
8484
  lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
7866
8485
  if (g.trigger_command) lines.push(` \u2328\uFE0F when running: \`${g.trigger_command.substring(0, 80)}\``);
7867
8486
  if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
@@ -7956,7 +8575,7 @@ async function getActiveGotchasForCommand(command) {
7956
8575
  const stmt = db.prepare(
7957
8576
  `SELECT file_path, description, severity, workaround, trigger_command
7958
8577
  FROM known_gotchas
7959
- WHERE status IN ('active', 'verified') AND trigger_command IS NOT NULL AND length(trigger_command) > 0
8578
+ WHERE status IN ('active', 'verified') AND (quarantined IS NULL OR quarantined = 0) AND trigger_command IS NOT NULL AND length(trigger_command) > 0
7960
8579
  ORDER BY
7961
8580
  CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END
7962
8581
  LIMIT 30`
@@ -7981,8 +8600,8 @@ async function getActiveGotchasForCommand(command) {
7981
8600
  }
7982
8601
  async function recordInjection(params) {
7983
8602
  try {
7984
- const fp = path17.join(getProjectRoot(), ".kuma", "injections.jsonl");
7985
- if (!fs14.existsSync(path17.dirname(fp))) fs14.mkdirSync(path17.dirname(fp), { recursive: true });
8603
+ const fp = path19.join(getProjectRoot(), ".kuma", "injections.jsonl");
8604
+ if (!fs16.existsSync(path19.dirname(fp))) fs16.mkdirSync(path19.dirname(fp), { recursive: true });
7986
8605
  const line = JSON.stringify({
7987
8606
  filePath: params.filePath || null,
7988
8607
  command: params.command || null,
@@ -7991,16 +8610,16 @@ async function recordInjection(params) {
7991
8610
  // conservative estimate: avoided re-research / re-discovery
7992
8611
  at: Date.now()
7993
8612
  });
7994
- fs14.appendFileSync(fp, line + "\n", "utf-8");
8613
+ fs16.appendFileSync(fp, line + "\n", "utf-8");
7995
8614
  } catch {
7996
8615
  }
7997
8616
  }
7998
8617
  function getInjectionStats(hours = 24) {
7999
8618
  try {
8000
- const fp = path17.join(getProjectRoot(), ".kuma", "injections.jsonl");
8001
- if (!fs14.existsSync(fp)) return { count: 0, savedMs: 0 };
8619
+ const fp = path19.join(getProjectRoot(), ".kuma", "injections.jsonl");
8620
+ if (!fs16.existsSync(fp)) return { count: 0, savedMs: 0 };
8002
8621
  const cutoff = Date.now() - hours * 60 * 60 * 1e3;
8003
- const lines = fs14.readFileSync(fp, "utf-8").split("\n");
8622
+ const lines = fs16.readFileSync(fp, "utf-8").split("\n");
8004
8623
  let count = 0;
8005
8624
  let savedMs = 0;
8006
8625
  for (const line of lines) {
@@ -8022,10 +8641,17 @@ function getInjectionStats(hours = 24) {
8022
8641
  async function setGotchaStatus(id, status, verifiedBy) {
8023
8642
  try {
8024
8643
  const db = await getDb();
8025
- db.run(
8026
- `UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
8027
- [status, verifiedBy || null, id]
8028
- );
8644
+ if (status === "verified") {
8645
+ db.run(
8646
+ `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 = ?`,
8647
+ [status, verifiedBy || null, id]
8648
+ );
8649
+ } else {
8650
+ db.run(
8651
+ `UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
8652
+ [status, verifiedBy || null, id]
8653
+ );
8654
+ }
8029
8655
  saveDb();
8030
8656
  return true;
8031
8657
  } catch {
@@ -8051,9 +8677,9 @@ async function getGotchasByStatus(status, limit = 20) {
8051
8677
  }
8052
8678
  function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8053
8679
  try {
8054
- const mdPath = path17.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
8055
- if (!fs14.existsSync(mdPath)) return false;
8056
- const content = fs14.readFileSync(mdPath, "utf-8");
8680
+ const mdPath = path19.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
8681
+ if (!fs16.existsSync(mdPath)) return false;
8682
+ const content = fs16.readFileSync(mdPath, "utf-8");
8057
8683
  const lines = content.split("\n");
8058
8684
  const normalizedTargets = targetPaths.map((p) => p.trim().toLowerCase()).filter(Boolean);
8059
8685
  if (normalizedTargets.length === 0) return false;
@@ -8070,9 +8696,9 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8070
8696
  modified = true;
8071
8697
  }
8072
8698
  const headingFile = headingMatch[1].trim().toLowerCase();
8073
- const headingBase = path17.basename(headingFile);
8699
+ const headingBase = path19.basename(headingFile);
8074
8700
  inMatchingSection = normalizedTargets.some(
8075
- (t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path17.basename(t)
8701
+ (t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path19.basename(t)
8076
8702
  );
8077
8703
  sectionHasStatus = false;
8078
8704
  outputLines.push(line);
@@ -8094,7 +8720,7 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8094
8720
  modified = true;
8095
8721
  }
8096
8722
  if (modified) {
8097
- fs14.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
8723
+ fs16.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
8098
8724
  }
8099
8725
  return modified;
8100
8726
  } catch {
@@ -8217,6 +8843,7 @@ async function deprecateGotcha(idOrScope, reason) {
8217
8843
  return { deprecated: 0, message: `\u274C Failed to deprecate gotcha: ${err}` };
8218
8844
  }
8219
8845
  }
8846
+ var GOTCHA_MERGE_THRESHOLD;
8220
8847
  var init_kumaGotchas = __esm({
8221
8848
  "src/engine/kumaGotchas.ts"() {
8222
8849
  "use strict";
@@ -8225,6 +8852,7 @@ var init_kumaGotchas = __esm({
8225
8852
  init_pathValidator();
8226
8853
  init_domainRules();
8227
8854
  init_kumaInject();
8855
+ GOTCHA_MERGE_THRESHOLD = 0.6;
8228
8856
  }
8229
8857
  });
8230
8858
 
@@ -8233,24 +8861,24 @@ var kumaMaintenance_exports = {};
8233
8861
  __export(kumaMaintenance_exports, {
8234
8862
  maybeRunAutoCleanup: () => maybeRunAutoCleanup
8235
8863
  });
8236
- import fs15 from "fs";
8237
- import path18 from "path";
8864
+ import fs17 from "fs";
8865
+ import path20 from "path";
8238
8866
  async function maybeRunAutoCleanup() {
8239
8867
  try {
8240
8868
  const root = getProjectRoot();
8241
- const stateFile = path18.join(root, GC_STATE_FILE);
8869
+ const stateFile = path20.join(root, GC_STATE_FILE);
8242
8870
  const now = Date.now();
8243
8871
  let lastRun = 0;
8244
- if (fs15.existsSync(stateFile)) {
8872
+ if (fs17.existsSync(stateFile)) {
8245
8873
  try {
8246
- lastRun = JSON.parse(fs15.readFileSync(stateFile, "utf-8")).lastRun || 0;
8874
+ lastRun = JSON.parse(fs17.readFileSync(stateFile, "utf-8")).lastRun || 0;
8247
8875
  } catch {
8248
8876
  lastRun = 0;
8249
8877
  }
8250
8878
  }
8251
8879
  if (now - lastRun < GC_INTERVAL_MS) return "";
8252
- fs15.mkdirSync(path18.dirname(stateFile), { recursive: true });
8253
- fs15.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
8880
+ fs17.mkdirSync(path20.dirname(stateFile), { recursive: true });
8881
+ fs17.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
8254
8882
  const { syncGotchasToDb: syncGotchasToDb2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
8255
8883
  await syncGotchasToDb2();
8256
8884
  const { runGarbageCollection: runGarbageCollection2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
@@ -8286,22 +8914,22 @@ __export(kumaPathRules_exports, {
8286
8914
  loadPathRules: () => loadPathRules,
8287
8915
  matchRulePath: () => matchRulePath
8288
8916
  });
8289
- import fs16 from "fs";
8290
- import path19 from "path";
8917
+ import fs18 from "fs";
8918
+ import path21 from "path";
8291
8919
  function rulesDir() {
8292
- return path19.join(getProjectRoot(), RULES_DIR);
8920
+ return path21.join(getProjectRoot(), RULES_DIR);
8293
8921
  }
8294
8922
  function ensureRulesDir() {
8295
8923
  try {
8296
8924
  const dir = rulesDir();
8297
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
8925
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
8298
8926
  } catch {
8299
8927
  }
8300
8928
  }
8301
8929
  function parseRuleFile(filePath) {
8302
8930
  try {
8303
- const raw = fs16.readFileSync(filePath, "utf-8");
8304
- const id = path19.basename(filePath, ".md");
8931
+ const raw = fs18.readFileSync(filePath, "utf-8");
8932
+ const id = path21.basename(filePath, ".md");
8305
8933
  const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
8306
8934
  if (!fm) {
8307
8935
  return { id, description: id, paths: ["*"], content: raw.trim(), filePath };
@@ -8326,7 +8954,7 @@ function loadPathRules() {
8326
8954
  ensureRulesDir();
8327
8955
  try {
8328
8956
  const dir = rulesDir();
8329
- return fs16.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseRuleFile(path19.join(dir, f))).filter((r) => r !== null);
8957
+ return fs18.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseRuleFile(path21.join(dir, f))).filter((r) => r !== null);
8330
8958
  } catch {
8331
8959
  return [];
8332
8960
  }
@@ -8414,8 +9042,8 @@ __export(kumaFlowCache_exports, {
8414
9042
  getFreshDomainFlow: () => getFreshDomainFlow,
8415
9043
  getHopsForDomain: () => getHopsForDomain
8416
9044
  });
8417
- import fs17 from "fs";
8418
- import path20 from "path";
9045
+ import fs19 from "fs";
9046
+ import path22 from "path";
8419
9047
  async function getFlowFreshness(domain) {
8420
9048
  try {
8421
9049
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
@@ -8450,26 +9078,26 @@ async function getFlowFreshness(domain) {
8450
9078
  }
8451
9079
  function resolveImport(fromFile, spec) {
8452
9080
  const root = getProjectRoot();
8453
- const fromDir = path20.dirname(fromFile);
9081
+ const fromDir = path22.dirname(fromFile);
8454
9082
  const candidates = [];
8455
9083
  if (spec.startsWith(".")) {
8456
- const abs = path20.resolve(fromDir, spec);
9084
+ const abs = path22.resolve(fromDir, spec);
8457
9085
  candidates.push(abs);
8458
9086
  } else {
8459
- candidates.push(path20.join(root, spec));
9087
+ candidates.push(path22.join(root, spec));
8460
9088
  }
8461
9089
  for (const c of candidates) {
8462
9090
  for (const ext of SOURCE_EXTENSIONS) {
8463
9091
  const withExt = `${c}${ext}`;
8464
- if (fs17.existsSync(withExt)) return path20.relative(root, withExt);
8465
- if (fs17.existsSync(c) && fs17.statSync(c).isDirectory()) {
8466
- const idx = path20.join(c, "index") + ext;
8467
- if (fs17.existsSync(idx)) return path20.relative(root, idx);
9092
+ if (fs19.existsSync(withExt)) return path22.relative(root, withExt);
9093
+ if (fs19.existsSync(c) && fs19.statSync(c).isDirectory()) {
9094
+ const idx = path22.join(c, "index") + ext;
9095
+ if (fs19.existsSync(idx)) return path22.relative(root, idx);
8468
9096
  }
8469
9097
  }
8470
- if (fs17.existsSync(c)) {
9098
+ if (fs19.existsSync(c)) {
8471
9099
  try {
8472
- if (fs17.statSync(c).isFile()) return path20.relative(root, c);
9100
+ if (fs19.statSync(c).isFile()) return path22.relative(root, c);
8473
9101
  } catch {
8474
9102
  }
8475
9103
  }
@@ -8478,13 +9106,13 @@ function resolveImport(fromFile, spec) {
8478
9106
  }
8479
9107
  function deriveHopsFromImports(entryFile, maxHops = 6) {
8480
9108
  const root = getProjectRoot();
8481
- const absEntry = path20.isAbsolute(entryFile) ? entryFile : path20.resolve(root, entryFile);
8482
- if (!fs17.existsSync(absEntry)) return [];
9109
+ const absEntry = path22.isAbsolute(entryFile) ? entryFile : path22.resolve(root, entryFile);
9110
+ if (!fs19.existsSync(absEntry)) return [];
8483
9111
  const hops = [];
8484
9112
  const seen = /* @__PURE__ */ new Set([entryFile]);
8485
- const fromName = path20.basename(absEntry);
9113
+ const fromName = path22.basename(absEntry);
8486
9114
  try {
8487
- const lines = fs17.readFileSync(absEntry, "utf-8").split("\n");
9115
+ const lines = fs19.readFileSync(absEntry, "utf-8").split("\n");
8488
9116
  for (const line of lines) {
8489
9117
  if (hops.length >= maxHops) break;
8490
9118
  const spec = matchImportPath(line);
@@ -8494,9 +9122,9 @@ function deriveHopsFromImports(entryFile, maxHops = 6) {
8494
9122
  seen.add(resolved);
8495
9123
  hops.push({
8496
9124
  from: fromName,
8497
- to: path20.basename(resolved),
9125
+ to: path22.basename(resolved),
8498
9126
  relation: "imports",
8499
- description: `${fromName} imports ${path20.basename(resolved)}`
9127
+ description: `${fromName} imports ${path22.basename(resolved)}`
8500
9128
  });
8501
9129
  }
8502
9130
  } catch {
@@ -8559,9 +9187,9 @@ async function getHopsForDomain(domain) {
8559
9187
  }
8560
9188
  try {
8561
9189
  const root = getProjectRoot();
8562
- const archPath = path20.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
8563
- if (fs17.existsSync(archPath)) {
8564
- const content = fs17.readFileSync(archPath, "utf-8");
9190
+ const archPath = path22.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
9191
+ if (fs19.existsSync(archPath)) {
9192
+ const content = fs19.readFileSync(archPath, "utf-8");
8565
9193
  const lines = content.split("\n");
8566
9194
  for (const line of lines) {
8567
9195
  const domMatch = line.match(new RegExp(`domain:\\s*${escapeRegex(domain)}\\s*\\|\\s*hops:\\s*(.+)`, "i"));
@@ -8606,7 +9234,7 @@ Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} |
8606
9234
  hops: derivedHops,
8607
9235
  filePaths: freshness.filePaths
8608
9236
  });
8609
- lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path20.basename(entry)} \u2014 cache refreshed.`);
9237
+ lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path22.basename(entry)} \u2014 cache refreshed.`);
8610
9238
  hops.splice(0, hops.length, ...derivedHops);
8611
9239
  } else {
8612
9240
  lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
@@ -8650,6 +9278,127 @@ var init_kumaFlowCache = __esm({
8650
9278
  }
8651
9279
  });
8652
9280
 
9281
+ // src/engine/packageBriefs.ts
9282
+ var packageBriefs_exports = {};
9283
+ __export(packageBriefs_exports, {
9284
+ BRIEFS_DIR: () => BRIEFS_DIR,
9285
+ briefFreshness: () => briefFreshness,
9286
+ buildPackageBrief: () => buildPackageBrief,
9287
+ readBrief: () => readBrief,
9288
+ refreshPackageBriefs: () => refreshPackageBriefs
9289
+ });
9290
+ import fs20 from "fs";
9291
+ import path23 from "path";
9292
+ function briefsDir(root) {
9293
+ return path23.join(root || getProjectRoot(), BRIEFS_DIR);
9294
+ }
9295
+ function slug(name) {
9296
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60) || "root";
9297
+ }
9298
+ async function buildPackageBrief(pkgName, pkgPath, _root) {
9299
+ const lines = [`# \u{1F4E6} ${pkgName}`, "", `_Auto-generated by Kuma \u2014 do not edit. Regenerate via map refresh._`, ""];
9300
+ if (pkgPath && pkgPath !== ".") lines.push(`path: \`${pkgPath}\``, "");
9301
+ try {
9302
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
9303
+ const db = await getDb2();
9304
+ const like = pkgPath && pkgPath !== "." ? `%${pkgPath}%` : `%`;
9305
+ const stmt = db.prepare(`
9306
+ SELECT n.file_path as fp, COUNT(*) as cnt FROM edges e
9307
+ JOIN nodes n ON n.id = e.target_id
9308
+ WHERE n.file_path LIKE ? AND n.file_path NOT LIKE '%test%'
9309
+ GROUP BY n.file_path ORDER BY cnt DESC LIMIT 8
9310
+ `);
9311
+ stmt.bind([like]);
9312
+ const hubs = [];
9313
+ while (stmt.step()) {
9314
+ const r = stmt.getAsObject();
9315
+ if (r.fp) hubs.push(`- \`${r.fp}\` (${r.cnt} refs)`);
9316
+ }
9317
+ stmt.free();
9318
+ lines.push(`## \u{1F3AF} Key files`, ...hubs.length > 0 ? hubs : ["- (no graph data yet \u2014 run a scan)"], "");
9319
+ try {
9320
+ const g = db.prepare(
9321
+ `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`
9322
+ );
9323
+ g.bind([like]);
9324
+ const rows = [];
9325
+ while (g.step()) {
9326
+ const r = g.getAsObject();
9327
+ rows.push(`- [${r.severity}] \`${r.file_path}\` \u2014 ${String(r.description).substring(0, 100)}`);
9328
+ }
9329
+ g.free();
9330
+ lines.push(`## \u26A0\uFE0F Gotchas`, ...rows.length > 0 ? rows : ["- none"], "");
9331
+ } catch {
9332
+ }
9333
+ try {
9334
+ const t = db.prepare(
9335
+ `SELECT DISTINCT file_path FROM nodes WHERE type = 'test' AND file_path LIKE ? LIMIT 10`
9336
+ );
9337
+ t.bind([like]);
9338
+ const tests = [];
9339
+ while (t.step()) {
9340
+ const r = t.getAsObject();
9341
+ if (r.file_path) tests.push(`- \`${r.file_path}\``);
9342
+ }
9343
+ t.free();
9344
+ lines.push(`## \u{1F9EA} Tests`, ...tests.length > 0 ? tests : ["- (run a scan to map tests)"], "");
9345
+ } catch {
9346
+ }
9347
+ } catch {
9348
+ lines.push("_(graph unavailable \u2014 brief will fill in after a scan)_", "");
9349
+ }
9350
+ return lines.join("\n");
9351
+ }
9352
+ async function refreshPackageBriefs() {
9353
+ const root = getProjectRoot();
9354
+ const dir = briefsDir(root);
9355
+ try {
9356
+ const ws = await getWorkspaceInfo(root);
9357
+ if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
9358
+ let count = 0;
9359
+ const pkgs = ws.isWorkspace ? ws.packages : [{ name: "root", path: ".", isRoot: true }];
9360
+ for (const p of pkgs.slice(0, 20)) {
9361
+ const pkg = p;
9362
+ const content = await buildPackageBrief(pkg.name, pkg.path, root);
9363
+ fs20.writeFileSync(path23.join(dir, `${slug(pkg.name)}.md`), content, "utf-8");
9364
+ count++;
9365
+ }
9366
+ return { count, dir };
9367
+ } catch {
9368
+ return { count: 0, dir };
9369
+ }
9370
+ }
9371
+ function briefFreshness(pkgName) {
9372
+ try {
9373
+ const fp = path23.join(briefsDir(), `${slug(pkgName)}.md`);
9374
+ if (!fs20.existsSync(fp)) return { exists: false, fresh: false, age: "missing" };
9375
+ const ageMs = Date.now() - fs20.statSync(fp).mtimeMs;
9376
+ return { exists: true, fresh: !isStale(Date.now() - ageMs, MAP_TTL_MS), age: formatAge(Date.now() - ageMs) };
9377
+ } catch {
9378
+ return { exists: false, fresh: false, age: "missing" };
9379
+ }
9380
+ }
9381
+ function readBrief(pkgName, maxChars = 1200) {
9382
+ try {
9383
+ const fp = path23.join(briefsDir(), `${slug(pkgName)}.md`);
9384
+ if (!fs20.existsSync(fp)) return null;
9385
+ const content = fs20.readFileSync(fp, "utf-8");
9386
+ return content.length > maxChars ? content.slice(0, maxChars) + "\n\u2026(truncated)" : content;
9387
+ } catch {
9388
+ return null;
9389
+ }
9390
+ }
9391
+ var BRIEFS_DIR;
9392
+ var init_packageBriefs = __esm({
9393
+ "src/engine/packageBriefs.ts"() {
9394
+ "use strict";
9395
+ init_pathValidator();
9396
+ init_workspaceIntelligence();
9397
+ init_cacheFreshness();
9398
+ BRIEFS_DIR = ".kuma/briefs";
9399
+ }
9400
+ });
9401
+
8653
9402
  // src/engine/kumaSearch.ts
8654
9403
  var kumaSearch_exports = {};
8655
9404
  __export(kumaSearch_exports, {
@@ -8662,8 +9411,8 @@ __export(kumaSearch_exports, {
8662
9411
  formatHybridResults: () => formatHybridResults,
8663
9412
  hybridSearch: () => hybridSearch
8664
9413
  });
8665
- import fs19 from "fs";
8666
- import path22 from "path";
9414
+ import fs22 from "fs";
9415
+ import path25 from "path";
8667
9416
  function expandQueryTerms(query) {
8668
9417
  const terms = query.toLowerCase().split(/[\s_-]+/).filter((t) => t.length > 2);
8669
9418
  const expanded = /* @__PURE__ */ new Map();
@@ -8726,13 +9475,13 @@ async function buildSearchVectors() {
8726
9475
  }
8727
9476
  try {
8728
9477
  const root = getProjectRoot();
8729
- const memDir = path22.join(root, ".kuma", "memories");
8730
- if (fs19.existsSync(memDir)) {
8731
- const files = fs19.readdirSync(memDir).filter((f) => f.endsWith(".md")).slice(0, SCALE_LIMITS.maxMemoryFiles);
9478
+ const memDir = path25.join(root, ".kuma", "memories");
9479
+ if (fs22.existsSync(memDir)) {
9480
+ const files = fs22.readdirSync(memDir).filter((f) => f.endsWith(".md")).slice(0, SCALE_LIMITS.maxMemoryFiles);
8732
9481
  for (const file of files) {
8733
9482
  try {
8734
- const fullPath = path22.join(memDir, file);
8735
- const content = fs19.readFileSync(fullPath, "utf-8");
9483
+ const fullPath = path25.join(memDir, file);
9484
+ const content = fs22.readFileSync(fullPath, "utf-8");
8736
9485
  const text = content.toLowerCase();
8737
9486
  const rawTerms = extractTerms(text);
8738
9487
  const termCounts = /* @__PURE__ */ new Map();
@@ -8743,7 +9492,7 @@ async function buildSearchVectors() {
8743
9492
  terms: termCounts,
8744
9493
  source: file.replace(/\.md$/, ""),
8745
9494
  sourceType: "memory",
8746
- recency: recencyScore(Math.floor(fs19.statSync(fullPath).mtimeMs / 1e3))
9495
+ recency: recencyScore(Math.floor(fs22.statSync(fullPath).mtimeMs / 1e3))
8747
9496
  });
8748
9497
  } catch {
8749
9498
  }
@@ -9060,14 +9809,14 @@ __export(architectureGuard_exports, {
9060
9809
  detectWorkspaceCircularDeps: () => detectWorkspaceCircularDeps,
9061
9810
  loadArchitectureRules: () => loadArchitectureRules
9062
9811
  });
9063
- import fs23 from "fs";
9064
- import path26 from "path";
9812
+ import fs26 from "fs";
9813
+ import path29 from "path";
9065
9814
  import fastGlob4 from "fast-glob";
9066
9815
  function loadArchitectureRules(root) {
9067
- const rulesPath = path26.join(root, ".kuma", "architecture.json");
9068
- if (fs23.existsSync(rulesPath)) {
9816
+ const rulesPath = path29.join(root, ".kuma", "architecture.json");
9817
+ if (fs26.existsSync(rulesPath)) {
9069
9818
  try {
9070
- const data = JSON.parse(fs23.readFileSync(rulesPath, "utf-8"));
9819
+ const data = JSON.parse(fs26.readFileSync(rulesPath, "utf-8"));
9071
9820
  if (Array.isArray(data.rules)) {
9072
9821
  return data.rules;
9073
9822
  }
@@ -9129,11 +9878,11 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
9129
9878
  const customRules = loadArchitectureRules(root);
9130
9879
  const importRegex = /(?:import|export\s+.*from|require\s*\()\s*['"]([^'"]+)['"]/g;
9131
9880
  for (const file of targetFiles) {
9132
- const fullPath = path26.isAbsolute(file) ? file : path26.join(root, file);
9133
- if (!fs23.existsSync(fullPath)) continue;
9881
+ const fullPath = path29.isAbsolute(file) ? file : path29.join(root, file);
9882
+ if (!fs26.existsSync(fullPath)) continue;
9134
9883
  let content = "";
9135
9884
  try {
9136
- content = fs23.readFileSync(fullPath, "utf-8");
9885
+ content = fs26.readFileSync(fullPath, "utf-8");
9137
9886
  } catch {
9138
9887
  continue;
9139
9888
  }
@@ -9143,7 +9892,7 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
9143
9892
  const importPath = match[1];
9144
9893
  if (wsInfo.isWorkspace && currentPkg && !currentPkg.isRoot) {
9145
9894
  if (importPath.startsWith("../") && (importPath.includes("/src/") || importPath.includes("/dist/"))) {
9146
- const resolvedTarget = path26.normalize(path26.resolve(path26.dirname(fullPath), importPath));
9895
+ const resolvedTarget = path29.normalize(path29.resolve(path29.dirname(fullPath), importPath));
9147
9896
  const targetPkg = findPackageForFile(resolvedTarget, wsInfo);
9148
9897
  if (targetPkg && targetPkg.name !== currentPkg.name) {
9149
9898
  violations.push({
@@ -9184,35 +9933,287 @@ var init_architectureGuard = __esm({
9184
9933
  }
9185
9934
  });
9186
9935
 
9936
+ // src/engine/docDrift.ts
9937
+ var docDrift_exports = {};
9938
+ __export(docDrift_exports, {
9939
+ collectDocFiles: () => collectDocFiles,
9940
+ extractClaims: () => extractClaims,
9941
+ formatDocDrift: () => formatDocDrift,
9942
+ scanDocDrift: () => scanDocDrift
9943
+ });
9944
+ import fs27 from "fs";
9945
+ import path30 from "path";
9946
+ import { execSync as execSync8 } from "child_process";
9947
+ function collectDocFiles(root) {
9948
+ const cwd = root || getProjectRoot();
9949
+ const out = [];
9950
+ const pushIfMd = (dir, rel) => {
9951
+ try {
9952
+ for (const f of fs27.readdirSync(dir)) {
9953
+ if (out.length >= MAX_DOCS) return;
9954
+ const full = path30.join(dir, f);
9955
+ try {
9956
+ if (fs27.statSync(full).isFile() && /\.md$/i.test(f)) out.push(rel(f));
9957
+ } catch {
9958
+ }
9959
+ }
9960
+ } catch {
9961
+ }
9962
+ };
9963
+ pushIfMd(path30.join(cwd, "docs"), (f) => path30.join("docs", f));
9964
+ pushIfMd(cwd, (f) => f);
9965
+ return out;
9966
+ }
9967
+ function isExemptDoc(relPath, content) {
9968
+ if (EXEMPT_PATTERN.test(relPath)) return true;
9969
+ const head = content.split("\n").slice(0, 15).join("\n");
9970
+ return EXEMPT_PATTERN.test(head);
9971
+ }
9972
+ function extractClaims(relPath, content) {
9973
+ const claims = [];
9974
+ const lines = content.split("\n");
9975
+ lines.forEach((line, idx) => {
9976
+ const lineNo = idx + 1;
9977
+ for (const m of line.matchAll(ENV_PATTERN)) {
9978
+ const name = m[1];
9979
+ if (ENV_ALLOWLIST_HINTS.some((h) => name.toLowerCase().includes(h))) {
9980
+ claims.push({ kind: "env", text: name, docFile: relPath, docLine: lineNo });
9981
+ }
9982
+ }
9983
+ for (const m of line.matchAll(ENDPOINT_PATTERN)) {
9984
+ const ep = m[1];
9985
+ if (ep.length > 60) continue;
9986
+ claims.push({ kind: "endpoint", text: ep, docFile: relPath, docLine: lineNo });
9987
+ }
9988
+ for (const m of line.matchAll(ABSENCE_PATTERN)) {
9989
+ const thing = (m[1] || "").trim();
9990
+ if (thing.length >= 3 && thing.length <= 40) {
9991
+ claims.push({ kind: "absence", text: thing, docFile: relPath, docLine: lineNo });
9992
+ }
9993
+ }
9994
+ });
9995
+ return claims;
9996
+ }
9997
+ function codeContainsBatch(root, needles) {
9998
+ const found = /* @__PURE__ */ new Set();
9999
+ const clean = [...new Set(needles.map((n) => n.replace(/"/g, "").trim()).filter((n) => n.length >= 3))];
10000
+ if (clean.length === 0) return found;
10001
+ for (let i = 0; i < clean.length; i += 50) {
10002
+ const chunk = clean.slice(i, i + 50);
10003
+ try {
10004
+ const args = chunk.map((n) => `-e "${n}"`).join(" ");
10005
+ const out = execSync8(
10006
+ `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`,
10007
+ { cwd: root, encoding: "utf-8", timeout: 8e3, maxBuffer: 256 * 1024 }
10008
+ ).trim();
10009
+ for (const n of chunk) {
10010
+ const isIdent = /^[A-Za-z0-9_]+$/.test(n);
10011
+ const hit = isIdent ? new RegExp(`\\b${n}\\b`).test(out) : out.includes(n);
10012
+ if (hit) found.add(n);
10013
+ }
10014
+ } catch {
10015
+ }
10016
+ }
10017
+ return found;
10018
+ }
10019
+ function scanDocDrift(root) {
10020
+ const cwd = root || getProjectRoot();
10021
+ const skipped = [];
10022
+ let claims = [];
10023
+ for (const rel of collectDocFiles(cwd)) {
10024
+ try {
10025
+ const content = fs27.readFileSync(path30.join(cwd, rel), "utf-8");
10026
+ if (isExemptDoc(rel, content)) {
10027
+ skipped.push(rel);
10028
+ continue;
10029
+ }
10030
+ claims.push(...extractClaims(rel, content));
10031
+ if (claims.length >= MAX_CLAIMS) break;
10032
+ } catch {
10033
+ }
10034
+ }
10035
+ claims = claims.slice(0, MAX_CLAIMS);
10036
+ const present = codeContainsBatch(cwd, claims.map((c) => c.text));
10037
+ const drifted = [];
10038
+ for (const c of claims) {
10039
+ if (c.kind === "absence") {
10040
+ if (present.has(c.text)) {
10041
+ drifted.push({ ...c, codeHint: `code references "${c.text}" but doc claims absence` });
10042
+ }
10043
+ } else {
10044
+ if (!present.has(c.text)) {
10045
+ drifted.push({ ...c, codeHint: `"${c.text}" found in no code/config file` });
10046
+ }
10047
+ }
10048
+ if (drifted.length >= 10) break;
10049
+ }
10050
+ return { checked: claims.length, drifted, skipped };
10051
+ }
10052
+ function formatDocDrift(checked, drifted, skipped) {
10053
+ if (drifted.length === 0) {
10054
+ return `\u{1F4C4} **Doc drift**: ${checked} claim(s) checked, no drift detected.`;
10055
+ }
10056
+ const lines = [
10057
+ `\u{1F4C4} **Doc drift detected** \u2014 ${drifted.length} stale claim(s) of ${checked} checked:`
10058
+ ];
10059
+ for (const d of drifted) {
10060
+ lines.push(` \u26A0\uFE0F [${d.kind}] "${d.text}" \u2014 ${d.docFile}:${d.docLine} vs ${d.codeHint}`);
10061
+ }
10062
+ if (skipped.length > 0) lines.push(` \u2139\uFE0F exempt (RFC/aspirational): ${skipped.slice(0, 3).join(", ")}`);
10063
+ lines.push(` \u{1F4A1} Confirm with a human, then patch the doc or the code.`);
10064
+ return lines.join("\n");
10065
+ }
10066
+ var MAX_DOCS, MAX_CLAIMS, EXEMPT_PATTERN, ENV_PATTERN, ENDPOINT_PATTERN, ABSENCE_PATTERN, ENV_ALLOWLIST_HINTS;
10067
+ var init_docDrift = __esm({
10068
+ "src/engine/docDrift.ts"() {
10069
+ "use strict";
10070
+ init_pathValidator();
10071
+ MAX_DOCS = 20;
10072
+ MAX_CLAIMS = 200;
10073
+ EXEMPT_PATTERN = /(rfc|proposal|draft|roadmap|aspirational|vision|wishlist)/i;
10074
+ ENV_PATTERN = /\b([A-Z][A-Z0-9_]{3,})\b/g;
10075
+ ENDPOINT_PATTERN = /(https?:\/\/[^\s)"']+|\/[a-z][a-z0-9/_.-]{2,})/g;
10076
+ ABSENCE_PATTERN = /\b(?:no|without|does not use|doesn't use|never)\s+([A-Za-z][A-Za-z0-9_. -]{2,40}?)(?:\.|,|;|$)/gi;
10077
+ ENV_ALLOWLIST_HINTS = ["key", "secret", "token", "url", "dsn", "endpoint", "host", "port", "id", "name", "path", "dir", "debug"];
10078
+ }
10079
+ });
10080
+
10081
+ // src/engine/guardLedger.ts
10082
+ var guardLedger_exports = {};
10083
+ __export(guardLedger_exports, {
10084
+ GUARD_FP_BUDGET_RATE: () => GUARD_FP_BUDGET_RATE,
10085
+ GUARD_FP_MIN_FEEDBACK: () => GUARD_FP_MIN_FEEDBACK,
10086
+ demoteSeverity: () => demoteSeverity,
10087
+ formatPrecisionLedger: () => formatPrecisionLedger,
10088
+ getDowntieredRules: () => getDowntieredRules,
10089
+ getRulePrecision: () => getRulePrecision,
10090
+ recordGuardFeedback: () => recordGuardFeedback,
10091
+ recordGuardFlag: () => recordGuardFlag
10092
+ });
10093
+ async function ensureLedgerSchema() {
10094
+ const db = await getDb();
10095
+ db.exec(`
10096
+ CREATE TABLE IF NOT EXISTS guard_ledger (
10097
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10098
+ rule TEXT NOT NULL,
10099
+ verdict TEXT NOT NULL DEFAULT 'flag',
10100
+ false_positive INTEGER NOT NULL DEFAULT 0,
10101
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
10102
+ );
10103
+ CREATE INDEX IF NOT EXISTS idx_ledger_rule ON guard_ledger(rule);
10104
+ `);
10105
+ saveDb();
10106
+ }
10107
+ async function recordGuardFlag(rule) {
10108
+ try {
10109
+ await ensureLedgerSchema();
10110
+ const db = await getDb();
10111
+ db.run(`INSERT INTO guard_ledger (rule, verdict) VALUES (?, 'flag')`, [rule]);
10112
+ saveDb();
10113
+ } catch {
10114
+ }
10115
+ }
10116
+ async function recordGuardFeedback(rule, wasFalsePositive) {
10117
+ try {
10118
+ await ensureLedgerSchema();
10119
+ const db = await getDb();
10120
+ db.run(
10121
+ `INSERT INTO guard_ledger (rule, verdict, false_positive) VALUES (?, 'feedback', ?)`,
10122
+ [rule, wasFalsePositive ? 1 : 0]
10123
+ );
10124
+ saveDb();
10125
+ } catch {
10126
+ }
10127
+ }
10128
+ async function getRulePrecision() {
10129
+ try {
10130
+ await ensureLedgerSchema();
10131
+ const db = await getDb();
10132
+ const stmt = db.prepare(`
10133
+ SELECT rule,
10134
+ SUM(CASE WHEN verdict = 'flag' THEN 1 ELSE 0 END) as flags,
10135
+ SUM(CASE WHEN verdict = 'feedback' THEN 1 ELSE 0 END) as feedbacks,
10136
+ SUM(CASE WHEN verdict = 'feedback' AND false_positive = 1 THEN 1 ELSE 0 END) as fps
10137
+ FROM guard_ledger GROUP BY rule
10138
+ `);
10139
+ const out = [];
10140
+ while (stmt.step()) {
10141
+ const r = stmt.getAsObject();
10142
+ const fpRate = r.feedbacks >= GUARD_FP_MIN_FEEDBACK ? r.fps / r.feedbacks : null;
10143
+ out.push({
10144
+ rule: r.rule,
10145
+ flags: Number(r.flags) || 0,
10146
+ feedbacks: Number(r.feedbacks) || 0,
10147
+ falsePositives: Number(r.fps) || 0,
10148
+ fpRate,
10149
+ overBudget: fpRate !== null && fpRate > GUARD_FP_BUDGET_RATE
10150
+ });
10151
+ }
10152
+ stmt.free();
10153
+ return out;
10154
+ } catch {
10155
+ return [];
10156
+ }
10157
+ }
10158
+ async function getDowntieredRules() {
10159
+ const precision = await getRulePrecision();
10160
+ return new Set(precision.filter((p) => p.overBudget).map((p) => p.rule));
10161
+ }
10162
+ function demoteSeverity(sev) {
10163
+ if (sev === "high") return "medium";
10164
+ if (sev === "medium") return "low";
10165
+ return "low";
10166
+ }
10167
+ function formatPrecisionLedger(rows) {
10168
+ if (rows.length === 0) return "\u{1F4CF} **Guard precision ledger** \u2014 no flags recorded yet.";
10169
+ const lines = ["\u{1F4CF} **Guard precision ledger** (per-rule, FP budget 50% / \u22655 feedbacks)", ""];
10170
+ for (const r of rows) {
10171
+ const fp = r.fpRate === null ? "n/a" : `${Math.round(r.fpRate * 100)}%`;
10172
+ lines.push(` ${r.overBudget ? "\u{1F53B}" : "\u2705"} \`${r.rule}\` \u2014 ${r.flags} flag(s), FP ${fp}${r.overBudget ? " OVER BUDGET (auto-downtiered)" : ""}`);
10173
+ }
10174
+ return lines.join("\n");
10175
+ }
10176
+ var GUARD_FP_BUDGET_RATE, GUARD_FP_MIN_FEEDBACK;
10177
+ var init_guardLedger = __esm({
10178
+ "src/engine/guardLedger.ts"() {
10179
+ "use strict";
10180
+ init_kumaDb();
10181
+ GUARD_FP_BUDGET_RATE = 0.5;
10182
+ GUARD_FP_MIN_FEEDBACK = 5;
10183
+ }
10184
+ });
10185
+
9187
10186
  // src/engine/kumaCheckpoint.ts
9188
10187
  var kumaCheckpoint_exports = {};
9189
10188
  __export(kumaCheckpoint_exports, {
9190
10189
  createCheckpoint: () => createCheckpoint,
10190
+ isRiskyEditPath: () => isRiskyEditPath,
9191
10191
  listCheckpoints: () => listCheckpoints,
10192
+ maybeAutoCheckpoint: () => maybeAutoCheckpoint,
9192
10193
  pruneCheckpoints: () => pruneCheckpoints,
9193
10194
  rollbackToCheckpoint: () => rollbackToCheckpoint
9194
10195
  });
9195
- import fs24 from "fs";
9196
- import path27 from "path";
10196
+ import fs28 from "fs";
10197
+ import path31 from "path";
9197
10198
  async function createCheckpoint(label, description) {
9198
10199
  try {
9199
10200
  const root = getProjectRoot();
9200
- const cpDir = path27.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
9201
- if (fs24.existsSync(cpDir)) {
10201
+ const cpDir = path31.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
10202
+ if (fs28.existsSync(cpDir)) {
9202
10203
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
9203
10204
  }
9204
- fs24.mkdirSync(cpDir, { recursive: true });
10205
+ fs28.mkdirSync(cpDir, { recursive: true });
9205
10206
  let gitHead;
9206
10207
  let isGitClean = false;
9207
10208
  try {
9208
- const { execSync: execSync7 } = await import("child_process");
9209
- gitHead = execSync7("git rev-parse HEAD", {
10209
+ const { execSync: execSync9 } = await import("child_process");
10210
+ gitHead = execSync9("git rev-parse HEAD", {
9210
10211
  cwd: root,
9211
10212
  encoding: "utf-8",
9212
10213
  timeout: 3e3,
9213
10214
  stdio: ["pipe", "pipe", "pipe"]
9214
10215
  }).trim();
9215
- const statusOut = execSync7("git status --porcelain", {
10216
+ const statusOut = execSync9("git status --porcelain", {
9216
10217
  cwd: root,
9217
10218
  encoding: "utf-8",
9218
10219
  timeout: 3e3,
@@ -9223,8 +10224,8 @@ async function createCheckpoint(label, description) {
9223
10224
  }
9224
10225
  const candidatePaths = /* @__PURE__ */ new Set();
9225
10226
  try {
9226
- const { execSync: execSync7 } = await import("child_process");
9227
- const gitOut = execSync7("git status --porcelain", {
10227
+ const { execSync: execSync9 } = await import("child_process");
10228
+ const gitOut = execSync9("git status --porcelain", {
9228
10229
  cwd: root,
9229
10230
  encoding: "utf-8",
9230
10231
  timeout: 4e3,
@@ -9267,15 +10268,15 @@ async function createCheckpoint(label, description) {
9267
10268
  }
9268
10269
  const files = [];
9269
10270
  for (const fp of candidatePaths) {
9270
- const fullPath = path27.resolve(root, fp);
10271
+ const fullPath = path31.resolve(root, fp);
9271
10272
  try {
9272
- if (fs24.existsSync(fullPath) && fs24.statSync(fullPath).isFile()) {
9273
- const content = fs24.readFileSync(fullPath, "utf-8");
10273
+ if (fs28.existsSync(fullPath) && fs28.statSync(fullPath).isFile()) {
10274
+ const content = fs28.readFileSync(fullPath, "utf-8");
9274
10275
  const hash = simpleHash2(content);
9275
- const snapFile = path27.join(cpDir, "files", `${fp}.kuma-snap`);
9276
- const fileDir = path27.dirname(snapFile);
9277
- if (!fs24.existsSync(fileDir)) fs24.mkdirSync(fileDir, { recursive: true });
9278
- fs24.writeFileSync(snapFile, content, "utf-8");
10276
+ const snapFile = path31.join(cpDir, "files", `${fp}.kuma-snap`);
10277
+ const fileDir = path31.dirname(snapFile);
10278
+ if (!fs28.existsSync(fileDir)) fs28.mkdirSync(fileDir, { recursive: true });
10279
+ fs28.writeFileSync(snapFile, content, "utf-8");
9279
10280
  files.push({ path: fp, hash });
9280
10281
  }
9281
10282
  } catch {
@@ -9283,7 +10284,7 @@ async function createCheckpoint(label, description) {
9283
10284
  }
9284
10285
  const db = await getDb();
9285
10286
  const dbData = db.export();
9286
- fs24.writeFileSync(path27.join(cpDir, "kuma.db"), Buffer.from(dbData));
10287
+ fs28.writeFileSync(path31.join(cpDir, "kuma.db"), Buffer.from(dbData));
9287
10288
  const manifest = {
9288
10289
  label,
9289
10290
  timestamp: Date.now(),
@@ -9293,8 +10294,8 @@ async function createCheckpoint(label, description) {
9293
10294
  dbSnapshot: true,
9294
10295
  description
9295
10296
  };
9296
- fs24.writeFileSync(
9297
- path27.join(cpDir, "manifest.json"),
10297
+ fs28.writeFileSync(
10298
+ path31.join(cpDir, "manifest.json"),
9298
10299
  JSON.stringify(manifest, null, 2),
9299
10300
  "utf-8"
9300
10301
  );
@@ -9322,29 +10323,29 @@ async function createCheckpoint(label, description) {
9322
10323
  async function rollbackToCheckpoint(label) {
9323
10324
  try {
9324
10325
  const root = getProjectRoot();
9325
- const cpDir = path27.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
9326
- const manifestPath = path27.join(cpDir, "manifest.json");
9327
- if (!fs24.existsSync(manifestPath)) {
10326
+ const cpDir = path31.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
10327
+ const manifestPath = path31.join(cpDir, "manifest.json");
10328
+ if (!fs28.existsSync(manifestPath)) {
9328
10329
  return `\u274C Checkpoint "${label}" not found. Available checkpoints:
9329
10330
 
9330
10331
  ${listCheckpoints()}`;
9331
10332
  }
9332
10333
  const manifest = JSON.parse(
9333
- fs24.readFileSync(manifestPath, "utf-8")
10334
+ fs28.readFileSync(manifestPath, "utf-8")
9334
10335
  );
9335
10336
  let restored = 0;
9336
10337
  let failed = 0;
9337
10338
  for (const f of manifest.files) {
9338
- const snapPath = path27.join(cpDir, "files", `${f.path}.kuma-snap`);
9339
- const legacyPath = path27.join(cpDir, "files", f.path);
9340
- const sourcePath = fs24.existsSync(snapPath) ? snapPath : legacyPath;
9341
- if (fs24.existsSync(sourcePath)) {
10339
+ const snapPath = path31.join(cpDir, "files", `${f.path}.kuma-snap`);
10340
+ const legacyPath = path31.join(cpDir, "files", f.path);
10341
+ const sourcePath = fs28.existsSync(snapPath) ? snapPath : legacyPath;
10342
+ if (fs28.existsSync(sourcePath)) {
9342
10343
  try {
9343
- const content = fs24.readFileSync(sourcePath, "utf-8");
9344
- const targetPath = path27.resolve(root, f.path);
9345
- const targetDir = path27.dirname(targetPath);
9346
- if (!fs24.existsSync(targetDir)) fs24.mkdirSync(targetDir, { recursive: true });
9347
- fs24.writeFileSync(targetPath, content, "utf-8");
10344
+ const content = fs28.readFileSync(sourcePath, "utf-8");
10345
+ const targetPath = path31.resolve(root, f.path);
10346
+ const targetDir = path31.dirname(targetPath);
10347
+ if (!fs28.existsSync(targetDir)) fs28.mkdirSync(targetDir, { recursive: true });
10348
+ fs28.writeFileSync(targetPath, content, "utf-8");
9348
10349
  restored++;
9349
10350
  } catch {
9350
10351
  failed++;
@@ -9353,8 +10354,8 @@ ${listCheckpoints()}`;
9353
10354
  }
9354
10355
  if (manifest.files.length === 0 && manifest.gitHead) {
9355
10356
  try {
9356
- const { execSync: execSync7 } = await import("child_process");
9357
- execSync7(`git checkout ${manifest.gitHead} -- .`, {
10357
+ const { execSync: execSync9 } = await import("child_process");
10358
+ execSync9(`git checkout ${manifest.gitHead} -- .`, {
9358
10359
  cwd: root,
9359
10360
  encoding: "utf-8",
9360
10361
  timeout: 8e3,
@@ -9365,12 +10366,12 @@ ${listCheckpoints()}`;
9365
10366
  failed++;
9366
10367
  }
9367
10368
  }
9368
- const dbSnapshotPath = path27.join(cpDir, "kuma.db");
9369
- if (fs24.existsSync(dbSnapshotPath)) {
9370
- const kumaDir = path27.join(root, ".kuma");
9371
- const dbPath = path27.join(kumaDir, "kuma.db");
9372
- const snapshotData = fs24.readFileSync(dbSnapshotPath);
9373
- fs24.writeFileSync(dbPath, snapshotData);
10369
+ const dbSnapshotPath = path31.join(cpDir, "kuma.db");
10370
+ if (fs28.existsSync(dbSnapshotPath)) {
10371
+ const kumaDir = path31.join(root, ".kuma");
10372
+ const dbPath = path31.join(kumaDir, "kuma.db");
10373
+ const snapshotData = fs28.readFileSync(dbSnapshotPath);
10374
+ fs28.writeFileSync(dbPath, snapshotData);
9374
10375
  resetDbInstance();
9375
10376
  }
9376
10377
  sessionMemory.recordToolCall("kuma_checkpoint_rollback", {
@@ -9395,18 +10396,18 @@ ${listCheckpoints()}`;
9395
10396
  function listCheckpoints() {
9396
10397
  try {
9397
10398
  const root = getProjectRoot();
9398
- const cpDir = path27.join(root, CHECKPOINT_DIR);
9399
- if (!fs24.existsSync(cpDir)) {
10399
+ const cpDir = path31.join(root, CHECKPOINT_DIR);
10400
+ if (!fs28.existsSync(cpDir)) {
9400
10401
  return "\u{1F4ED} No checkpoints found. Use kuma_safety({ action: 'checkpoint', label: 'pre-feature-x' }) to create one.";
9401
10402
  }
9402
- const entries = fs24.readdirSync(cpDir);
10403
+ const entries = fs28.readdirSync(cpDir);
9403
10404
  const checkpoints = [];
9404
10405
  for (const entry of entries) {
9405
- const manifestPath = path27.join(cpDir, entry, "manifest.json");
9406
- if (fs24.existsSync(manifestPath)) {
10406
+ const manifestPath = path31.join(cpDir, entry, "manifest.json");
10407
+ if (fs28.existsSync(manifestPath)) {
9407
10408
  try {
9408
10409
  const manifest = JSON.parse(
9409
- fs24.readFileSync(manifestPath, "utf-8")
10410
+ fs28.readFileSync(manifestPath, "utf-8")
9410
10411
  );
9411
10412
  checkpoints.push({ label: entry, manifest });
9412
10413
  } catch {
@@ -9438,12 +10439,12 @@ function listCheckpoints() {
9438
10439
  function pruneCheckpoints(keep = 5) {
9439
10440
  try {
9440
10441
  const root = getProjectRoot();
9441
- const cpDir = path27.join(root, CHECKPOINT_DIR);
9442
- if (!fs24.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
9443
- const entries = fs24.readdirSync(cpDir).map((e) => ({ name: e, time: fs24.statSync(path27.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
10442
+ const cpDir = path31.join(root, CHECKPOINT_DIR);
10443
+ if (!fs28.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
10444
+ const entries = fs28.readdirSync(cpDir).map((e) => ({ name: e, time: fs28.statSync(path31.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
9444
10445
  let removed = 0;
9445
10446
  for (let i = keep; i < entries.length; i++) {
9446
- fs24.rmSync(path27.join(cpDir, entries[i].name), { recursive: true, force: true });
10447
+ fs28.rmSync(path31.join(cpDir, entries[i].name), { recursive: true, force: true });
9447
10448
  removed++;
9448
10449
  }
9449
10450
  return removed > 0 ? `\u{1F9F9} Pruned **${removed} old checkpoint(s)**. Kept ${Math.min(keep, entries.length)} most recent.` : "\u2705 No checkpoints needed pruning.";
@@ -9463,7 +10464,61 @@ function simpleHash2(str) {
9463
10464
  }
9464
10465
  return Math.abs(hash).toString(36);
9465
10466
  }
9466
- var CHECKPOINT_DIR;
10467
+ function isRiskyEditPath(filePath) {
10468
+ return RISKY_EDIT_PATTERN.test(filePath);
10469
+ }
10470
+ function loadAutoCpDone(root) {
10471
+ try {
10472
+ const fp = path31.join(root, AUTO_CP_STATE);
10473
+ if (fs28.existsSync(fp)) {
10474
+ const parsed = JSON.parse(fs28.readFileSync(fp, "utf-8"));
10475
+ return parsed?.autoCheckpoints ?? {};
10476
+ }
10477
+ } catch {
10478
+ }
10479
+ return {};
10480
+ }
10481
+ function saveAutoCpDone(root, done) {
10482
+ try {
10483
+ const fp = path31.join(root, AUTO_CP_STATE);
10484
+ const parsed = fs28.existsSync(fp) ? JSON.parse(fs28.readFileSync(fp, "utf-8")) : {};
10485
+ parsed.autoCheckpoints = done;
10486
+ fs28.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
10487
+ } catch {
10488
+ }
10489
+ }
10490
+ function maybeAutoCheckpoint(filePath) {
10491
+ try {
10492
+ if (!isRiskyEditPath(filePath)) return null;
10493
+ const root = getProjectRoot();
10494
+ const done = loadAutoCpDone(root);
10495
+ if (done[filePath]) return null;
10496
+ const abs = path31.isAbsolute(filePath) ? filePath : path31.join(root, filePath);
10497
+ if (!fs28.existsSync(abs) || !fs28.statSync(abs).isFile()) return null;
10498
+ const label = `auto-pre-${sanitizeLabel(path31.basename(filePath))}`;
10499
+ const cpDir = path31.join(root, CHECKPOINT_DIR, label);
10500
+ if (!fs28.existsSync(cpDir)) fs28.mkdirSync(cpDir, { recursive: true });
10501
+ fs28.copyFileSync(abs, path31.join(cpDir, path31.basename(abs)));
10502
+ fs28.writeFileSync(
10503
+ path31.join(cpDir, "manifest.json"),
10504
+ JSON.stringify({
10505
+ label,
10506
+ timestamp: Date.now(),
10507
+ files: [{ path: filePath, hash: "" }],
10508
+ dbSnapshot: false,
10509
+ description: `Auto-checkpoint before risky edit of ${filePath} (issue #32)`,
10510
+ auto: true
10511
+ }),
10512
+ "utf-8"
10513
+ );
10514
+ done[filePath] = true;
10515
+ saveAutoCpDone(root, done);
10516
+ return label;
10517
+ } catch {
10518
+ return null;
10519
+ }
10520
+ }
10521
+ var CHECKPOINT_DIR, RISKY_EDIT_PATTERN, AUTO_CP_STATE;
9467
10522
  var init_kumaCheckpoint = __esm({
9468
10523
  "src/engine/kumaCheckpoint.ts"() {
9469
10524
  "use strict";
@@ -9471,6 +10526,8 @@ var init_kumaCheckpoint = __esm({
9471
10526
  init_pathValidator();
9472
10527
  init_sessionMemory();
9473
10528
  CHECKPOINT_DIR = ".kuma/checkpoints";
10529
+ RISKY_EDIT_PATTERN = /(migrations?|drizzle|schema|prisma|\.sql$|\.db$)/i;
10530
+ AUTO_CP_STATE = ".kuma/hook-state.json";
9474
10531
  }
9475
10532
  });
9476
10533
 
@@ -9483,8 +10540,8 @@ __export(init_exports, {
9483
10540
  generateInitMdContent: () => generateInitMdContent,
9484
10541
  runInit: () => runInit
9485
10542
  });
9486
- import fs25 from "fs";
9487
- import path28 from "path";
10543
+ import fs29 from "fs";
10544
+ import path32 from "path";
9488
10545
  function configFilePath(type) {
9489
10546
  switch (type) {
9490
10547
  case "claude":
@@ -9586,11 +10643,10 @@ function opencodeAgentsMdTemplate() {
9586
10643
  "Record what matters, skip what doesn't.",
9587
10644
  "",
9588
10645
  "<kuma_obedience>",
9589
- "## \u{1F512} Obedience (non-negotiable)",
9590
- '- MUST call `kuma_kuma_context({ action: "init" })` (or `kuma_context({ action: "init" })`) at session start.',
9591
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
9592
- '- MUST call `kuma_kuma_safety({ action: "verify" })` (or `kuma_safety({ action: "verify" })`) after edits.',
9593
- "- MUST NOT call actions outside the core actions.",
10646
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
10647
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
10648
+ '- AGENT-EXPECTED (~70%): call `kuma_kuma_context({ action: "init" })` (or `kuma_context`) first; record gotchas IMMEDIATELY; verify after edits.',
10649
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
9594
10650
  "</kuma_obedience>",
9595
10651
  "",
9596
10652
  "<kuma_workflow>",
@@ -9754,6 +10810,7 @@ function geminiTemplate() {
9754
10810
  '- **Gotchas:** Call `kuma_memory({ action: "gotcha", scope: "<file>", content: "<description>" })` IMMEDIATELY when any quirk/bug is discovered.',
9755
10811
  '- **Decisions:** Call `kuma_memory({ action: "decision", title: "...", rationale: "..." })` when choosing between options.',
9756
10812
  '- **Post-Edits:** Call `kuma_safety({ action: "verify" })` after modifying code to run affected package/unit tests.',
10813
+ "- **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.",
9757
10814
  "</kuma_obedience>",
9758
10815
  "",
9759
10816
  "### \u{1F375} Matcha & Kuma Harmony",
@@ -9765,9 +10822,12 @@ function geminiTemplate() {
9765
10822
  function opencodePluginTemplate() {
9766
10823
  return [
9767
10824
  "/**",
9768
- " * \u{1F43B} Kuma MCP \u2014 OpenCode Plugin",
9769
- " * Auto-injects gotcha warnings and safety guidance before file edits and commands in OpenCode.",
10825
+ " * \u{1F43B} Kuma MCP \u2014 OpenCode Plugin (risk-sensitive, token-efficient)",
9770
10826
  " * OpenCode loads all plugins from .opencode/plugins/*.js automatically.",
10827
+ " *",
10828
+ " * - tool.execute.before: throws ONLY on critical/high gotchas for the",
10829
+ " * file being edited (thrown message IS shown to the agent).",
10830
+ " * Medium/low gotchas stay silent (0 tokens) \u2014 use MCP tools for those.",
9771
10831
  " */",
9772
10832
  'import fs from "node:fs";',
9773
10833
  'import path from "node:path";',
@@ -9783,24 +10843,26 @@ function opencodePluginTemplate() {
9783
10843
  " return process.cwd();",
9784
10844
  "}",
9785
10845
  "",
9786
- "function getActiveGotchasForFile(filePath) {",
10846
+ "function getBlockingGotchasForFile(filePath) {",
9787
10847
  " try {",
9788
10848
  " const root = getProjectRoot();",
9789
10849
  ' const gotchasPath = path.join(root, ".kuma", "KNOWN_GOTCHAS.md");',
9790
10850
  " if (!fs.existsSync(gotchasPath)) return [];",
9791
10851
  ' const content = fs.readFileSync(gotchasPath, "utf-8");',
9792
- " const gotchas = [];",
9793
- ' const normalizedTarget = filePath.replace(/^[./]+/, "");',
10852
+ " const blocking = [];",
10853
+ ' const normalizedTarget = String(filePath).replace(/^[./]+/, "");',
9794
10854
  " const sections = content.split(/###\\s+/);",
9795
10855
  " for (const section of sections) {",
9796
10856
  " if (!section.trim()) continue;",
9797
10857
  ' const lines = section.split("\\n");',
9798
10858
  ' const title = lines[0] || "";',
9799
- " if (title.includes(normalizedTarget) || section.includes(normalizedTarget)) {",
9800
- " gotchas.push(title.trim());",
9801
- " }",
10859
+ " if (!title.includes(normalizedTarget) && !section.includes(normalizedTarget)) continue;",
10860
+ " const sevMatch = section.match(/\\*\\*Severity\\*\\*:\\s*(critical|high|medium|low)/i);",
10861
+ ' const severity = (sevMatch ? sevMatch[1] : "medium").toLowerCase();',
10862
+ ' if (severity !== "critical" && severity !== "high") continue;',
10863
+ " blocking.push(`[${severity}] ${title.trim()}`);",
9802
10864
  " }",
9803
- " return gotchas;",
10865
+ " return blocking.slice(0, 3);",
9804
10866
  " } catch {",
9805
10867
  " return [];",
9806
10868
  " }",
@@ -9809,20 +10871,35 @@ function opencodePluginTemplate() {
9809
10871
  "export const KumaPlugin = async () => {",
9810
10872
  " return {",
9811
10873
  ' "tool.execute.before": async (input, output) => {',
9812
- ' const tool = (input.tool || "").toLowerCase();',
10874
+ ' const tool = String(input.tool || "").toLowerCase();',
9813
10875
  " const args = output.args || {};",
9814
10876
  ' const targetPath = args.filePath || args.targetFile || args.path || args.file || "";',
9815
- ' if (targetPath && (tool.includes("edit") || tool.includes("write") || tool.includes("patch"))) {',
9816
- " const gotchas = getActiveGotchasForFile(String(targetPath));",
9817
- " if (gotchas.length > 0) {",
9818
- " console.warn(",
9819
- " `\\n\u{1F43B} [Kuma Alert] Active gotchas found for ${targetPath}:\\n` +",
9820
- ' gotchas.map((g) => ` \u26A0\uFE0F ${g}`).join("\\n") +',
9821
- ' `\\n\u{1F449} Remember to record any new gotchas via kuma_memory({ action: "gotcha" })\\n`',
9822
- " );",
9823
- " }",
10877
+ " if (!targetPath) return;",
10878
+ ' if (!(tool.includes("edit") || tool.includes("write") || tool.includes("patch"))) return;',
10879
+ " const blocking = getBlockingGotchasForFile(String(targetPath));",
10880
+ " if (blocking.length > 0) {",
10881
+ " throw new Error(",
10882
+ " `\u{1F43B} [Kuma] ${blocking.length} critical/high gotcha(s) for ${targetPath}:\\n` +",
10883
+ ' blocking.map((g) => ` \u{1F534} ${g}`).join("\\n") +',
10884
+ ' `\\nRead .kuma/KNOWN_GOTCHAS.md for workarounds, or run kuma_memory({ action: "gotcha" }) to list.`',
10885
+ " );",
9824
10886
  " }",
9825
10887
  " },",
10888
+ ' "file.edited": async (input) => {',
10889
+ " // Post-edit verify nudge (log channel only \u2014 never blocks).",
10890
+ ' const fp = input.filePath || input.path || (input.file && input.file.path) || "";',
10891
+ ' if (fp) console.warn(`\u{1F43B} [Kuma] ${fp} edited \u2014 consider kuma_safety({ action: "verify", scope: "${fp}" })`);',
10892
+ " },",
10893
+ ' "experimental.session.compacting": async (input, output) => {',
10894
+ " // Compaction survival set via the native channel (docs: plugins#compaction).",
10895
+ " try {",
10896
+ ' const { execSync } = await import("node:child_process");',
10897
+ ' const ctx = execSync("kuma hook pre-compact", { encoding: "utf-8", timeout: 8000 });',
10898
+ ' const parsed = JSON.parse(ctx || "{}");',
10899
+ ' const add = parsed?.hookSpecificOutput?.hookSpecificOutput?.additionalContext || "";',
10900
+ " if (add && output && Array.isArray(output.context)) output.context.push(add);",
10901
+ " } catch { /* compacting must never fail the session */ }",
10902
+ " },",
9826
10903
  " };",
9827
10904
  "};",
9828
10905
  ""
@@ -9882,15 +10959,26 @@ function generateInitMdContent() {
9882
10959
  "---",
9883
10960
  "",
9884
10961
  "<kuma_obedience>",
9885
- "## \u{1F512} OBEDIENCE (non-negotiable)",
10962
+ "## \u{1F512} OBEDIENCE \u2014 honest model (issue #39)",
9886
10963
  "",
9887
- '1. **MUST call `kuma_context({ action: "init" })`** at session start \u2014 before any code edit.',
9888
- '2. **MUST record gotchas IMMEDIATELY** when a bug/quirk is found (`kuma_memory({ action: "gotcha" })`).',
9889
- '3. **MUST call `kuma_safety({ action: "verify" })`** after code edits.',
9890
- "4. **MUST NOT call actions outside the core actions** in this file \u2014 anything else is rejected by the MCP schema.",
9891
- "5. If a Kuma call fails or returns an error, **re-read this file and retry** \u2014 do not silently continue.",
10964
+ "Rules are split by what actually guarantees them. A rule written as",
10965
+ "mandatory but silently skippable is worse than an explicit best-effort \u2014",
10966
+ "so every rule below names its mechanism or its fallback.",
9892
10967
  "",
9893
- "> These rules are enforced: the MCP schema rejects unknown actions, and Claude Code hooks inject gotchas before every edit.",
10968
+ "### HARNESS-ENFORCED (guaranteed by mechanism, zero agent choice)",
10969
+ "",
10970
+ "- **Unknown actions rejected** \u2014 enforced by the MCP schema (invalid action = error, not silence).",
10971
+ '- **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.',
10972
+ "- **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).",
10973
+ '- **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.',
10974
+ "",
10975
+ "### AGENT-EXPECTED (best-effort, designed for ~70% compliance)",
10976
+ "",
10977
+ '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.',
10978
+ '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.',
10979
+ '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.',
10980
+ "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.",
10981
+ "5. If a Kuma call fails or returns an error, **re-read this file and retry** \u2014 do not silently continue.",
9894
10982
  "</kuma_obedience>",
9895
10983
  "",
9896
10984
  "---",
@@ -10067,11 +11155,10 @@ function handleOpencodeSecondary(root, results) {
10067
11155
  "Record what matters, skip what doesn't.",
10068
11156
  "",
10069
11157
  "<kuma_obedience>",
10070
- "## \u{1F512} Obedience (non-negotiable)",
10071
- '- MUST call `kuma_kuma_context({ action: "init" })` at session start.',
10072
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
10073
- '- MUST call `kuma_kuma_safety({ action: "verify" })` after edits.',
10074
- "- MUST NOT call actions outside the core actions.",
11158
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
11159
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
11160
+ '- AGENT-EXPECTED (~70%): call `kuma_kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; verify after edits.',
11161
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
10075
11162
  "</kuma_obedience>",
10076
11163
  "",
10077
11164
  "<kuma_workflow>",
@@ -10088,21 +11175,21 @@ function handleOpencodeSecondary(root, results) {
10088
11175
  "",
10089
11176
  "\u{1F4D6} Full rules: `.kuma/init.md`"
10090
11177
  ].join("\n");
10091
- const skillPath = path28.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
11178
+ const skillPath = path32.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
10092
11179
  if (!results.some((r) => r.filePath === ".agents/skills/kuma/SKILL.md")) {
10093
11180
  try {
10094
- const dir = path28.dirname(skillPath);
10095
- if (fs25.existsSync(skillPath)) {
10096
- const existing = fs25.readFileSync(skillPath, "utf-8");
11181
+ const dir = path32.dirname(skillPath);
11182
+ if (fs29.existsSync(skillPath)) {
11183
+ const existing = fs29.readFileSync(skillPath, "utf-8");
10097
11184
  if (existing.includes("kuma-mcp")) {
10098
11185
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "skipped" });
10099
11186
  } else {
10100
- fs25.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
11187
+ fs29.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
10101
11188
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "appended" });
10102
11189
  }
10103
11190
  } else {
10104
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10105
- fs25.writeFileSync(skillPath, opencodeContent, "utf-8");
11191
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11192
+ fs29.writeFileSync(skillPath, opencodeContent, "utf-8");
10106
11193
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "created" });
10107
11194
  }
10108
11195
  } catch (err) {
@@ -10114,21 +11201,21 @@ function handleOpencodeSecondary(root, results) {
10114
11201
  });
10115
11202
  }
10116
11203
  }
10117
- const opencodeSkillPath = path28.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
11204
+ const opencodeSkillPath = path32.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
10118
11205
  if (!results.some((r) => r.filePath === ".opencode/skills/kuma/SKILL.md")) {
10119
11206
  try {
10120
- const dir = path28.dirname(opencodeSkillPath);
10121
- if (fs25.existsSync(opencodeSkillPath)) {
10122
- const existing = fs25.readFileSync(opencodeSkillPath, "utf-8");
11207
+ const dir = path32.dirname(opencodeSkillPath);
11208
+ if (fs29.existsSync(opencodeSkillPath)) {
11209
+ const existing = fs29.readFileSync(opencodeSkillPath, "utf-8");
10123
11210
  if (existing.includes("kuma-mcp")) {
10124
11211
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "skipped" });
10125
11212
  } else {
10126
- fs25.writeFileSync(opencodeSkillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
11213
+ fs29.writeFileSync(opencodeSkillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
10127
11214
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "appended" });
10128
11215
  }
10129
11216
  } else {
10130
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10131
- fs25.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
11217
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11218
+ fs29.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
10132
11219
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "created" });
10133
11220
  }
10134
11221
  } catch (err) {
@@ -10140,21 +11227,21 @@ function handleOpencodeSecondary(root, results) {
10140
11227
  });
10141
11228
  }
10142
11229
  }
10143
- const pluginPath = path28.resolve(root, ".opencode", "plugins", "kuma.js");
11230
+ const pluginPath = path32.resolve(root, ".opencode", "plugins", "kuma.js");
10144
11231
  if (!results.some((r) => r.filePath === ".opencode/plugins/kuma.js")) {
10145
11232
  try {
10146
- const dir = path28.dirname(pluginPath);
10147
- if (fs25.existsSync(pluginPath)) {
10148
- const existing = fs25.readFileSync(pluginPath, "utf-8");
11233
+ const dir = path32.dirname(pluginPath);
11234
+ if (fs29.existsSync(pluginPath)) {
11235
+ const existing = fs29.readFileSync(pluginPath, "utf-8");
10149
11236
  if (existing.includes("KumaPlugin") || existing.includes("kuma")) {
10150
11237
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "skipped" });
10151
11238
  } else {
10152
- fs25.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
11239
+ fs29.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
10153
11240
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "appended" });
10154
11241
  }
10155
11242
  } else {
10156
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10157
- fs25.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
11243
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11244
+ fs29.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
10158
11245
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "created" });
10159
11246
  }
10160
11247
  } catch (err) {
@@ -10168,21 +11255,21 @@ function handleOpencodeSecondary(root, results) {
10168
11255
  }
10169
11256
  }
10170
11257
  function handleCodexSecondary(root, results) {
10171
- const tomlPath = path28.resolve(root, ".codex/config.toml");
11258
+ const tomlPath = path32.resolve(root, ".codex/config.toml");
10172
11259
  if (results.some((r) => r.filePath === ".codex/config.toml")) return;
10173
11260
  try {
10174
- const dir = path28.dirname(tomlPath);
10175
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10176
- if (fs25.existsSync(tomlPath)) {
10177
- const existingContent = fs25.readFileSync(tomlPath, "utf-8");
11261
+ const dir = path32.dirname(tomlPath);
11262
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11263
+ if (fs29.existsSync(tomlPath)) {
11264
+ const existingContent = fs29.readFileSync(tomlPath, "utf-8");
10178
11265
  if (existingContent.includes("kuma")) {
10179
11266
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
10180
11267
  return;
10181
11268
  }
10182
- fs25.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
11269
+ fs29.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
10183
11270
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
10184
11271
  } else {
10185
- fs25.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
11272
+ fs29.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
10186
11273
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
10187
11274
  }
10188
11275
  } catch (err) {
@@ -10195,11 +11282,11 @@ function handleCodexSecondary(root, results) {
10195
11282
  }
10196
11283
  }
10197
11284
  function handleQwenSecondary(root, results) {
10198
- const settingsPath = path28.resolve(root, "settings.json");
11285
+ const settingsPath = path32.resolve(root, "settings.json");
10199
11286
  if (results.some((r) => r.filePath === "settings.json")) return;
10200
11287
  try {
10201
- if (fs25.existsSync(settingsPath)) {
10202
- const existingContent = fs25.readFileSync(settingsPath, "utf-8");
11288
+ if (fs29.existsSync(settingsPath)) {
11289
+ const existingContent = fs29.readFileSync(settingsPath, "utf-8");
10203
11290
  if (existingContent.includes("kuma")) {
10204
11291
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
10205
11292
  try {
@@ -10207,7 +11294,7 @@ function handleQwenSecondary(root, results) {
10207
11294
  parsed.mcpServers = parsed.mcpServers || {};
10208
11295
  if (!parsed.mcpServers.kuma) {
10209
11296
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10210
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11297
+ fs29.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10211
11298
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
10212
11299
  return;
10213
11300
  }
@@ -10222,13 +11309,13 @@ function handleQwenSecondary(root, results) {
10222
11309
  parsed.mcpServers = parsed.mcpServers || {};
10223
11310
  if (!parsed.mcpServers.kuma) {
10224
11311
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10225
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11312
+ fs29.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10226
11313
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
10227
11314
  }
10228
11315
  } catch {
10229
11316
  }
10230
11317
  } else {
10231
- fs25.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
11318
+ fs29.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
10232
11319
  results.push({ type: "qwen", filePath: "settings.json", action: "created" });
10233
11320
  }
10234
11321
  } catch (err) {
@@ -10251,11 +11338,10 @@ function getCombinedAgentsMd(selectedTypes) {
10251
11338
  "## SESSION WORKFLOW",
10252
11339
  "",
10253
11340
  "<kuma_obedience>",
10254
- "## \u{1F512} Obedience (non-negotiable)",
10255
- '- MUST call `[kuma_]context({ action: "init" })` at session start.',
10256
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
10257
- '- MUST call `[kuma_]safety({ action: "verify" })` after edits.',
10258
- "- MUST NOT call actions outside the 13 core actions.",
11341
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
11342
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
11343
+ '- AGENT-EXPECTED (~70%): call `[kuma_]context({ action: "init" })` first; record gotchas IMMEDIATELY; verify after edits.',
11344
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
10259
11345
  "</kuma_obedience>",
10260
11346
  "",
10261
11347
  "<kuma_workflow>",
@@ -10331,12 +11417,12 @@ function getCombinedAgentsMd(selectedTypes) {
10331
11417
  return lines.join("\n");
10332
11418
  }
10333
11419
  function handleAntigravityMcpConfig(root, results) {
10334
- const mcpPath = path28.resolve(root, ".agents/mcp_config.json");
11420
+ const mcpPath = path32.resolve(root, ".agents/mcp_config.json");
10335
11421
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
10336
11422
  try {
10337
- const mcpDir = path28.dirname(mcpPath);
10338
- if (fs25.existsSync(mcpPath)) {
10339
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
11423
+ const mcpDir = path32.dirname(mcpPath);
11424
+ if (fs29.existsSync(mcpPath)) {
11425
+ const existingContent = fs29.readFileSync(mcpPath, "utf-8");
10340
11426
  if (existingContent.includes("kuma")) {
10341
11427
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "skipped" });
10342
11428
  return;
@@ -10345,14 +11431,14 @@ function handleAntigravityMcpConfig(root, results) {
10345
11431
  const parsed = JSON.parse(existingContent);
10346
11432
  parsed.mcpServers = parsed.mcpServers || {};
10347
11433
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10348
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11434
+ fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10349
11435
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
10350
11436
  } catch {
10351
11437
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "error", error: "Invalid JSON in existing mcp_config.json" });
10352
11438
  }
10353
11439
  } else {
10354
- if (!fs25.existsSync(mcpDir)) fs25.mkdirSync(mcpDir, { recursive: true });
10355
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
11440
+ if (!fs29.existsSync(mcpDir)) fs29.mkdirSync(mcpDir, { recursive: true });
11441
+ fs29.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
10356
11442
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
10357
11443
  }
10358
11444
  } catch (err) {
@@ -10365,21 +11451,21 @@ function handleAntigravityMcpConfig(root, results) {
10365
11451
  }
10366
11452
  }
10367
11453
  function handleAntigravityRules(root, results) {
10368
- const rulePath = path28.resolve(root, ".agents", "rules", "kuma.md");
11454
+ const rulePath = path32.resolve(root, ".agents", "rules", "kuma.md");
10369
11455
  if (results.some((r) => r.filePath === ".agents/rules/kuma.md")) return;
10370
11456
  try {
10371
- const dir = path28.dirname(rulePath);
10372
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10373
- if (fs25.existsSync(rulePath)) {
10374
- const existing = fs25.readFileSync(rulePath, "utf-8");
11457
+ const dir = path32.dirname(rulePath);
11458
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11459
+ if (fs29.existsSync(rulePath)) {
11460
+ const existing = fs29.readFileSync(rulePath, "utf-8");
10375
11461
  if (existing.includes("Kuma MCP") || existing.includes("kuma-mcp")) {
10376
11462
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "skipped" });
10377
11463
  return;
10378
11464
  }
10379
- fs25.writeFileSync(rulePath, existing.trimEnd() + "\n\n---\n\n" + antigravityRuleTemplate(), "utf-8");
11465
+ fs29.writeFileSync(rulePath, existing.trimEnd() + "\n\n---\n\n" + antigravityRuleTemplate(), "utf-8");
10380
11466
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "appended" });
10381
11467
  } else {
10382
- fs25.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
11468
+ fs29.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
10383
11469
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "created" });
10384
11470
  }
10385
11471
  } catch (err) {
@@ -10392,19 +11478,19 @@ function handleAntigravityRules(root, results) {
10392
11478
  }
10393
11479
  }
10394
11480
  function handleGeminiMd(root, results) {
10395
- const geminiPath = path28.resolve(root, "GEMINI.md");
11481
+ const geminiPath = path32.resolve(root, "GEMINI.md");
10396
11482
  if (results.some((r) => r.filePath === "GEMINI.md")) return;
10397
11483
  try {
10398
- if (fs25.existsSync(geminiPath)) {
10399
- const existing = fs25.readFileSync(geminiPath, "utf-8");
11484
+ if (fs29.existsSync(geminiPath)) {
11485
+ const existing = fs29.readFileSync(geminiPath, "utf-8");
10400
11486
  if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
10401
11487
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "skipped" });
10402
11488
  return;
10403
11489
  }
10404
- fs25.writeFileSync(geminiPath, existing.trimEnd() + "\n\n---\n\n" + geminiTemplate(), "utf-8");
11490
+ fs29.writeFileSync(geminiPath, existing.trimEnd() + "\n\n---\n\n" + geminiTemplate(), "utf-8");
10405
11491
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "appended" });
10406
11492
  } else {
10407
- fs25.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
11493
+ fs29.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
10408
11494
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "created" });
10409
11495
  }
10410
11496
  } catch (err) {
@@ -10422,19 +11508,19 @@ function handleAntigravitySecondary(root, results) {
10422
11508
  handleGeminiMd(root, results);
10423
11509
  }
10424
11510
  function handleWindsurfSecondary(root, results) {
10425
- const rulesPath = path28.resolve(root, ".windsurfrules");
11511
+ const rulesPath = path32.resolve(root, ".windsurfrules");
10426
11512
  if (results.some((r) => r.filePath === ".windsurfrules")) return;
10427
11513
  try {
10428
- if (fs25.existsSync(rulesPath)) {
10429
- const existing = fs25.readFileSync(rulesPath, "utf-8");
11514
+ if (fs29.existsSync(rulesPath)) {
11515
+ const existing = fs29.readFileSync(rulesPath, "utf-8");
10430
11516
  if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
10431
11517
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "skipped" });
10432
11518
  return;
10433
11519
  }
10434
- fs25.writeFileSync(rulesPath, existing.trimEnd() + "\n\n---\n\n" + windsurfrulesTemplate(), "utf-8");
11520
+ fs29.writeFileSync(rulesPath, existing.trimEnd() + "\n\n---\n\n" + windsurfrulesTemplate(), "utf-8");
10435
11521
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "appended" });
10436
11522
  } else {
10437
- fs25.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
11523
+ fs29.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
10438
11524
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "created" });
10439
11525
  }
10440
11526
  } catch (err) {
@@ -10447,21 +11533,21 @@ function handleWindsurfSecondary(root, results) {
10447
11533
  }
10448
11534
  }
10449
11535
  function handleClineSecondary(root, results) {
10450
- const rooPath = path28.resolve(root, ".roo", "rules", "kuma.md");
11536
+ const rooPath = path32.resolve(root, ".roo", "rules", "kuma.md");
10451
11537
  if (!results.some((r) => r.filePath === ".roo/rules/kuma.md")) {
10452
11538
  try {
10453
- const dir = path28.dirname(rooPath);
10454
- if (fs25.existsSync(rooPath)) {
10455
- const existing = fs25.readFileSync(rooPath, "utf-8");
11539
+ const dir = path32.dirname(rooPath);
11540
+ if (fs29.existsSync(rooPath)) {
11541
+ const existing = fs29.readFileSync(rooPath, "utf-8");
10456
11542
  if (existing.includes("kuma-mcp") || existing.includes("Kuma MCP")) {
10457
11543
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "skipped" });
10458
11544
  } else {
10459
- fs25.writeFileSync(rooPath, existing.trimEnd() + "\n\n---\n\n" + rooRulesTemplate(), "utf-8");
11545
+ fs29.writeFileSync(rooPath, existing.trimEnd() + "\n\n---\n\n" + rooRulesTemplate(), "utf-8");
10460
11546
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "appended" });
10461
11547
  }
10462
11548
  } else {
10463
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10464
- fs25.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
11549
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11550
+ fs29.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
10465
11551
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "created" });
10466
11552
  }
10467
11553
  } catch (err) {
@@ -10473,21 +11559,21 @@ function handleClineSecondary(root, results) {
10473
11559
  });
10474
11560
  }
10475
11561
  }
10476
- const traePath = path28.resolve(root, ".trae", "rules", "kuma.md");
11562
+ const traePath = path32.resolve(root, ".trae", "rules", "kuma.md");
10477
11563
  if (!results.some((r) => r.filePath === ".trae/rules/kuma.md")) {
10478
11564
  try {
10479
- const dir = path28.dirname(traePath);
10480
- if (fs25.existsSync(traePath)) {
10481
- const existing = fs25.readFileSync(traePath, "utf-8");
11565
+ const dir = path32.dirname(traePath);
11566
+ if (fs29.existsSync(traePath)) {
11567
+ const existing = fs29.readFileSync(traePath, "utf-8");
10482
11568
  if (existing.includes("Kuma MCP")) {
10483
11569
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "skipped" });
10484
11570
  } else {
10485
- fs25.writeFileSync(traePath, existing.trimEnd() + "\n\n---\n\n" + traeRulesTemplate(), "utf-8");
11571
+ fs29.writeFileSync(traePath, existing.trimEnd() + "\n\n---\n\n" + traeRulesTemplate(), "utf-8");
10486
11572
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "appended" });
10487
11573
  }
10488
11574
  } else {
10489
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10490
- fs25.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
11575
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11576
+ fs29.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
10491
11577
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "created" });
10492
11578
  }
10493
11579
  } catch (err) {
@@ -10499,21 +11585,21 @@ function handleClineSecondary(root, results) {
10499
11585
  });
10500
11586
  }
10501
11587
  }
10502
- const qoderPath = path28.resolve(root, ".qoder", "rules", "kuma.md");
11588
+ const qoderPath = path32.resolve(root, ".qoder", "rules", "kuma.md");
10503
11589
  if (!results.some((r) => r.filePath === ".qoder/rules/kuma.md")) {
10504
11590
  try {
10505
- const dir = path28.dirname(qoderPath);
10506
- if (fs25.existsSync(qoderPath)) {
10507
- const existing = fs25.readFileSync(qoderPath, "utf-8");
11591
+ const dir = path32.dirname(qoderPath);
11592
+ if (fs29.existsSync(qoderPath)) {
11593
+ const existing = fs29.readFileSync(qoderPath, "utf-8");
10508
11594
  if (existing.includes("Kuma MCP")) {
10509
11595
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "skipped" });
10510
11596
  } else {
10511
- fs25.writeFileSync(qoderPath, existing.trimEnd() + "\n\n---\n\n" + qoderRulesTemplate(), "utf-8");
11597
+ fs29.writeFileSync(qoderPath, existing.trimEnd() + "\n\n---\n\n" + qoderRulesTemplate(), "utf-8");
10512
11598
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "appended" });
10513
11599
  }
10514
11600
  } else {
10515
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10516
- fs25.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
11601
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11602
+ fs29.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
10517
11603
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "created" });
10518
11604
  }
10519
11605
  } catch (err) {
@@ -10527,18 +11613,18 @@ function handleClineSecondary(root, results) {
10527
11613
  }
10528
11614
  }
10529
11615
  function handleAiderSecondary(root, results) {
10530
- const ymlPath = path28.resolve(root, ".aider.conf.yml");
11616
+ const ymlPath = path32.resolve(root, ".aider.conf.yml");
10531
11617
  if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
10532
11618
  try {
10533
11619
  const conventionsRef = "read: CONVENTIONS.md";
10534
- if (fs25.existsSync(ymlPath)) {
10535
- const existingContent = fs25.readFileSync(ymlPath, "utf-8");
11620
+ if (fs29.existsSync(ymlPath)) {
11621
+ const existingContent = fs29.readFileSync(ymlPath, "utf-8");
10536
11622
  if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
10537
11623
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
10538
11624
  return;
10539
11625
  }
10540
11626
  const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
10541
- fs25.writeFileSync(ymlPath, newContent, "utf-8");
11627
+ fs29.writeFileSync(ymlPath, newContent, "utf-8");
10542
11628
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
10543
11629
  } else {
10544
11630
  const content = [
@@ -10548,7 +11634,7 @@ function handleAiderSecondary(root, results) {
10548
11634
  conventionsRef,
10549
11635
  ""
10550
11636
  ].join("\n");
10551
- fs25.writeFileSync(ymlPath, content, "utf-8");
11637
+ fs29.writeFileSync(ymlPath, content, "utf-8");
10552
11638
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
10553
11639
  }
10554
11640
  } catch (err) {
@@ -10561,10 +11647,10 @@ function handleAiderSecondary(root, results) {
10561
11647
  }
10562
11648
  }
10563
11649
  function handleCopilotSecondary(root, results) {
10564
- const skillPath = path28.resolve(root, ".github/skills/kuma/SKILL.md");
11650
+ const skillPath = path32.resolve(root, ".github/skills/kuma/SKILL.md");
10565
11651
  if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
10566
11652
  try {
10567
- const dir = path28.dirname(skillPath);
11653
+ const dir = path32.dirname(skillPath);
10568
11654
  const content = [
10569
11655
  "---",
10570
11656
  "name: kuma-mcp",
@@ -10575,17 +11661,17 @@ function handleCopilotSecondary(root, results) {
10575
11661
  "",
10576
11662
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules."
10577
11663
  ].join("\n");
10578
- if (fs25.existsSync(skillPath)) {
10579
- const existingContent = fs25.readFileSync(skillPath, "utf-8");
11664
+ if (fs29.existsSync(skillPath)) {
11665
+ const existingContent = fs29.readFileSync(skillPath, "utf-8");
10580
11666
  if (existingContent.includes("kuma")) {
10581
11667
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
10582
11668
  return;
10583
11669
  }
10584
- fs25.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
11670
+ fs29.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
10585
11671
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
10586
11672
  } else {
10587
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10588
- fs25.writeFileSync(skillPath, content, "utf-8");
11673
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11674
+ fs29.writeFileSync(skillPath, content, "utf-8");
10589
11675
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
10590
11676
  }
10591
11677
  } catch (err) {
@@ -10598,21 +11684,21 @@ function handleCopilotSecondary(root, results) {
10598
11684
  }
10599
11685
  }
10600
11686
  function handleOpenclawSecondary(root, results) {
10601
- const mcpPath = path28.resolve(root, ".agents/mcp_config.json");
11687
+ const mcpPath = path32.resolve(root, ".agents/mcp_config.json");
10602
11688
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
10603
11689
  try {
10604
- const dir = path28.dirname(mcpPath);
10605
- if (fs25.existsSync(mcpPath)) {
10606
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
11690
+ const dir = path32.dirname(mcpPath);
11691
+ if (fs29.existsSync(mcpPath)) {
11692
+ const existingContent = fs29.readFileSync(mcpPath, "utf-8");
10607
11693
  if (existingContent.includes("kuma")) return;
10608
11694
  const parsed = JSON.parse(existingContent);
10609
11695
  parsed.mcpServers = parsed.mcpServers || {};
10610
11696
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10611
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11697
+ fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10612
11698
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
10613
11699
  } else {
10614
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10615
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
11700
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11701
+ fs29.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
10616
11702
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
10617
11703
  }
10618
11704
  } catch (err) {
@@ -10625,20 +11711,20 @@ function handleOpenclawSecondary(root, results) {
10625
11711
  }
10626
11712
  }
10627
11713
  function handleInitMdGeneration(root, results) {
10628
- const initMdPath = path28.resolve(root, ".kuma/init.md");
11714
+ const initMdPath = path32.resolve(root, ".kuma/init.md");
10629
11715
  try {
10630
- const kumaDir = path28.dirname(initMdPath);
10631
- if (!fs25.existsSync(kumaDir)) fs25.mkdirSync(kumaDir, { recursive: true });
10632
- if (fs25.existsSync(initMdPath)) {
10633
- const existing = fs25.readFileSync(initMdPath, "utf-8");
11716
+ const kumaDir = path32.dirname(initMdPath);
11717
+ if (!fs29.existsSync(kumaDir)) fs29.mkdirSync(kumaDir, { recursive: true });
11718
+ if (fs29.existsSync(initMdPath)) {
11719
+ const existing = fs29.readFileSync(initMdPath, "utf-8");
10634
11720
  if (existing.includes("_Generated by Kuma MCP_")) {
10635
11721
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "skipped" });
10636
11722
  return;
10637
11723
  }
10638
- fs25.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
11724
+ fs29.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
10639
11725
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "appended" });
10640
11726
  } else {
10641
- fs25.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
11727
+ fs29.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
10642
11728
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "created" });
10643
11729
  }
10644
11730
  } catch (err) {
@@ -10651,20 +11737,20 @@ function handleInitMdGeneration(root, results) {
10651
11737
  }
10652
11738
  }
10653
11739
  function handleCodewhaleSecondary(root, results) {
10654
- const mcpPath = path28.resolve(root, ".codewhale/mcp.json");
11740
+ const mcpPath = path32.resolve(root, ".codewhale/mcp.json");
10655
11741
  if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
10656
11742
  try {
10657
- const dir = path28.dirname(mcpPath);
10658
- if (fs25.existsSync(mcpPath)) {
10659
- const existingContent = fs25.readFileSync(mcpPath, "utf-8");
11743
+ const dir = path32.dirname(mcpPath);
11744
+ if (fs29.existsSync(mcpPath)) {
11745
+ const existingContent = fs29.readFileSync(mcpPath, "utf-8");
10660
11746
  if (existingContent.includes("kuma")) return;
10661
11747
  const parsed = JSON.parse(existingContent);
10662
11748
  parsed.mcpServers = parsed.mcpServers || {};
10663
11749
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10664
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11750
+ fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10665
11751
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
10666
11752
  } else {
10667
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11753
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
10668
11754
  const config = {
10669
11755
  mcpServers: {
10670
11756
  kuma: {
@@ -10674,7 +11760,7 @@ function handleCodewhaleSecondary(root, results) {
10674
11760
  }
10675
11761
  }
10676
11762
  };
10677
- fs25.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
11763
+ fs29.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
10678
11764
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
10679
11765
  }
10680
11766
  } catch (err) {
@@ -10687,7 +11773,7 @@ function handleCodewhaleSecondary(root, results) {
10687
11773
  }
10688
11774
  }
10689
11775
  function handleClaudeSecondary(root, results) {
10690
- const settingsPath = path28.resolve(root, ".claude", "settings.json");
11776
+ const settingsPath = path32.resolve(root, ".claude", "settings.json");
10691
11777
  const hookBlock = {
10692
11778
  hooks: {
10693
11779
  PreToolUse: [
@@ -10699,32 +11785,56 @@ function handleClaudeSecondary(root, results) {
10699
11785
  matcher: "Bash",
10700
11786
  hooks: [{ type: "command", command: "kuma hook pre-bash" }]
10701
11787
  }
11788
+ ],
11789
+ // Issue #38: SessionStart serves the cached repo brief before the
11790
+ // first agent turn — zero agent choice involved.
11791
+ SessionStart: [
11792
+ {
11793
+ matcher: "startup",
11794
+ hooks: [{ type: "command", command: "kuma hook session-start" }]
11795
+ }
11796
+ ],
11797
+ // Compaction survival set (goal + critical gotchas + dirty files).
11798
+ PreCompact: [
11799
+ {
11800
+ matcher: "",
11801
+ hooks: [{ type: "command", command: "kuma hook pre-compact" }]
11802
+ }
10702
11803
  ]
10703
11804
  }
10704
11805
  };
10705
11806
  try {
10706
- if (fs25.existsSync(settingsPath)) {
10707
- const existing = JSON.parse(fs25.readFileSync(settingsPath, "utf-8"));
11807
+ if (fs29.existsSync(settingsPath)) {
11808
+ const existing = JSON.parse(fs29.readFileSync(settingsPath, "utf-8"));
11809
+ const existingHooks = existing.hooks || {};
11810
+ const mergedPre = [...existingHooks.PreToolUse || []];
11811
+ for (const entry of hookBlock.hooks.PreToolUse) {
11812
+ if (!mergedPre.some((h) => h.matcher === entry.matcher)) mergedPre.push(entry);
11813
+ }
11814
+ const mergedStart = [...existingHooks.SessionStart || []];
11815
+ for (const entry of hookBlock.hooks.SessionStart) {
11816
+ const hasKuma = mergedStart.some(
11817
+ (h) => JSON.stringify(h.hooks || []).includes("kuma hook session-start")
11818
+ );
11819
+ if (!hasKuma) mergedStart.push(entry);
11820
+ }
11821
+ const mergedCompact = [...existingHooks.PreCompact || []];
11822
+ const hasPreCompact = mergedCompact.some(
11823
+ (h) => JSON.stringify(h.hooks || []).includes("kuma hook pre-compact")
11824
+ );
11825
+ if (!hasPreCompact) {
11826
+ mergedCompact.push(...hookBlock.hooks.PreCompact);
11827
+ }
10708
11828
  const merged = {
10709
11829
  ...existing,
10710
- hooks: {
10711
- ...existing.hooks || {},
10712
- ...hookBlock.hooks
10713
- }
11830
+ hooks: { ...existingHooks, PreToolUse: mergedPre, SessionStart: mergedStart, PreCompact: mergedCompact }
10714
11831
  };
10715
- const matchers = new Set((merged.hooks.PreToolUse || []).map((h) => h.matcher));
10716
- for (const entry of hookBlock.hooks.PreToolUse) {
10717
- if (!matchers.has(entry.matcher)) {
10718
- merged.hooks.PreToolUse = [...merged.hooks.PreToolUse || [], entry];
10719
- matchers.add(entry.matcher);
10720
- }
10721
- }
10722
- fs25.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
11832
+ fs29.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
10723
11833
  results.push({ type: "claude", filePath: ".claude/settings.json", action: "appended" });
10724
11834
  } else {
10725
- const dir = path28.dirname(settingsPath);
10726
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10727
- fs25.writeFileSync(settingsPath, JSON.stringify(hookBlock, null, 2) + "\n", "utf-8");
11835
+ const dir = path32.dirname(settingsPath);
11836
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11837
+ fs29.writeFileSync(settingsPath, JSON.stringify(hookBlock, null, 2) + "\n", "utf-8");
10728
11838
  results.push({ type: "claude", filePath: ".claude/settings.json", action: "created" });
10729
11839
  }
10730
11840
  } catch (err) {
@@ -10735,10 +11845,10 @@ function handleClaudeSecondary(root, results) {
10735
11845
  error: err instanceof Error ? err.message : String(err)
10736
11846
  });
10737
11847
  }
10738
- const skillPath = path28.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
11848
+ const skillPath = path32.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
10739
11849
  if (!results.some((r) => r.filePath === ".claude/skills/kuma/SKILL.md")) {
10740
11850
  try {
10741
- const dir = path28.dirname(skillPath);
11851
+ const dir = path32.dirname(skillPath);
10742
11852
  const content = [
10743
11853
  "---",
10744
11854
  "name: kuma-mcp",
@@ -10750,17 +11860,17 @@ function handleClaudeSecondary(root, results) {
10750
11860
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules.",
10751
11861
  "\u{1F4C2} Readable state: `.kuma/KNOWN_GOTCHAS.md` (gotchas) \xB7 `.kuma/memories/decisions.md` (decisions)"
10752
11862
  ].join("\n");
10753
- if (fs25.existsSync(skillPath)) {
10754
- const existing = fs25.readFileSync(skillPath, "utf-8");
11863
+ if (fs29.existsSync(skillPath)) {
11864
+ const existing = fs29.readFileSync(skillPath, "utf-8");
10755
11865
  if (existing.includes("kuma-mcp")) {
10756
11866
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "skipped" });
10757
11867
  } else {
10758
- fs25.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + content, "utf-8");
11868
+ fs29.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + content, "utf-8");
10759
11869
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "appended" });
10760
11870
  }
10761
11871
  } else {
10762
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10763
- fs25.writeFileSync(skillPath, content, "utf-8");
11872
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11873
+ fs29.writeFileSync(skillPath, content, "utf-8");
10764
11874
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "created" });
10765
11875
  }
10766
11876
  } catch (err) {
@@ -10773,19 +11883,120 @@ function handleClaudeSecondary(root, results) {
10773
11883
  }
10774
11884
  }
10775
11885
  }
11886
+ function writeHooksFile(root, relPath, type, results, build) {
11887
+ const fullPath = path32.resolve(root, relPath);
11888
+ try {
11889
+ let existing = {};
11890
+ let existed = false;
11891
+ if (fs29.existsSync(fullPath)) {
11892
+ existed = true;
11893
+ try {
11894
+ existing = JSON.parse(fs29.readFileSync(fullPath, "utf-8"));
11895
+ } catch {
11896
+ existing = {};
11897
+ }
11898
+ } else {
11899
+ const dir = path32.dirname(fullPath);
11900
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11901
+ }
11902
+ const { doc, changed } = build(existing);
11903
+ if (!changed) {
11904
+ results.push({ type, filePath: relPath, action: "skipped" });
11905
+ return;
11906
+ }
11907
+ fs29.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
11908
+ results.push({ type, filePath: relPath, action: existed ? "appended" : "created" });
11909
+ } catch (err) {
11910
+ results.push({ type, filePath: relPath, action: "error", error: err instanceof Error ? err.message : String(err) });
11911
+ }
11912
+ }
11913
+ function dedupeCommands(list, entry) {
11914
+ const cmd = String(entry.command || "");
11915
+ if (list.some((e) => String(e.command || "") === cmd)) return false;
11916
+ list.push(entry);
11917
+ return true;
11918
+ }
11919
+ function handleCursorHooks(root, results) {
11920
+ writeHooksFile(root, ".cursor/hooks.json", "cursor", results, (existing) => {
11921
+ const doc = { version: 1, ...existing };
11922
+ const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
11923
+ let changed = false;
11924
+ const after = Array.isArray(hooks.afterFileEdit) ? [...hooks.afterFileEdit] : [];
11925
+ changed = dedupeCommands(after, { command: "kuma hook pre-edit" }) || changed;
11926
+ const before = Array.isArray(hooks.beforeShellExecution) ? [...hooks.beforeShellExecution] : [];
11927
+ changed = dedupeCommands(before, { command: "kuma hook pre-bash" }) || changed;
11928
+ hooks.afterFileEdit = after;
11929
+ hooks.beforeShellExecution = before;
11930
+ doc.version = 1;
11931
+ doc.hooks = hooks;
11932
+ return { doc, changed };
11933
+ });
11934
+ }
11935
+ function handleWindsurfHooks(root, results) {
11936
+ writeHooksFile(root, ".windsurf/hooks.json", "windsurf", results, (existing) => {
11937
+ const doc = { ...existing };
11938
+ const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
11939
+ let changed = false;
11940
+ const preWrite = Array.isArray(hooks.pre_write_code) ? [...hooks.pre_write_code] : [];
11941
+ changed = dedupeCommands(preWrite, { command: "kuma hook pre-edit", show_output: false }) || changed;
11942
+ const preRun = Array.isArray(hooks.pre_run_command) ? [...hooks.pre_run_command] : [];
11943
+ changed = dedupeCommands(preRun, { command: "kuma hook pre-bash", show_output: false }) || changed;
11944
+ hooks.pre_write_code = preWrite;
11945
+ hooks.pre_run_command = preRun;
11946
+ doc.hooks = hooks;
11947
+ return { doc, changed };
11948
+ });
11949
+ }
11950
+ function handleCopilotHooks(root, results) {
11951
+ writeHooksFile(root, ".vscode/settings.json", "copilot", results, (existing) => {
11952
+ const doc = { ...existing };
11953
+ const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
11954
+ let changed = false;
11955
+ const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
11956
+ const hasEdit = pre.some((e) => String(e.command || "") === "kuma hook pre-edit");
11957
+ if (!hasEdit) {
11958
+ pre.push({ matcher: "Edit|Write|MultiEdit", type: "command", command: "kuma hook pre-edit", timeout: 15 });
11959
+ changed = true;
11960
+ }
11961
+ const hasBash = pre.some((e) => String(e.command || "") === "kuma hook pre-bash");
11962
+ if (!hasBash) {
11963
+ pre.push({ matcher: "Bash", type: "command", command: "kuma hook pre-bash", timeout: 15 });
11964
+ changed = true;
11965
+ }
11966
+ hooks.PreToolUse = pre;
11967
+ doc.hooks = hooks;
11968
+ return { doc, changed };
11969
+ });
11970
+ }
11971
+ function handleCodexHooks(root, results) {
11972
+ writeHooksFile(root, ".codex/hooks.json", "codex", results, (existing) => {
11973
+ const doc = { ...existing };
11974
+ const hooks = doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {};
11975
+ let changed = false;
11976
+ const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
11977
+ const hasEdit = pre.some((e) => String(e.command || "") === "kuma hook pre-edit");
11978
+ if (!hasEdit) {
11979
+ pre.push({ matcher: "Edit|Write", type: "command", command: "kuma hook pre-edit" });
11980
+ changed = true;
11981
+ }
11982
+ hooks.PreToolUse = pre;
11983
+ doc.hooks = hooks;
11984
+ return { doc, changed };
11985
+ });
11986
+ }
10776
11987
  function handleCursorGotchaRules(root, results) {
10777
- const rulesDir2 = path28.resolve(root, ".cursor", "rules", "kuma-gotchas");
11988
+ const rulesDir2 = path32.resolve(root, ".cursor", "rules", "kuma-gotchas");
10778
11989
  try {
10779
11990
  let gotchas = [];
10780
11991
  try {
10781
11992
  gotchas = getActiveGotchas().filter((g) => g.severity === "high" || g.severity === "critical");
10782
11993
  } catch {
10783
11994
  }
10784
- if (fs25.existsSync(rulesDir2)) {
10785
- for (const f of fs25.readdirSync(rulesDir2)) {
11995
+ if (fs29.existsSync(rulesDir2)) {
11996
+ for (const f of fs29.readdirSync(rulesDir2)) {
10786
11997
  if (f.endsWith(".mdc")) {
10787
11998
  try {
10788
- fs25.rmSync(path28.join(rulesDir2, f), { force: true });
11999
+ fs29.rmSync(path32.join(rulesDir2, f), { force: true });
10789
12000
  } catch {
10790
12001
  }
10791
12002
  }
@@ -10795,13 +12006,13 @@ function handleCursorGotchaRules(root, results) {
10795
12006
  results.push({ type: "cursor", filePath: ".cursor/rules/kuma-gotchas/", action: "skipped" });
10796
12007
  return;
10797
12008
  }
10798
- fs25.mkdirSync(rulesDir2, { recursive: true });
12009
+ fs29.mkdirSync(rulesDir2, { recursive: true });
10799
12010
  const seen = /* @__PURE__ */ new Set();
10800
12011
  let created = 0;
10801
12012
  for (const g of gotchas) {
10802
- 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);
10803
- if (!slug || seen.has(slug)) continue;
10804
- seen.add(slug);
12013
+ 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);
12014
+ if (!slug2 || seen.has(slug2)) continue;
12015
+ seen.add(slug2);
10805
12016
  const md = [
10806
12017
  "---",
10807
12018
  `description: "KUMA gotcha \u2014 ${g.description.substring(0, 80)}"`,
@@ -10816,7 +12027,7 @@ function handleCursorGotchaRules(root, results) {
10816
12027
  "`kuma_context({ action: 'history', target: '<file>' })`",
10817
12028
  ""
10818
12029
  ].join("\n");
10819
- fs25.writeFileSync(path28.join(rulesDir2, `${slug}.mdc`), md, "utf-8");
12030
+ fs29.writeFileSync(path32.join(rulesDir2, `${slug2}.mdc`), md, "utf-8");
10820
12031
  created++;
10821
12032
  }
10822
12033
  results.push({
@@ -10843,28 +12054,28 @@ function runInit(options) {
10843
12054
  let agentsMdHandled = false;
10844
12055
  for (const type of selected) {
10845
12056
  const relativePath = configFilePath(type);
10846
- const fullPath = path28.resolve(root, relativePath);
12057
+ const fullPath = path32.resolve(root, relativePath);
10847
12058
  const getTemplate = TEMPLATES[type];
10848
12059
  try {
10849
12060
  if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
10850
12061
  agentsMdHandled = true;
10851
12062
  const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
10852
- if (fs25.existsSync(fullPath)) {
12063
+ if (fs29.existsSync(fullPath)) {
10853
12064
  if (options.skipExisting) {
10854
12065
  results.push({ type, filePath: relativePath, action: "skipped" });
10855
12066
  } else {
10856
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
12067
+ const existingContent = fs29.readFileSync(fullPath, "utf-8");
10857
12068
  if (existingContent.includes("_Generated by Kuma MCP_")) {
10858
12069
  results.push({ type, filePath: relativePath, action: "skipped" });
10859
12070
  } else {
10860
- fs25.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
12071
+ fs29.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
10861
12072
  results.push({ type, filePath: relativePath, action: "appended" });
10862
12073
  }
10863
12074
  }
10864
12075
  } else {
10865
- const dir = path28.dirname(fullPath);
10866
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10867
- fs25.writeFileSync(fullPath, combinedContent, "utf-8");
12076
+ const dir = path32.dirname(fullPath);
12077
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
12078
+ fs29.writeFileSync(fullPath, combinedContent, "utf-8");
10868
12079
  results.push({ type, filePath: relativePath, action: "created" });
10869
12080
  }
10870
12081
  if (selectedSet.has("opencode")) handleOpencodeSecondary(root, results);
@@ -10876,12 +12087,12 @@ function runInit(options) {
10876
12087
  continue;
10877
12088
  } else {
10878
12089
  const template = getTemplate();
10879
- if (fs25.existsSync(fullPath)) {
12090
+ if (fs29.existsSync(fullPath)) {
10880
12091
  if (options.skipExisting) {
10881
12092
  results.push({ type, filePath: relativePath, action: "skipped" });
10882
12093
  continue;
10883
12094
  }
10884
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
12095
+ const existingContent = fs29.readFileSync(fullPath, "utf-8");
10885
12096
  if (existingContent.includes("_Generated by Kuma MCP_")) {
10886
12097
  if (type === "antigravity") {
10887
12098
  handleAntigravitySecondary(root, results);
@@ -10898,14 +12109,14 @@ function runInit(options) {
10898
12109
  continue;
10899
12110
  }
10900
12111
  const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
10901
- fs25.writeFileSync(fullPath, newContent, "utf-8");
12112
+ fs29.writeFileSync(fullPath, newContent, "utf-8");
10902
12113
  results.push({ type, filePath: relativePath, action: "appended" });
10903
12114
  } else {
10904
- const dir = path28.dirname(fullPath);
10905
- if (!fs25.existsSync(dir)) {
10906
- fs25.mkdirSync(dir, { recursive: true });
12115
+ const dir = path32.dirname(fullPath);
12116
+ if (!fs29.existsSync(dir)) {
12117
+ fs29.mkdirSync(dir, { recursive: true });
10907
12118
  }
10908
- fs25.writeFileSync(fullPath, template, "utf-8");
12119
+ fs29.writeFileSync(fullPath, template, "utf-8");
10909
12120
  results.push({ type, filePath: relativePath, action: "created" });
10910
12121
  }
10911
12122
  if (type === "antigravity") {
@@ -10936,6 +12147,16 @@ function runInit(options) {
10936
12147
  }
10937
12148
  if (selectedSet.has("cursor")) {
10938
12149
  handleCursorGotchaRules(root, results);
12150
+ handleCursorHooks(root, results);
12151
+ }
12152
+ if (selectedSet.has("windsurf")) {
12153
+ handleWindsurfHooks(root, results);
12154
+ }
12155
+ if (selectedSet.has("copilot")) {
12156
+ handleCopilotHooks(root, results);
12157
+ }
12158
+ if (selectedSet.has("codex")) {
12159
+ handleCodexHooks(root, results);
10939
12160
  }
10940
12161
  return results;
10941
12162
  }
@@ -11020,11 +12241,10 @@ var init_init = __esm({
11020
12241
  "Record what matters, skip what doesn't.",
11021
12242
  "",
11022
12243
  "<kuma_obedience>",
11023
- "## \u{1F512} Obedience (non-negotiable)",
11024
- '- MUST call `kuma_context({ action: "init" })` at session start.',
11025
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
11026
- '- MUST call `kuma_safety({ action: "verify" })` after edits.',
11027
- "- MUST NOT call actions outside the core actions.",
12244
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
12245
+ "- 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.",
12246
+ '- AGENT-EXPECTED (best-effort ~70%): call `kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; `kuma_safety({ action: "verify" })` after edits.',
12247
+ "- 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).",
11028
12248
  "</kuma_obedience>",
11029
12249
  "",
11030
12250
  "<kuma_workflow>",
@@ -11063,6 +12283,72 @@ var init_init = __esm({
11063
12283
  }
11064
12284
  });
11065
12285
 
12286
+ // src/engine/compactionContext.ts
12287
+ var compactionContext_exports = {};
12288
+ __export(compactionContext_exports, {
12289
+ getCompactionContext: () => getCompactionContext
12290
+ });
12291
+ import fs30 from "fs";
12292
+ import path33 from "path";
12293
+ async function getCompactionContext() {
12294
+ const parts = [];
12295
+ try {
12296
+ const { sessionMemory: sessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
12297
+ const summary = sessionMemory2.getSummary();
12298
+ const goal = String(summary.currentGoal || "");
12299
+ if (goal) parts.push(`Goal: ${goal.substring(0, 120)}`);
12300
+ const modified = (summary.modifiedFiles || []).length;
12301
+ if (modified > 0) parts.push(`Modified: ${modified} file(s)`);
12302
+ } catch {
12303
+ }
12304
+ try {
12305
+ const dbFile = path33.join(getProjectRoot(), ".kuma", "kuma.db");
12306
+ if (fs30.existsSync(dbFile)) {
12307
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
12308
+ const db = await getDb2();
12309
+ const stmt = db.prepare(
12310
+ `SELECT file_path, description FROM known_gotchas
12311
+ WHERE status IN ('active','verified') AND severity IN ('critical','high')
12312
+ AND (quarantined IS NULL OR quarantined = 0)
12313
+ ORDER BY updated_at DESC LIMIT 3`
12314
+ );
12315
+ const rows = [];
12316
+ while (stmt.step()) {
12317
+ const r = stmt.getAsObject();
12318
+ rows.push(`[${r.file_path}] ${String(r.description).substring(0, 80)}`);
12319
+ }
12320
+ stmt.free();
12321
+ if (rows.length > 0) parts.push(`Gotchas: ${rows.join(" | ")}`);
12322
+ }
12323
+ } catch {
12324
+ }
12325
+ try {
12326
+ const { getDirtyFiles: getDirtyFiles2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12327
+ const dirty = getDirtyFiles2().slice(0, 5);
12328
+ if (dirty.length > 0) parts.push(`Dirty: ${dirty.join(", ")}`);
12329
+ } catch {
12330
+ }
12331
+ try {
12332
+ const cpDir = path33.join(getProjectRoot(), ".kuma", "checkpoints");
12333
+ if (fs30.existsSync(cpDir)) {
12334
+ const cps = fs30.readdirSync(cpDir).slice(0, 3);
12335
+ if (cps.length > 0) parts.push(`Checkpoints: ${cps.join(", ")}`);
12336
+ }
12337
+ } catch {
12338
+ }
12339
+ let out = parts.join("\n");
12340
+ if (out.length > BUDGET) out = out.slice(0, BUDGET) + "\u2026";
12341
+ return out;
12342
+ }
12343
+ var BUDGET;
12344
+ var init_compactionContext = __esm({
12345
+ "src/engine/compactionContext.ts"() {
12346
+ "use strict";
12347
+ init_pathValidator();
12348
+ BUDGET = 600;
12349
+ }
12350
+ });
12351
+
11066
12352
  // src/utils/agentDetector.ts
11067
12353
  var agentDetector_exports = {};
11068
12354
  __export(agentDetector_exports, {
@@ -11071,19 +12357,19 @@ __export(agentDetector_exports, {
11071
12357
  getSkillPath: () => getSkillPath,
11072
12358
  skillExists: () => skillExists
11073
12359
  });
11074
- import fs26 from "fs";
11075
- import path29 from "path";
12360
+ import fs31 from "fs";
12361
+ import path34 from "path";
11076
12362
  function checkFile(root, filePath) {
11077
12363
  try {
11078
- return fs26.existsSync(path29.join(root, filePath));
12364
+ return fs31.existsSync(path34.join(root, filePath));
11079
12365
  } catch {
11080
12366
  return false;
11081
12367
  }
11082
12368
  }
11083
12369
  function checkDir(root, dirPath) {
11084
12370
  try {
11085
- const fullPath = path29.join(root, dirPath);
11086
- return fs26.existsSync(fullPath) && fs26.statSync(fullPath).isDirectory();
12371
+ const fullPath = path34.join(root, dirPath);
12372
+ return fs31.existsSync(fullPath) && fs31.statSync(fullPath).isDirectory();
11087
12373
  } catch {
11088
12374
  return false;
11089
12375
  }
@@ -11151,7 +12437,7 @@ function getAgentLabel(type) {
11151
12437
  function skillExists(type, projectRoot) {
11152
12438
  const root = projectRoot ?? getProjectRoot();
11153
12439
  const skillPath = getSkillPath(type);
11154
- return fs26.existsSync(path29.join(root, skillPath));
12440
+ return fs31.existsSync(path34.join(root, skillPath));
11155
12441
  }
11156
12442
  var AGENT_DETECTORS;
11157
12443
  var init_agentDetector = __esm({
@@ -11692,11 +12978,10 @@ var init_skillGenerator = __esm({
11692
12978
  "use strict";
11693
12979
  CORE_ACTIONS = [
11694
12980
  "<kuma_obedience>",
11695
- "## \u{1F512} Obedience (non-negotiable)",
11696
- '- MUST call `kuma_context({ action: "init" })` at session start.',
11697
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
11698
- '- MUST call `kuma_safety({ action: "verify" })` after edits to resolve affected tests.',
11699
- "- MUST NOT call actions outside the core actions.",
12981
+ "## \u{1F512} Obedience \u2014 honest model (issue #39)",
12982
+ "- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
12983
+ '- AGENT-EXPECTED (~70%): `kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; `kuma_safety({ action: "verify" })` after edits.',
12984
+ "- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
11700
12985
  "</kuma_obedience>",
11701
12986
  "",
11702
12987
  "<kuma_workflow>",
@@ -11748,8 +13033,8 @@ init_kumaGraph();
11748
13033
  init_kumaMemory();
11749
13034
  init_kumaDriftDetector();
11750
13035
  init_pathValidator();
11751
- import fs18 from "fs";
11752
- import path21 from "path";
13036
+ import fs21 from "fs";
13037
+ import path24 from "path";
11753
13038
  var CONTEXT_ALIASES = {
11754
13039
  "research": "research",
11755
13040
  "search": "research",
@@ -11795,33 +13080,43 @@ var CONTEXT_ALIASES = {
11795
13080
  "anti-duplication": "reuse"
11796
13081
  };
11797
13082
  async function handleContext(params) {
13083
+ let dirtySynced = 0;
11798
13084
  try {
13085
+ const { getDirtyFiles: getDirtyFiles2, clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
11799
13086
  const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
13087
+ dirtySynced = getDirtyFiles2().length;
11800
13088
  await syncModifiedFiles2(30);
13089
+ if (dirtySynced > 0) clearDirty2();
11801
13090
  } catch {
11802
13091
  }
11803
13092
  const rawAction = params.action || "init";
11804
13093
  const resolvedAction = CONTEXT_ALIASES[rawAction.toLowerCase()] || rawAction;
11805
13094
  const action = resolvedAction;
13095
+ let obediencePrefix = "";
13096
+ if (action !== "init" && !sessionMemory.hasToolCall("kuma_context_init")) {
13097
+ obediencePrefix = `\u26A0\uFE0F **Obedience nudge**: \`kuma_context({ action: "init" })\` was not called yet this session. Run it first so project brief + fresh gotchas are loaded. Continuing anyway\u2026
13098
+
13099
+ `;
13100
+ }
11806
13101
  switch (action) {
11807
13102
  case "init":
11808
13103
  return handleInit(params);
11809
13104
  case "research":
11810
- return handleResearch(params);
13105
+ return obediencePrefix + await handleResearch(params, dirtySynced);
11811
13106
  case "history":
11812
- return handleHistory(params);
13107
+ return obediencePrefix + await handleHistory(params);
11813
13108
  case "flow":
11814
- return handleFlow(params);
13109
+ return obediencePrefix + await handleFlow(params);
11815
13110
  case "map":
11816
- return handleMap(params);
13111
+ return obediencePrefix + await handleMap(params);
11817
13112
  case "impact":
11818
- return handleImpact(params);
13113
+ return obediencePrefix + await handleImpact(params);
11819
13114
  case "cluster":
11820
- return handleCluster(params);
13115
+ return obediencePrefix + await handleCluster(params);
11821
13116
  case "skeleton":
11822
- return handleSkeleton(params);
13117
+ return obediencePrefix + await handleSkeleton(params);
11823
13118
  case "reuse":
11824
- return handleReuse(params);
13119
+ return obediencePrefix + await handleReuse(params);
11825
13120
  default:
11826
13121
  return `Unknown action "${action}". Use: init, research, history, flow, map, impact, cluster, skeleton, reuse`;
11827
13122
  }
@@ -12014,11 +13309,15 @@ function sessionFocusAdvice(focus) {
12014
13309
  return "Record gotchas & decisions as they happen \u2014 skip what isn't worth it.";
12015
13310
  }
12016
13311
  }
12017
- async function handleResearch(params) {
13312
+ async function handleResearch(params, dirtySynced = 0) {
12018
13313
  const scope = params.scope || "project";
12019
13314
  sessionMemory.setGoal(`Researching: ${scope}`);
12020
13315
  sessionMemory.recordToolCall("kuma_context_research", { scope });
12021
13316
  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`, ""];
13317
+ if (dirtySynced > 0) {
13318
+ lines.push(`\u{1F5FA}\uFE0F Map patched post-edit: ${dirtySynced} dirty file(s) synced before this research (no rebuild asked).`);
13319
+ lines.push("");
13320
+ }
12022
13321
  lines.push("**Step 1/5: Loading Research Cache**");
12023
13322
  const cached = await getResearchCache(scope);
12024
13323
  let record = null;
@@ -12032,7 +13331,18 @@ async function handleResearch(params) {
12032
13331
  const ageSeconds = Math.floor((Date.now() - (record.validatedAt ? new Date(record.validatedAt).getTime() : 0)) / 1e3);
12033
13332
  const ageStr = ageSeconds > 86400 ? `${Math.floor(ageSeconds / 86400)}d` : ageSeconds > 3600 ? `${Math.floor(ageSeconds / 3600)}h` : `${Math.floor(ageSeconds / 60)}m`;
12034
13333
  lines.push(` \u2705 Found cached research (${ageStr} old)`);
12035
- if (ageSeconds > 86400) lines.push(` ${ageSeconds > 604800 ? "\u{1F534}" : "\u{1F7E1}"} **Staleness:** Cache is ${ageStr} old \u2014 may be stale`);
13334
+ try {
13335
+ const { isStale: isStale2, RESEARCH_TTL_MS: RESEARCH_TTL_MS2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
13336
+ const validatedMs = record.validatedAt ? new Date(record.validatedAt).getTime() : 0;
13337
+ if (isStale2(validatedMs, RESEARCH_TTL_MS2)) {
13338
+ lines.push(` \u23F3 **STALE**: older than TTL 7d \u2014 re-researching fresh below`);
13339
+ record = null;
13340
+ } else {
13341
+ lines.push(` \u2705 Fresh (TTL 7d)`);
13342
+ }
13343
+ } catch {
13344
+ }
13345
+ if (ageSeconds > 86400 && record) lines.push(` ${ageSeconds > 604800 ? "\u{1F534}" : "\u{1F7E1}"} **Staleness:** Cache is ${ageStr} old \u2014 may be stale`);
12036
13346
  } else lines.push(" \u23F3 No cached research \u2014 starting fresh");
12037
13347
  lines.push("");
12038
13348
  lines.push("**Step 2/5: Staleness Check**");
@@ -12141,10 +13451,10 @@ async function handleHistory(params) {
12141
13451
  lines.push(traceFormatted);
12142
13452
  } else {
12143
13453
  try {
12144
- const { execSync: execSync7 } = await import("child_process");
13454
+ const { execSync: execSync9 } = await import("child_process");
12145
13455
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
12146
13456
  const root = getProjectRoot2();
12147
- const gitHistory = execSync7(
13457
+ const gitHistory = execSync9(
12148
13458
  `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
12149
13459
  { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
12150
13460
  ).trim();
@@ -12176,7 +13486,7 @@ ${gitHistory}`);
12176
13486
  try {
12177
13487
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
12178
13488
  const db = await getDb2();
12179
- const base = path21.basename(target);
13489
+ const base = path24.basename(target);
12180
13490
  const stmt = db.prepare(`
12181
13491
  SELECT description, last_verified_at FROM known_gotchas
12182
13492
  WHERE status = 'resolved' AND (file_path LIKE ? OR file_path LIKE ?)
@@ -12189,9 +13499,9 @@ ${gitHistory}`);
12189
13499
  if (resolved.length === 0) {
12190
13500
  try {
12191
13501
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
12192
- const mdPath = path21.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
12193
- if (fs18.existsSync(mdPath)) {
12194
- const mdContent = fs18.readFileSync(mdPath, "utf-8");
13502
+ const mdPath = path24.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
13503
+ if (fs21.existsSync(mdPath)) {
13504
+ const mdContent = fs21.readFileSync(mdPath, "utf-8");
12195
13505
  const sections = mdContent.split(/^###\s+/m);
12196
13506
  for (const sec of sections) {
12197
13507
  const secLower = sec.toLowerCase();
@@ -12236,7 +13546,18 @@ async function handleMap(_params) {
12236
13546
  sessionMemory.recordToolCall("kuma_context_map", {});
12237
13547
  const { getWorkspaceInfo: getWorkspaceInfo2, formatWorkspaceMap: formatWorkspaceMap2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
12238
13548
  const wsInfo = await getWorkspaceInfo2();
12239
- return formatWorkspaceMap2(wsInfo);
13549
+ const base = formatWorkspaceMap2(wsInfo);
13550
+ try {
13551
+ const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
13552
+ const { count } = await refreshPackageBriefs2();
13553
+ if (count > 0) {
13554
+ return base + `
13555
+
13556
+ \u{1F4DA} **Pre-computed briefs**: ${count} package brief(s) refreshed in \`.kuma/briefs/\` \u2014 read instead of grep-walking (saves ~5x discovery tokens).`;
13557
+ }
13558
+ } catch {
13559
+ }
13560
+ return base;
12240
13561
  }
12241
13562
  async function handleImpact(params) {
12242
13563
  const target = params.target || params.scope || "project";
@@ -12252,8 +13573,8 @@ init_kumaDb();
12252
13573
  init_kumaMemory();
12253
13574
  init_pathValidator();
12254
13575
  init_pathValidator();
12255
- import fs20 from "fs";
12256
- import path23 from "path";
13576
+ import fs23 from "fs";
13577
+ import path26 from "path";
12257
13578
  var MEMORY_ALIASES = {
12258
13579
  "decision": "decision",
12259
13580
  "adr": "decision",
@@ -12277,7 +13598,11 @@ var MEMORY_ALIASES = {
12277
13598
  "gotcha-resolve": "gotcha",
12278
13599
  "deprecate_gotcha": "gotcha",
12279
13600
  "deprecate-gotcha": "gotcha",
12280
- "deprecate": "gotcha"
13601
+ "deprecate": "gotcha",
13602
+ "test_outcome": "test_outcome",
13603
+ "test-outcome": "test_outcome",
13604
+ "testoutcome": "test_outcome",
13605
+ "test-record": "test_outcome"
12281
13606
  };
12282
13607
  async function handleMemory(params) {
12283
13608
  const rawAction = params.action || "search";
@@ -12295,9 +13620,23 @@ async function handleMemory(params) {
12295
13620
  return handleArchFlow(params);
12296
13621
  case "gotcha":
12297
13622
  return handleGotchaAction(params);
13623
+ case "test_outcome":
13624
+ return handleTestOutcome(params);
12298
13625
  default:
12299
- return `Unknown action "${action}". Use: gotcha, decision, arch_flow, research_save, search`;
13626
+ return `Unknown action "${action}". Use: gotcha, decision, arch_flow, research_save, search, test_outcome`;
13627
+ }
13628
+ }
13629
+ async function handleTestOutcome(params) {
13630
+ const changedRaw = params.scope || params.target || "";
13631
+ const failedRaw = params.content || params.description || "";
13632
+ if (!changedRaw.trim()) {
13633
+ 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.';
12300
13634
  }
13635
+ const changed = changedRaw.split(",").map((s) => s.trim()).filter(Boolean);
13636
+ const failed = failedRaw.split(",").map((s) => s.trim()).filter(Boolean);
13637
+ const { recordTestOutcome: recordTestOutcome2 } = await Promise.resolve().then(() => (init_testHistory(), testHistory_exports));
13638
+ const { recorded } = await recordTestOutcome2(changed, failed);
13639
+ 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.`;
12301
13640
  }
12302
13641
  async function handleDecision(params) {
12303
13642
  const title = params.title || params.content || (params.target ? `Decision regarding ${params.target}` : params.scope ? `Decision for ${params.scope}` : "");
@@ -12346,9 +13685,9 @@ async function handleResearchSave(params) {
12346
13685
  } catch {
12347
13686
  }
12348
13687
  try {
12349
- const researchDir = path23.join(getProjectRoot(), ".kuma", "research");
12350
- if (!fs20.existsSync(researchDir)) fs20.mkdirSync(researchDir, { recursive: true });
12351
- fs20.writeFileSync(path23.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
13688
+ const researchDir = path26.join(getProjectRoot(), ".kuma", "research");
13689
+ if (!fs23.existsSync(researchDir)) fs23.mkdirSync(researchDir, { recursive: true });
13690
+ fs23.writeFileSync(path26.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
12352
13691
  } catch {
12353
13692
  }
12354
13693
  sessionMemory.recordMemoryAction("research_save");
@@ -12544,9 +13883,9 @@ init_sessionMemory();
12544
13883
  init_pathValidator();
12545
13884
  init_sessionMemory();
12546
13885
  init_kumaShared();
12547
- import fs21 from "fs";
12548
- import path24 from "path";
12549
- import { execSync as execSync5 } from "child_process";
13886
+ import fs24 from "fs";
13887
+ import path27 from "path";
13888
+ import { execSync as execSync6 } from "child_process";
12550
13889
  var SCRIPT_PATCH_PATTERNS = [
12551
13890
  "writeFileSync",
12552
13891
  "writeFile",
@@ -12572,20 +13911,20 @@ function findPatchScripts(projectRoot) {
12572
13911
  const warnings = [];
12573
13912
  const recentFiles = scanRecentFiles(projectRoot);
12574
13913
  for (const file of recentFiles) {
12575
- const ext = path24.extname(file).toLowerCase();
13914
+ const ext = path27.extname(file).toLowerCase();
12576
13915
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
12577
- const relativePath = path24.relative(projectRoot, file);
13916
+ const relativePath = path27.relative(projectRoot, file);
12578
13917
  if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".") || relativePath.startsWith(".kuma/scratch")) {
12579
13918
  continue;
12580
13919
  }
12581
13920
  try {
12582
- const content = fs21.readFileSync(file, "utf-8").toLowerCase();
13921
+ const content = fs24.readFileSync(file, "utf-8").toLowerCase();
12583
13922
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
12584
13923
  if (matchedPattern) {
12585
13924
  warnings.push({
12586
13925
  severity: "high",
12587
13926
  pattern: "script-patching",
12588
- message: `Created script file that modifies other files: ${path24.basename(file)}`,
13927
+ message: `Created script file that modifies other files: ${path27.basename(file)}`,
12589
13928
  suggestion: "Use your native edit tools instead \u2014 targeted edits with auto-backup and rollback",
12590
13929
  evidence: `File: ${relativePath} contains '${matchedPattern}'`,
12591
13930
  filePath: relativePath
@@ -12619,15 +13958,15 @@ function detectBashGrepUsage() {
12619
13958
  function scanRecentFiles(projectRoot) {
12620
13959
  const recent = [];
12621
13960
  try {
12622
- const entries = fs21.readdirSync(projectRoot, { withFileTypes: true });
13961
+ const entries = fs24.readdirSync(projectRoot, { withFileTypes: true });
12623
13962
  const now = Date.now();
12624
13963
  const recentThreshold = 30 * 60 * 1e3;
12625
13964
  for (const entry of entries) {
12626
13965
  if (!entry.isFile()) continue;
12627
13966
  try {
12628
- const stat = fs21.statSync(path24.join(projectRoot, entry.name));
13967
+ const stat = fs24.statSync(path27.join(projectRoot, entry.name));
12629
13968
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
12630
- recent.push(path24.join(projectRoot, entry.name));
13969
+ recent.push(path27.join(projectRoot, entry.name));
12631
13970
  }
12632
13971
  } catch {
12633
13972
  continue;
@@ -12640,7 +13979,7 @@ function scanRecentFiles(projectRoot) {
12640
13979
  function detectGitPatchScripts(projectRoot) {
12641
13980
  const warnings = [];
12642
13981
  try {
12643
- const statusStdout = execSync5("git status --porcelain", {
13982
+ const statusStdout = execSync6("git status --porcelain", {
12644
13983
  cwd: projectRoot,
12645
13984
  encoding: "utf-8",
12646
13985
  timeout: 3e3
@@ -12651,14 +13990,14 @@ function detectGitPatchScripts(projectRoot) {
12651
13990
  const prefix = line.substring(0, 2);
12652
13991
  if (prefix !== "??" && prefix !== "A ") continue;
12653
13992
  const file = line.substring(3).trim();
12654
- const ext = path24.extname(file).toLowerCase();
13993
+ const ext = path27.extname(file).toLowerCase();
12655
13994
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
12656
13995
  const isRootLevel = !file.includes("/");
12657
13996
  const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
12658
13997
  if (!isRootLevel && !isScriptsDir) continue;
12659
- const fullPath = path24.join(projectRoot, file);
12660
- if (fs21.existsSync(fullPath)) {
12661
- const content = fs21.readFileSync(fullPath, "utf-8").toLowerCase();
13998
+ const fullPath = path27.join(projectRoot, file);
13999
+ if (fs24.existsSync(fullPath)) {
14000
+ const content = fs24.readFileSync(fullPath, "utf-8").toLowerCase();
12662
14001
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
12663
14002
  if (matchedPattern) {
12664
14003
  warnings.push({
@@ -12673,7 +14012,7 @@ function detectGitPatchScripts(projectRoot) {
12673
14012
  }
12674
14013
  }
12675
14014
  }
12676
- const deletedStdout = execSync5("git diff --name-only --diff-filter=D HEAD", {
14015
+ const deletedStdout = execSync6("git diff --name-only --diff-filter=D HEAD", {
12677
14016
  cwd: projectRoot,
12678
14017
  encoding: "utf-8",
12679
14018
  timeout: 3e3
@@ -12681,7 +14020,7 @@ function detectGitPatchScripts(projectRoot) {
12681
14020
  if (deletedStdout) {
12682
14021
  const deletedFiles = deletedStdout.split("\n").filter(Boolean);
12683
14022
  for (const file of deletedFiles) {
12684
- const ext = path24.extname(file).toLowerCase();
14023
+ const ext = path27.extname(file).toLowerCase();
12685
14024
  if (SCRIPT_EXTENSIONS.includes(ext)) {
12686
14025
  warnings.push({
12687
14026
  severity: "high",
@@ -12743,21 +14082,21 @@ function detectAllAntiPatterns() {
12743
14082
  // src/engine/contextSnapshot.ts
12744
14083
  init_sessionMemory();
12745
14084
  init_pathValidator();
12746
- import fs22 from "fs";
12747
- import path25 from "path";
12748
- import { execSync as execSync6 } from "child_process";
14085
+ import fs25 from "fs";
14086
+ import path28 from "path";
14087
+ import { execSync as execSync7 } from "child_process";
12749
14088
  var SNAPSHOT_DIR = "context-snapshots";
12750
14089
  function snapshotDir() {
12751
- return path25.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
14090
+ return path28.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
12752
14091
  }
12753
14092
  function ensureSnapshotDir() {
12754
14093
  const dir = snapshotDir();
12755
- if (!fs22.existsSync(dir)) {
12756
- fs22.mkdirSync(dir, { recursive: true });
14094
+ if (!fs25.existsSync(dir)) {
14095
+ fs25.mkdirSync(dir, { recursive: true });
12757
14096
  }
12758
14097
  }
12759
14098
  function snapshotFilePath(timestamp) {
12760
- return path25.join(snapshotDir(), `${timestamp}.json`);
14099
+ return path28.join(snapshotDir(), `${timestamp}.json`);
12761
14100
  }
12762
14101
  function saveSnapshot(goal) {
12763
14102
  try {
@@ -12778,7 +14117,7 @@ function saveSnapshot(goal) {
12778
14117
  hasConventions: !!summary.hasConventions
12779
14118
  };
12780
14119
  try {
12781
- fs22.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
14120
+ fs25.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
12782
14121
  } catch {
12783
14122
  }
12784
14123
  sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
@@ -12817,7 +14156,7 @@ function formatSnapshot(snapshot) {
12817
14156
  function getGitDiffStat2() {
12818
14157
  try {
12819
14158
  const root = getProjectRoot();
12820
- const stdout = execSync6("git diff --stat", {
14159
+ const stdout = execSync7("git diff --stat", {
12821
14160
  cwd: root,
12822
14161
  encoding: "utf-8",
12823
14162
  timeout: 3e3
@@ -12864,24 +14203,62 @@ async function handleKumaGuard(params) {
12864
14203
  });
12865
14204
  }
12866
14205
  const drifts = [];
14206
+ const info = [];
14207
+ let verified = stats.hasRunTests;
14208
+ let uncommittedModifiedCount = stats.modifiedFiles.length;
12867
14209
  if (check === "all" || check === "drift") {
12868
14210
  const unresolvedCount = getUnresolvedCount(stats.failedFiles);
12869
14211
  const gitStat = getGitDiffStat();
12870
14212
  const editCalls = stats.toolCalls.filter((c) => isEditTool(c.toolName)).length;
14213
+ verified = stats.hasRunTests || hasVerificationEvidence(stats.toolCalls);
14214
+ const uncommitted = getUncommittedFiles();
14215
+ const modifiedPaths = stats.modifiedFiles.map((f) => String(f.filePath || f.path || ""));
14216
+ const uncommittedModified = modifiedPaths.filter(
14217
+ (p) => [...uncommitted].some((u) => u === p || u.endsWith(p) || p.endsWith(u))
14218
+ );
14219
+ const committedOnly = modifiedPaths.length - uncommittedModified.length;
14220
+ uncommittedModifiedCount = uncommittedModified.length;
12871
14221
  drifts.push(...buildDriftMessages(
12872
- stats.modifiedFiles.length,
12873
- stats.hasRunTests,
14222
+ uncommittedModified.length,
14223
+ verified,
12874
14224
  unresolvedCount,
12875
14225
  gitStat
12876
14226
  ));
12877
- if (stats.modifiedFiles.length > 0 && !stats.hasRunTests) {
14227
+ if (committedOnly > 0 && uncommittedModified.length === 0) {
14228
+ info.push(`${committedOnly} file(s) differ from base branch but are committed \u2014 legitimate feature state, not drift (no action needed)`);
14229
+ }
14230
+ if (verified && uncommittedModified.length > 0) {
14231
+ info.push(`Verification already ran in-session (test/typecheck evidence found) \u2014 no re-run demanded`);
14232
+ }
14233
+ if (uncommittedModified.length > 0 && !verified) {
12878
14234
  warnings.push({
12879
14235
  severity: "medium",
12880
14236
  pattern: "no-test-after-edit",
12881
- message: `${stats.modifiedFiles.length} file(s) modified without running tests`,
14237
+ message: `${uncommittedModified.length} uncommitted file(s) modified without running tests`,
12882
14238
  suggestion: "Run the project's test/typecheck command to verify changes"
12883
14239
  });
12884
14240
  }
14241
+ try {
14242
+ const { detectTestWeakening: detectTestWeakening2 } = await Promise.resolve().then(() => (init_kumaShared(), kumaShared_exports));
14243
+ const weakened = detectTestWeakening2(modifiedPaths);
14244
+ for (const w of weakened) {
14245
+ warnings.push({
14246
+ severity: "high",
14247
+ pattern: "test-weakening",
14248
+ message: `Test-weakening diff detected: ${w} \u2014 assertions removed without replacement`,
14249
+ suggestion: "Restore removed assertions or justify why the weakened test still guards the behavior"
14250
+ });
14251
+ }
14252
+ } catch {
14253
+ }
14254
+ try {
14255
+ const { scanDocDrift: scanDocDrift2, formatDocDrift: formatDocDrift2 } = await Promise.resolve().then(() => (init_docDrift(), docDrift_exports));
14256
+ const { checked, drifted, skipped } = scanDocDrift2();
14257
+ if (drifted.length > 0) {
14258
+ info.push(formatDocDrift2(checked, drifted.slice(0, 5), skipped));
14259
+ }
14260
+ } catch {
14261
+ }
12885
14262
  if (editCalls > 5) {
12886
14263
  warnings.push({
12887
14264
  severity: "low",
@@ -12953,6 +14330,25 @@ async function handleKumaGuard(params) {
12953
14330
  }
12954
14331
  return formatSnapshot(snapshot);
12955
14332
  }
14333
+ try {
14334
+ const { recordGuardFlag: recordGuardFlag2, getDowntieredRules: getDowntieredRules2, demoteSeverity: demoteSeverity2 } = await Promise.resolve().then(() => (init_guardLedger(), guardLedger_exports));
14335
+ for (const w of warnings) {
14336
+ try {
14337
+ await recordGuardFlag2(w.pattern);
14338
+ } catch {
14339
+ }
14340
+ }
14341
+ const downtiered = await getDowntieredRules2();
14342
+ if (downtiered.size > 0) {
14343
+ for (const w of warnings) {
14344
+ if (downtiered.has(w.pattern) && (w.severity === "high" || w.severity === "medium")) {
14345
+ w.severity = demoteSeverity2(w.severity);
14346
+ w.message = `\u{1F53B} [auto-downtiered: rule over FP budget] ${w.message}`;
14347
+ }
14348
+ }
14349
+ }
14350
+ } catch {
14351
+ }
12956
14352
  const hasWarnings = warnings.length > 0;
12957
14353
  const hasDrifts = drifts.length > 0;
12958
14354
  const onTrack = !hasWarnings && !hasDrifts;
@@ -12961,8 +14357,8 @@ async function handleKumaGuard(params) {
12961
14357
  warnings,
12962
14358
  loop.isLooping,
12963
14359
  getUnresolvedCount(stats.failedFiles),
12964
- stats.modifiedFiles.length,
12965
- stats.hasRunTests,
14360
+ uncommittedModifiedCount,
14361
+ verified,
12966
14362
  countEditCalls(stats.toolCalls)
12967
14363
  );
12968
14364
  const report = {
@@ -12970,6 +14366,7 @@ async function handleKumaGuard(params) {
12970
14366
  onTrack,
12971
14367
  warnings,
12972
14368
  drifts,
14369
+ info,
12973
14370
  suggestion,
12974
14371
  stats: {
12975
14372
  goal: stats.goal,
@@ -13000,28 +14397,136 @@ async function handleKumaGuard(params) {
13000
14397
  return JSON.stringify(reportWithRecordings, null, 2);
13001
14398
  }
13002
14399
 
14400
+ // src/engine/costLedger.ts
14401
+ init_kumaDb();
14402
+ async function ensureCostSchema() {
14403
+ const db = await getDb();
14404
+ db.exec(`
14405
+ CREATE TABLE IF NOT EXISTS cost_ledger (
14406
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14407
+ tool TEXT NOT NULL,
14408
+ action TEXT,
14409
+ duration_ms INTEGER NOT NULL DEFAULT 0,
14410
+ output_chars INTEGER NOT NULL DEFAULT 0,
14411
+ outcome TEXT NOT NULL DEFAULT 'ok',
14412
+ tokens INTEGER,
14413
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
14414
+ );
14415
+ CREATE INDEX IF NOT EXISTS idx_cost_tool ON cost_ledger(tool);
14416
+ `);
14417
+ saveDb();
14418
+ }
14419
+ async function recordCost(r) {
14420
+ try {
14421
+ await ensureCostSchema();
14422
+ const db = await getDb();
14423
+ db.run(
14424
+ `INSERT INTO cost_ledger (tool, action, duration_ms, output_chars, outcome, tokens) VALUES (?, ?, ?, ?, ?, ?)`,
14425
+ [r.tool, r.action || null, Math.round(r.durationMs), r.outputChars, r.outcome, r.tokens ?? null]
14426
+ );
14427
+ saveDb();
14428
+ } catch {
14429
+ }
14430
+ }
14431
+ async function getCostStats(limit = 15) {
14432
+ try {
14433
+ await ensureCostSchema();
14434
+ const db = await getDb();
14435
+ const stmt = db.prepare(`
14436
+ SELECT tool, COUNT(*) as calls,
14437
+ SUM(CASE WHEN outcome = 'error' THEN 1 ELSE 0 END) as errors,
14438
+ AVG(duration_ms) as avgMs, AVG(output_chars) as avgChars,
14439
+ SUM(tokens) as totalTokens
14440
+ FROM cost_ledger GROUP BY tool ORDER BY calls DESC LIMIT ?
14441
+ `);
14442
+ stmt.bind([limit]);
14443
+ const out = [];
14444
+ while (stmt.step()) {
14445
+ const r = stmt.getAsObject();
14446
+ out.push({
14447
+ tool: String(r.tool),
14448
+ calls: Number(r.calls) || 0,
14449
+ errors: Number(r.errors) || 0,
14450
+ avgMs: Math.round(Number(r.avgMs) || 0),
14451
+ avgChars: Math.round(Number(r.avgChars) || 0),
14452
+ totalTokens: r.totalTokens === null ? null : Number(r.totalTokens)
14453
+ });
14454
+ }
14455
+ stmt.free();
14456
+ return out;
14457
+ } catch {
14458
+ return [];
14459
+ }
14460
+ }
14461
+ function formatCostStats(rows) {
14462
+ if (rows.length === 0) return "\u{1F4B0} **Cost ledger** \u2014 no tool calls recorded yet.";
14463
+ const lines = ["\u{1F4B0} **Cost per tool** (duration \xD7 output size \u2192 cost-per-success signal)", ""];
14464
+ for (const r of rows) {
14465
+ const err = r.errors > 0 ? `, \u274C${r.errors}` : "";
14466
+ const tok = r.totalTokens !== null ? `, ${r.totalTokens} tokens` : "";
14467
+ lines.push(` \u2022 \`${r.tool}\` \u2014 ${r.calls} call(s), ~${r.avgMs}ms, ~${r.avgChars} chars${err}${tok}`);
14468
+ }
14469
+ return lines.join("\n");
14470
+ }
14471
+ async function withCost(tool, action, fn) {
14472
+ const start = Date.now();
14473
+ try {
14474
+ const result = await fn();
14475
+ const chars = typeof result === "string" ? result.length : JSON.stringify(result ?? "").length;
14476
+ await recordCost({ tool, action, durationMs: Date.now() - start, outputChars: chars, outcome: "ok" });
14477
+ return { result, outcome: "ok" };
14478
+ } catch (err) {
14479
+ await recordCost({ tool, action, durationMs: Date.now() - start, outputChars: String(err).length, outcome: "error" });
14480
+ throw err;
14481
+ }
14482
+ }
14483
+
13003
14484
  // src/tools/kumaSafetyTool.ts
13004
14485
  async function handleSafety(params) {
13005
14486
  const { action } = params;
13006
14487
  sessionMemory.recordToolCall("kuma_safety", { action });
14488
+ let prefix = "";
14489
+ if (!sessionMemory.hasToolCall("kuma_context_init")) {
14490
+ prefix = `\u26A0\uFE0F **Obedience nudge**: \`kuma_context({ action: "init" })\` was not called yet this session. Run it first so project brief + fresh gotchas are loaded. Continuing anyway\u2026
14491
+
14492
+ `;
14493
+ }
13007
14494
  switch (action) {
13008
14495
  case "guard":
13009
- return handleGuard(params);
14496
+ return prefix + await handleGuard(params);
13010
14497
  case "verify":
13011
- return handleVerify(params);
14498
+ return prefix + await handleVerify(params);
13012
14499
  case "checkpoint":
13013
- return handleCheckpoint(params);
14500
+ return prefix + await handleCheckpoint(params);
13014
14501
  case "rollback_label":
13015
- return handleRollbackLabel(params);
14502
+ return prefix + await handleRollbackLabel(params);
14503
+ case "cost":
14504
+ return prefix + await handleCost();
13016
14505
  default:
13017
- return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label`;
14506
+ return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label, cost`;
13018
14507
  }
13019
14508
  }
14509
+ async function handleCost() {
14510
+ sessionMemory.recordToolCall("kuma_safety_cost", {});
14511
+ const stats = await getCostStats();
14512
+ return formatCostStats(stats);
14513
+ }
13020
14514
  async function handleGuard(params) {
13021
- return await handleKumaGuard({
14515
+ const base = await handleKumaGuard({
13022
14516
  goal: params.guardGoal,
13023
14517
  check: params.guardCheck || "all"
13024
14518
  });
14519
+ try {
14520
+ const summary = sessionMemory.getSummary();
14521
+ const modified = summary.modifiedFiles?.length || 0;
14522
+ if (modified > 0 && !sessionMemory.hasToolCall("kuma_safety_verify")) {
14523
+ return base + `
14524
+
14525
+ \u26A0\uFE0F **Post-edit nudge**: ${modified} file(s) modified but \`kuma_safety({ action: "verify" })\` not called yet. Run verify to close the edit\u2192verify loop.`;
14526
+ }
14527
+ } catch {
14528
+ }
14529
+ return base;
13025
14530
  }
13026
14531
  async function handleCheckpoint(params) {
13027
14532
  sessionMemory.recordToolCall("kuma_safety_checkpoint", { label: params.label });
@@ -13042,6 +14547,17 @@ async function handleVerify(params) {
13042
14547
  const { analyzeImpact: analyzeImpact2, formatImpact: formatImpact2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
13043
14548
  const impact = await analyzeImpact2(target);
13044
14549
  const formattedImpact = formatImpact2(impact);
14550
+ let gotchaScan = "";
14551
+ try {
14552
+ const { getFreshGotchasForFile: getFreshGotchasForFile2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
14553
+ const gotchas = await getFreshGotchasForFile2(target, 5, true);
14554
+ if (gotchas.length > 0) {
14555
+ gotchaScan = "\n\n\u26A0\uFE0F **Gotcha-scan** \u2014 this scope touches file(s) with known gotchas:\n" + gotchas.map((g) => ` ${g.stale ? "\u26AA(stale)" : "\u{1F7E1}"} [${g.severity}] ${g.filePath} \u2014 ${g.description.substring(0, 120)}`).join("\n") + "\n \u2192 Confirm the fix does not reintroduce them.";
14556
+ } else {
14557
+ gotchaScan = "\n\n\u2705 **Gotcha-scan**: no known gotchas for this scope.";
14558
+ }
14559
+ } catch {
14560
+ }
13045
14561
  let recommendedCmd = "";
13046
14562
  try {
13047
14563
  const { getWorkspaceInfo: getWorkspaceInfo2, findPackageForFile: findPackageForFile2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
@@ -13077,7 +14593,8 @@ async function handleVerify(params) {
13077
14593
  formattedImpact,
13078
14594
  "",
13079
14595
  `\u{1F4CB} **Recommended Scoped Test Command:**`,
13080
- ` \`${recommendedCmd}\``
14596
+ ` \`${recommendedCmd}\``,
14597
+ gotchaScan
13081
14598
  ].join("\n");
13082
14599
  }
13083
14600
  return [
@@ -13166,12 +14683,12 @@ function registerAllTools(server) {
13166
14683
  const contextHandler = async (params) => {
13167
14684
  try {
13168
14685
  await ensureInitialized();
13169
- const text = await handleContext({
14686
+ const { result: text } = await withCost("kuma_context", params.action, () => handleContext({
13170
14687
  action: params.action,
13171
14688
  scope: params.scope,
13172
14689
  target: params.target,
13173
14690
  goal: params.goal
13174
- });
14691
+ }));
13175
14692
  return { content: [{ type: "text", text }] };
13176
14693
  } catch (err) {
13177
14694
  return { content: [{ type: "text", text: `Error in kuma_context: ${err}` }], isError: true };
@@ -13179,10 +14696,10 @@ function registerAllTools(server) {
13179
14696
  };
13180
14697
  server.tool("kuma_context", contextDesc, contextSchema, contextHandler);
13181
14698
  server.tool("kuma_kuma_context", contextDesc, contextSchema, contextHandler);
13182
- 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).";
14699
+ 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).";
13183
14700
  const memorySchema = {
13184
- action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve"]).describe(
13185
- "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"
14701
+ action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve", "test_outcome"]).describe(
14702
+ "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"
13186
14703
  ),
13187
14704
  scope: z.string().optional().describe("File path for gotcha \u2014 or scope for research_save/search"),
13188
14705
  target: z.string().optional().describe("Target file, gotcha ID, or component"),
@@ -13204,7 +14721,7 @@ function registerAllTools(server) {
13204
14721
  const memoryHandler = async (params) => {
13205
14722
  try {
13206
14723
  await ensureInitialized();
13207
- const text = await handleMemory({
14724
+ const { result: text } = await withCost("kuma_memory", params.action, () => handleMemory({
13208
14725
  action: params.action,
13209
14726
  scope: params.scope,
13210
14727
  target: params.target,
@@ -13222,7 +14739,7 @@ function registerAllTools(server) {
13222
14739
  description: params.description,
13223
14740
  trigger_command: params.trigger_command,
13224
14741
  id: params.id
13225
- });
14742
+ }));
13226
14743
  return { content: [{ type: "text", text }] };
13227
14744
  } catch (err) {
13228
14745
  return { content: [{ type: "text", text: `Error in kuma_memory: ${err}` }], isError: true };
@@ -13230,10 +14747,10 @@ function registerAllTools(server) {
13230
14747
  };
13231
14748
  server.tool("kuma_memory", memoryDesc, memorySchema, memoryHandler);
13232
14749
  server.tool("kuma_kuma_memory", memoryDesc, memorySchema, memoryHandler);
13233
- 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).";
14750
+ 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).";
13234
14751
  const safetySchema = {
13235
- action: z.enum(["guard", "verify", "checkpoint", "rollback_label"]).describe(
13236
- "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"
14752
+ action: z.enum(["guard", "verify", "checkpoint", "rollback_label", "cost"]).describe(
14753
+ "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"
13237
14754
  ),
13238
14755
  scope: z.string().optional().describe("Scope for verify (e.g. 'auth', file path)"),
13239
14756
  target: z.string().optional().describe("Target test file or scope for verify"),
@@ -13249,7 +14766,7 @@ function registerAllTools(server) {
13249
14766
  const safetyHandler = async (params) => {
13250
14767
  try {
13251
14768
  await ensureInitialized();
13252
- const text = await handleSafety({
14769
+ const { result: text } = await withCost("kuma_safety", params.action, () => handleSafety({
13253
14770
  action: params.action,
13254
14771
  guardGoal: params.guardGoal,
13255
14772
  guardCheck: params.guardCheck,
@@ -13261,7 +14778,7 @@ function registerAllTools(server) {
13261
14778
  force: params.force,
13262
14779
  label: params.label,
13263
14780
  description: params.description
13264
- });
14781
+ }));
13265
14782
  return { content: [{ type: "text", text }] };
13266
14783
  } catch (err) {
13267
14784
  return { content: [{ type: "text", text: `Error in kuma_safety: ${err}` }], isError: true };
@@ -13296,6 +14813,10 @@ Usage:
13296
14813
  npx @plumpslabs/kuma init --help Show this help
13297
14814
  npx @plumpslabs/kuma hook pre-edit Claude Code PreToolUse hook \u2014 inject gotcha/decision/history before edits
13298
14815
  npx @plumpslabs/kuma hook pre-bash Claude Code PreToolUse hook \u2014 inject command-triggered gotchas before Bash
14816
+ npx @plumpslabs/kuma hook session-start SessionStart hook \u2014 cached repo brief before the first turn
14817
+ npx @plumpslabs/kuma hook pre-compact PreCompact hook \u2014 survival set (goal + critical gotchas + dirty)
14818
+ npx @plumpslabs/kuma hook post-commit Git post-commit \u2014 periodic incremental map sync (no rebuild)
14819
+ npx @plumpslabs/kuma hook install-git Install the git post-commit hook (idempotent)
13299
14820
 
13300
14821
  Available config files:
13301
14822
  --claude CLAUDE.md (Claude Code)
@@ -13388,14 +14909,14 @@ async function main() {
13388
14909
  console.error("");
13389
14910
  let killedCount = 0;
13390
14911
  try {
13391
- const path30 = await import("path");
13392
- const fs27 = await import("fs");
13393
- const lockDir = path30.resolve(process.cwd(), ".kuma/verifier.lock");
13394
- if (fs27.existsSync(lockDir)) {
13395
- const pidFile = path30.join(lockDir, "pid");
13396
- if (fs27.existsSync(pidFile)) {
14912
+ const path35 = await import("path");
14913
+ const fs32 = await import("fs");
14914
+ const lockDir = path35.resolve(process.cwd(), ".kuma/verifier.lock");
14915
+ if (fs32.existsSync(lockDir)) {
14916
+ const pidFile = path35.join(lockDir, "pid");
14917
+ if (fs32.existsSync(pidFile)) {
13397
14918
  try {
13398
- const pid = parseInt(fs27.readFileSync(pidFile, "utf-8"), 10);
14919
+ const pid = parseInt(fs32.readFileSync(pidFile, "utf-8"), 10);
13399
14920
  if (pid && pid !== process.pid) {
13400
14921
  try {
13401
14922
  process.kill(-pid, "SIGKILL");
@@ -13411,7 +14932,7 @@ async function main() {
13411
14932
  } catch {
13412
14933
  }
13413
14934
  }
13414
- fs27.rmSync(lockDir, { recursive: true, force: true });
14935
+ fs32.rmSync(lockDir, { recursive: true, force: true });
13415
14936
  }
13416
14937
  if (killedCount === 0) {
13417
14938
  console.error("\u2705 No running verifications found to kill.");
@@ -13419,25 +14940,25 @@ async function main() {
13419
14940
  } catch {
13420
14941
  }
13421
14942
  try {
13422
- const { execSync: execSync7 } = await import("child_process");
14943
+ const { execSync: execSync9 } = await import("child_process");
13423
14944
  try {
13424
- execSync7("pkill -f 'jest' 2>/dev/null || true");
14945
+ execSync9("pkill -f 'jest' 2>/dev/null || true");
13425
14946
  } catch {
13426
14947
  }
13427
14948
  try {
13428
- execSync7("pkill -f 'jest-worker' 2>/dev/null || true");
14949
+ execSync9("pkill -f 'jest-worker' 2>/dev/null || true");
13429
14950
  } catch {
13430
14951
  }
13431
14952
  try {
13432
- execSync7("pkill -f 'pnpm test' 2>/dev/null || true");
14953
+ execSync9("pkill -f 'pnpm test' 2>/dev/null || true");
13433
14954
  } catch {
13434
14955
  }
13435
14956
  try {
13436
- execSync7("pkill -f 'npm test' 2>/dev/null || true");
14957
+ execSync9("pkill -f 'npm test' 2>/dev/null || true");
13437
14958
  } catch {
13438
14959
  }
13439
14960
  try {
13440
- execSync7("pkill -f 'yarn test' 2>/dev/null || true");
14961
+ execSync9("pkill -f 'yarn test' 2>/dev/null || true");
13441
14962
  } catch {
13442
14963
  }
13443
14964
  console.error("\u2705 Killed orphaned test processes");
@@ -13505,6 +15026,20 @@ async function main() {
13505
15026
  }
13506
15027
  const target = hook.filePaths[0];
13507
15028
  await trackFileEditLoop2(target);
15029
+ try {
15030
+ const { markDirty: markDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
15031
+ markDirty2(target);
15032
+ } catch {
15033
+ }
15034
+ try {
15035
+ const { maybeAutoCheckpoint: maybeAutoCheckpoint2 } = await Promise.resolve().then(() => (init_kumaCheckpoint(), kumaCheckpoint_exports));
15036
+ const autoCp = maybeAutoCheckpoint2(target);
15037
+ if (autoCp) {
15038
+ const { recordInjection: recordInjection2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
15039
+ await recordInjection2({ filePath: target, kind: "edit" });
15040
+ }
15041
+ } catch {
15042
+ }
13508
15043
  if (!checkInjectDedupe2(target)) {
13509
15044
  process.stdout.write("{}");
13510
15045
  process.exit(0);
@@ -13526,9 +15061,12 @@ async function main() {
13526
15061
  if (args[0] === "hook" && args[1] === "pre-bash") {
13527
15062
  try {
13528
15063
  const stdin = await readStdin();
13529
- const { getCommandContext: getCommandContext2, buildHookResponse: buildHookResponse2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
13530
- const data = JSON.parse(stdin || "{}");
13531
- const command = data && typeof data === "object" && data.tool_input && typeof data.tool_input.command === "string" ? data.tool_input.command : "";
15064
+ const { getCommandContext: getCommandContext2, buildHookResponse: buildHookResponse2, parseHookInput: parseHookInput2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
15065
+ let command = "";
15066
+ try {
15067
+ command = parseHookInput2(stdin).command || "";
15068
+ } catch {
15069
+ }
13532
15070
  if (!command.trim()) {
13533
15071
  process.stdout.write("{}");
13534
15072
  process.exit(0);
@@ -13547,9 +15085,138 @@ async function main() {
13547
15085
  }
13548
15086
  process.exit(0);
13549
15087
  }
15088
+ if (args[0] === "hook" && args[1] === "pre-compact") {
15089
+ try {
15090
+ const { getCompactionContext: getCompactionContext2 } = await Promise.resolve().then(() => (init_compactionContext(), compactionContext_exports));
15091
+ const { buildHookResponse: buildHookResponse2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
15092
+ const context = await getCompactionContext2();
15093
+ const body = context.trim() ? `\u{1F4E6} [KUMA survival set \u2014 preserve across compaction]
15094
+ ${context}` : "";
15095
+ process.stdout.write(buildHookResponse2(body));
15096
+ } catch {
15097
+ process.stdout.write("{}");
15098
+ }
15099
+ process.exit(0);
15100
+ }
15101
+ if (args[0] === "hook" && args[1] === "post-commit") {
15102
+ try {
15103
+ const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
15104
+ const result = await syncModifiedFiles2(50);
15105
+ try {
15106
+ const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
15107
+ await refreshPackageBriefs2();
15108
+ } catch {
15109
+ }
15110
+ try {
15111
+ const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
15112
+ clearDirty2();
15113
+ } catch {
15114
+ }
15115
+ process.stdout.write(`\u{1F43B} [Kuma] post-commit map sync: ${result.filesScanned} file(s), ${result.nodeCount} nodes, ${result.edgeCount} edges.`);
15116
+ } catch (err) {
15117
+ process.stdout.write(`\u{1F43B} [Kuma] post-commit sync skipped: ${err}`);
15118
+ }
15119
+ process.exit(0);
15120
+ }
15121
+ if (args[0] === "hook" && args[1] === "install-git") {
15122
+ try {
15123
+ const fsMod = await import("fs");
15124
+ const pathMod = await import("path");
15125
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
15126
+ const hookPath = pathMod.join(getProjectRoot2(), ".git", "hooks", "post-commit");
15127
+ const line = "kuma hook post-commit >/dev/null 2>&1 || true";
15128
+ if (fsMod.existsSync(hookPath)) {
15129
+ const existing = fsMod.readFileSync(hookPath, "utf-8");
15130
+ if (existing.includes("kuma hook post-commit")) {
15131
+ process.stdout.write("\u{1F43B} [Kuma] git post-commit hook already installed.");
15132
+ } else {
15133
+ fsMod.appendFileSync(hookPath, `
15134
+ # Kuma periodic mapping (industry auto-map)
15135
+ ${line}
15136
+ `, "utf-8");
15137
+ process.stdout.write("\u{1F43B} [Kuma] appended to existing git post-commit hook.");
15138
+ }
15139
+ } else {
15140
+ fsMod.writeFileSync(hookPath, `#!/bin/sh
15141
+ # Kuma periodic mapping (industry auto-map)
15142
+ ${line}
15143
+ `, { mode: 493 });
15144
+ process.stdout.write("\u{1F43B} [Kuma] git post-commit hook installed.");
15145
+ }
15146
+ } catch (err) {
15147
+ process.stdout.write(`\u{1F43B} [Kuma] install-git failed: ${err}`);
15148
+ }
15149
+ process.exit(0);
15150
+ }
13550
15151
  if (args[0] === "--hook") {
13551
15152
  process.exit(0);
13552
15153
  }
15154
+ if (args[0] === "hook" && args[1] === "session-start") {
15155
+ try {
15156
+ const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
15157
+ const root = getProjectRoot2();
15158
+ const proj = root.split("/").pop() || "unknown";
15159
+ const lines = [`\u{1F43B} [Kuma session brief] ${proj}`];
15160
+ try {
15161
+ const { execSync: execSync9 } = await import("child_process");
15162
+ const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
15163
+ cwd: root,
15164
+ encoding: "utf-8",
15165
+ timeout: 2e3,
15166
+ stdio: ["pipe", "pipe", "pipe"]
15167
+ }).trim();
15168
+ if (branch) lines.push(`git: ${branch}`);
15169
+ } catch {
15170
+ }
15171
+ try {
15172
+ const fsMod = await import("fs");
15173
+ const pathMod = await import("path");
15174
+ const dbFile = pathMod.join(root, ".kuma", "kuma.db");
15175
+ if (fsMod.existsSync(dbFile)) {
15176
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
15177
+ const db = await getDb2();
15178
+ const stmt = db.prepare(
15179
+ `SELECT COUNT(*) as cnt FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0)`
15180
+ );
15181
+ if (stmt.step()) lines.push(`active gotchas: ${stmt.getAsObject().cnt}`);
15182
+ stmt.free();
15183
+ const top = db.prepare(
15184
+ `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`
15185
+ );
15186
+ while (top.step()) {
15187
+ const r = top.getAsObject();
15188
+ lines.push(` [${r.severity}] ${r.file_path} \u2014 ${String(r.description).substring(0, 100)}`);
15189
+ }
15190
+ top.free();
15191
+ } else {
15192
+ const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
15193
+ const gotchas = getActiveGotchas2().filter((g) => g.severity === "critical" || g.severity === "high").slice(0, 3);
15194
+ lines.push(`active gotchas: ${getActiveGotchas2().length} (markdown layer)`);
15195
+ for (const g of gotchas) lines.push(` [${g.severity}] ${g.filePath} \u2014 ${g.description.substring(0, 100)}`);
15196
+ }
15197
+ } catch {
15198
+ }
15199
+ lines.push(`MUST: kuma_context({ action: "init" }) first; record gotchas immediately; verify after edits.`);
15200
+ try {
15201
+ const { readBrief: readBrief2, briefFreshness: briefFreshness2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
15202
+ const { getWorkspaceInfo: getWorkspaceInfo2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
15203
+ const ws = await getWorkspaceInfo2(root);
15204
+ const firstPkg = ws.isWorkspace && ws.packages.length > 0 ? ws.packages[0].name : "root";
15205
+ const fresh = briefFreshness2(firstPkg);
15206
+ if (fresh.exists && fresh.fresh) {
15207
+ const brief = readBrief2(firstPkg, 800);
15208
+ if (brief) lines.push("", "\u{1F4DA} [cached brief \u2014 read, don't re-discover]", brief);
15209
+ } else {
15210
+ lines.push(`\u{1F4DA} No fresh brief (age: ${fresh.age}) \u2014 run kuma_context({ action: "map" }) once to pre-compute.`);
15211
+ }
15212
+ } catch {
15213
+ }
15214
+ process.stdout.write(lines.join("\n"));
15215
+ } catch {
15216
+ process.stdout.write('\u{1F43B} [Kuma session brief] unavailable \u2014 run kuma_context({ action: "init" }).');
15217
+ }
15218
+ process.exit(0);
15219
+ }
13553
15220
  if (args[0] === "init") {
13554
15221
  const flags = args.slice(1);
13555
15222
  if (flags.includes("--help") || flags.includes("-h")) {
@@ -13610,23 +15277,23 @@ async function main() {
13610
15277
  });
13611
15278
  const output = formatInitResults(results);
13612
15279
  console.log(output);
13613
- const fs27 = await import("fs");
13614
- const path30 = await import("path");
13615
- const matchaSkills = path30.resolve(process.cwd(), "skills/matcha/SKILL.md");
13616
- const matchaAgents = path30.resolve(
15280
+ const fs32 = await import("fs");
15281
+ const path35 = await import("path");
15282
+ const matchaSkills = path35.resolve(process.cwd(), "skills/matcha/SKILL.md");
15283
+ const matchaAgents = path35.resolve(
13617
15284
  process.cwd(),
13618
15285
  ".agents/skills/matcha/SKILL.md"
13619
15286
  );
13620
- const matchaRootSkills = path30.resolve(
15287
+ const matchaRootSkills = path35.resolve(
13621
15288
  process.cwd(),
13622
15289
  "skills/matcha/SKILL.md"
13623
15290
  );
13624
- const matchaAgentsMd = path30.resolve(process.cwd(), "AGENTS.md");
13625
- const matchaWindsurfRules = path30.resolve(
15291
+ const matchaAgentsMd = path35.resolve(process.cwd(), "AGENTS.md");
15292
+ const matchaWindsurfRules = path35.resolve(
13626
15293
  process.cwd(),
13627
15294
  ".windsurf"
13628
15295
  );
13629
- if (fs27.existsSync(matchaSkills) || fs27.existsSync(matchaAgents) || fs27.existsSync(matchaRootSkills) || fs27.existsSync(matchaAgentsMd) || fs27.existsSync(matchaWindsurfRules)) {
15296
+ if (fs32.existsSync(matchaSkills) || fs32.existsSync(matchaAgents) || fs32.existsSync(matchaRootSkills) || fs32.existsSync(matchaAgentsMd) || fs32.existsSync(matchaWindsurfRules)) {
13630
15297
  console.error(
13631
15298
  "\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
13632
15299
  );
@@ -13640,13 +15307,13 @@ async function main() {
13640
15307
  (async () => {
13641
15308
  try {
13642
15309
  const { generateInitMdContent: generateInitMdContent2 } = await Promise.resolve().then(() => (init_init(), init_exports));
13643
- const fs27 = await import("fs");
13644
- const path30 = await import("path");
13645
- const initMdPath = path30.resolve(process.cwd(), ".kuma/init.md");
13646
- if (!fs27.existsSync(initMdPath)) {
13647
- const kumaDir = path30.dirname(initMdPath);
13648
- if (!fs27.existsSync(kumaDir)) fs27.mkdirSync(kumaDir, { recursive: true });
13649
- fs27.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
15310
+ const fs32 = await import("fs");
15311
+ const path35 = await import("path");
15312
+ const initMdPath = path35.resolve(process.cwd(), ".kuma/init.md");
15313
+ if (!fs32.existsSync(initMdPath)) {
15314
+ const kumaDir = path35.dirname(initMdPath);
15315
+ if (!fs32.existsSync(kumaDir)) fs32.mkdirSync(kumaDir, { recursive: true });
15316
+ fs32.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
13650
15317
  console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
13651
15318
  }
13652
15319
  } catch (err) {
@@ -13657,8 +15324,8 @@ async function main() {
13657
15324
  try {
13658
15325
  const { detectAgent: detectAgent2, getSkillPath: getSkillPath2, getAgentLabel: getAgentLabel2 } = await Promise.resolve().then(() => (init_agentDetector(), agentDetector_exports));
13659
15326
  const { generateSkill: generateSkill2, getSecondaryFiles: getSecondaryFiles2 } = await Promise.resolve().then(() => (init_skillGenerator(), skillGenerator_exports));
13660
- const fs27 = await import("fs");
13661
- const path30 = await import("path");
15327
+ const fs32 = await import("fs");
15328
+ const path35 = await import("path");
13662
15329
  const detection = detectAgent2();
13663
15330
  if (!detection.primary) {
13664
15331
  console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
@@ -13666,22 +15333,22 @@ async function main() {
13666
15333
  }
13667
15334
  const agentType = detection.primary;
13668
15335
  const skillPath = getSkillPath2(agentType);
13669
- const fullPath = path30.resolve(process.cwd(), skillPath);
13670
- if (fs27.existsSync(fullPath)) {
15336
+ const fullPath = path35.resolve(process.cwd(), skillPath);
15337
+ if (fs32.existsSync(fullPath)) {
13671
15338
  console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel2(agentType)} \u2014 skipping`);
13672
15339
  return;
13673
15340
  }
13674
- const dir = path30.dirname(fullPath);
13675
- if (!fs27.existsSync(dir)) fs27.mkdirSync(dir, { recursive: true });
13676
- fs27.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
15341
+ const dir = path35.dirname(fullPath);
15342
+ if (!fs32.existsSync(dir)) fs32.mkdirSync(dir, { recursive: true });
15343
+ fs32.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
13677
15344
  console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel2(agentType)}`);
13678
15345
  const secondaryFiles = getSecondaryFiles2(agentType);
13679
15346
  for (const sf of secondaryFiles) {
13680
- const sfPath = path30.resolve(process.cwd(), sf.path);
13681
- const sfDir = path30.dirname(sfPath);
13682
- if (!fs27.existsSync(sfPath)) {
13683
- if (!fs27.existsSync(sfDir)) fs27.mkdirSync(sfDir, { recursive: true });
13684
- fs27.writeFileSync(sfPath, sf.content, "utf-8");
15347
+ const sfPath = path35.resolve(process.cwd(), sf.path);
15348
+ const sfDir = path35.dirname(sfPath);
15349
+ if (!fs32.existsSync(sfPath)) {
15350
+ if (!fs32.existsSync(sfDir)) fs32.mkdirSync(sfDir, { recursive: true });
15351
+ fs32.writeFileSync(sfPath, sf.content, "utf-8");
13685
15352
  console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
13686
15353
  }
13687
15354
  }