@plumpslabs/kuma 2.4.13 → 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();
@@ -2134,6 +2151,23 @@ No unresolved issues.`;
2134
2151
  this.state.metrics.filesEdited++;
2135
2152
  this.save();
2136
2153
  }
2154
+ // ============================================================
2155
+ // MICROCOMPACT — session-scoped shown-set (in-memory only)
2156
+ // ============================================================
2157
+ // Hook processes are separate OS processes, so cross-hook dedupe lives
2158
+ // in hook-state.json (15min window). THIS set covers the MCP server
2159
+ // process: same gotcha/decision text is never injected twice in one
2160
+ // server run — repeats collapse to zero tokens.
2161
+ shownKeys = /* @__PURE__ */ new Set();
2162
+ /** True when this exact payload was already injected this server run. */
2163
+ wasShown(kind, key) {
2164
+ return this.shownKeys.has(`${kind}::${key}`);
2165
+ }
2166
+ /** Mark payloads as injected. Bounded at 500 keys (FIFO-ish reset). */
2167
+ markShown(kind, keys) {
2168
+ for (const k of keys) this.shownKeys.add(`${kind}::${k}`);
2169
+ if (this.shownKeys.size > 500) this.shownKeys = new Set([...this.shownKeys].slice(-500));
2170
+ }
2137
2171
  /**
2138
2172
  * Get metrics summary for session.
2139
2173
  */
@@ -4543,14 +4577,163 @@ var init_workspaceIntelligence = __esm({
4543
4577
  }
4544
4578
  });
4545
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
+
4546
4688
  // src/engine/impactAnalysis.ts
4547
4689
  var impactAnalysis_exports = {};
4548
4690
  __export(impactAnalysis_exports, {
4549
- calculateBlastRadius: () => calculateBlastRadius
4691
+ calculateBlastRadius: () => calculateBlastRadius,
4692
+ classifyDiffShape: () => classifyDiffShape,
4693
+ getIndexerStatus: () => getIndexerStatus
4550
4694
  });
4551
4695
  import fs9 from "fs";
4552
4696
  import path11 from "path";
4697
+ import { execSync as execSync4 } from "child_process";
4553
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
+ }
4554
4737
  async function findImportingFiles(targetFile, root) {
4555
4738
  const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
4556
4739
  try {
@@ -4587,7 +4770,7 @@ async function findImportingFiles(targetFile, root) {
4587
4770
  }
4588
4771
  callStmt.free();
4589
4772
  if (dependents.length > 0) {
4590
- return dependents;
4773
+ return { dependents, evidence: dependents.map((d) => `${d} (graph edge)`) };
4591
4774
  }
4592
4775
  } catch {
4593
4776
  }
@@ -4603,6 +4786,7 @@ async function findImportingFiles(targetFile, root) {
4603
4786
  }
4604
4787
  );
4605
4788
  const dependents = [];
4789
+ const evidence = [];
4606
4790
  const importRegex = new RegExp(
4607
4791
  `(?:import|from|require\\s*\\(|export\\s+.*from)\\s*['"][^'"]*\\b${baseName}(?:\\.[a-zA-Z0-9]+)?['"]`,
4608
4792
  "i"
@@ -4612,18 +4796,21 @@ async function findImportingFiles(targetFile, root) {
4612
4796
  const fullPath = path11.join(root, f);
4613
4797
  try {
4614
4798
  const content = fs9.readFileSync(fullPath, "utf-8");
4615
- if (importRegex.test(content)) {
4799
+ const lines = content.split("\n");
4800
+ const hitIdx = lines.findIndex((l) => importRegex.test(l));
4801
+ if (hitIdx >= 0) {
4616
4802
  dependents.push(f);
4803
+ evidence.push(`${f}:${hitIdx + 1} (import match)`);
4617
4804
  }
4618
4805
  } catch {
4619
4806
  }
4620
4807
  }
4621
- return dependents;
4808
+ return { dependents, evidence };
4622
4809
  } catch {
4623
- return [];
4810
+ return { dependents: [], evidence: [] };
4624
4811
  }
4625
4812
  }
4626
- async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = []) {
4813
+ async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = [], exactOnly = false) {
4627
4814
  const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
4628
4815
  try {
4629
4816
  const db = await getDb();
@@ -4652,14 +4839,10 @@ async function findRelatedTests(targetFile, root, owningPackage, additionalDepen
4652
4839
  const testFiles = [];
4653
4840
  const stems = /* @__PURE__ */ new Set();
4654
4841
  if (baseName) stems.add(baseName);
4655
- for (const part of baseName.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
4656
- stems.add(part);
4657
- }
4658
- for (const dep of additionalDependents.slice(0, 3)) {
4659
- const depBase = path11.basename(dep, path11.extname(dep));
4660
- if (depBase) stems.add(depBase);
4661
- for (const part of depBase.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
4662
- stems.add(part);
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);
4663
4846
  }
4664
4847
  }
4665
4848
  const testPatterns = [];
@@ -4748,6 +4931,22 @@ function assessRisk(target, targetType, directDependents, downstreamPackages, af
4748
4931
  }
4749
4932
  return { risk, riskFlags };
4750
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
+ }
4751
4950
  async function calculateBlastRadius(target, options = {}) {
4752
4951
  const root = options.root || getProjectRoot();
4753
4952
  const wsInfo = await getWorkspaceInfo(root);
@@ -4772,66 +4971,51 @@ async function calculateBlastRadius(target, options = {}) {
4772
4971
  downstreamPackages.push(...rev.map((p) => p.name));
4773
4972
  }
4774
4973
  let directDependents = [];
4974
+ let evidence = [];
4775
4975
  if (targetType === "file" || targetType === "symbol") {
4776
- directDependents = await findImportingFiles(target, root);
4777
- }
4778
- const normLower = target.toLowerCase();
4779
- const isUserDestructive = /(user|account|member|customer|profile).*(delet|destro|remov|wipe|terminat)|(delet|destro|remov|wipe|terminat).*(user|account|member|customer|profile)/i.test(target);
4780
- const isSecurityTopic = /(auth|security|session|permission|jwt|token|middleware)/i.test(target);
4781
- if (isUserDestructive || isSecurityTopic || directDependents.length === 0) {
4782
- try {
4783
- const db = await getDb();
4784
- const tokens = normLower.split(/[\s_-]+/).filter((t) => t.length >= 3);
4785
- for (const token of tokens) {
4786
- const stmt = db.prepare(`SELECT DISTINCT file_path, name FROM nodes WHERE (name LIKE ? OR file_path LIKE ?) LIMIT 5`);
4787
- stmt.bind([`%${token}%`, `%${token}%`]);
4788
- while (stmt.step()) {
4789
- const row = stmt.getAsObject();
4790
- const fp = row.file_path || row.name;
4791
- if (fp && !directDependents.includes(fp) && fp !== target) {
4792
- directDependents.push(fp);
4793
- }
4794
- }
4795
- stmt.free();
4796
- }
4797
- const authMiddlewareMatches = await fastGlob3(
4798
- [
4799
- "**/*auth-middleware*.*",
4800
- "**/*auth_middleware*.*",
4801
- "**/*auth.middleware*.*",
4802
- "**/middleware/*auth*.*",
4803
- "**/middlewares/*auth*.*",
4804
- "**/guards/*auth*.*",
4805
- "**/security/*auth*.*",
4806
- "**/lib/*auth*.*"
4807
- ],
4808
- {
4809
- cwd: root,
4810
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kuma/**"],
4811
- onlyFiles: true
4812
- }
4813
- );
4814
- for (const am of authMiddlewareMatches) {
4815
- if (!directDependents.includes(am) && am !== target) {
4816
- directDependents.push(am);
4817
- }
4818
- }
4819
- } 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);
4820
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
+ ));
4821
5013
  }
4822
- const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents);
4823
- const { risk, riskFlags } = assessRisk(
4824
- target,
4825
- targetType,
4826
- directDependents,
4827
- downstreamPackages,
4828
- affectedTests,
4829
- owningPackage
4830
- );
4831
5014
  const summaryLines = [
4832
5015
  `\u{1F3AF} **Impact / Blast Radius: \`${target}\`**`,
4833
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`,
4834
5017
  `\u26A0\uFE0F **Risk Level: ${risk.toUpperCase()}**`,
5018
+ `\u{1F4D0} Diff shape: \`${diffShape}\` (${shapeDetail})`,
4835
5019
  owningPackage ? `\u{1F4E6} Owning package: \`${owningPackage.name}\` (${owningPackage.path})` : ""
4836
5020
  ];
4837
5021
  if (riskFlags.length > 0) {
@@ -4858,11 +5042,22 @@ async function calculateBlastRadius(target, options = {}) {
4858
5042
  summaryLines.push(` ... and ${directDependents.length - 6} more`);
4859
5043
  }
4860
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
+ }
4861
5055
  if (affectedTests.length > 0) {
4862
5056
  summaryLines.push("");
4863
5057
  summaryLines.push(`\u{1F9EA} **Affected Test Suites (${affectedTests.length}):**`);
4864
5058
  for (const t of affectedTests.slice(0, 5)) {
4865
- summaryLines.push(` \u2022 \`${t}\``);
5059
+ const why = testReasons.get(t) ? ` \u2014 ${testReasons.get(t)}` : "";
5060
+ summaryLines.push(` \u2022 \`${t}\`${why}`);
4866
5061
  }
4867
5062
  if (affectedTests.length > 5) {
4868
5063
  summaryLines.push(` ... and ${affectedTests.length - 5} more`);
@@ -4881,15 +5076,52 @@ async function calculateBlastRadius(target, options = {}) {
4881
5076
  risk,
4882
5077
  riskFlags,
4883
5078
  summary: summaryLines.filter(Boolean).join("\n"),
4884
- confidence: 0.85
5079
+ confidence: nonRuntime ? 0.95 : 0.85,
5080
+ diffShape,
5081
+ evidence
4885
5082
  };
4886
5083
  }
5084
+ var RUNTIME_CODE_EXTS, COMMENT_PREFIXES;
4887
5085
  var init_impactAnalysis = __esm({
4888
5086
  "src/engine/impactAnalysis.ts"() {
4889
5087
  "use strict";
4890
5088
  init_pathValidator();
4891
5089
  init_kumaDb();
4892
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
+ };
4893
5125
  }
4894
5126
  });
4895
5127
 
@@ -5963,27 +6195,27 @@ async function codebaseSearchFallback(query, limit = 20) {
5963
6195
  if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
5964
6196
  let entries = [];
5965
6197
  try {
5966
- entries = fs27.readdirSync(dir);
6198
+ entries = fs32.readdirSync(dir);
5967
6199
  } catch {
5968
6200
  return;
5969
6201
  }
5970
6202
  for (const entry of entries) {
5971
6203
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
5972
- const full = path30.join(dir, entry);
6204
+ const full = path35.join(dir, entry);
5973
6205
  try {
5974
- const stat = fs27.statSync(full);
6206
+ const stat = fs32.statSync(full);
5975
6207
  if (stat.isDirectory()) {
5976
6208
  walk2(full, depth + 1);
5977
6209
  } else if (entry.toLowerCase().includes(query.toLowerCase())) {
5978
- results.push(path30.relative(root, full));
6210
+ results.push(path35.relative(root, full));
5979
6211
  }
5980
6212
  } catch {
5981
6213
  }
5982
6214
  }
5983
6215
  };
5984
6216
  var walk = walk2;
5985
- const fs27 = await import("fs");
5986
- const path30 = await import("path");
6217
+ const fs32 = await import("fs");
6218
+ const path35 = await import("path");
5987
6219
  const root = process.cwd();
5988
6220
  const results = [];
5989
6221
  const MAX_WALK_DEPTH = 8;
@@ -6116,9 +6348,9 @@ ${summary}`;
6116
6348
  }
6117
6349
  async function codebaseImpactFallback(target) {
6118
6350
  try {
6119
- const { execSync: execSync7 } = await import("child_process");
6351
+ const { execSync: execSync9 } = await import("child_process");
6120
6352
  const root = process.cwd();
6121
- const grepResult = execSync7(
6353
+ const grepResult = execSync9(
6122
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`,
6123
6355
  { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
6124
6356
  ).trim();
@@ -6245,7 +6477,28 @@ var init_kumaGraph = __esm({
6245
6477
  });
6246
6478
 
6247
6479
  // src/utils/kumaShared.ts
