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