6248
- 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";
6249
6502
  function isEditTool(toolName) {
6250
6503
  const n = (toolName || "").toLowerCase();
6251
6504
  return n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply") || n.includes("replace");
@@ -6283,7 +6536,7 @@ function getSessionStats(inputGoal) {
6283
6536
  function getGitDiffStat(timeout = 3e3) {
6284
6537
  try {
6285
6538
  const root = getProjectRoot();
6286
- return execSync4("git diff --stat", {
6539
+ return execSync5("git diff --stat", {
6287
6540
  cwd: root,
6288
6541
  encoding: "utf-8",
6289
6542
  timeout
@@ -6292,6 +6545,83 @@ function getGitDiffStat(timeout = 3e3) {
6292
6545
  return "";
6293
6546
  }
6294
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
+ }
6295
6625
  function getUnresolvedCount(failedFiles) {
6296
6626
  let count = 0;
6297
6627
  for (const f of failedFiles) {
@@ -6301,6 +6631,28 @@ function getUnresolvedCount(failedFiles) {
6301
6631
  }
6302
6632
  return count;
6303
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
+ }
6304
6656
  function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
6305
6657
  const drifts = [];
6306
6658
  if (modifiedFiles > 0 && !hasRunTests) {
@@ -6339,7 +6691,7 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
6339
6691
  if (!goal) {
6340
6692
  return "No goal set \u2014 use goal parameter or setGoal to track intent";
6341
6693
  }
6342
- if (modifiedFiles === 0) {
6694
+ if (modifiedFiles === 0 && !hasRunTests) {
6343
6695
  return "Start by exploring what exists before writing code";
6344
6696
  }
6345
6697
  return "On track \u2014 continue with current approach";
@@ -6347,11 +6699,43 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
6347
6699
  function countEditCalls(toolCalls) {
6348
6700
  return toolCalls.filter((c) => isEditTool(c.toolName)).length;
6349
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;
6350
6731
  var init_kumaShared = __esm({
6351
6732
  "src/utils/kumaShared.ts"() {
6352
6733
  "use strict";
6353
6734
  init_sessionMemory();
6354
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/;
6355
6739
  }
6356
6740
  });
6357
6741
 
@@ -6365,19 +6749,19 @@ __export(kumaMemory_exports, {
6365
6749
  scoreMemoryRelevance: () => scoreMemoryRelevance,
6366
6750
  shouldRecordDecision: () => shouldRecordDecision
6367
6751
  });
6368
- import fs10 from "fs";
6369
- import path12 from "path";
6752
+ import fs11 from "fs";
6753
+ import path13 from "path";
6370
6754
  function scoreMemoryRelevance(context, limit = 5) {
6371
6755
  const results = [];
6372
- const kumaDir = path12.join(getProjectRoot(), MEMORY_DIR);
6373
- if (!fs10.existsSync(kumaDir)) return [];
6756
+ const kumaDir = path13.join(getProjectRoot(), MEMORY_DIR);
6757
+ if (!fs11.existsSync(kumaDir)) return [];
6374
6758
  const terms = Array.from(new Set(context.toLowerCase().split(/[\s/._-]+/).filter((w) => w.length > 2)));
6375
6759
  if (terms.length === 0) return [];
6376
6760
  try {
6377
- const files = fs10.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
6761
+ const files = fs11.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
6378
6762
  for (const file of files) {
6379
6763
  try {
6380
- const content = fs10.readFileSync(path12.join(kumaDir, file), "utf-8");
6764
+ const content = fs11.readFileSync(path13.join(kumaDir, file), "utf-8");
6381
6765
  const lower = content.toLowerCase();
6382
6766
  let matchCount = 0;
6383
6767
  for (const term of terms) {
@@ -6548,13 +6932,86 @@ var init_kumaMemory = __esm({
6548
6932
  }
6549
6933
  });
6550
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
+
6551
7008
  // src/engine/codeReuseEngine.ts
6552
7009
  var codeReuseEngine_exports = {};
6553
7010
  __export(codeReuseEngine_exports, {
6554
7011
  findReusableSymbols: () => findReusableSymbols
6555
7012
  });
6556
- import path13 from "path";
6557
- import fs11 from "fs";
7013
+ import path15 from "path";
7014
+ import fs13 from "fs";
6558
7015
  async function findReusableSymbols(query, scope) {
6559
7016
  const cleanQuery = query.trim().toLowerCase();
6560
7017
  const searchTokens = cleanQuery.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((t) => t.length >= 2);
@@ -6677,7 +7134,7 @@ function detectRecommendedUtilDirectory() {
6677
7134
  "utils"
6678
7135
  ];
6679
7136
  for (const c of candidates) {
6680
- if (fs11.existsSync(path13.resolve(root, c))) {
7137
+ if (fs13.existsSync(path15.resolve(root, c))) {
6681
7138
  return c;
6682
7139
  }
6683
7140
  }
@@ -6696,7 +7153,7 @@ var domainClusterEngine_exports = {};
6696
7153
  __export(domainClusterEngine_exports, {
6697
7154
  buildSubsystemClusters: () => buildSubsystemClusters
6698
7155
  });
6699
- import path14 from "path";
7156
+ import path16 from "path";
6700
7157
  async function buildSubsystemClusters() {
6701
7158
  const db = await getDb();
6702
7159
  const fileStmt = db.prepare(`
@@ -6773,7 +7230,7 @@ async function buildSubsystemClusters() {
6773
7230
  for (const [cKey, files] of clusterFileMap.entries()) {
6774
7231
  for (const f of files) {
6775
7232
  fileToCluster.set(f, cKey);
6776
- fileToCluster.set(path14.normalize(f), cKey);
7233
+ fileToCluster.set(path16.normalize(f), cKey);
6777
7234
  }
6778
7235
  }
6779
7236
  const clusterInteractions = /* @__PURE__ */ new Map();
@@ -6781,8 +7238,8 @@ async function buildSubsystemClusters() {
6781
7238
  const srcFile = extractFilePathFromNodeId(e.source);
6782
7239
  const tgtFile = extractFilePathFromNodeId(e.target);
6783
7240
  if (!srcFile || !tgtFile) continue;
6784
- const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(path14.normalize(srcFile));
6785
- const tgtCluster = fileToCluster.get(tgtFile) || fileToCluster.get(path14.normalize(tgtFile));
7241
+ const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(path16.normalize(srcFile));
7242
+ const tgtCluster = fileToCluster.get(tgtFile) || fileToCluster.get(path16.normalize(tgtFile));
6786
7243
  if (srcCluster && tgtCluster && srcCluster !== tgtCluster) {
6787
7244
  const pairKey = `${srcCluster}->${tgtCluster}`;
6788
7245
  const stats = clusterInteractions.get(pairKey) || { calls: 0, imports: 0 };
@@ -6804,13 +7261,13 @@ async function buildSubsystemClusters() {
6804
7261
  const clusterId = nodeId("feature_domain", cKey);
6805
7262
  const name = formatClusterName(cKey);
6806
7263
  const role = inferSubsystemRole(cKey, files);
6807
- const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path14.basename(f))).slice(0, 3);
7264
+ const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path16.basename(f))).slice(0, 3);
6808
7265
  if (entrypoints.length === 0 && files.length > 0) {
6809
7266
  entrypoints.push(files[0]);
6810
7267
  }
6811
7268
  const allSyms = [];
6812
7269
  for (const f of files) {
6813
- const syms = fileSymbols.get(f) || fileSymbols.get(path14.normalize(f)) || [];
7270
+ const syms = fileSymbols.get(f) || fileSymbols.get(path16.normalize(f)) || [];
6814
7271
  for (const s of syms) {
6815
7272
  if (!allSyms.includes(s)) allSyms.push(s);
6816
7273
  }
@@ -6989,7 +7446,7 @@ function inferSubsystemRole(key, files) {
6989
7446
  if (k.includes("util") || k.includes("common")) {
6990
7447
  return "Shared path validation, configuration loading, and platform detection helpers.";
6991
7448
  }
6992
- 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(", ")}.`;
6993
7450
  }
6994
7451
  function inferTypedVerb(sourceKey, targetKey, _stats) {
6995
7452
  const src = sourceKey.toLowerCase();
@@ -7068,18 +7525,18 @@ __export(domainRules_exports, {
7068
7525
  readLayer: () => readLayer,
7069
7526
  writeLayer: () => writeLayer
7070
7527
  });
7071
- import fs12 from "fs";
7072
- import path15 from "path";
7528
+ import fs14 from "fs";
7529
+ import path17 from "path";
7073
7530
  function layerFilePath(layer) {
7074
- return path15.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
7531
+ return path17.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
7075
7532
  }
7076
7533
  function ensureLayerFile(layer) {
7077
7534
  const fp = layerFilePath(layer);
7078
- if (!fs12.existsSync(fp)) {
7535
+ if (!fs14.existsSync(fp)) {
7079
7536
  const template = layer === "arch_flow" ? ARCH_FLOW_TEMPLATE : GOTCHAS_TEMPLATE;
7080
- const dir = path15.dirname(fp);
7081
- if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
7082
- fs12.writeFileSync(fp, template, "utf-8");
7537
+ const dir = path17.dirname(fp);
7538
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
7539
+ fs14.writeFileSync(fp, template, "utf-8");
7083
7540
  console.error(`[DomainRules] Auto-created ${LAYER_FILES[layer].filename}`);
7084
7541
  }
7085
7542
  return fp;
@@ -7087,7 +7544,7 @@ function ensureLayerFile(layer) {
7087
7544
  function readLayer(layer) {
7088
7545
  try {
7089
7546
  const fp = ensureLayerFile(layer);
7090
- return fs12.readFileSync(fp, "utf-8");
7547
+ return fs14.readFileSync(fp, "utf-8");
7091
7548
  } catch (err) {
7092
7549
  return `Error reading ${layer}: ${err}`;
7093
7550
  }
@@ -7095,7 +7552,7 @@ function readLayer(layer) {
7095
7552
  function writeLayer(layer, content) {
7096
7553
  try {
7097
7554
  const fp = ensureLayerFile(layer);
7098
- fs12.writeFileSync(fp, content, "utf-8");
7555
+ fs14.writeFileSync(fp, content, "utf-8");
7099
7556
  const label = LAYER_FILES[layer].label;
7100
7557
  sessionMemory.recordToolCall("kuma_memory", { action: `write_${layer}` });
7101
7558
  return `\u2705 **${label}** updated successfully.`;
@@ -7106,9 +7563,9 @@ function writeLayer(layer, content) {
7106
7563
  function appendToLayer(layer, entry) {
7107
7564
  try {
7108
7565
  const fp = ensureLayerFile(layer);
7109
- const existing = fs12.readFileSync(fp, "utf-8");
7566
+ const existing = fs14.readFileSync(fp, "utf-8");
7110
7567
  const newContent = existing.trimEnd() + "\n\n" + entry.trim() + "\n";
7111
- fs12.writeFileSync(fp, newContent, "utf-8");
7568
+ fs14.writeFileSync(fp, newContent, "utf-8");
7112
7569
  return `\u2705 Entry added to **${LAYER_FILES[layer].label}**.`;
7113
7570
  } catch (err) {
7114
7571
  return `\u274C Failed to append: ${err}`;
@@ -7118,7 +7575,7 @@ function getActiveGotchas() {
7118
7575
  const results = [];
7119
7576
  try {
7120
7577
  const fp = ensureLayerFile("gotcha");
7121
- const content = fs12.readFileSync(fp, "utf-8");
7578
+ const content = fs14.readFileSync(fp, "utf-8");
7122
7579
  const lines = content.split("\n");
7123
7580
  let currentSection = "";
7124
7581
  let currentFilePath = "";
@@ -7154,9 +7611,9 @@ function getActiveGotchas() {
7154
7611
  function checkFileGotchas(filePath) {
7155
7612
  const gotchas = getActiveGotchas();
7156
7613
  const warnings = [];
7157
- const base = path15.basename(filePath);
7614
+ const base = path17.basename(filePath);
7158
7615
  for (const g of gotchas) {
7159
- const gBase = path15.basename(g.filePath);
7616
+ const gBase = path17.basename(g.filePath);
7160
7617
  if (filePath.includes(g.filePath) || g.filePath.includes(filePath) || base && gBase && base === gBase) {
7161
7618
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
7162
7619
  warnings.push(`${icon} **Gotcha**: ${g.description} (${g.severity})`);
@@ -7256,8 +7713,8 @@ __export(kumaInject_exports, {
7256
7713
  parseHookInput: () => parseHookInput,
7257
7714
  trackFileEditLoop: () => trackFileEditLoop
7258
7715
  });
7259
- import fs13 from "fs";
7260
- import path16 from "path";
7716
+ import fs15 from "fs";
7717
+ import path18 from "path";
7261
7718
  function hashFileContent(filePath) {
7262
7719
  return hashFile(filePath);
7263
7720
  }
@@ -7268,16 +7725,17 @@ function isGotchaFresh(storedHash, currentHash) {
7268
7725
  }
7269
7726
  async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false) {
7270
7727
  const root = getProjectRoot();
7271
- const base = path16.basename(filePath);
7728
+ const base = path18.basename(filePath);
7272
7729
  try {
7273
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7274
- if (fs13.existsSync(dbFile)) {
7730
+ const dbFile = path18.join(root, ".kuma", "kuma.db");
7731
+ if (fs15.existsSync(dbFile)) {
7275
7732
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7276
7733
  const db = await getDb2();
7277
7734
  const stmt = db.prepare(`
7278
7735
  SELECT id, file_path, description, severity, workaround, content_hash
7279
7736
  FROM known_gotchas
7280
7737
  WHERE status IN ('active', 'verified')
7738
+ AND (quarantined IS NULL OR quarantined = 0)
7281
7739
  AND (
7282
7740
  file_path = ?
7283
7741
  OR file_path LIKE ?
@@ -7311,11 +7769,11 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
7311
7769
  } catch {
7312
7770
  }
7313
7771
  try {
7314
- if (!fs13.existsSync(path16.join(root, ".kuma"))) return [];
7772
+ if (!fs15.existsSync(path18.join(root, ".kuma"))) return [];
7315
7773
  const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
7316
7774
  const allGotchas = getActiveGotchas2();
7317
7775
  const matched = allGotchas.filter(
7318
- (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
7319
7777
  );
7320
7778
  if (matched.length > 0) {
7321
7779
  return matched.slice(0, limit).map((g) => ({
@@ -7334,8 +7792,8 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
7334
7792
  async function getFileTrace(filePath, limit = 8) {
7335
7793
  try {
7336
7794
  const root = getProjectRoot();
7337
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7338
- if (!fs13.existsSync(dbFile)) return [];
7795
+ const dbFile = path18.join(root, ".kuma", "kuma.db");
7796
+ if (!fs15.existsSync(dbFile)) return [];
7339
7797
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7340
7798
  const db = await getDb2();
7341
7799
  const stmt = db.prepare(`
@@ -7345,7 +7803,7 @@ async function getFileTrace(filePath, limit = 8) {
7345
7803
  ORDER BY cl.created_at DESC
7346
7804
  LIMIT ?
7347
7805
  `);
7348
- stmt.bind([`%${path16.basename(filePath)}%`, limit]);
7806
+ stmt.bind([`%${path18.basename(filePath)}%`, limit]);
7349
7807
  const rows = [];
7350
7808
  while (stmt.step()) rows.push(stmt.getAsObject());
7351
7809
  stmt.free();
@@ -7363,7 +7821,7 @@ async function getFileTrace(filePath, limit = 8) {
7363
7821
  function formatFileTrace(entries, filePath) {
7364
7822
  if (entries.length === 0) return "";
7365
7823
  const lines = [
7366
- `\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:`
7367
7825
  ];
7368
7826
  for (const e of entries) {
7369
7827
  const date = new Date((e.createdAt || 0) * 1e3).toLocaleDateString("en-US", {
@@ -7379,12 +7837,12 @@ function formatFileTrace(entries, filePath) {
7379
7837
  async function getDecisionsForFile(filePath, limit = 3) {
7380
7838
  const decisions = [];
7381
7839
  const root = getProjectRoot();
7382
- const base = path16.basename(filePath);
7840
+ const base = path18.basename(filePath);
7383
7841
  const baseWithoutExt = base.replace(/\.[^.]+$/, "");
7384
7842
  try {
7385
- const decisionsMdPath = path16.join(root, ".kuma", "memories", "decisions.md");
7386
- if (fs13.existsSync(decisionsMdPath)) {
7387
- 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");
7388
7846
  const sections = content.split(/^##\s+/m);
7389
7847
  for (const section of sections) {
7390
7848
  if (!section.trim() || section.startsWith("# ")) continue;
@@ -7455,7 +7913,17 @@ async function getDecisionsForFile(filePath, limit = 3) {
7455
7913
  }
7456
7914
  async function getRelevantContext(filePath, goal) {
7457
7915
  const parts = [];
7458
- 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
+ );
7459
7927
  if (gotchas.length > 0) {
7460
7928
  const lines = ["\u26A0\uFE0F ACTIVE GOTCHAS (fresh):"];
7461
7929
  for (const g of gotchas) {
@@ -7465,7 +7933,12 @@ async function getRelevantContext(filePath, goal) {
7465
7933
  }
7466
7934
  parts.push(lines.join("\n"));
7467
7935
  }
7468
- 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)));
7469
7942
  if (decisions.length > 0) {
7470
7943
  parts.push("\u{1F4CC} RELEVANT DECISIONS:\n " + decisions.join("\n "));
7471
7944
  }
@@ -7474,13 +7947,13 @@ async function getRelevantContext(filePath, goal) {
7474
7947
  if (traceStr) parts.push(traceStr);
7475
7948
  try {
7476
7949
  const root = getProjectRoot();
7477
- if (fs13.existsSync(path16.join(root, ".kuma", "kuma.db"))) {
7950
+ if (fs15.existsSync(path18.join(root, ".kuma", "kuma.db"))) {
7478
7951
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7479
7952
  const db = await getDb2();
7480
7953
  const stmt = db.prepare(
7481
7954
  `SELECT COUNT(*) FROM known_gotchas WHERE status = 'resolved' AND file_path LIKE ?`
7482
7955
  );
7483
- stmt.bind([`%${path16.basename(filePath)}%`]);
7956
+ stmt.bind([`%${path18.basename(filePath)}%`]);
7484
7957
  const found = stmt.step();
7485
7958
  const count = found ? Number(stmt.getAsObject()["COUNT(*)"]) : 0;
7486
7959
  stmt.free();
@@ -7491,7 +7964,7 @@ async function getRelevantContext(filePath, goal) {
7491
7964
  } catch {
7492
7965
  }
7493
7966
  if (gotchas.length > 0) {
7494
- const scopeHint = path16.basename(path16.dirname(filePath)) || "current change";
7967
+ const scopeHint = path18.basename(path18.dirname(filePath)) || "current change";
7495
7968
  parts.push(
7496
7969
  `\u2705 AFTER editing, verify: kuma_safety({ action: "verify", scope: "${scopeHint}" })`
7497
7970
  );
@@ -7507,11 +7980,11 @@ async function getRelevantContext(filePath, goal) {
7507
7980
  async function getFileSummary2(filePath) {
7508
7981
  try {
7509
7982
  const root = getProjectRoot();
7510
- const dbFile = path16.join(root, ".kuma", "kuma.db");
7511
- if (!fs13.existsSync(dbFile)) return null;
7983
+ const dbFile = path18.join(root, ".kuma", "kuma.db");
7984
+ if (!fs15.existsSync(dbFile)) return null;
7512
7985
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
7513
7986
  const db = await getDb2();
7514
- const base = path16.basename(filePath);
7987
+ const base = path18.basename(filePath);
7515
7988
  const stmt = db.prepare(`
7516
7989
  SELECT
7517
7990
  (SELECT COUNT(*) FROM known_gotchas WHERE status = 'active' AND (file_path = ? OR file_path LIKE ?)) as active_gotchas,
@@ -7603,9 +8076,9 @@ async function getCommandContext(command) {
7603
8076
  }
7604
8077
  function loadHookState(root) {
7605
8078
  try {
7606
- const fp = path16.join(root, HOOK_STATE_FILE);
7607
- if (fs13.existsSync(fp)) {
7608
- 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"));
7609
8082
  }
7610
8083
  } catch {
7611
8084
  }
@@ -7613,10 +8086,10 @@ function loadHookState(root) {
7613
8086
  }
7614
8087
  function saveHookState(root, state) {
7615
8088
  try {
7616
- const fp = path16.join(root, HOOK_STATE_FILE);
7617
- const dir = path16.dirname(fp);
7618
- if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7619
- fs13.writeFileSync(fp, JSON.stringify(state), "utf-8");
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");
7620
8093
  } catch {
7621
8094
  }
7622
8095
  }
@@ -7650,7 +8123,7 @@ async function trackFileEditLoop(filePath) {
7650
8123
  saveHookState(root, state);
7651
8124
  if (recorded) {
7652
8125
  try {
7653
- 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;
7654
8127
  const { addGotcha: addGotcha2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
7655
8128
  await addGotcha2({
7656
8129
  filePath,
@@ -7688,20 +8161,24 @@ __export(kumaGotchas_exports, {
7688
8161
  addGotcha: () => addGotcha,
7689
8162
  checkGotchasForFile: () => checkGotchasForFile,
7690
8163
  deprecateGotcha: () => deprecateGotcha,
8164
+ findSimilarGotchaId: () => findSimilarGotchaId,
7691
8165
  getActiveGotchasForCommand: () => getActiveGotchasForCommand,
7692
8166
  getGotchasByStatus: () => getGotchasByStatus,
7693
8167
  getInjectionStats: () => getInjectionStats,
8168
+ jaccardSimilarity: () => jaccardSimilarity,
7694
8169
  listGotchas: () => listGotchas,
7695
8170
  recordInjection: () => recordInjection,
7696
8171
  resolveGotcha: () => resolveGotcha,
7697
8172
  resolveGotchasForScope: () => resolveGotchasForScope,
8173
+ screenForInjection: () => screenForInjection,
7698
8174
  setGotchaStatus: () => setGotchaStatus,
7699
8175
  syncGotchaStatusToMarkdown: () => syncGotchaStatusToMarkdown,
7700
8176
  syncGotchasGraph: () => syncGotchasGraph,
7701
- syncGotchasToDb: () => syncGotchasToDb
8177
+ syncGotchasToDb: () => syncGotchasToDb,
8178
+ tokenizeForSimilarity: () => tokenizeForSimilarity
7702
8179
  });
7703
- import fs14 from "fs";
7704
- import path17 from "path";
8180
+ import fs16 from "fs";
8181
+ import path19 from "path";
7705
8182
  async function ensureGotchasSchema() {
7706
8183
  const db = await getDb();
7707
8184
  db.exec(`
@@ -7721,7 +8198,11 @@ async function ensureGotchasSchema() {
7721
8198
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
7722
8199
  last_verified_at INTEGER,
7723
8200
  scope_package TEXT,
7724
- 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
7725
8206
  );
7726
8207
  CREATE INDEX IF NOT EXISTS idx_gotchas_file ON known_gotchas(file_path);
7727
8208
  CREATE INDEX IF NOT EXISTS idx_gotchas_severity ON known_gotchas(severity);
@@ -7747,6 +8228,15 @@ async function ensureGotchasSchema() {
7747
8228
  if (!cols.includes("verified_by")) {
7748
8229
  db.run(`ALTER TABLE known_gotchas ADD COLUMN verified_by TEXT`);
7749
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
+ }
7750
8240
  const schema = db.exec(`SELECT sql FROM sqlite_master WHERE type='table' AND name='known_gotchas'`);
7751
8241
  const gotchaSql = schema[0]?.values?.[0]?.[0] || "";
7752
8242
  if (gotchaSql && !gotchaSql.includes("candidate")) {
@@ -7767,7 +8257,11 @@ async function ensureGotchasSchema() {
7767
8257
  CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
7768
8258
  last_verified_at INTEGER,
7769
8259
  scope_package TEXT,
7770
- 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
7771
8265
  )`);
7772
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`);
7773
8267
  db.run(`DROP TABLE known_gotchas_old`);
@@ -7776,12 +8270,30 @@ async function ensureGotchasSchema() {
7776
8270
  }
7777
8271
  saveDb();
7778
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
+ }
7779
8287
  async function addGotcha(entry) {
7780
8288
  try {
7781
8289
  await ensureGotchasSchema();
7782
8290
  const db = await getDb();
7783
8291
  const severity = entry.severity || "medium";
7784
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");
7785
8297
  const contentHash = hashFileContent(entry.filePath);
7786
8298
  const trap = entry.description;
7787
8299
  const ruleFix = entry.workaround || "Review implementation carefully before modifying.";
@@ -7806,15 +8318,23 @@ async function addGotcha(entry) {
7806
8318
  existingId = row.id;
7807
8319
  }
7808
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
+ }
7809
8329
  if (existingId !== null) {
7810
8330
  db.run(
7811
- `UPDATE known_gotchas SET severity = ?, workaround = ?, content_hash = ?, trigger_command = ?, status = ?, scope_package = coalesce(?, scope_package), verified_by = coalesce(?, verified_by), updated_at = strftime('%s','now') WHERE id = ?`,
7812
- [severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, existingId]
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]
7813
8333
  );
7814
8334
  } else {
7815
8335
  db.run(
7816
- `INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7817
- [entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null]
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]
7818
8338
  );
7819
8339
  }
7820
8340
  const mdResult = appendToLayer("gotcha", formatted);
@@ -7846,12 +8366,50 @@ async function addGotcha(entry) {
7846
8366
  } catch {
7847
8367
  }
7848
8368
  const action = existingId !== null ? "updated" : "recorded";
7849
- 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}]
7850
8372
  ${mdResult}`;
7851
8373
  } catch (err) {
7852
8374
  return `\u274C Failed to add gotcha: ${err}`;
7853
8375
  }
7854
8376
  }
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) {
8389
+ try {
8390
+ const db = await getDb();
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
+ }
7855
8413
  async function listGotchas(params) {
7856
8414
  try {
7857
8415
  await ensureGotchasSchema();
@@ -7917,8 +8475,12 @@ async function listGotchas(params) {
7917
8475
  const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
7918
8476
  const ageSec = nowSec - (Number(g.created_at) || nowSec);
7919
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" : "";
7920
8482
  const updated = g.updated_at ? ` \xB7 lastSeen ${new Date(Number(g.updated_at) * 1e3).toISOString().split("T")[0]}` : "";
7921
- lines.push(`${icon} [${g.severity}]${isNew} ${g.file_path}${updated}`);
8483
+ lines.push(`${icon} [${g.severity}]${isNew}${expired}${quar} ${g.file_path}${updated} (${ageDays}d old, tier: ${tier})`);
7922
8484
  lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
7923
8485
  if (g.trigger_command) lines.push(` \u2328\uFE0F when running: \`${g.trigger_command.substring(0, 80)}\``);
7924
8486
  if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
@@ -8013,7 +8575,7 @@ async function getActiveGotchasForCommand(command) {
8013
8575
  const stmt = db.prepare(
8014
8576
  `SELECT file_path, description, severity, workaround, trigger_command
8015
8577
  FROM known_gotchas
8016
- 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
8017
8579
  ORDER BY
8018
8580
  CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END
8019
8581
  LIMIT 30`
@@ -8038,8 +8600,8 @@ async function getActiveGotchasForCommand(command) {
8038
8600
  }
8039
8601
  async function recordInjection(params) {
8040
8602
  try {
8041
- const fp = path17.join(getProjectRoot(), ".kuma", "injections.jsonl");
8042
- 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 });
8043
8605
  const line = JSON.stringify({
8044
8606
  filePath: params.filePath || null,
8045
8607
  command: params.command || null,
@@ -8048,16 +8610,16 @@ async function recordInjection(params) {
8048
8610
  // conservative estimate: avoided re-research / re-discovery
8049
8611
  at: Date.now()
8050
8612
  });
8051
- fs14.appendFileSync(fp, line + "\n", "utf-8");
8613
+ fs16.appendFileSync(fp, line + "\n", "utf-8");
8052
8614
  } catch {
8053
8615
  }
8054
8616
  }
8055
8617
  function getInjectionStats(hours = 24) {
8056
8618
  try {
8057
- const fp = path17.join(getProjectRoot(), ".kuma", "injections.jsonl");
8058
- 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 };
8059
8621
  const cutoff = Date.now() - hours * 60 * 60 * 1e3;
8060
- const lines = fs14.readFileSync(fp, "utf-8").split("\n");
8622
+ const lines = fs16.readFileSync(fp, "utf-8").split("\n");
8061
8623
  let count = 0;
8062
8624
  let savedMs = 0;
8063
8625
  for (const line of lines) {
@@ -8079,11 +8641,18 @@ function getInjectionStats(hours = 24) {
8079
8641
  async function setGotchaStatus(id, status, verifiedBy) {
8080
8642
  try {
8081
8643
  const db = await getDb();
8082
- db.run(
8083
- `UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
8084
- [status, verifiedBy || null, id]
8085
- );
8086
- saveDb();
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
+ }
8655
+ saveDb();
8087
8656
  return true;
8088
8657
  } catch {
8089
8658
  return false;
@@ -8108,9 +8677,9 @@ async function getGotchasByStatus(status, limit = 20) {
8108
8677
  }
8109
8678
  function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8110
8679
  try {
8111
- const mdPath = path17.join(getProjectRoot(), ".kuma", "KNOWN_GOTCHAS.md");
8112
- if (!fs14.existsSync(mdPath)) return false;
8113
- const content = fs14.readFileSync(mdPath, "utf-8");
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");
8114
8683
  const lines = content.split("\n");
8115
8684
  const normalizedTargets = targetPaths.map((p) => p.trim().toLowerCase()).filter(Boolean);
8116
8685
  if (normalizedTargets.length === 0) return false;
@@ -8127,9 +8696,9 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8127
8696
  modified = true;
8128
8697
  }
8129
8698
  const headingFile = headingMatch[1].trim().toLowerCase();
8130
- const headingBase = path17.basename(headingFile);
8699
+ const headingBase = path19.basename(headingFile);
8131
8700
  inMatchingSection = normalizedTargets.some(
8132
- (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)
8133
8702
  );
8134
8703
  sectionHasStatus = false;
8135
8704
  outputLines.push(line);
@@ -8151,7 +8720,7 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
8151
8720
  modified = true;
8152
8721
  }
8153
8722
  if (modified) {
8154
- fs14.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
8723
+ fs16.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
8155
8724
  }
8156
8725
  return modified;
8157
8726
  } catch {
@@ -8274,6 +8843,7 @@ async function deprecateGotcha(idOrScope, reason) {
8274
8843
  return { deprecated: 0, message: `\u274C Failed to deprecate gotcha: ${err}` };
8275
8844
  }
8276
8845
  }
8846
+ var GOTCHA_MERGE_THRESHOLD;
8277
8847
  var init_kumaGotchas = __esm({
8278
8848
  "src/engine/kumaGotchas.ts"() {
8279
8849
  "use strict";
@@ -8282,6 +8852,7 @@ var init_kumaGotchas = __esm({
8282
8852
  init_pathValidator();
8283
8853
  init_domainRules();
8284
8854
  init_kumaInject();
8855
+ GOTCHA_MERGE_THRESHOLD = 0.6;
8285
8856
  }
8286
8857
  });
8287
8858
 
@@ -8290,24 +8861,24 @@ var kumaMaintenance_exports = {};
8290
8861
  __export(kumaMaintenance_exports, {
8291
8862
  maybeRunAutoCleanup: () => maybeRunAutoCleanup
8292
8863
  });
8293
- import fs15 from "fs";
8294
- import path18 from "path";
8864
+ import fs17 from "fs";
8865
+ import path20 from "path";
8295
8866
  async function maybeRunAutoCleanup() {
8296
8867
  try {
8297
8868
  const root = getProjectRoot();
8298
- const stateFile = path18.join(root, GC_STATE_FILE);
8869
+ const stateFile = path20.join(root, GC_STATE_FILE);
8299
8870
  const now = Date.now();
8300
8871
  let lastRun = 0;
8301
- if (fs15.existsSync(stateFile)) {
8872
+ if (fs17.existsSync(stateFile)) {
8302
8873
  try {
8303
- lastRun = JSON.parse(fs15.readFileSync(stateFile, "utf-8")).lastRun || 0;
8874
+ lastRun = JSON.parse(fs17.readFileSync(stateFile, "utf-8")).lastRun || 0;
8304
8875
  } catch {
8305
8876
  lastRun = 0;
8306
8877
  }
8307
8878
  }
8308
8879
  if (now - lastRun < GC_INTERVAL_MS) return "";
8309
- fs15.mkdirSync(path18.dirname(stateFile), { recursive: true });
8310
- fs15.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
8880
+ fs17.mkdirSync(path20.dirname(stateFile), { recursive: true });
8881
+ fs17.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
8311
8882
  const { syncGotchasToDb: syncGotchasToDb2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
8312
8883
  await syncGotchasToDb2();
8313
8884
  const { runGarbageCollection: runGarbageCollection2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
@@ -8343,22 +8914,22 @@ __export(kumaPathRules_exports, {
8343
8914
  loadPathRules: () => loadPathRules,
8344
8915
  matchRulePath: () => matchRulePath
8345
8916
  });
8346
- import fs16 from "fs";
8347
- import path19 from "path";
8917
+ import fs18 from "fs";
8918
+ import path21 from "path";
8348
8919
  function rulesDir() {
8349
- return path19.join(getProjectRoot(), RULES_DIR);
8920
+ return path21.join(getProjectRoot(), RULES_DIR);
8350
8921
  }
8351
8922
  function ensureRulesDir() {
8352
8923
  try {
8353
8924
  const dir = rulesDir();
8354
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
8925
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
8355
8926
  } catch {
8356
8927
  }
8357
8928
  }
8358
8929
  function parseRuleFile(filePath) {
8359
8930
  try {
8360
- const raw = fs16.readFileSync(filePath, "utf-8");
8361
- const id = path19.basename(filePath, ".md");
8931
+ const raw = fs18.readFileSync(filePath, "utf-8");
8932
+ const id = path21.basename(filePath, ".md");
8362
8933
  const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
8363
8934
  if (!fm) {
8364
8935
  return { id, description: id, paths: ["*"], content: raw.trim(), filePath };
@@ -8383,7 +8954,7 @@ function loadPathRules() {
8383
8954
  ensureRulesDir();
8384
8955
  try {
8385
8956
  const dir = rulesDir();
8386
- 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);
8387
8958
  } catch {
8388
8959
  return [];
8389
8960
  }
@@ -8471,8 +9042,8 @@ __export(kumaFlowCache_exports, {
8471
9042
  getFreshDomainFlow: () => getFreshDomainFlow,
8472
9043
  getHopsForDomain: () => getHopsForDomain
8473
9044
  });
8474
- import fs17 from "fs";
8475
- import path20 from "path";
9045
+ import fs19 from "fs";
9046
+ import path22 from "path";
8476
9047
  async function getFlowFreshness(domain) {
8477
9048
  try {
8478
9049
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
@@ -8507,26 +9078,26 @@ async function getFlowFreshness(domain) {
8507
9078
  }
8508
9079
  function resolveImport(fromFile, spec) {
8509
9080
  const root = getProjectRoot();
8510
- const fromDir = path20.dirname(fromFile);
9081
+ const fromDir = path22.dirname(fromFile);
8511
9082
  const candidates = [];
8512
9083
  if (spec.startsWith(".")) {
8513
- const abs = path20.resolve(fromDir, spec);
9084
+ const abs = path22.resolve(fromDir, spec);
8514
9085
  candidates.push(abs);
8515
9086
  } else {
8516
- candidates.push(path20.join(root, spec));
9087
+ candidates.push(path22.join(root, spec));
8517
9088
  }
8518
9089
  for (const c of candidates) {
8519
9090
  for (const ext of SOURCE_EXTENSIONS) {
8520
9091
  const withExt = `${c}${ext}`;
8521
- if (fs17.existsSync(withExt)) return path20.relative(root, withExt);
8522
- if (fs17.existsSync(c) && fs17.statSync(c).isDirectory()) {
8523
- const idx = path20.join(c, "index") + ext;
8524
- if (fs17.existsSync(idx)) return path20.relative(root, idx);
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);
8525
9096
  }
8526
9097
  }
8527
- if (fs17.existsSync(c)) {
9098
+ if (fs19.existsSync(c)) {
8528
9099
  try {
8529
- if (fs17.statSync(c).isFile()) return path20.relative(root, c);
9100
+ if (fs19.statSync(c).isFile()) return path22.relative(root, c);
8530
9101
  } catch {
8531
9102
  }
8532
9103
  }
@@ -8535,13 +9106,13 @@ function resolveImport(fromFile, spec) {
8535
9106
  }
8536
9107
  function deriveHopsFromImports(entryFile, maxHops = 6) {
8537
9108
  const root = getProjectRoot();
8538
- const absEntry = path20.isAbsolute(entryFile) ? entryFile : path20.resolve(root, entryFile);
8539
- if (!fs17.existsSync(absEntry)) return [];
9109
+ const absEntry = path22.isAbsolute(entryFile) ? entryFile : path22.resolve(root, entryFile);
9110
+ if (!fs19.existsSync(absEntry)) return [];
8540
9111
  const hops = [];
8541
9112
  const seen = /* @__PURE__ */ new Set([entryFile]);
8542
- const fromName = path20.basename(absEntry);
9113
+ const fromName = path22.basename(absEntry);
8543
9114
  try {
8544
- const lines = fs17.readFileSync(absEntry, "utf-8").split("\n");
9115
+ const lines = fs19.readFileSync(absEntry, "utf-8").split("\n");
8545
9116
  for (const line of lines) {
8546
9117
  if (hops.length >= maxHops) break;
8547
9118
  const spec = matchImportPath(line);
@@ -8551,9 +9122,9 @@ function deriveHopsFromImports(entryFile, maxHops = 6) {
8551
9122
  seen.add(resolved);
8552
9123
  hops.push({
8553
9124
  from: fromName,
8554
- to: path20.basename(resolved),
9125
+ to: path22.basename(resolved),
8555
9126
  relation: "imports",
8556
- description: `${fromName} imports ${path20.basename(resolved)}`
9127
+ description: `${fromName} imports ${path22.basename(resolved)}`
8557
9128
  });
8558
9129
  }
8559
9130
  } catch {
@@ -8616,9 +9187,9 @@ async function getHopsForDomain(domain) {
8616
9187
  }
8617
9188
  try {
8618
9189
  const root = getProjectRoot();
8619
- const archPath = path20.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
8620
- if (fs17.existsSync(archPath)) {
8621
- const content = fs17.readFileSync(archPath, "utf-8");
9190
+ const archPath = path22.join(root, ".kuma", "ARCHITECTURE_FLOW.md");
9191
+ if (fs19.existsSync(archPath)) {
9192
+ const content = fs19.readFileSync(archPath, "utf-8");
8622
9193
  const lines = content.split("\n");
8623
9194
  for (const line of lines) {
8624
9195
  const domMatch = line.match(new RegExp(`domain:\\s*${escapeRegex(domain)}\\s*\\|\\s*hops:\\s*(.+)`, "i"));
@@ -8663,7 +9234,7 @@ Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} |
8663
9234
  hops: derivedHops,
8664
9235
  filePaths: freshness.filePaths
8665
9236
  });
8666
- 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.`);
8667
9238
  hops.splice(0, hops.length, ...derivedHops);
8668
9239
  } else {
8669
9240
  lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
@@ -8707,6 +9278,127 @@ var init_kumaFlowCache = __esm({
8707
9278
  }
8708
9279
  });
8709
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
+
8710
9402
  // src/engine/kumaSearch.ts
8711
9403
  var kumaSearch_exports = {};
8712
9404
  __export(kumaSearch_exports, {
@@ -8719,8 +9411,8 @@ __export(kumaSearch_exports, {
8719
9411
  formatHybridResults: () => formatHybridResults,
8720
9412
  hybridSearch: () => hybridSearch
8721
9413
  });
8722
- import fs19 from "fs";
8723
- import path22 from "path";
9414
+ import fs22 from "fs";
9415
+ import path25 from "path";
8724
9416
  function expandQueryTerms(query) {
8725
9417
  const terms = query.toLowerCase().split(/[\s_-]+/).filter((t) => t.length > 2);
8726
9418
  const expanded = /* @__PURE__ */ new Map();
@@ -8783,13 +9475,13 @@ async function buildSearchVectors() {
8783
9475
  }
8784
9476
  try {
8785
9477
  const root = getProjectRoot();
8786
- const memDir = path22.join(root, ".kuma", "memories");
8787
- if (fs19.existsSync(memDir)) {
8788
- const files = fs19.readdirSync(memDir).filter((f) => f.endsWith(".md")).slice(0, SCALE_LIMITS.maxMemoryFiles);
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);
8789
9481
  for (const file of files) {
8790
9482
  try {
8791
- const fullPath = path22.join(memDir, file);
8792
- const content = fs19.readFileSync(fullPath, "utf-8");
9483
+ const fullPath = path25.join(memDir, file);
9484
+ const content = fs22.readFileSync(fullPath, "utf-8");
8793
9485
  const text = content.toLowerCase();
8794
9486
  const rawTerms = extractTerms(text);
8795
9487
  const termCounts = /* @__PURE__ */ new Map();
@@ -8800,7 +9492,7 @@ async function buildSearchVectors() {
8800
9492
  terms: termCounts,
8801
9493
  source: file.replace(/\.md$/, ""),
8802
9494
  sourceType: "memory",
8803
- recency: recencyScore(Math.floor(fs19.statSync(fullPath).mtimeMs / 1e3))
9495
+ recency: recencyScore(Math.floor(fs22.statSync(fullPath).mtimeMs / 1e3))
8804
9496
  });
8805
9497
  } catch {
8806
9498
  }
@@ -9117,14 +9809,14 @@ __export(architectureGuard_exports, {
9117
9809
  detectWorkspaceCircularDeps: () => detectWorkspaceCircularDeps,
9118
9810
  loadArchitectureRules: () => loadArchitectureRules
9119
9811
  });
9120
- import fs23 from "fs";
9121
- import path26 from "path";
9812
+ import fs26 from "fs";
9813
+ import path29 from "path";
9122
9814
  import fastGlob4 from "fast-glob";
9123
9815
  function loadArchitectureRules(root) {
9124
- const rulesPath = path26.join(root, ".kuma", "architecture.json");
9125
- if (fs23.existsSync(rulesPath)) {
9816
+ const rulesPath = path29.join(root, ".kuma", "architecture.json");
9817
+ if (fs26.existsSync(rulesPath)) {
9126
9818
  try {
9127
- const data = JSON.parse(fs23.readFileSync(rulesPath, "utf-8"));
9819
+ const data = JSON.parse(fs26.readFileSync(rulesPath, "utf-8"));
9128
9820
  if (Array.isArray(data.rules)) {
9129
9821
  return data.rules;
9130
9822
  }
@@ -9186,11 +9878,11 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
9186
9878
  const customRules = loadArchitectureRules(root);
9187
9879
  const importRegex = /(?:import|export\s+.*from|require\s*\()\s*['"]([^'"]+)['"]/g;
9188
9880
  for (const file of targetFiles) {
9189
- const fullPath = path26.isAbsolute(file) ? file : path26.join(root, file);
9190
- if (!fs23.existsSync(fullPath)) continue;
9881
+ const fullPath = path29.isAbsolute(file) ? file : path29.join(root, file);
9882
+ if (!fs26.existsSync(fullPath)) continue;
9191
9883
  let content = "";
9192
9884
  try {
9193
- content = fs23.readFileSync(fullPath, "utf-8");
9885
+ content = fs26.readFileSync(fullPath, "utf-8");
9194
9886
  } catch {
9195
9887
  continue;
9196
9888
  }
@@ -9200,7 +9892,7 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
9200
9892
  const importPath = match[1];
9201
9893
  if (wsInfo.isWorkspace && currentPkg && !currentPkg.isRoot) {
9202
9894
  if (importPath.startsWith("../") && (importPath.includes("/src/") || importPath.includes("/dist/"))) {
9203
- const resolvedTarget = path26.normalize(path26.resolve(path26.dirname(fullPath), importPath));
9895
+ const resolvedTarget = path29.normalize(path29.resolve(path29.dirname(fullPath), importPath));
9204
9896
  const targetPkg = findPackageForFile(resolvedTarget, wsInfo);
9205
9897
  if (targetPkg && targetPkg.name !== currentPkg.name) {
9206
9898
  violations.push({
@@ -9241,6 +9933,256 @@ var init_architectureGuard = __esm({
9241
9933
  }
9242
9934
  });
9243
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
+
9244
10186
  // src/engine/kumaCheckpoint.ts
9245
10187
  var kumaCheckpoint_exports = {};
9246
10188
  __export(kumaCheckpoint_exports, {
@@ -9251,27 +10193,27 @@ __export(kumaCheckpoint_exports, {
9251
10193
  pruneCheckpoints: () => pruneCheckpoints,
9252
10194
  rollbackToCheckpoint: () => rollbackToCheckpoint
9253
10195
  });
9254
- import fs24 from "fs";
9255
- import path27 from "path";
10196
+ import fs28 from "fs";
10197
+ import path31 from "path";
9256
10198
  async function createCheckpoint(label, description) {
9257
10199
  try {
9258
10200
  const root = getProjectRoot();
9259
- const cpDir = path27.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
9260
- if (fs24.existsSync(cpDir)) {
10201
+ const cpDir = path31.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
10202
+ if (fs28.existsSync(cpDir)) {
9261
10203
  return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
9262
10204
  }
9263
- fs24.mkdirSync(cpDir, { recursive: true });
10205
+ fs28.mkdirSync(cpDir, { recursive: true });
9264
10206
  let gitHead;
9265
10207
  let isGitClean = false;
9266
10208
  try {
9267
- const { execSync: execSync7 } = await import("child_process");
9268
- gitHead = execSync7("git rev-parse HEAD", {
10209
+ const { execSync: execSync9 } = await import("child_process");
10210
+ gitHead = execSync9("git rev-parse HEAD", {
9269
10211
  cwd: root,
9270
10212
  encoding: "utf-8",
9271
10213
  timeout: 3e3,
9272
10214
  stdio: ["pipe", "pipe", "pipe"]
9273
10215
  }).trim();
9274
- const statusOut = execSync7("git status --porcelain", {
10216
+ const statusOut = execSync9("git status --porcelain", {
9275
10217
  cwd: root,
9276
10218
  encoding: "utf-8",
9277
10219
  timeout: 3e3,
@@ -9282,8 +10224,8 @@ async function createCheckpoint(label, description) {
9282
10224
  }
9283
10225
  const candidatePaths = /* @__PURE__ */ new Set();
9284
10226
  try {
9285
- const { execSync: execSync7 } = await import("child_process");
9286
- const gitOut = execSync7("git status --porcelain", {
10227
+ const { execSync: execSync9 } = await import("child_process");
10228
+ const gitOut = execSync9("git status --porcelain", {
9287
10229
  cwd: root,
9288
10230
  encoding: "utf-8",
9289
10231
  timeout: 4e3,
@@ -9326,15 +10268,15 @@ async function createCheckpoint(label, description) {
9326
10268
  }
9327
10269
  const files = [];
9328
10270
  for (const fp of candidatePaths) {
9329
- const fullPath = path27.resolve(root, fp);
10271
+ const fullPath = path31.resolve(root, fp);
9330
10272
  try {
9331
- if (fs24.existsSync(fullPath) && fs24.statSync(fullPath).isFile()) {
9332
- const content = fs24.readFileSync(fullPath, "utf-8");
10273
+ if (fs28.existsSync(fullPath) && fs28.statSync(fullPath).isFile()) {
10274
+ const content = fs28.readFileSync(fullPath, "utf-8");
9333
10275
  const hash = simpleHash2(content);
9334
- const snapFile = path27.join(cpDir, "files", `${fp}.kuma-snap`);
9335
- const fileDir = path27.dirname(snapFile);
9336
- if (!fs24.existsSync(fileDir)) fs24.mkdirSync(fileDir, { recursive: true });
9337
- fs24.writeFileSync(snapFile, content, "utf-8");
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");
9338
10280
  files.push({ path: fp, hash });
9339
10281
  }
9340
10282
  } catch {
@@ -9342,7 +10284,7 @@ async function createCheckpoint(label, description) {
9342
10284
  }
9343
10285
  const db = await getDb();
9344
10286
  const dbData = db.export();
9345
- fs24.writeFileSync(path27.join(cpDir, "kuma.db"), Buffer.from(dbData));
10287
+ fs28.writeFileSync(path31.join(cpDir, "kuma.db"), Buffer.from(dbData));
9346
10288
  const manifest = {
9347
10289
  label,
9348
10290
  timestamp: Date.now(),
@@ -9352,8 +10294,8 @@ async function createCheckpoint(label, description) {
9352
10294
  dbSnapshot: true,
9353
10295
  description
9354
10296
  };
9355
- fs24.writeFileSync(
9356
- path27.join(cpDir, "manifest.json"),
10297
+ fs28.writeFileSync(
10298
+ path31.join(cpDir, "manifest.json"),
9357
10299
  JSON.stringify(manifest, null, 2),
9358
10300
  "utf-8"
9359
10301
  );
@@ -9381,29 +10323,29 @@ async function createCheckpoint(label, description) {
9381
10323
  async function rollbackToCheckpoint(label) {
9382
10324
  try {
9383
10325
  const root = getProjectRoot();
9384
- const cpDir = path27.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
9385
- const manifestPath = path27.join(cpDir, "manifest.json");
9386
- if (!fs24.existsSync(manifestPath)) {
10326
+ const cpDir = path31.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
10327
+ const manifestPath = path31.join(cpDir, "manifest.json");
10328
+ if (!fs28.existsSync(manifestPath)) {
9387
10329
  return `\u274C Checkpoint "${label}" not found. Available checkpoints:
9388
10330
 
9389
10331
  ${listCheckpoints()}`;
9390
10332
  }
9391
10333
  const manifest = JSON.parse(
9392
- fs24.readFileSync(manifestPath, "utf-8")
10334
+ fs28.readFileSync(manifestPath, "utf-8")
9393
10335
  );
9394
10336
  let restored = 0;
9395
10337
  let failed = 0;
9396
10338
  for (const f of manifest.files) {
9397
- const snapPath = path27.join(cpDir, "files", `${f.path}.kuma-snap`);
9398
- const legacyPath = path27.join(cpDir, "files", f.path);
9399
- const sourcePath = fs24.existsSync(snapPath) ? snapPath : legacyPath;
9400
- if (fs24.existsSync(sourcePath)) {
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)) {
9401
10343
  try {
9402
- const content = fs24.readFileSync(sourcePath, "utf-8");
9403
- const targetPath = path27.resolve(root, f.path);
9404
- const targetDir = path27.dirname(targetPath);
9405
- if (!fs24.existsSync(targetDir)) fs24.mkdirSync(targetDir, { recursive: true });
9406
- fs24.writeFileSync(targetPath, content, "utf-8");
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");
9407
10349
  restored++;
9408
10350
  } catch {
9409
10351
  failed++;
@@ -9412,8 +10354,8 @@ ${listCheckpoints()}`;
9412
10354
  }
9413
10355
  if (manifest.files.length === 0 && manifest.gitHead) {
9414
10356
  try {
9415
- const { execSync: execSync7 } = await import("child_process");
9416
- execSync7(`git checkout ${manifest.gitHead} -- .`, {
10357
+ const { execSync: execSync9 } = await import("child_process");
10358
+ execSync9(`git checkout ${manifest.gitHead} -- .`, {
9417
10359
  cwd: root,
9418
10360
  encoding: "utf-8",
9419
10361
  timeout: 8e3,
@@ -9424,12 +10366,12 @@ ${listCheckpoints()}`;
9424
10366
  failed++;
9425
10367
  }
9426
10368
  }
9427
- const dbSnapshotPath = path27.join(cpDir, "kuma.db");
9428
- if (fs24.existsSync(dbSnapshotPath)) {
9429
- const kumaDir = path27.join(root, ".kuma");
9430
- const dbPath = path27.join(kumaDir, "kuma.db");
9431
- const snapshotData = fs24.readFileSync(dbSnapshotPath);
9432
- fs24.writeFileSync(dbPath, snapshotData);
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);
9433
10375
  resetDbInstance();
9434
10376
  }
9435
10377
  sessionMemory.recordToolCall("kuma_checkpoint_rollback", {
@@ -9454,18 +10396,18 @@ ${listCheckpoints()}`;
9454
10396
  function listCheckpoints() {
9455
10397
  try {
9456
10398
  const root = getProjectRoot();
9457
- const cpDir = path27.join(root, CHECKPOINT_DIR);
9458
- if (!fs24.existsSync(cpDir)) {
10399
+ const cpDir = path31.join(root, CHECKPOINT_DIR);
10400
+ if (!fs28.existsSync(cpDir)) {
9459
10401
  return "\u{1F4ED} No checkpoints found. Use kuma_safety({ action: 'checkpoint', label: 'pre-feature-x' }) to create one.";
9460
10402
  }
9461
- const entries = fs24.readdirSync(cpDir);
10403
+ const entries = fs28.readdirSync(cpDir);
9462
10404
  const checkpoints = [];
9463
10405
  for (const entry of entries) {
9464
- const manifestPath = path27.join(cpDir, entry, "manifest.json");
9465
- if (fs24.existsSync(manifestPath)) {
10406
+ const manifestPath = path31.join(cpDir, entry, "manifest.json");
10407
+ if (fs28.existsSync(manifestPath)) {
9466
10408
  try {
9467
10409
  const manifest = JSON.parse(
9468
- fs24.readFileSync(manifestPath, "utf-8")
10410
+ fs28.readFileSync(manifestPath, "utf-8")
9469
10411
  );
9470
10412
  checkpoints.push({ label: entry, manifest });
9471
10413
  } catch {
@@ -9497,12 +10439,12 @@ function listCheckpoints() {
9497
10439
  function pruneCheckpoints(keep = 5) {
9498
10440
  try {
9499
10441
  const root = getProjectRoot();
9500
- const cpDir = path27.join(root, CHECKPOINT_DIR);
9501
- if (!fs24.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
9502
- const entries = fs24.readdirSync(cpDir).map((e) => ({ name: e, time: fs24.statSync(path27.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
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);
9503
10445
  let removed = 0;
9504
10446
  for (let i = keep; i < entries.length; i++) {
9505
- 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 });
9506
10448
  removed++;
9507
10449
  }
9508
10450
  return removed > 0 ? `\u{1F9F9} Pruned **${removed} old checkpoint(s)**. Kept ${Math.min(keep, entries.length)} most recent.` : "\u2705 No checkpoints needed pruning.";
@@ -9527,9 +10469,9 @@ function isRiskyEditPath(filePath) {
9527
10469
  }
9528
10470
  function loadAutoCpDone(root) {
9529
10471
  try {
9530
- const fp = path27.join(root, AUTO_CP_STATE);
9531
- if (fs24.existsSync(fp)) {
9532
- const parsed = JSON.parse(fs24.readFileSync(fp, "utf-8"));
10472
+ const fp = path31.join(root, AUTO_CP_STATE);
10473
+ if (fs28.existsSync(fp)) {
10474
+ const parsed = JSON.parse(fs28.readFileSync(fp, "utf-8"));
9533
10475
  return parsed?.autoCheckpoints ?? {};
9534
10476
  }
9535
10477
  } catch {
@@ -9538,10 +10480,10 @@ function loadAutoCpDone(root) {
9538
10480
  }
9539
10481
  function saveAutoCpDone(root, done) {
9540
10482
  try {
9541
- const fp = path27.join(root, AUTO_CP_STATE);
9542
- const parsed = fs24.existsSync(fp) ? JSON.parse(fs24.readFileSync(fp, "utf-8")) : {};
10483
+ const fp = path31.join(root, AUTO_CP_STATE);
10484
+ const parsed = fs28.existsSync(fp) ? JSON.parse(fs28.readFileSync(fp, "utf-8")) : {};
9543
10485
  parsed.autoCheckpoints = done;
9544
- fs24.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
10486
+ fs28.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
9545
10487
  } catch {
9546
10488
  }
9547
10489
  }
@@ -9551,14 +10493,14 @@ function maybeAutoCheckpoint(filePath) {
9551
10493
  const root = getProjectRoot();
9552
10494
  const done = loadAutoCpDone(root);
9553
10495
  if (done[filePath]) return null;
9554
- const abs = path27.isAbsolute(filePath) ? filePath : path27.join(root, filePath);
9555
- if (!fs24.existsSync(abs) || !fs24.statSync(abs).isFile()) return null;
9556
- const label = `auto-pre-${sanitizeLabel(path27.basename(filePath))}`;
9557
- const cpDir = path27.join(root, CHECKPOINT_DIR, label);
9558
- if (!fs24.existsSync(cpDir)) fs24.mkdirSync(cpDir, { recursive: true });
9559
- fs24.copyFileSync(abs, path27.join(cpDir, path27.basename(abs)));
9560
- fs24.writeFileSync(
9561
- path27.join(cpDir, "manifest.json"),
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"),
9562
10504
  JSON.stringify({
9563
10505
  label,
9564
10506
  timestamp: Date.now(),
@@ -9598,8 +10540,8 @@ __export(init_exports, {
9598
10540
  generateInitMdContent: () => generateInitMdContent,
9599
10541
  runInit: () => runInit
9600
10542
  });
9601
- import fs25 from "fs";
9602
- import path28 from "path";
10543
+ import fs29 from "fs";
10544
+ import path32 from "path";
9603
10545
  function configFilePath(type) {
9604
10546
  switch (type) {
9605
10547
  case "claude":
@@ -9701,11 +10643,10 @@ function opencodeAgentsMdTemplate() {
9701
10643
  "Record what matters, skip what doesn't.",
9702
10644
  "",
9703
10645
  "<kuma_obedience>",
9704
- "## \u{1F512} Obedience (non-negotiable)",
9705
- '- MUST call `kuma_kuma_context({ action: "init" })` (or `kuma_context({ action: "init" })`) at session start.',
9706
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
9707
- '- MUST call `kuma_kuma_safety({ action: "verify" })` (or `kuma_safety({ action: "verify" })`) after edits.',
9708
- "- MUST NOT call actions outside the core actions.",
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.",
9709
10650
  "</kuma_obedience>",
9710
10651
  "",
9711
10652
  "<kuma_workflow>",
@@ -9869,6 +10810,7 @@ function geminiTemplate() {
9869
10810
  '- **Gotchas:** Call `kuma_memory({ action: "gotcha", scope: "<file>", content: "<description>" })` IMMEDIATELY when any quirk/bug is discovered.',
9870
10811
  '- **Decisions:** Call `kuma_memory({ action: "decision", title: "...", rationale: "..." })` when choosing between options.',
9871
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.",
9872
10814
  "</kuma_obedience>",
9873
10815
  "",
9874
10816
  "### \u{1F375} Matcha & Kuma Harmony",
@@ -9948,6 +10890,16 @@ function opencodePluginTemplate() {
9948
10890
  ' const fp = input.filePath || input.path || (input.file && input.file.path) || "";',
9949
10891
  ' if (fp) console.warn(`\u{1F43B} [Kuma] ${fp} edited \u2014 consider kuma_safety({ action: "verify", scope: "${fp}" })`);',
9950
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
+ " },",
9951
10903
  " };",
9952
10904
  "};",
9953
10905
  ""
@@ -10007,15 +10959,26 @@ function generateInitMdContent() {
10007
10959
  "---",
10008
10960
  "",
10009
10961
  "<kuma_obedience>",
10010
- "## \u{1F512} OBEDIENCE (non-negotiable)",
10962
+ "## \u{1F512} OBEDIENCE \u2014 honest model (issue #39)",
10011
10963
  "",
10012
- '1. **MUST call `kuma_context({ action: "init" })`** at session start \u2014 before any code edit.',
10013
- '2. **MUST record gotchas IMMEDIATELY** when a bug/quirk is found (`kuma_memory({ action: "gotcha" })`).',
10014
- '3. **MUST call `kuma_safety({ action: "verify" })`** after code edits.',
10015
- "4. **MUST NOT call actions outside the core actions** in this file \u2014 anything else is rejected by the MCP schema.",
10016
- "5. If a Kuma call fails or returns an error, **re-read this file and retry** \u2014 do not silently continue.",
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.",
10967
+ "",
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.',
10017
10974
  "",
10018
- "> These rules are enforced: the MCP schema rejects unknown actions, and Claude Code hooks inject gotchas before every edit.",
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.",
10019
10982
  "</kuma_obedience>",
10020
10983
  "",
10021
10984
  "---",
@@ -10192,11 +11155,10 @@ function handleOpencodeSecondary(root, results) {
10192
11155
  "Record what matters, skip what doesn't.",
10193
11156
  "",
10194
11157
  "<kuma_obedience>",
10195
- "## \u{1F512} Obedience (non-negotiable)",
10196
- '- MUST call `kuma_kuma_context({ action: "init" })` at session start.',
10197
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
10198
- '- MUST call `kuma_kuma_safety({ action: "verify" })` after edits.',
10199
- "- MUST NOT call actions outside the core actions.",
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.",
10200
11162
  "</kuma_obedience>",
10201
11163
  "",
10202
11164
  "<kuma_workflow>",
@@ -10213,21 +11175,21 @@ function handleOpencodeSecondary(root, results) {
10213
11175
  "",
10214
11176
  "\u{1F4D6} Full rules: `.kuma/init.md`"
10215
11177
  ].join("\n");
10216
- const skillPath = path28.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
11178
+ const skillPath = path32.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
10217
11179
  if (!results.some((r) => r.filePath === ".agents/skills/kuma/SKILL.md")) {
10218
11180
  try {
10219
- const dir = path28.dirname(skillPath);
10220
- if (fs25.existsSync(skillPath)) {
10221
- 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");
10222
11184
  if (existing.includes("kuma-mcp")) {
10223
11185
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "skipped" });
10224
11186
  } else {
10225
- 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");
10226
11188
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "appended" });
10227
11189
  }
10228
11190
  } else {
10229
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10230
- fs25.writeFileSync(skillPath, opencodeContent, "utf-8");
11191
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11192
+ fs29.writeFileSync(skillPath, opencodeContent, "utf-8");
10231
11193
  results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "created" });
10232
11194
  }
10233
11195
  } catch (err) {
@@ -10239,21 +11201,21 @@ function handleOpencodeSecondary(root, results) {
10239
11201
  });
10240
11202
  }
10241
11203
  }
10242
- const opencodeSkillPath = path28.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
11204
+ const opencodeSkillPath = path32.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
10243
11205
  if (!results.some((r) => r.filePath === ".opencode/skills/kuma/SKILL.md")) {
10244
11206
  try {
10245
- const dir = path28.dirname(opencodeSkillPath);
10246
- if (fs25.existsSync(opencodeSkillPath)) {
10247
- 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");
10248
11210
  if (existing.includes("kuma-mcp")) {
10249
11211
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "skipped" });
10250
11212
  } else {
10251
- 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");
10252
11214
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "appended" });
10253
11215
  }
10254
11216
  } else {
10255
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10256
- fs25.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
11217
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11218
+ fs29.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
10257
11219
  results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "created" });
10258
11220
  }
10259
11221
  } catch (err) {
@@ -10265,21 +11227,21 @@ function handleOpencodeSecondary(root, results) {
10265
11227
  });
10266
11228
  }
10267
11229
  }
10268
- const pluginPath = path28.resolve(root, ".opencode", "plugins", "kuma.js");
11230
+ const pluginPath = path32.resolve(root, ".opencode", "plugins", "kuma.js");
10269
11231
  if (!results.some((r) => r.filePath === ".opencode/plugins/kuma.js")) {
10270
11232
  try {
10271
- const dir = path28.dirname(pluginPath);
10272
- if (fs25.existsSync(pluginPath)) {
10273
- 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");
10274
11236
  if (existing.includes("KumaPlugin") || existing.includes("kuma")) {
10275
11237
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "skipped" });
10276
11238
  } else {
10277
- fs25.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
11239
+ fs29.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
10278
11240
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "appended" });
10279
11241
  }
10280
11242
  } else {
10281
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10282
- fs25.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
11243
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11244
+ fs29.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
10283
11245
  results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "created" });
10284
11246
  }
10285
11247
  } catch (err) {
@@ -10293,21 +11255,21 @@ function handleOpencodeSecondary(root, results) {
10293
11255
  }
10294
11256
  }
10295
11257
  function handleCodexSecondary(root, results) {
10296
- const tomlPath = path28.resolve(root, ".codex/config.toml");
11258
+ const tomlPath = path32.resolve(root, ".codex/config.toml");
10297
11259
  if (results.some((r) => r.filePath === ".codex/config.toml")) return;
10298
11260
  try {
10299
- const dir = path28.dirname(tomlPath);
10300
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10301
- if (fs25.existsSync(tomlPath)) {
10302
- const existingContent = fs25.readFileSync(tomlPath, "utf-8");
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");
10303
11265
  if (existingContent.includes("kuma")) {
10304
11266
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
10305
11267
  return;
10306
11268
  }
10307
- fs25.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
11269
+ fs29.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
10308
11270
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
10309
11271
  } else {
10310
- fs25.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
11272
+ fs29.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
10311
11273
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
10312
11274
  }
10313
11275
  } catch (err) {
@@ -10320,11 +11282,11 @@ function handleCodexSecondary(root, results) {
10320
11282
  }
10321
11283
  }
10322
11284
  function handleQwenSecondary(root, results) {
10323
- const settingsPath = path28.resolve(root, "settings.json");
11285
+ const settingsPath = path32.resolve(root, "settings.json");
10324
11286
  if (results.some((r) => r.filePath === "settings.json")) return;
10325
11287
  try {
10326
- if (fs25.existsSync(settingsPath)) {
10327
- const existingContent = fs25.readFileSync(settingsPath, "utf-8");
11288
+ if (fs29.existsSync(settingsPath)) {
11289
+ const existingContent = fs29.readFileSync(settingsPath, "utf-8");
10328
11290
  if (existingContent.includes("kuma")) {
10329
11291
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
10330
11292
  try {
@@ -10332,7 +11294,7 @@ function handleQwenSecondary(root, results) {
10332
11294
  parsed.mcpServers = parsed.mcpServers || {};
10333
11295
  if (!parsed.mcpServers.kuma) {
10334
11296
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10335
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11297
+ fs29.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10336
11298
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
10337
11299
  return;
10338
11300
  }
@@ -10347,13 +11309,13 @@ function handleQwenSecondary(root, results) {
10347
11309
  parsed.mcpServers = parsed.mcpServers || {};
10348
11310
  if (!parsed.mcpServers.kuma) {
10349
11311
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10350
- fs25.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11312
+ fs29.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10351
11313
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
10352
11314
  }
10353
11315
  } catch {
10354
11316
  }
10355
11317
  } else {
10356
- fs25.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
11318
+ fs29.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
10357
11319
  results.push({ type: "qwen", filePath: "settings.json", action: "created" });
10358
11320
  }
10359
11321
  } catch (err) {
@@ -10376,11 +11338,10 @@ function getCombinedAgentsMd(selectedTypes) {
10376
11338
  "## SESSION WORKFLOW",
10377
11339
  "",
10378
11340
  "<kuma_obedience>",
10379
- "## \u{1F512} Obedience (non-negotiable)",
10380
- '- MUST call `[kuma_]context({ action: "init" })` at session start.',
10381
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
10382
- '- MUST call `[kuma_]safety({ action: "verify" })` after edits.',
10383
- "- MUST NOT call actions outside the 13 core actions.",
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.",
10384
11345
  "</kuma_obedience>",
10385
11346
  "",
10386
11347
  "<kuma_workflow>",
@@ -10456,12 +11417,12 @@ function getCombinedAgentsMd(selectedTypes) {
10456
11417
  return lines.join("\n");
10457
11418
  }
10458
11419
  function handleAntigravityMcpConfig(root, results) {
10459
- const mcpPath = path28.resolve(root, ".agents/mcp_config.json");
11420
+ const mcpPath = path32.resolve(root, ".agents/mcp_config.json");
10460
11421
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
10461
11422
  try {
10462
- const mcpDir = path28.dirname(mcpPath);
10463
- if (fs25.existsSync(mcpPath)) {
10464
- 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");
10465
11426
  if (existingContent.includes("kuma")) {
10466
11427
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "skipped" });
10467
11428
  return;
@@ -10470,14 +11431,14 @@ function handleAntigravityMcpConfig(root, results) {
10470
11431
  const parsed = JSON.parse(existingContent);
10471
11432
  parsed.mcpServers = parsed.mcpServers || {};
10472
11433
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10473
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11434
+ fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10474
11435
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
10475
11436
  } catch {
10476
11437
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "error", error: "Invalid JSON in existing mcp_config.json" });
10477
11438
  }
10478
11439
  } else {
10479
- if (!fs25.existsSync(mcpDir)) fs25.mkdirSync(mcpDir, { recursive: true });
10480
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
11440
+ if (!fs29.existsSync(mcpDir)) fs29.mkdirSync(mcpDir, { recursive: true });
11441
+ fs29.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
10481
11442
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
10482
11443
  }
10483
11444
  } catch (err) {
@@ -10490,21 +11451,21 @@ function handleAntigravityMcpConfig(root, results) {
10490
11451
  }
10491
11452
  }
10492
11453
  function handleAntigravityRules(root, results) {
10493
- const rulePath = path28.resolve(root, ".agents", "rules", "kuma.md");
11454
+ const rulePath = path32.resolve(root, ".agents", "rules", "kuma.md");
10494
11455
  if (results.some((r) => r.filePath === ".agents/rules/kuma.md")) return;
10495
11456
  try {
10496
- const dir = path28.dirname(rulePath);
10497
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10498
- if (fs25.existsSync(rulePath)) {
10499
- const existing = fs25.readFileSync(rulePath, "utf-8");
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");
10500
11461
  if (existing.includes("Kuma MCP") || existing.includes("kuma-mcp")) {
10501
11462
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "skipped" });
10502
11463
  return;
10503
11464
  }
10504
- 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");
10505
11466
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "appended" });
10506
11467
  } else {
10507
- fs25.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
11468
+ fs29.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
10508
11469
  results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "created" });
10509
11470
  }
10510
11471
  } catch (err) {
@@ -10517,19 +11478,19 @@ function handleAntigravityRules(root, results) {
10517
11478
  }
10518
11479
  }
10519
11480
  function handleGeminiMd(root, results) {
10520
- const geminiPath = path28.resolve(root, "GEMINI.md");
11481
+ const geminiPath = path32.resolve(root, "GEMINI.md");
10521
11482
  if (results.some((r) => r.filePath === "GEMINI.md")) return;
10522
11483
  try {
10523
- if (fs25.existsSync(geminiPath)) {
10524
- const existing = fs25.readFileSync(geminiPath, "utf-8");
11484
+ if (fs29.existsSync(geminiPath)) {
11485
+ const existing = fs29.readFileSync(geminiPath, "utf-8");
10525
11486
  if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
10526
11487
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "skipped" });
10527
11488
  return;
10528
11489
  }
10529
- 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");
10530
11491
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "appended" });
10531
11492
  } else {
10532
- fs25.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
11493
+ fs29.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
10533
11494
  results.push({ type: "antigravity", filePath: "GEMINI.md", action: "created" });
10534
11495
  }
10535
11496
  } catch (err) {
@@ -10547,19 +11508,19 @@ function handleAntigravitySecondary(root, results) {
10547
11508
  handleGeminiMd(root, results);
10548
11509
  }
10549
11510
  function handleWindsurfSecondary(root, results) {
10550
- const rulesPath = path28.resolve(root, ".windsurfrules");
11511
+ const rulesPath = path32.resolve(root, ".windsurfrules");
10551
11512
  if (results.some((r) => r.filePath === ".windsurfrules")) return;
10552
11513
  try {
10553
- if (fs25.existsSync(rulesPath)) {
10554
- const existing = fs25.readFileSync(rulesPath, "utf-8");
11514
+ if (fs29.existsSync(rulesPath)) {
11515
+ const existing = fs29.readFileSync(rulesPath, "utf-8");
10555
11516
  if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
10556
11517
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "skipped" });
10557
11518
  return;
10558
11519
  }
10559
- 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");
10560
11521
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "appended" });
10561
11522
  } else {
10562
- fs25.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
11523
+ fs29.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
10563
11524
  results.push({ type: "windsurf", filePath: ".windsurfrules", action: "created" });
10564
11525
  }
10565
11526
  } catch (err) {
@@ -10572,21 +11533,21 @@ function handleWindsurfSecondary(root, results) {
10572
11533
  }
10573
11534
  }
10574
11535
  function handleClineSecondary(root, results) {
10575
- const rooPath = path28.resolve(root, ".roo", "rules", "kuma.md");
11536
+ const rooPath = path32.resolve(root, ".roo", "rules", "kuma.md");
10576
11537
  if (!results.some((r) => r.filePath === ".roo/rules/kuma.md")) {
10577
11538
  try {
10578
- const dir = path28.dirname(rooPath);
10579
- if (fs25.existsSync(rooPath)) {
10580
- 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");
10581
11542
  if (existing.includes("kuma-mcp") || existing.includes("Kuma MCP")) {
10582
11543
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "skipped" });
10583
11544
  } else {
10584
- 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");
10585
11546
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "appended" });
10586
11547
  }
10587
11548
  } else {
10588
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10589
- fs25.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
11549
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11550
+ fs29.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
10590
11551
  results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "created" });
10591
11552
  }
10592
11553
  } catch (err) {
@@ -10598,21 +11559,21 @@ function handleClineSecondary(root, results) {
10598
11559
  });
10599
11560
  }
10600
11561
  }
10601
- const traePath = path28.resolve(root, ".trae", "rules", "kuma.md");
11562
+ const traePath = path32.resolve(root, ".trae", "rules", "kuma.md");
10602
11563
  if (!results.some((r) => r.filePath === ".trae/rules/kuma.md")) {
10603
11564
  try {
10604
- const dir = path28.dirname(traePath);
10605
- if (fs25.existsSync(traePath)) {
10606
- 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");
10607
11568
  if (existing.includes("Kuma MCP")) {
10608
11569
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "skipped" });
10609
11570
  } else {
10610
- 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");
10611
11572
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "appended" });
10612
11573
  }
10613
11574
  } else {
10614
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10615
- fs25.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
11575
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11576
+ fs29.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
10616
11577
  results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "created" });
10617
11578
  }
10618
11579
  } catch (err) {
@@ -10624,21 +11585,21 @@ function handleClineSecondary(root, results) {
10624
11585
  });
10625
11586
  }
10626
11587
  }
10627
- const qoderPath = path28.resolve(root, ".qoder", "rules", "kuma.md");
11588
+ const qoderPath = path32.resolve(root, ".qoder", "rules", "kuma.md");
10628
11589
  if (!results.some((r) => r.filePath === ".qoder/rules/kuma.md")) {
10629
11590
  try {
10630
- const dir = path28.dirname(qoderPath);
10631
- if (fs25.existsSync(qoderPath)) {
10632
- 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");
10633
11594
  if (existing.includes("Kuma MCP")) {
10634
11595
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "skipped" });
10635
11596
  } else {
10636
- 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");
10637
11598
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "appended" });
10638
11599
  }
10639
11600
  } else {
10640
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10641
- fs25.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
11601
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11602
+ fs29.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
10642
11603
  results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "created" });
10643
11604
  }
10644
11605
  } catch (err) {
@@ -10652,18 +11613,18 @@ function handleClineSecondary(root, results) {
10652
11613
  }
10653
11614
  }
10654
11615
  function handleAiderSecondary(root, results) {
10655
- const ymlPath = path28.resolve(root, ".aider.conf.yml");
11616
+ const ymlPath = path32.resolve(root, ".aider.conf.yml");
10656
11617
  if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
10657
11618
  try {
10658
11619
  const conventionsRef = "read: CONVENTIONS.md";
10659
- if (fs25.existsSync(ymlPath)) {
10660
- const existingContent = fs25.readFileSync(ymlPath, "utf-8");
11620
+ if (fs29.existsSync(ymlPath)) {
11621
+ const existingContent = fs29.readFileSync(ymlPath, "utf-8");
10661
11622
  if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
10662
11623
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
10663
11624
  return;
10664
11625
  }
10665
11626
  const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
10666
- fs25.writeFileSync(ymlPath, newContent, "utf-8");
11627
+ fs29.writeFileSync(ymlPath, newContent, "utf-8");
10667
11628
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
10668
11629
  } else {
10669
11630
  const content = [
@@ -10673,7 +11634,7 @@ function handleAiderSecondary(root, results) {
10673
11634
  conventionsRef,
10674
11635
  ""
10675
11636
  ].join("\n");
10676
- fs25.writeFileSync(ymlPath, content, "utf-8");
11637
+ fs29.writeFileSync(ymlPath, content, "utf-8");
10677
11638
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
10678
11639
  }
10679
11640
  } catch (err) {
@@ -10686,10 +11647,10 @@ function handleAiderSecondary(root, results) {
10686
11647
  }
10687
11648
  }
10688
11649
  function handleCopilotSecondary(root, results) {
10689
- const skillPath = path28.resolve(root, ".github/skills/kuma/SKILL.md");
11650
+ const skillPath = path32.resolve(root, ".github/skills/kuma/SKILL.md");
10690
11651
  if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
10691
11652
  try {
10692
- const dir = path28.dirname(skillPath);
11653
+ const dir = path32.dirname(skillPath);
10693
11654
  const content = [
10694
11655
  "---",
10695
11656
  "name: kuma-mcp",
@@ -10700,17 +11661,17 @@ function handleCopilotSecondary(root, results) {
10700
11661
  "",
10701
11662
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules."
10702
11663
  ].join("\n");
10703
- if (fs25.existsSync(skillPath)) {
10704
- const existingContent = fs25.readFileSync(skillPath, "utf-8");
11664
+ if (fs29.existsSync(skillPath)) {
11665
+ const existingContent = fs29.readFileSync(skillPath, "utf-8");
10705
11666
  if (existingContent.includes("kuma")) {
10706
11667
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
10707
11668
  return;
10708
11669
  }
10709
- fs25.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
11670
+ fs29.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
10710
11671
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
10711
11672
  } else {
10712
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10713
- fs25.writeFileSync(skillPath, content, "utf-8");
11673
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11674
+ fs29.writeFileSync(skillPath, content, "utf-8");
10714
11675
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
10715
11676
  }
10716
11677
  } catch (err) {
@@ -10723,21 +11684,21 @@ function handleCopilotSecondary(root, results) {
10723
11684
  }
10724
11685
  }
10725
11686
  function handleOpenclawSecondary(root, results) {
10726
- const mcpPath = path28.resolve(root, ".agents/mcp_config.json");
11687
+ const mcpPath = path32.resolve(root, ".agents/mcp_config.json");
10727
11688
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
10728
11689
  try {
10729
- const dir = path28.dirname(mcpPath);
10730
- if (fs25.existsSync(mcpPath)) {
10731
- 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");
10732
11693
  if (existingContent.includes("kuma")) return;
10733
11694
  const parsed = JSON.parse(existingContent);
10734
11695
  parsed.mcpServers = parsed.mcpServers || {};
10735
11696
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10736
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11697
+ fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10737
11698
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
10738
11699
  } else {
10739
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10740
- fs25.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
11700
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11701
+ fs29.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
10741
11702
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
10742
11703
  }
10743
11704
  } catch (err) {
@@ -10750,20 +11711,20 @@ function handleOpenclawSecondary(root, results) {
10750
11711
  }
10751
11712
  }
10752
11713
  function handleInitMdGeneration(root, results) {
10753
- const initMdPath = path28.resolve(root, ".kuma/init.md");
11714
+ const initMdPath = path32.resolve(root, ".kuma/init.md");
10754
11715
  try {
10755
- const kumaDir = path28.dirname(initMdPath);
10756
- if (!fs25.existsSync(kumaDir)) fs25.mkdirSync(kumaDir, { recursive: true });
10757
- if (fs25.existsSync(initMdPath)) {
10758
- const existing = fs25.readFileSync(initMdPath, "utf-8");
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");
10759
11720
  if (existing.includes("_Generated by Kuma MCP_")) {
10760
11721
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "skipped" });
10761
11722
  return;
10762
11723
  }
10763
- fs25.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
11724
+ fs29.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
10764
11725
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "appended" });
10765
11726
  } else {
10766
- fs25.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
11727
+ fs29.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
10767
11728
  results.push({ type: "claude", filePath: ".kuma/init.md", action: "created" });
10768
11729
  }
10769
11730
  } catch (err) {
@@ -10776,20 +11737,20 @@ function handleInitMdGeneration(root, results) {
10776
11737
  }
10777
11738
  }
10778
11739
  function handleCodewhaleSecondary(root, results) {
10779
- const mcpPath = path28.resolve(root, ".codewhale/mcp.json");
11740
+ const mcpPath = path32.resolve(root, ".codewhale/mcp.json");
10780
11741
  if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
10781
11742
  try {
10782
- const dir = path28.dirname(mcpPath);
10783
- if (fs25.existsSync(mcpPath)) {
10784
- 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");
10785
11746
  if (existingContent.includes("kuma")) return;
10786
11747
  const parsed = JSON.parse(existingContent);
10787
11748
  parsed.mcpServers = parsed.mcpServers || {};
10788
11749
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
10789
- fs25.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
11750
+ fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
10790
11751
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
10791
11752
  } else {
10792
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11753
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
10793
11754
  const config = {
10794
11755
  mcpServers: {
10795
11756
  kuma: {
@@ -10799,7 +11760,7 @@ function handleCodewhaleSecondary(root, results) {
10799
11760
  }
10800
11761
  }
10801
11762
  };
10802
- fs25.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
11763
+ fs29.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
10803
11764
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
10804
11765
  }
10805
11766
  } catch (err) {
@@ -10812,7 +11773,7 @@ function handleCodewhaleSecondary(root, results) {
10812
11773
  }
10813
11774
  }
10814
11775
  function handleClaudeSecondary(root, results) {
10815
- const settingsPath = path28.resolve(root, ".claude", "settings.json");
11776
+ const settingsPath = path32.resolve(root, ".claude", "settings.json");
10816
11777
  const hookBlock = {
10817
11778
  hooks: {
10818
11779
  PreToolUse: [
@@ -10824,32 +11785,56 @@ function handleClaudeSecondary(root, results) {
10824
11785
  matcher: "Bash",
10825
11786
  hooks: [{ type: "command", command: "kuma hook pre-bash" }]
10826
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
+ }
10827
11803
  ]
10828
11804
  }
10829
11805
  };
10830
11806
  try {
10831
- if (fs25.existsSync(settingsPath)) {
10832
- 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
+ }
10833
11828
  const merged = {
10834
11829
  ...existing,
10835
- hooks: {
10836
- ...existing.hooks || {},
10837
- ...hookBlock.hooks
10838
- }
11830
+ hooks: { ...existingHooks, PreToolUse: mergedPre, SessionStart: mergedStart, PreCompact: mergedCompact }
10839
11831
  };
10840
- const matchers = new Set((merged.hooks.PreToolUse || []).map((h) => h.matcher));
10841
- for (const entry of hookBlock.hooks.PreToolUse) {
10842
- if (!matchers.has(entry.matcher)) {
10843
- merged.hooks.PreToolUse = [...merged.hooks.PreToolUse || [], entry];
10844
- matchers.add(entry.matcher);
10845
- }
10846
- }
10847
- fs25.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
11832
+ fs29.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
10848
11833
  results.push({ type: "claude", filePath: ".claude/settings.json", action: "appended" });
10849
11834
  } else {
10850
- const dir = path28.dirname(settingsPath);
10851
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10852
- fs25.writeFileSync(settingsPath, JSON.stringify(hookBlock, null, 2) + "\n", "utf-8");
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");
10853
11838
  results.push({ type: "claude", filePath: ".claude/settings.json", action: "created" });
10854
11839
  }
10855
11840
  } catch (err) {
@@ -10860,10 +11845,10 @@ function handleClaudeSecondary(root, results) {
10860
11845
  error: err instanceof Error ? err.message : String(err)
10861
11846
  });
10862
11847
  }
10863
- const skillPath = path28.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
11848
+ const skillPath = path32.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
10864
11849
  if (!results.some((r) => r.filePath === ".claude/skills/kuma/SKILL.md")) {
10865
11850
  try {
10866
- const dir = path28.dirname(skillPath);
11851
+ const dir = path32.dirname(skillPath);
10867
11852
  const content = [
10868
11853
  "---",
10869
11854
  "name: kuma-mcp",
@@ -10875,17 +11860,17 @@ function handleClaudeSecondary(root, results) {
10875
11860
  "\u{1F4D6} Read `.kuma/init.md` for detailed rules.",
10876
11861
  "\u{1F4C2} Readable state: `.kuma/KNOWN_GOTCHAS.md` (gotchas) \xB7 `.kuma/memories/decisions.md` (decisions)"
10877
11862
  ].join("\n");
10878
- if (fs25.existsSync(skillPath)) {
10879
- const existing = fs25.readFileSync(skillPath, "utf-8");
11863
+ if (fs29.existsSync(skillPath)) {
11864
+ const existing = fs29.readFileSync(skillPath, "utf-8");
10880
11865
  if (existing.includes("kuma-mcp")) {
10881
11866
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "skipped" });
10882
11867
  } else {
10883
- 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");
10884
11869
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "appended" });
10885
11870
  }
10886
11871
  } else {
10887
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
10888
- fs25.writeFileSync(skillPath, content, "utf-8");
11872
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
11873
+ fs29.writeFileSync(skillPath, content, "utf-8");
10889
11874
  results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "created" });
10890
11875
  }
10891
11876
  } catch (err) {
@@ -10899,27 +11884,27 @@ function handleClaudeSecondary(root, results) {
10899
11884
  }
10900
11885
  }
10901
11886
  function writeHooksFile(root, relPath, type, results, build) {
10902
- const fullPath = path28.resolve(root, relPath);
11887
+ const fullPath = path32.resolve(root, relPath);
10903
11888
  try {
10904
11889
  let existing = {};
10905
11890
  let existed = false;
10906
- if (fs25.existsSync(fullPath)) {
11891
+ if (fs29.existsSync(fullPath)) {
10907
11892
  existed = true;
10908
11893
  try {
10909
- existing = JSON.parse(fs25.readFileSync(fullPath, "utf-8"));
11894
+ existing = JSON.parse(fs29.readFileSync(fullPath, "utf-8"));
10910
11895
  } catch {
10911
11896
  existing = {};
10912
11897
  }
10913
11898
  } else {
10914
- const dir = path28.dirname(fullPath);
10915
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11899
+ const dir = path32.dirname(fullPath);
11900
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
10916
11901
  }
10917
11902
  const { doc, changed } = build(existing);
10918
11903
  if (!changed) {
10919
11904
  results.push({ type, filePath: relPath, action: "skipped" });
10920
11905
  return;
10921
11906
  }
10922
- fs25.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
11907
+ fs29.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
10923
11908
  results.push({ type, filePath: relPath, action: existed ? "appended" : "created" });
10924
11909
  } catch (err) {
10925
11910
  results.push({ type, filePath: relPath, action: "error", error: err instanceof Error ? err.message : String(err) });
@@ -11000,18 +11985,18 @@ function handleCodexHooks(root, results) {
11000
11985
  });
11001
11986
  }
11002
11987
  function handleCursorGotchaRules(root, results) {
11003
- const rulesDir2 = path28.resolve(root, ".cursor", "rules", "kuma-gotchas");
11988
+ const rulesDir2 = path32.resolve(root, ".cursor", "rules", "kuma-gotchas");
11004
11989
  try {
11005
11990
  let gotchas = [];
11006
11991
  try {
11007
11992
  gotchas = getActiveGotchas().filter((g) => g.severity === "high" || g.severity === "critical");
11008
11993
  } catch {
11009
11994
  }
11010
- if (fs25.existsSync(rulesDir2)) {
11011
- for (const f of fs25.readdirSync(rulesDir2)) {
11995
+ if (fs29.existsSync(rulesDir2)) {
11996
+ for (const f of fs29.readdirSync(rulesDir2)) {
11012
11997
  if (f.endsWith(".mdc")) {
11013
11998
  try {
11014
- fs25.rmSync(path28.join(rulesDir2, f), { force: true });
11999
+ fs29.rmSync(path32.join(rulesDir2, f), { force: true });
11015
12000
  } catch {
11016
12001
  }
11017
12002
  }
@@ -11021,13 +12006,13 @@ function handleCursorGotchaRules(root, results) {
11021
12006
  results.push({ type: "cursor", filePath: ".cursor/rules/kuma-gotchas/", action: "skipped" });
11022
12007
  return;
11023
12008
  }
11024
- fs25.mkdirSync(rulesDir2, { recursive: true });
12009
+ fs29.mkdirSync(rulesDir2, { recursive: true });
11025
12010
  const seen = /* @__PURE__ */ new Set();
11026
12011
  let created = 0;
11027
12012
  for (const g of gotchas) {
11028
- const slug = g.filePath.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.(ts|js|tsx|jsx|py|go|rs|java|rb|php)$/i, "").slice(0, 60);
11029
- if (!slug || seen.has(slug)) continue;
11030
- seen.add(slug);
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);
11031
12016
  const md = [
11032
12017
  "---",
11033
12018
  `description: "KUMA gotcha \u2014 ${g.description.substring(0, 80)}"`,
@@ -11042,7 +12027,7 @@ function handleCursorGotchaRules(root, results) {
11042
12027
  "`kuma_context({ action: 'history', target: '<file>' })`",
11043
12028
  ""
11044
12029
  ].join("\n");
11045
- fs25.writeFileSync(path28.join(rulesDir2, `${slug}.mdc`), md, "utf-8");
12030
+ fs29.writeFileSync(path32.join(rulesDir2, `${slug2}.mdc`), md, "utf-8");
11046
12031
  created++;
11047
12032
  }
11048
12033
  results.push({
@@ -11069,28 +12054,28 @@ function runInit(options) {
11069
12054
  let agentsMdHandled = false;
11070
12055
  for (const type of selected) {
11071
12056
  const relativePath = configFilePath(type);
11072
- const fullPath = path28.resolve(root, relativePath);
12057
+ const fullPath = path32.resolve(root, relativePath);
11073
12058
  const getTemplate = TEMPLATES[type];
11074
12059
  try {
11075
12060
  if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
11076
12061
  agentsMdHandled = true;
11077
12062
  const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
11078
- if (fs25.existsSync(fullPath)) {
12063
+ if (fs29.existsSync(fullPath)) {
11079
12064
  if (options.skipExisting) {
11080
12065
  results.push({ type, filePath: relativePath, action: "skipped" });
11081
12066
  } else {
11082
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
12067
+ const existingContent = fs29.readFileSync(fullPath, "utf-8");
11083
12068
  if (existingContent.includes("_Generated by Kuma MCP_")) {
11084
12069
  results.push({ type, filePath: relativePath, action: "skipped" });
11085
12070
  } else {
11086
- fs25.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
12071
+ fs29.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
11087
12072
  results.push({ type, filePath: relativePath, action: "appended" });
11088
12073
  }
11089
12074
  }
11090
12075
  } else {
11091
- const dir = path28.dirname(fullPath);
11092
- if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11093
- 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");
11094
12079
  results.push({ type, filePath: relativePath, action: "created" });
11095
12080
  }
11096
12081
  if (selectedSet.has("opencode")) handleOpencodeSecondary(root, results);
@@ -11102,12 +12087,12 @@ function runInit(options) {
11102
12087
  continue;
11103
12088
  } else {
11104
12089
  const template = getTemplate();
11105
- if (fs25.existsSync(fullPath)) {
12090
+ if (fs29.existsSync(fullPath)) {
11106
12091
  if (options.skipExisting) {
11107
12092
  results.push({ type, filePath: relativePath, action: "skipped" });
11108
12093
  continue;
11109
12094
  }
11110
- const existingContent = fs25.readFileSync(fullPath, "utf-8");
12095
+ const existingContent = fs29.readFileSync(fullPath, "utf-8");
11111
12096
  if (existingContent.includes("_Generated by Kuma MCP_")) {
11112
12097
  if (type === "antigravity") {
11113
12098
  handleAntigravitySecondary(root, results);
@@ -11124,14 +12109,14 @@ function runInit(options) {
11124
12109
  continue;
11125
12110
  }
11126
12111
  const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
11127
- fs25.writeFileSync(fullPath, newContent, "utf-8");
12112
+ fs29.writeFileSync(fullPath, newContent, "utf-8");
11128
12113
  results.push({ type, filePath: relativePath, action: "appended" });
11129
12114
  } else {
11130
- const dir = path28.dirname(fullPath);
11131
- if (!fs25.existsSync(dir)) {
11132
- fs25.mkdirSync(dir, { recursive: true });
12115
+ const dir = path32.dirname(fullPath);
12116
+ if (!fs29.existsSync(dir)) {
12117
+ fs29.mkdirSync(dir, { recursive: true });
11133
12118
  }
11134
- fs25.writeFileSync(fullPath, template, "utf-8");
12119
+ fs29.writeFileSync(fullPath, template, "utf-8");
11135
12120
  results.push({ type, filePath: relativePath, action: "created" });
11136
12121
  }
11137
12122
  if (type === "antigravity") {
@@ -11256,11 +12241,10 @@ var init_init = __esm({
11256
12241
  "Record what matters, skip what doesn't.",
11257
12242
  "",
11258
12243
  "<kuma_obedience>",
11259
- "## \u{1F512} Obedience (non-negotiable)",
11260
- '- MUST call `kuma_context({ action: "init" })` at session start.',
11261
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
11262
- '- MUST call `kuma_safety({ action: "verify" })` after edits.',
11263
- "- MUST NOT call actions outside the core actions.",
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).",
11264
12248
  "</kuma_obedience>",
11265
12249
  "",
11266
12250
  "<kuma_workflow>",
@@ -11299,6 +12283,72 @@ var init_init = __esm({
11299
12283
  }
11300
12284
  });
11301
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
+
11302
12352
  // src/utils/agentDetector.ts
11303
12353
  var agentDetector_exports = {};
11304
12354
  __export(agentDetector_exports, {
@@ -11307,19 +12357,19 @@ __export(agentDetector_exports, {
11307
12357
  getSkillPath: () => getSkillPath,
11308
12358
  skillExists: () => skillExists
11309
12359
  });
11310
- import fs26 from "fs";
11311
- import path29 from "path";
12360
+ import fs31 from "fs";
12361
+ import path34 from "path";
11312
12362
  function checkFile(root, filePath) {
11313
12363
  try {
11314
- return fs26.existsSync(path29.join(root, filePath));
12364
+ return fs31.existsSync(path34.join(root, filePath));
11315
12365
  } catch {
11316
12366
  return false;
11317
12367
  }
11318
12368
  }
11319
12369
  function checkDir(root, dirPath) {
11320
12370
  try {
11321
- const fullPath = path29.join(root, dirPath);
11322
- return fs26.existsSync(fullPath) && fs26.statSync(fullPath).isDirectory();
12371
+ const fullPath = path34.join(root, dirPath);
12372
+ return fs31.existsSync(fullPath) && fs31.statSync(fullPath).isDirectory();
11323
12373
  } catch {
11324
12374
  return false;
11325
12375
  }
@@ -11387,7 +12437,7 @@ function getAgentLabel(type) {
11387
12437
  function skillExists(type, projectRoot) {
11388
12438
  const root = projectRoot ?? getProjectRoot();
11389
12439
  const skillPath = getSkillPath(type);
11390
- return fs26.existsSync(path29.join(root, skillPath));
12440
+ return fs31.existsSync(path34.join(root, skillPath));
11391
12441
  }
11392
12442
  var AGENT_DETECTORS;
11393
12443
  var init_agentDetector = __esm({
@@ -11928,11 +12978,10 @@ var init_skillGenerator = __esm({
11928
12978
  "use strict";
11929
12979
  CORE_ACTIONS = [
11930
12980
  "<kuma_obedience>",
11931
- "## \u{1F512} Obedience (non-negotiable)",
11932
- '- MUST call `kuma_context({ action: "init" })` at session start.',
11933
- "- MUST record gotchas IMMEDIATELY when a bug/quirk is found.",
11934
- '- MUST call `kuma_safety({ action: "verify" })` after edits to resolve affected tests.',
11935
- "- MUST NOT call actions outside the core actions.",
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.",
11936
12985
  "</kuma_obedience>",
11937
12986
  "",
11938
12987
  "<kuma_workflow>",
@@ -11984,8 +13033,8 @@ init_kumaGraph();
11984
13033
  init_kumaMemory();
11985
13034
  init_kumaDriftDetector();
11986
13035
  init_pathValidator();
11987
- import fs18 from "fs";
11988
- import path21 from "path";
13036
+ import fs21 from "fs";
13037
+ import path24 from "path";
11989
13038
  var CONTEXT_ALIASES = {
11990
13039
  "research": "research",
11991
13040
  "search": "research",
@@ -12031,9 +13080,13 @@ var CONTEXT_ALIASES = {
12031
13080
  "anti-duplication": "reuse"
12032
13081
  };
12033
13082
  async function handleContext(params) {
13083
+ let dirtySynced = 0;
12034
13084
  try {
13085
+ const { getDirtyFiles: getDirtyFiles2, clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12035
13086
  const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
13087
+ dirtySynced = getDirtyFiles2().length;
12036
13088
  await syncModifiedFiles2(30);
13089
+ if (dirtySynced > 0) clearDirty2();
12037
13090
  } catch {
12038
13091
  }
12039
13092
  const rawAction = params.action || "init";
@@ -12049,7 +13102,7 @@ async function handleContext(params) {
12049
13102
  case "init":
12050
13103
  return handleInit(params);
12051
13104
  case "research":
12052
- return obediencePrefix + await handleResearch(params);
13105
+ return obediencePrefix + await handleResearch(params, dirtySynced);
12053
13106
  case "history":
12054
13107
  return obediencePrefix + await handleHistory(params);
12055
13108
  case "flow":
@@ -12256,11 +13309,15 @@ function sessionFocusAdvice(focus) {
12256
13309
  return "Record gotchas & decisions as they happen \u2014 skip what isn't worth it.";
12257
13310
  }
12258
13311
  }
12259
- async function handleResearch(params) {
13312
+ async function handleResearch(params, dirtySynced = 0) {
12260
13313
  const scope = params.scope || "project";
12261
13314
  sessionMemory.setGoal(`Researching: ${scope}`);
12262
13315
  sessionMemory.recordToolCall("kuma_context_research", { scope });
12263
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
+ }
12264
13321
  lines.push("**Step 1/5: Loading Research Cache**");
12265
13322
  const cached = await getResearchCache(scope);
12266
13323
  let record = null;
@@ -12274,7 +13331,18 @@ async function handleResearch(params) {
12274
13331
  const ageSeconds = Math.floor((Date.now() - (record.validatedAt ? new Date(record.validatedAt).getTime() : 0)) / 1e3);
12275
13332
  const ageStr = ageSeconds > 86400 ? `${Math.floor(ageSeconds / 86400)}d` : ageSeconds > 3600 ? `${Math.floor(ageSeconds / 3600)}h` : `${Math.floor(ageSeconds / 60)}m`;
12276
13333
  lines.push(` \u2705 Found cached research (${ageStr} old)`);
12277
- if (ageSeconds > 86400) lines.push(` ${ageSeconds > 604800 ? "\u{1F534}" : "\u{1F7E1}"} **Staleness:** Cache is ${ageStr} old \u2014 may be stale`);
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`);
12278
13346
  } else lines.push(" \u23F3 No cached research \u2014 starting fresh");
12279
13347
  lines.push("");
12280
13348
  lines.push("**Step 2/5: Staleness Check**");
@@ -12383,10 +13451,10 @@ async function handleHistory(params) {
12383
13451
  lines.push(traceFormatted);
12384
13452
  } else {
12385
13453
  try {
12386
- const { execSync: execSync7 } = await import("child_process");
13454
+ const { execSync: execSync9 } = await import("child_process");
12387
13455
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
12388
13456
  const root = getProjectRoot2();
12389
- const gitHistory = execSync7(
13457
+ const gitHistory = execSync9(
12390
13458
  `git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
12391
13459
  { cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
12392
13460
  ).trim();
@@ -12418,7 +13486,7 @@ ${gitHistory}`);
12418
13486
  try {
12419
13487
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
12420
13488
  const db = await getDb2();
12421
- const base = path21.basename(target);
13489
+ const base = path24.basename(target);
12422
13490
  const stmt = db.prepare(`
12423
13491
  SELECT description, last_verified_at FROM known_gotchas
12424
13492
  WHERE status = 'resolved' AND (file_path LIKE ? OR file_path LIKE ?)
@@ -12431,9 +13499,9 @@ ${gitHistory}`);
12431
13499
  if (resolved.length === 0) {
12432
13500
  try {
12433
13501
  const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
12434
- const mdPath = path21.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
12435
- if (fs18.existsSync(mdPath)) {
12436
- const mdContent = fs18.readFileSync(mdPath, "utf-8");
13502
+ const mdPath = path24.join(getProjectRoot2(), ".kuma", "KNOWN_GOTCHAS.md");
13503
+ if (fs21.existsSync(mdPath)) {
13504
+ const mdContent = fs21.readFileSync(mdPath, "utf-8");
12437
13505
  const sections = mdContent.split(/^###\s+/m);
12438
13506
  for (const sec of sections) {
12439
13507
  const secLower = sec.toLowerCase();
@@ -12478,7 +13546,18 @@ async function handleMap(_params) {
12478
13546
  sessionMemory.recordToolCall("kuma_context_map", {});
12479
13547
  const { getWorkspaceInfo: getWorkspaceInfo2, formatWorkspaceMap: formatWorkspaceMap2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
12480
13548
  const wsInfo = await getWorkspaceInfo2();
12481
- 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;
12482
13561
  }
12483
13562
  async function handleImpact(params) {
12484
13563
  const target = params.target || params.scope || "project";
@@ -12494,8 +13573,8 @@ init_kumaDb();
12494
13573
  init_kumaMemory();
12495
13574
  init_pathValidator();
12496
13575
  init_pathValidator();
12497
- import fs20 from "fs";
12498
- import path23 from "path";
13576
+ import fs23 from "fs";
13577
+ import path26 from "path";
12499
13578
  var MEMORY_ALIASES = {
12500
13579
  "decision": "decision",
12501
13580
  "adr": "decision",
@@ -12519,7 +13598,11 @@ var MEMORY_ALIASES = {
12519
13598
  "gotcha-resolve": "gotcha",
12520
13599
  "deprecate_gotcha": "gotcha",
12521
13600
  "deprecate-gotcha": "gotcha",
12522
- "deprecate": "gotcha"
13601
+ "deprecate": "gotcha",
13602
+ "test_outcome": "test_outcome",
13603
+ "test-outcome": "test_outcome",
13604
+ "testoutcome": "test_outcome",
13605
+ "test-record": "test_outcome"
12523
13606
  };
12524
13607
  async function handleMemory(params) {
12525
13608
  const rawAction = params.action || "search";
@@ -12537,10 +13620,24 @@ async function handleMemory(params) {
12537
13620
  return handleArchFlow(params);
12538
13621
  case "gotcha":
12539
13622
  return handleGotchaAction(params);
13623
+ case "test_outcome":
13624
+ return handleTestOutcome(params);
12540
13625
  default:
12541
- 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`;
12542
13627
  }
12543
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.';
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.`;
13640
+ }
12544
13641
  async function handleDecision(params) {
12545
13642
  const title = params.title || params.content || (params.target ? `Decision regarding ${params.target}` : params.scope ? `Decision for ${params.scope}` : "");
12546
13643
  const rationale = params.rationale || params.description || (params.content && params.content !== title ? params.content : "");
@@ -12588,9 +13685,9 @@ async function handleResearchSave(params) {
12588
13685
  } catch {
12589
13686
  }
12590
13687
  try {
12591
- const researchDir = path23.join(getProjectRoot(), ".kuma", "research");
12592
- if (!fs20.existsSync(researchDir)) fs20.mkdirSync(researchDir, { recursive: true });
12593
- fs20.writeFileSync(path23.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
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");
12594
13691
  } catch {
12595
13692
  }
12596
13693
  sessionMemory.recordMemoryAction("research_save");
@@ -12786,9 +13883,9 @@ init_sessionMemory();
12786
13883
  init_pathValidator();
12787
13884
  init_sessionMemory();
12788
13885
  init_kumaShared();
12789
- import fs21 from "fs";
12790
- import path24 from "path";
12791
- 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";
12792
13889
  var SCRIPT_PATCH_PATTERNS = [
12793
13890
  "writeFileSync",
12794
13891
  "writeFile",
@@ -12814,20 +13911,20 @@ function findPatchScripts(projectRoot) {
12814
13911
  const warnings = [];
12815
13912
  const recentFiles = scanRecentFiles(projectRoot);
12816
13913
  for (const file of recentFiles) {
12817
- const ext = path24.extname(file).toLowerCase();
13914
+ const ext = path27.extname(file).toLowerCase();
12818
13915
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
12819
- const relativePath = path24.relative(projectRoot, file);
13916
+ const relativePath = path27.relative(projectRoot, file);
12820
13917
  if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".") || relativePath.startsWith(".kuma/scratch")) {
12821
13918
  continue;
12822
13919
  }
12823
13920
  try {
12824
- const content = fs21.readFileSync(file, "utf-8").toLowerCase();
13921
+ const content = fs24.readFileSync(file, "utf-8").toLowerCase();
12825
13922
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
12826
13923
  if (matchedPattern) {
12827
13924
  warnings.push({
12828
13925
  severity: "high",
12829
13926
  pattern: "script-patching",
12830
- message: `Created script file that modifies other files: ${path24.basename(file)}`,
13927
+ message: `Created script file that modifies other files: ${path27.basename(file)}`,
12831
13928
  suggestion: "Use your native edit tools instead \u2014 targeted edits with auto-backup and rollback",
12832
13929
  evidence: `File: ${relativePath} contains '${matchedPattern}'`,
12833
13930
  filePath: relativePath
@@ -12861,15 +13958,15 @@ function detectBashGrepUsage() {
12861
13958
  function scanRecentFiles(projectRoot) {
12862
13959
  const recent = [];
12863
13960
  try {
12864
- const entries = fs21.readdirSync(projectRoot, { withFileTypes: true });
13961
+ const entries = fs24.readdirSync(projectRoot, { withFileTypes: true });
12865
13962
  const now = Date.now();
12866
13963
  const recentThreshold = 30 * 60 * 1e3;
12867
13964
  for (const entry of entries) {
12868
13965
  if (!entry.isFile()) continue;
12869
13966
  try {
12870
- const stat = fs21.statSync(path24.join(projectRoot, entry.name));
13967
+ const stat = fs24.statSync(path27.join(projectRoot, entry.name));
12871
13968
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
12872
- recent.push(path24.join(projectRoot, entry.name));
13969
+ recent.push(path27.join(projectRoot, entry.name));
12873
13970
  }
12874
13971
  } catch {
12875
13972
  continue;
@@ -12882,7 +13979,7 @@ function scanRecentFiles(projectRoot) {
12882
13979
  function detectGitPatchScripts(projectRoot) {
12883
13980
  const warnings = [];
12884
13981
  try {
12885
- const statusStdout = execSync5("git status --porcelain", {
13982
+ const statusStdout = execSync6("git status --porcelain", {
12886
13983
  cwd: projectRoot,
12887
13984
  encoding: "utf-8",
12888
13985
  timeout: 3e3
@@ -12893,14 +13990,14 @@ function detectGitPatchScripts(projectRoot) {
12893
13990
  const prefix = line.substring(0, 2);
12894
13991
  if (prefix !== "??" && prefix !== "A ") continue;
12895
13992
  const file = line.substring(3).trim();
12896
- const ext = path24.extname(file).toLowerCase();
13993
+ const ext = path27.extname(file).toLowerCase();
12897
13994
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
12898
13995
  const isRootLevel = !file.includes("/");
12899
13996
  const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
12900
13997
  if (!isRootLevel && !isScriptsDir) continue;
12901
- const fullPath = path24.join(projectRoot, file);
12902
- if (fs21.existsSync(fullPath)) {
12903
- const content = fs21.readFileSync(fullPath, "utf-8").toLowerCase();
13998
+ const fullPath = path27.join(projectRoot, file);
13999
+ if (fs24.existsSync(fullPath)) {
14000
+ const content = fs24.readFileSync(fullPath, "utf-8").toLowerCase();
12904
14001
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
12905
14002
  if (matchedPattern) {
12906
14003
  warnings.push({
@@ -12915,7 +14012,7 @@ function detectGitPatchScripts(projectRoot) {
12915
14012
  }
12916
14013
  }
12917
14014
  }
12918
- const deletedStdout = execSync5("git diff --name-only --diff-filter=D HEAD", {
14015
+ const deletedStdout = execSync6("git diff --name-only --diff-filter=D HEAD", {
12919
14016
  cwd: projectRoot,
12920
14017
  encoding: "utf-8",
12921
14018
  timeout: 3e3
@@ -12923,7 +14020,7 @@ function detectGitPatchScripts(projectRoot) {
12923
14020
  if (deletedStdout) {
12924
14021
  const deletedFiles = deletedStdout.split("\n").filter(Boolean);
12925
14022
  for (const file of deletedFiles) {
12926
- const ext = path24.extname(file).toLowerCase();
14023
+ const ext = path27.extname(file).toLowerCase();
12927
14024
  if (SCRIPT_EXTENSIONS.includes(ext)) {
12928
14025
  warnings.push({
12929
14026
  severity: "high",
@@ -12985,21 +14082,21 @@ function detectAllAntiPatterns() {
12985
14082
  // src/engine/contextSnapshot.ts
12986
14083
  init_sessionMemory();
12987
14084
  init_pathValidator();
12988
- import fs22 from "fs";
12989
- import path25 from "path";
12990
- 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";
12991
14088
  var SNAPSHOT_DIR = "context-snapshots";
12992
14089
  function snapshotDir() {
12993
- return path25.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
14090
+ return path28.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
12994
14091
  }
12995
14092
  function ensureSnapshotDir() {
12996
14093
  const dir = snapshotDir();
12997
- if (!fs22.existsSync(dir)) {
12998
- fs22.mkdirSync(dir, { recursive: true });
14094
+ if (!fs25.existsSync(dir)) {
14095
+ fs25.mkdirSync(dir, { recursive: true });
12999
14096
  }
13000
14097
  }
13001
14098
  function snapshotFilePath(timestamp) {
13002
- return path25.join(snapshotDir(), `${timestamp}.json`);
14099
+ return path28.join(snapshotDir(), `${timestamp}.json`);
13003
14100
  }
13004
14101
  function saveSnapshot(goal) {
13005
14102
  try {
@@ -13020,7 +14117,7 @@ function saveSnapshot(goal) {
13020
14117
  hasConventions: !!summary.hasConventions
13021
14118
  };
13022
14119
  try {
13023
- 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");
13024
14121
  } catch {
13025
14122
  }
13026
14123
  sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
@@ -13059,7 +14156,7 @@ function formatSnapshot(snapshot) {
13059
14156
  function getGitDiffStat2() {
13060
14157
  try {
13061
14158
  const root = getProjectRoot();
13062
- const stdout = execSync6("git diff --stat", {
14159
+ const stdout = execSync7("git diff --stat", {
13063
14160
  cwd: root,
13064
14161
  encoding: "utf-8",
13065
14162
  timeout: 3e3
@@ -13106,24 +14203,62 @@ async function handleKumaGuard(params) {
13106
14203
  });
13107
14204
  }
13108
14205
  const drifts = [];
14206
+ const info = [];
14207
+ let verified = stats.hasRunTests;
14208
+ let uncommittedModifiedCount = stats.modifiedFiles.length;
13109
14209
  if (check === "all" || check === "drift") {
13110
14210
  const unresolvedCount = getUnresolvedCount(stats.failedFiles);
13111
14211
  const gitStat = getGitDiffStat();
13112
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;
13113
14221
  drifts.push(...buildDriftMessages(
13114
- stats.modifiedFiles.length,
13115
- stats.hasRunTests,
14222
+ uncommittedModified.length,
14223
+ verified,
13116
14224
  unresolvedCount,
13117
14225
  gitStat
13118
14226
  ));
13119
- 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) {
13120
14234
  warnings.push({
13121
14235
  severity: "medium",
13122
14236
  pattern: "no-test-after-edit",
13123
- message: `${stats.modifiedFiles.length} file(s) modified without running tests`,
14237
+ message: `${uncommittedModified.length} uncommitted file(s) modified without running tests`,
13124
14238
  suggestion: "Run the project's test/typecheck command to verify changes"
13125
14239
  });
13126
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
+ }
13127
14262
  if (editCalls > 5) {
13128
14263
  warnings.push({
13129
14264
  severity: "low",
@@ -13195,6 +14330,25 @@ async function handleKumaGuard(params) {
13195
14330
  }
13196
14331
  return formatSnapshot(snapshot);
13197
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
+ }
13198
14352
  const hasWarnings = warnings.length > 0;
13199
14353
  const hasDrifts = drifts.length > 0;
13200
14354
  const onTrack = !hasWarnings && !hasDrifts;
@@ -13203,8 +14357,8 @@ async function handleKumaGuard(params) {
13203
14357
  warnings,
13204
14358
  loop.isLooping,
13205
14359
  getUnresolvedCount(stats.failedFiles),
13206
- stats.modifiedFiles.length,
13207
- stats.hasRunTests,
14360
+ uncommittedModifiedCount,
14361
+ verified,
13208
14362
  countEditCalls(stats.toolCalls)
13209
14363
  );
13210
14364
  const report = {
@@ -13212,6 +14366,7 @@ async function handleKumaGuard(params) {
13212
14366
  onTrack,
13213
14367
  warnings,
13214
14368
  drifts,
14369
+ info,
13215
14370
  suggestion,
13216
14371
  stats: {
13217
14372
  goal: stats.goal,
@@ -13242,6 +14397,90 @@ async function handleKumaGuard(params) {
13242
14397
  return JSON.stringify(reportWithRecordings, null, 2);
13243
14398
  }
13244
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
+
13245
14484
  // src/tools/kumaSafetyTool.ts
13246
14485
  async function handleSafety(params) {
13247
14486
  const { action } = params;
@@ -13261,10 +14500,17 @@ async function handleSafety(params) {
13261
14500
  return prefix + await handleCheckpoint(params);
13262
14501
  case "rollback_label":
13263
14502
  return prefix + await handleRollbackLabel(params);
14503
+ case "cost":
14504
+ return prefix + await handleCost();
13264
14505
  default:
13265
- return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label`;
14506
+ return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label, cost`;
13266
14507
  }
13267
14508
  }
14509
+ async function handleCost() {
14510
+ sessionMemory.recordToolCall("kuma_safety_cost", {});
14511
+ const stats = await getCostStats();
14512
+ return formatCostStats(stats);
14513
+ }
13268
14514
  async function handleGuard(params) {
13269
14515
  const base = await handleKumaGuard({
13270
14516
  goal: params.guardGoal,
@@ -13437,12 +14683,12 @@ function registerAllTools(server) {
13437
14683
  const contextHandler = async (params) => {
13438
14684
  try {
13439
14685
  await ensureInitialized();
13440
- const text = await handleContext({
14686
+ const { result: text } = await withCost("kuma_context", params.action, () => handleContext({
13441
14687
  action: params.action,
13442
14688
  scope: params.scope,
13443
14689
  target: params.target,
13444
14690
  goal: params.goal
13445
- });
14691
+ }));
13446
14692
  return { content: [{ type: "text", text }] };
13447
14693
  } catch (err) {
13448
14694
  return { content: [{ type: "text", text: `Error in kuma_context: ${err}` }], isError: true };
@@ -13450,10 +14696,10 @@ function registerAllTools(server) {
13450
14696
  };
13451
14697
  server.tool("kuma_context", contextDesc, contextSchema, contextHandler);
13452
14698
  server.tool("kuma_kuma_context", contextDesc, contextSchema, contextHandler);
13453
- const memoryDesc = "Persistent knowledge. Record what matters, skip what doesn't. This is what saves time in future sessions. Don't record what grep/glob answers faster (functions, imports, types, components)." + CORE_NOTE + "`gotcha` (IMMEDIATELY when you find a bug/quirk), `decision` (when choosing between options), `arch_flow` (after tracing a complete flow, max 5 core files), `research_save` (after exploring an area), `search` (quick lookup of memory + knowledge graph), `gotcha_resolve` (archive fixed bugs).";
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).";
13454
14700
  const memorySchema = {
13455
- action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve"]).describe(
13456
- "gotcha=record bug/quirk, decision=ADR, arch_flow=record architecture flow, research_save=save findings, search=quick memory+graph lookup, gotcha_resolve=archive fixed bug"
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"
13457
14703
  ),
13458
14704
  scope: z.string().optional().describe("File path for gotcha \u2014 or scope for research_save/search"),
13459
14705
  target: z.string().optional().describe("Target file, gotcha ID, or component"),
@@ -13475,7 +14721,7 @@ function registerAllTools(server) {
13475
14721
  const memoryHandler = async (params) => {
13476
14722
  try {
13477
14723
  await ensureInitialized();
13478
- const text = await handleMemory({
14724
+ const { result: text } = await withCost("kuma_memory", params.action, () => handleMemory({
13479
14725
  action: params.action,
13480
14726
  scope: params.scope,
13481
14727
  target: params.target,
@@ -13493,7 +14739,7 @@ function registerAllTools(server) {
13493
14739
  description: params.description,
13494
14740
  trigger_command: params.trigger_command,
13495
14741
  id: params.id
13496
- });
14742
+ }));
13497
14743
  return { content: [{ type: "text", text }] };
13498
14744
  } catch (err) {
13499
14745
  return { content: [{ type: "text", text: `Error in kuma_memory: ${err}` }], isError: true };
@@ -13501,10 +14747,10 @@ function registerAllTools(server) {
13501
14747
  };
13502
14748
  server.tool("kuma_memory", memoryDesc, memorySchema, memoryHandler);
13503
14749
  server.tool("kuma_kuma_memory", memoryDesc, memorySchema, memoryHandler);
13504
- const safetyDesc = "Safety & verification. Use at task boundaries, not on every edit." + CORE_NOTE + "`guard` (before risky work: anti-patterns, drift, loops), `verify` (after edits: scoped tests + validation), `checkpoint` + `rollback_label` (the one rollback mechanism: snapshot before risky work, restore after).";
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).";
13505
14751
  const safetySchema = {
13506
- action: z.enum(["guard", "verify", "checkpoint", "rollback_label"]).describe(
13507
- "guard=anti-patterns/drift/loops before risky work, verify=scoped tests after edits, checkpoint=labeled snapshot before risky work, rollback_label=restore a labeled snapshot"
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"
13508
14754
  ),
13509
14755
  scope: z.string().optional().describe("Scope for verify (e.g. 'auth', file path)"),
13510
14756
  target: z.string().optional().describe("Target test file or scope for verify"),
@@ -13520,7 +14766,7 @@ function registerAllTools(server) {
13520
14766
  const safetyHandler = async (params) => {
13521
14767
  try {
13522
14768
  await ensureInitialized();
13523
- const text = await handleSafety({
14769
+ const { result: text } = await withCost("kuma_safety", params.action, () => handleSafety({
13524
14770
  action: params.action,
13525
14771
  guardGoal: params.guardGoal,
13526
14772
  guardCheck: params.guardCheck,
@@ -13532,7 +14778,7 @@ function registerAllTools(server) {
13532
14778
  force: params.force,
13533
14779
  label: params.label,
13534
14780
  description: params.description
13535
- });
14781
+ }));
13536
14782
  return { content: [{ type: "text", text }] };
13537
14783
  } catch (err) {
13538
14784
  return { content: [{ type: "text", text: `Error in kuma_safety: ${err}` }], isError: true };
@@ -13567,6 +14813,10 @@ Usage:
13567
14813
  npx @plumpslabs/kuma init --help Show this help
13568
14814
  npx @plumpslabs/kuma hook pre-edit Claude Code PreToolUse hook \u2014 inject gotcha/decision/history before edits
13569
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)
13570
14820
 
13571
14821
  Available config files:
13572
14822
  --claude CLAUDE.md (Claude Code)
@@ -13659,14 +14909,14 @@ async function main() {
13659
14909
  console.error("");
13660
14910
  let killedCount = 0;
13661
14911
  try {
13662
- const path30 = await import("path");
13663
- const fs27 = await import("fs");
13664
- const lockDir = path30.resolve(process.cwd(), ".kuma/verifier.lock");
13665
- if (fs27.existsSync(lockDir)) {
13666
- const pidFile = path30.join(lockDir, "pid");
13667
- if (fs27.existsSync(pidFile)) {
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)) {
13668
14918
  try {
13669
- const pid = parseInt(fs27.readFileSync(pidFile, "utf-8"), 10);
14919
+ const pid = parseInt(fs32.readFileSync(pidFile, "utf-8"), 10);
13670
14920
  if (pid && pid !== process.pid) {
13671
14921
  try {
13672
14922
  process.kill(-pid, "SIGKILL");
@@ -13682,7 +14932,7 @@ async function main() {
13682
14932
  } catch {
13683
14933
  }
13684
14934
  }
13685
- fs27.rmSync(lockDir, { recursive: true, force: true });
14935
+ fs32.rmSync(lockDir, { recursive: true, force: true });
13686
14936
  }
13687
14937
  if (killedCount === 0) {
13688
14938
  console.error("\u2705 No running verifications found to kill.");
@@ -13690,25 +14940,25 @@ async function main() {
13690
14940
  } catch {
13691
14941
  }
13692
14942
  try {
13693
- const { execSync: execSync7 } = await import("child_process");
14943
+ const { execSync: execSync9 } = await import("child_process");
13694
14944
  try {
13695
- execSync7("pkill -f 'jest' 2>/dev/null || true");
14945
+ execSync9("pkill -f 'jest' 2>/dev/null || true");
13696
14946
  } catch {
13697
14947
  }
13698
14948
  try {
13699
- execSync7("pkill -f 'jest-worker' 2>/dev/null || true");
14949
+ execSync9("pkill -f 'jest-worker' 2>/dev/null || true");
13700
14950
  } catch {
13701
14951
  }
13702
14952
  try {
13703
- execSync7("pkill -f 'pnpm test' 2>/dev/null || true");
14953
+ execSync9("pkill -f 'pnpm test' 2>/dev/null || true");
13704
14954
  } catch {
13705
14955
  }
13706
14956
  try {
13707
- execSync7("pkill -f 'npm test' 2>/dev/null || true");
14957
+ execSync9("pkill -f 'npm test' 2>/dev/null || true");
13708
14958
  } catch {
13709
14959
  }
13710
14960
  try {
13711
- execSync7("pkill -f 'yarn test' 2>/dev/null || true");
14961
+ execSync9("pkill -f 'yarn test' 2>/dev/null || true");
13712
14962
  } catch {
13713
14963
  }
13714
14964
  console.error("\u2705 Killed orphaned test processes");
@@ -13776,6 +15026,11 @@ async function main() {
13776
15026
  }
13777
15027
  const target = hook.filePaths[0];
13778
15028
  await trackFileEditLoop2(target);
15029
+ try {
15030
+ const { markDirty: markDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
15031
+ markDirty2(target);
15032
+ } catch {
15033
+ }
13779
15034
  try {
13780
15035
  const { maybeAutoCheckpoint: maybeAutoCheckpoint2 } = await Promise.resolve().then(() => (init_kumaCheckpoint(), kumaCheckpoint_exports));
13781
15036
  const autoCp = maybeAutoCheckpoint2(target);
@@ -13830,9 +15085,138 @@ async function main() {
13830
15085
  }
13831
15086
  process.exit(0);
13832
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
+ }
13833
15151
  if (args[0] === "--hook") {
13834
15152
  process.exit(0);
13835
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
+ }
13836
15220
  if (args[0] === "init") {
13837
15221
  const flags = args.slice(1);
13838
15222
  if (flags.includes("--help") || flags.includes("-h")) {
@@ -13893,23 +15277,23 @@ async function main() {
13893
15277
  });
13894
15278
  const output = formatInitResults(results);
13895
15279
  console.log(output);
13896
- const fs27 = await import("fs");
13897
- const path30 = await import("path");
13898
- const matchaSkills = path30.resolve(process.cwd(), "skills/matcha/SKILL.md");
13899
- const matchaAgents = path30.resolve(
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(
13900
15284
  process.cwd(),
13901
15285
  ".agents/skills/matcha/SKILL.md"
13902
15286
  );
13903
- const matchaRootSkills = path30.resolve(
15287
+ const matchaRootSkills = path35.resolve(
13904
15288
  process.cwd(),
13905
15289
  "skills/matcha/SKILL.md"
13906
15290
  );
13907
- const matchaAgentsMd = path30.resolve(process.cwd(), "AGENTS.md");
13908
- const matchaWindsurfRules = path30.resolve(
15291
+ const matchaAgentsMd = path35.resolve(process.cwd(), "AGENTS.md");
15292
+ const matchaWindsurfRules = path35.resolve(
13909
15293
  process.cwd(),
13910
15294
  ".windsurf"
13911
15295
  );
13912
- 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)) {
13913
15297
  console.error(
13914
15298
  "\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
13915
15299
  );
@@ -13923,13 +15307,13 @@ async function main() {
13923
15307
  (async () => {
13924
15308
  try {
13925
15309
  const { generateInitMdContent: generateInitMdContent2 } = await Promise.resolve().then(() => (init_init(), init_exports));
13926
- const fs27 = await import("fs");
13927
- const path30 = await import("path");
13928
- const initMdPath = path30.resolve(process.cwd(), ".kuma/init.md");
13929
- if (!fs27.existsSync(initMdPath)) {
13930
- const kumaDir = path30.dirname(initMdPath);
13931
- if (!fs27.existsSync(kumaDir)) fs27.mkdirSync(kumaDir, { recursive: true });
13932
- fs27.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
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");
13933
15317
  console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
13934
15318
  }
13935
15319
  } catch (err) {
@@ -13940,8 +15324,8 @@ async function main() {
13940
15324
  try {
13941
15325
  const { detectAgent: detectAgent2, getSkillPath: getSkillPath2, getAgentLabel: getAgentLabel2 } = await Promise.resolve().then(() => (init_agentDetector(), agentDetector_exports));
13942
15326
  const { generateSkill: generateSkill2, getSecondaryFiles: getSecondaryFiles2 } = await Promise.resolve().then(() => (init_skillGenerator(), skillGenerator_exports));
13943
- const fs27 = await import("fs");
13944
- const path30 = await import("path");
15327
+ const fs32 = await import("fs");
15328
+ const path35 = await import("path");
13945
15329
  const detection = detectAgent2();
13946
15330
  if (!detection.primary) {
13947
15331
  console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
@@ -13949,22 +15333,22 @@ async function main() {
13949
15333
  }
13950
15334
  const agentType = detection.primary;
13951
15335
  const skillPath = getSkillPath2(agentType);
13952
- const fullPath = path30.resolve(process.cwd(), skillPath);
13953
- if (fs27.existsSync(fullPath)) {
15336
+ const fullPath = path35.resolve(process.cwd(), skillPath);
15337
+ if (fs32.existsSync(fullPath)) {
13954
15338
  console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel2(agentType)} \u2014 skipping`);
13955
15339
  return;
13956
15340
  }
13957
- const dir = path30.dirname(fullPath);
13958
- if (!fs27.existsSync(dir)) fs27.mkdirSync(dir, { recursive: true });
13959
- 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");
13960
15344
  console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel2(agentType)}`);
13961
15345
  const secondaryFiles = getSecondaryFiles2(agentType);
13962
15346
  for (const sf of secondaryFiles) {
13963
- const sfPath = path30.resolve(process.cwd(), sf.path);
13964
- const sfDir = path30.dirname(sfPath);
13965
- if (!fs27.existsSync(sfPath)) {
13966
- if (!fs27.existsSync(sfDir)) fs27.mkdirSync(sfDir, { recursive: true });
13967
- fs27.writeFileSync(sfPath, sf.content, "utf-8");
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");
13968
15352
  console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
13969
15353
  }
13970
15354
  }