@plumpslabs/kuma 2.4.13 → 2.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1986 -602
- 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();
|
|
@@ -2134,6 +2151,23 @@ No unresolved issues.`;
|
|
|
2134
2151
|
this.state.metrics.filesEdited++;
|
|
2135
2152
|
this.save();
|
|
2136
2153
|
}
|
|
2154
|
+
// ============================================================
|
|
2155
|
+
// MICROCOMPACT — session-scoped shown-set (in-memory only)
|
|
2156
|
+
// ============================================================
|
|
2157
|
+
// Hook processes are separate OS processes, so cross-hook dedupe lives
|
|
2158
|
+
// in hook-state.json (15min window). THIS set covers the MCP server
|
|
2159
|
+
// process: same gotcha/decision text is never injected twice in one
|
|
2160
|
+
// server run — repeats collapse to zero tokens.
|
|
2161
|
+
shownKeys = /* @__PURE__ */ new Set();
|
|
2162
|
+
/** True when this exact payload was already injected this server run. */
|
|
2163
|
+
wasShown(kind, key) {
|
|
2164
|
+
return this.shownKeys.has(`${kind}::${key}`);
|
|
2165
|
+
}
|
|
2166
|
+
/** Mark payloads as injected. Bounded at 500 keys (FIFO-ish reset). */
|
|
2167
|
+
markShown(kind, keys) {
|
|
2168
|
+
for (const k of keys) this.shownKeys.add(`${kind}::${k}`);
|
|
2169
|
+
if (this.shownKeys.size > 500) this.shownKeys = new Set([...this.shownKeys].slice(-500));
|
|
2170
|
+
}
|
|
2137
2171
|
/**
|
|
2138
2172
|
* Get metrics summary for session.
|
|
2139
2173
|
*/
|
|
@@ -4543,14 +4577,163 @@ var init_workspaceIntelligence = __esm({
|
|
|
4543
4577
|
}
|
|
4544
4578
|
});
|
|
4545
4579
|
|
|
4580
|
+
// src/engine/testHistory.ts
|
|
4581
|
+
var testHistory_exports = {};
|
|
4582
|
+
__export(testHistory_exports, {
|
|
4583
|
+
fileSig: () => fileSig,
|
|
4584
|
+
markTestFlaky: () => markTestFlaky,
|
|
4585
|
+
rankTestsByHistory: () => rankTestsByHistory,
|
|
4586
|
+
recordTestOutcome: () => recordTestOutcome
|
|
4587
|
+
});
|
|
4588
|
+
async function ensureTestHistorySchema() {
|
|
4589
|
+
const db = await getDb();
|
|
4590
|
+
db.exec(`
|
|
4591
|
+
CREATE TABLE IF NOT EXISTS test_history (
|
|
4592
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4593
|
+
file_sig TEXT NOT NULL,
|
|
4594
|
+
test_path TEXT NOT NULL,
|
|
4595
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
4596
|
+
flaky INTEGER NOT NULL DEFAULT 0,
|
|
4597
|
+
runs INTEGER NOT NULL DEFAULT 1,
|
|
4598
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
4599
|
+
);
|
|
4600
|
+
CREATE INDEX IF NOT EXISTS idx_th_sig ON test_history(file_sig);
|
|
4601
|
+
CREATE INDEX IF NOT EXISTS idx_th_test ON test_history(test_path);
|
|
4602
|
+
`);
|
|
4603
|
+
saveDb();
|
|
4604
|
+
}
|
|
4605
|
+
function fileSig(filePath) {
|
|
4606
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
4607
|
+
const base = (parts.pop() || "").split(".")[0];
|
|
4608
|
+
const dir = parts.slice(-2).join("/");
|
|
4609
|
+
return `${dir}/${base}`.toLowerCase();
|
|
4610
|
+
}
|
|
4611
|
+
async function recordTestOutcome(changedFiles, failedTests) {
|
|
4612
|
+
try {
|
|
4613
|
+
await ensureTestHistorySchema();
|
|
4614
|
+
const db = await getDb();
|
|
4615
|
+
let recorded = 0;
|
|
4616
|
+
for (const f of changedFiles.slice(0, 10)) {
|
|
4617
|
+
const sig = fileSig(f);
|
|
4618
|
+
for (const t of failedTests.slice(0, 10)) {
|
|
4619
|
+
const find = db.prepare(`SELECT id, failed, flaky, runs FROM test_history WHERE file_sig = ? AND test_path = ?`);
|
|
4620
|
+
find.bind([sig, t]);
|
|
4621
|
+
if (find.step()) {
|
|
4622
|
+
const row = find.getAsObject();
|
|
4623
|
+
db.run(`UPDATE test_history SET failed = 1, runs = runs + 1, updated_at = strftime('%s','now') WHERE id = ?`, [row.id]);
|
|
4624
|
+
find.free();
|
|
4625
|
+
} else {
|
|
4626
|
+
find.free();
|
|
4627
|
+
db.run(`INSERT INTO test_history (file_sig, test_path, failed) VALUES (?, ?, 1)`, [sig, t]);
|
|
4628
|
+
}
|
|
4629
|
+
recorded++;
|
|
4630
|
+
}
|
|
4631
|
+
if (failedTests.length === 0) {
|
|
4632
|
+
db.run(
|
|
4633
|
+
`INSERT INTO test_history (file_sig, test_path, failed) VALUES (?, ?, 0)
|
|
4634
|
+
ON CONFLICT DO NOTHING`,
|
|
4635
|
+
[sig, "__green__"]
|
|
4636
|
+
);
|
|
4637
|
+
recorded++;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
void recorded;
|
|
4641
|
+
saveDb();
|
|
4642
|
+
return { recorded };
|
|
4643
|
+
} catch {
|
|
4644
|
+
return { recorded: 0 };
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
async function markTestFlaky(changedFiles, testPath) {
|
|
4648
|
+
try {
|
|
4649
|
+
await ensureTestHistorySchema();
|
|
4650
|
+
const db = await getDb();
|
|
4651
|
+
for (const f of changedFiles.slice(0, 10)) {
|
|
4652
|
+
db.run(`UPDATE test_history SET flaky = 1 WHERE file_sig = ? AND test_path = ?`, [fileSig(f), testPath]);
|
|
4653
|
+
}
|
|
4654
|
+
saveDb();
|
|
4655
|
+
} catch {
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
async function rankTestsByHistory(changedFile, candidates) {
|
|
4659
|
+
let history = [];
|
|
4660
|
+
try {
|
|
4661
|
+
await ensureTestHistorySchema();
|
|
4662
|
+
const db = await getDb();
|
|
4663
|
+
const stmt = db.prepare(`SELECT test_path, failed, flaky, runs FROM test_history WHERE file_sig = ?`);
|
|
4664
|
+
stmt.bind([fileSig(changedFile)]);
|
|
4665
|
+
while (stmt.step()) history.push(stmt.getAsObject());
|
|
4666
|
+
stmt.free();
|
|
4667
|
+
} catch {
|
|
4668
|
+
}
|
|
4669
|
+
const byPath = new Map(history.map((h) => [h.test_path, h]));
|
|
4670
|
+
return candidates.map((c, i) => {
|
|
4671
|
+
const h = byPath.get(c);
|
|
4672
|
+
if (h && h.failed === 1 && h.flaky === 0) {
|
|
4673
|
+
return { path: c, score: 100 - i * 0.01 + Math.min(h.runs, 10), reason: `\u{1F525} failed for this area before (${h.runs}x)` };
|
|
4674
|
+
}
|
|
4675
|
+
if (h && h.flaky === 1) {
|
|
4676
|
+
return { path: c, score: -1, reason: "\u26AA flaky \u2014 down-weighted, run last" };
|
|
4677
|
+
}
|
|
4678
|
+
return { path: c, score: 10 - i * 0.01, reason: "static match" };
|
|
4679
|
+
}).sort((a, b) => b.score - a.score);
|
|
4680
|
+
}
|
|
4681
|
+
var init_testHistory = __esm({
|
|
4682
|
+
"src/engine/testHistory.ts"() {
|
|
4683
|
+
"use strict";
|
|
4684
|
+
init_kumaDb();
|
|
4685
|
+
}
|
|
4686
|
+
});
|
|
4687
|
+
|
|
4546
4688
|
// src/engine/impactAnalysis.ts
|
|
4547
4689
|
var impactAnalysis_exports = {};
|
|
4548
4690
|
__export(impactAnalysis_exports, {
|
|
4549
|
-
calculateBlastRadius: () => calculateBlastRadius
|
|
4691
|
+
calculateBlastRadius: () => calculateBlastRadius,
|
|
4692
|
+
classifyDiffShape: () => classifyDiffShape,
|
|
4693
|
+
getIndexerStatus: () => getIndexerStatus
|
|
4550
4694
|
});
|
|
4551
4695
|
import fs9 from "fs";
|
|
4552
4696
|
import path11 from "path";
|
|
4697
|
+
import { execSync as execSync4 } from "child_process";
|
|
4553
4698
|
import fastGlob3 from "fast-glob";
|
|
4699
|
+
function extOf(target) {
|
|
4700
|
+
const base = path11.basename(target);
|
|
4701
|
+
const dot = base.lastIndexOf(".");
|
|
4702
|
+
return dot > 0 ? base.slice(dot).toLowerCase() : "";
|
|
4703
|
+
}
|
|
4704
|
+
function isExampleOrDocName(target) {
|
|
4705
|
+
const base = path11.basename(target).toLowerCase();
|
|
4706
|
+
return base.endsWith(".example") || base.startsWith(".env") || base.endsWith(".md") || base.endsWith(".txt") || base.endsWith(".rst") || base === "license" || base.startsWith("license.");
|
|
4707
|
+
}
|
|
4708
|
+
function classifyDiffShape(target, root) {
|
|
4709
|
+
if (isExampleOrDocName(target)) {
|
|
4710
|
+
return { shape: "config-or-docs", detail: "non-runtime file (docs/config/example)" };
|
|
4711
|
+
}
|
|
4712
|
+
const ext = extOf(target);
|
|
4713
|
+
if (ext && !RUNTIME_CODE_EXTS.has(ext)) {
|
|
4714
|
+
return { shape: "config-or-docs", detail: `non-runtime extension (${ext})` };
|
|
4715
|
+
}
|
|
4716
|
+
try {
|
|
4717
|
+
const cwd = root || getProjectRoot();
|
|
4718
|
+
const diff = execSync4(`git diff --unified=0 -- "${target}"`, {
|
|
4719
|
+
cwd,
|
|
4720
|
+
encoding: "utf-8",
|
|
4721
|
+
timeout: 4e3,
|
|
4722
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4723
|
+
}).trim();
|
|
4724
|
+
if (!diff) return { shape: "runtime-code", detail: "no working-tree diff; analyzed by file kind" };
|
|
4725
|
+
const prefixes = COMMENT_PREFIXES[ext] || ["//", "#", "*", "/*", "--"];
|
|
4726
|
+
const changed = diff.split("\n").filter((l) => (l.startsWith("+") || l.startsWith("-")) && !l.startsWith("+++") && !l.startsWith("---"));
|
|
4727
|
+
if (changed.length === 0) return { shape: "runtime-code", detail: "no working-tree diff; analyzed by file kind" };
|
|
4728
|
+
const allTrivial = changed.every((l) => {
|
|
4729
|
+
const body = l.slice(1).trim();
|
|
4730
|
+
return body === "" || prefixes.some((p) => body.startsWith(p));
|
|
4731
|
+
});
|
|
4732
|
+
if (allTrivial) return { shape: "comment-only", detail: `${changed.length} changed line(s), all comments/blank` };
|
|
4733
|
+
} catch {
|
|
4734
|
+
}
|
|
4735
|
+
return { shape: "runtime-code", detail: "runtime code change" };
|
|
4736
|
+
}
|
|
4554
4737
|
async function findImportingFiles(targetFile, root) {
|
|
4555
4738
|
const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
|
|
4556
4739
|
try {
|
|
@@ -4587,7 +4770,7 @@ async function findImportingFiles(targetFile, root) {
|
|
|
4587
4770
|
}
|
|
4588
4771
|
callStmt.free();
|
|
4589
4772
|
if (dependents.length > 0) {
|
|
4590
|
-
return dependents;
|
|
4773
|
+
return { dependents, evidence: dependents.map((d) => `${d} (graph edge)`) };
|
|
4591
4774
|
}
|
|
4592
4775
|
} catch {
|
|
4593
4776
|
}
|
|
@@ -4603,6 +4786,7 @@ async function findImportingFiles(targetFile, root) {
|
|
|
4603
4786
|
}
|
|
4604
4787
|
);
|
|
4605
4788
|
const dependents = [];
|
|
4789
|
+
const evidence = [];
|
|
4606
4790
|
const importRegex = new RegExp(
|
|
4607
4791
|
`(?:import|from|require\\s*\\(|export\\s+.*from)\\s*['"][^'"]*\\b${baseName}(?:\\.[a-zA-Z0-9]+)?['"]`,
|
|
4608
4792
|
"i"
|
|
@@ -4612,18 +4796,21 @@ async function findImportingFiles(targetFile, root) {
|
|
|
4612
4796
|
const fullPath = path11.join(root, f);
|
|
4613
4797
|
try {
|
|
4614
4798
|
const content = fs9.readFileSync(fullPath, "utf-8");
|
|
4615
|
-
|
|
4799
|
+
const lines = content.split("\n");
|
|
4800
|
+
const hitIdx = lines.findIndex((l) => importRegex.test(l));
|
|
4801
|
+
if (hitIdx >= 0) {
|
|
4616
4802
|
dependents.push(f);
|
|
4803
|
+
evidence.push(`${f}:${hitIdx + 1} (import match)`);
|
|
4617
4804
|
}
|
|
4618
4805
|
} catch {
|
|
4619
4806
|
}
|
|
4620
4807
|
}
|
|
4621
|
-
return dependents;
|
|
4808
|
+
return { dependents, evidence };
|
|
4622
4809
|
} catch {
|
|
4623
|
-
return [];
|
|
4810
|
+
return { dependents: [], evidence: [] };
|
|
4624
4811
|
}
|
|
4625
4812
|
}
|
|
4626
|
-
async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = []) {
|
|
4813
|
+
async function findRelatedTests(targetFile, root, owningPackage, additionalDependents = [], exactOnly = false) {
|
|
4627
4814
|
const normTarget = normalizeScope(path11.normalize(targetFile)) || path11.normalize(targetFile);
|
|
4628
4815
|
try {
|
|
4629
4816
|
const db = await getDb();
|
|
@@ -4652,14 +4839,10 @@ async function findRelatedTests(targetFile, root, owningPackage, additionalDepen
|
|
|
4652
4839
|
const testFiles = [];
|
|
4653
4840
|
const stems = /* @__PURE__ */ new Set();
|
|
4654
4841
|
if (baseName) stems.add(baseName);
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
const depBase = path11.basename(dep, path11.extname(dep));
|
|
4660
|
-
if (depBase) stems.add(depBase);
|
|
4661
|
-
for (const part of depBase.split(/[-_.\s]+/).filter((p) => p.length >= 3)) {
|
|
4662
|
-
stems.add(part);
|
|
4842
|
+
if (!exactOnly) {
|
|
4843
|
+
for (const dep of additionalDependents.slice(0, 3)) {
|
|
4844
|
+
const depBase = path11.basename(dep, path11.extname(dep));
|
|
4845
|
+
if (depBase) stems.add(depBase);
|
|
4663
4846
|
}
|
|
4664
4847
|
}
|
|
4665
4848
|
const testPatterns = [];
|
|
@@ -4748,6 +4931,22 @@ function assessRisk(target, targetType, directDependents, downstreamPackages, af
|
|
|
4748
4931
|
}
|
|
4749
4932
|
return { risk, riskFlags };
|
|
4750
4933
|
}
|
|
4934
|
+
async function getIndexerStatus() {
|
|
4935
|
+
let graphEdges = false;
|
|
4936
|
+
try {
|
|
4937
|
+
const db = await getDb();
|
|
4938
|
+
const stmt = db.prepare(`SELECT COUNT(*) as cnt FROM edges WHERE type IN ('imports','depends_on','calls','tests')`);
|
|
4939
|
+
if (stmt.step()) graphEdges = Number(stmt.getAsObject().cnt) > 0;
|
|
4940
|
+
stmt.free();
|
|
4941
|
+
} catch {
|
|
4942
|
+
}
|
|
4943
|
+
return {
|
|
4944
|
+
tier: graphEdges ? 1 : 2,
|
|
4945
|
+
graphEdges,
|
|
4946
|
+
languages: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "rs"],
|
|
4947
|
+
note: graphEdges ? "Tier 1: graph edges grounded the verdict (Tier 2 ripgrep fallback available)" : "Tier 2: ripgrep import-regex fallback (no graph edges; run a scan to reach Tier 1; LSP indexers are future work per #35)"
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4751
4950
|
async function calculateBlastRadius(target, options = {}) {
|
|
4752
4951
|
const root = options.root || getProjectRoot();
|
|
4753
4952
|
const wsInfo = await getWorkspaceInfo(root);
|
|
@@ -4772,66 +4971,51 @@ async function calculateBlastRadius(target, options = {}) {
|
|
|
4772
4971
|
downstreamPackages.push(...rev.map((p) => p.name));
|
|
4773
4972
|
}
|
|
4774
4973
|
let directDependents = [];
|
|
4974
|
+
let evidence = [];
|
|
4775
4975
|
if (targetType === "file" || targetType === "symbol") {
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
const
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
if (fp && !directDependents.includes(fp) && fp !== target) {
|
|
4792
|
-
directDependents.push(fp);
|
|
4793
|
-
}
|
|
4794
|
-
}
|
|
4795
|
-
stmt.free();
|
|
4796
|
-
}
|
|
4797
|
-
const authMiddlewareMatches = await fastGlob3(
|
|
4798
|
-
[
|
|
4799
|
-
"**/*auth-middleware*.*",
|
|
4800
|
-
"**/*auth_middleware*.*",
|
|
4801
|
-
"**/*auth.middleware*.*",
|
|
4802
|
-
"**/middleware/*auth*.*",
|
|
4803
|
-
"**/middlewares/*auth*.*",
|
|
4804
|
-
"**/guards/*auth*.*",
|
|
4805
|
-
"**/security/*auth*.*",
|
|
4806
|
-
"**/lib/*auth*.*"
|
|
4807
|
-
],
|
|
4808
|
-
{
|
|
4809
|
-
cwd: root,
|
|
4810
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kuma/**"],
|
|
4811
|
-
onlyFiles: true
|
|
4812
|
-
}
|
|
4813
|
-
);
|
|
4814
|
-
for (const am of authMiddlewareMatches) {
|
|
4815
|
-
if (!directDependents.includes(am) && am !== target) {
|
|
4816
|
-
directDependents.push(am);
|
|
4817
|
-
}
|
|
4818
|
-
}
|
|
4819
|
-
} catch {
|
|
4976
|
+
const found = await findImportingFiles(target, root);
|
|
4977
|
+
directDependents = found.dependents;
|
|
4978
|
+
evidence = found.evidence;
|
|
4979
|
+
}
|
|
4980
|
+
const { shape: diffShape, detail: shapeDetail } = classifyDiffShape(target, root);
|
|
4981
|
+
const nonRuntime = diffShape !== "runtime-code";
|
|
4982
|
+
const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents, nonRuntime);
|
|
4983
|
+
let testReasons = /* @__PURE__ */ new Map();
|
|
4984
|
+
try {
|
|
4985
|
+
const { rankTestsByHistory: rankTestsByHistory2 } = await Promise.resolve().then(() => (init_testHistory(), testHistory_exports));
|
|
4986
|
+
const ranked = await rankTestsByHistory2(target, affectedTests);
|
|
4987
|
+
affectedTests.length = 0;
|
|
4988
|
+
for (const r of ranked) {
|
|
4989
|
+
affectedTests.push(r.path);
|
|
4990
|
+
if (r.reason !== "static match") testReasons.set(r.path, r.reason);
|
|
4820
4991
|
}
|
|
4992
|
+
} catch {
|
|
4993
|
+
}
|
|
4994
|
+
let risk;
|
|
4995
|
+
let riskFlags;
|
|
4996
|
+
if (nonRuntime) {
|
|
4997
|
+
risk = "low";
|
|
4998
|
+
riskFlags = [
|
|
4999
|
+
`${diffShape === "comment-only" ? "Comment-only change" : "Non-runtime file"} (${shapeDetail}) \u2014 risk capped at LOW`,
|
|
5000
|
+
...directDependents.length > 0 || affectedTests.length > 0 ? [`Grounded refs kept for reference: ${directDependents.length} importer(s), ${affectedTests.length} test(s)`] : []
|
|
5001
|
+
];
|
|
5002
|
+
directDependents = [];
|
|
5003
|
+
affectedTests.length = 0;
|
|
5004
|
+
} else {
|
|
5005
|
+
({ risk, riskFlags } = assessRisk(
|
|
5006
|
+
target,
|
|
5007
|
+
targetType,
|
|
5008
|
+
directDependents,
|
|
5009
|
+
downstreamPackages,
|
|
5010
|
+
affectedTests,
|
|
5011
|
+
owningPackage
|
|
5012
|
+
));
|
|
4821
5013
|
}
|
|
4822
|
-
const affectedTests = await findRelatedTests(target, root, owningPackage, directDependents);
|
|
4823
|
-
const { risk, riskFlags } = assessRisk(
|
|
4824
|
-
target,
|
|
4825
|
-
targetType,
|
|
4826
|
-
directDependents,
|
|
4827
|
-
downstreamPackages,
|
|
4828
|
-
affectedTests,
|
|
4829
|
-
owningPackage
|
|
4830
|
-
);
|
|
4831
5014
|
const summaryLines = [
|
|
4832
5015
|
`\u{1F3AF} **Impact / Blast Radius: \`${target}\`**`,
|
|
4833
5016
|
`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
|
|
4834
5017
|
`\u26A0\uFE0F **Risk Level: ${risk.toUpperCase()}**`,
|
|
5018
|
+
`\u{1F4D0} Diff shape: \`${diffShape}\` (${shapeDetail})`,
|
|
4835
5019
|
owningPackage ? `\u{1F4E6} Owning package: \`${owningPackage.name}\` (${owningPackage.path})` : ""
|
|
4836
5020
|
];
|
|
4837
5021
|
if (riskFlags.length > 0) {
|
|
@@ -4858,11 +5042,22 @@ async function calculateBlastRadius(target, options = {}) {
|
|
|
4858
5042
|
summaryLines.push(` ... and ${directDependents.length - 6} more`);
|
|
4859
5043
|
}
|
|
4860
5044
|
}
|
|
5045
|
+
if (evidence.length > 0) {
|
|
5046
|
+
summaryLines.push("");
|
|
5047
|
+
summaryLines.push(`\u{1F50E} **Evidence (${evidence.length}):**`);
|
|
5048
|
+
for (const e of evidence.slice(0, 3)) {
|
|
5049
|
+
summaryLines.push(` \u2022 \`${e}\``);
|
|
5050
|
+
}
|
|
5051
|
+
if (evidence.length > 3) {
|
|
5052
|
+
summaryLines.push(` ... and ${evidence.length - 3} more`);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
4861
5055
|
if (affectedTests.length > 0) {
|
|
4862
5056
|
summaryLines.push("");
|
|
4863
5057
|
summaryLines.push(`\u{1F9EA} **Affected Test Suites (${affectedTests.length}):**`);
|
|
4864
5058
|
for (const t of affectedTests.slice(0, 5)) {
|
|
4865
|
-
|
|
5059
|
+
const why = testReasons.get(t) ? ` \u2014 ${testReasons.get(t)}` : "";
|
|
5060
|
+
summaryLines.push(` \u2022 \`${t}\`${why}`);
|
|
4866
5061
|
}
|
|
4867
5062
|
if (affectedTests.length > 5) {
|
|
4868
5063
|
summaryLines.push(` ... and ${affectedTests.length - 5} more`);
|
|
@@ -4881,15 +5076,52 @@ async function calculateBlastRadius(target, options = {}) {
|
|
|
4881
5076
|
risk,
|
|
4882
5077
|
riskFlags,
|
|
4883
5078
|
summary: summaryLines.filter(Boolean).join("\n"),
|
|
4884
|
-
confidence: 0.85
|
|
5079
|
+
confidence: nonRuntime ? 0.95 : 0.85,
|
|
5080
|
+
diffShape,
|
|
5081
|
+
evidence
|
|
4885
5082
|
};
|
|
4886
5083
|
}
|
|
5084
|
+
var RUNTIME_CODE_EXTS, COMMENT_PREFIXES;
|
|
4887
5085
|
var init_impactAnalysis = __esm({
|
|
4888
5086
|
"src/engine/impactAnalysis.ts"() {
|
|
4889
5087
|
"use strict";
|
|
4890
5088
|
init_pathValidator();
|
|
4891
5089
|
init_kumaDb();
|
|
4892
5090
|
init_workspaceIntelligence();
|
|
5091
|
+
RUNTIME_CODE_EXTS = /* @__PURE__ */ new Set([
|
|
5092
|
+
".ts",
|
|
5093
|
+
".tsx",
|
|
5094
|
+
".js",
|
|
5095
|
+
".jsx",
|
|
5096
|
+
".mjs",
|
|
5097
|
+
".cjs",
|
|
5098
|
+
".py",
|
|
5099
|
+
".go",
|
|
5100
|
+
".rs",
|
|
5101
|
+
".java",
|
|
5102
|
+
".rb",
|
|
5103
|
+
".php",
|
|
5104
|
+
".swift",
|
|
5105
|
+
".kt",
|
|
5106
|
+
".cs",
|
|
5107
|
+
".scala",
|
|
5108
|
+
".vue",
|
|
5109
|
+
".svelte"
|
|
5110
|
+
]);
|
|
5111
|
+
COMMENT_PREFIXES = {
|
|
5112
|
+
".ts": ["//", "*", "/*"],
|
|
5113
|
+
".tsx": ["//", "*", "/*"],
|
|
5114
|
+
".js": ["//", "*", "/*"],
|
|
5115
|
+
".jsx": ["//", "*", "/*"],
|
|
5116
|
+
".mjs": ["//", "*", "/*"],
|
|
5117
|
+
".cjs": ["//", "*", "/*"],
|
|
5118
|
+
".py": ["#"],
|
|
5119
|
+
".go": ["//"],
|
|
5120
|
+
".rs": ["//"],
|
|
5121
|
+
".java": ["//", "*", "/*"],
|
|
5122
|
+
".rb": ["#"],
|
|
5123
|
+
".php": ["//", "#", "*"]
|
|
5124
|
+
};
|
|
4893
5125
|
}
|
|
4894
5126
|
});
|
|
4895
5127
|
|
|
@@ -5963,27 +6195,27 @@ async function codebaseSearchFallback(query, limit = 20) {
|
|
|
5963
6195
|
if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
|
|
5964
6196
|
let entries = [];
|
|
5965
6197
|
try {
|
|
5966
|
-
entries =
|
|
6198
|
+
entries = fs32.readdirSync(dir);
|
|
5967
6199
|
} catch {
|
|
5968
6200
|
return;
|
|
5969
6201
|
}
|
|
5970
6202
|
for (const entry of entries) {
|
|
5971
6203
|
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
|
|
5972
|
-
const full =
|
|
6204
|
+
const full = path35.join(dir, entry);
|
|
5973
6205
|
try {
|
|
5974
|
-
const stat =
|
|
6206
|
+
const stat = fs32.statSync(full);
|
|
5975
6207
|
if (stat.isDirectory()) {
|
|
5976
6208
|
walk2(full, depth + 1);
|
|
5977
6209
|
} else if (entry.toLowerCase().includes(query.toLowerCase())) {
|
|
5978
|
-
results.push(
|
|
6210
|
+
results.push(path35.relative(root, full));
|
|
5979
6211
|
}
|
|
5980
6212
|
} catch {
|
|
5981
6213
|
}
|
|
5982
6214
|
}
|
|
5983
6215
|
};
|
|
5984
6216
|
var walk = walk2;
|
|
5985
|
-
const
|
|
5986
|
-
const
|
|
6217
|
+
const fs32 = await import("fs");
|
|
6218
|
+
const path35 = await import("path");
|
|
5987
6219
|
const root = process.cwd();
|
|
5988
6220
|
const results = [];
|
|
5989
6221
|
const MAX_WALK_DEPTH = 8;
|
|
@@ -6116,9 +6348,9 @@ ${summary}`;
|
|
|
6116
6348
|
}
|
|
6117
6349
|
async function codebaseImpactFallback(target) {
|
|
6118
6350
|
try {
|
|
6119
|
-
const { execSync:
|
|
6351
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
6120
6352
|
const root = process.cwd();
|
|
6121
|
-
const grepResult =
|
|
6353
|
+
const grepResult = execSync9(
|
|
6122
6354
|
`grep -rn ${grepIncludeFlags()} --include="*.json" --include="*.md" -l "${target.replace(/"/g, '\\"')}" . 2>/dev/null | grep -v node_modules | grep -v .git | grep -v dist | grep -v .kuma | head -50`,
|
|
6123
6355
|
{ cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
|
|
6124
6356
|
).trim();
|
|
@@ -6245,7 +6477,28 @@ var init_kumaGraph = __esm({
|
|
|
6245
6477
|
});
|
|
6246
6478
|
|
|
6247
6479
|
// src/utils/kumaShared.ts
|
|
6248
|
-
|
|
6480
|
+
var kumaShared_exports = {};
|
|
6481
|
+
__export(kumaShared_exports, {
|
|
6482
|
+
buildDriftMessages: () => buildDriftMessages,
|
|
6483
|
+
checkLadderViolations: () => checkLadderViolations,
|
|
6484
|
+
countEditCalls: () => countEditCalls,
|
|
6485
|
+
detectTestWeakening: () => detectTestWeakening,
|
|
6486
|
+
getBranchCommittedFiles: () => getBranchCommittedFiles,
|
|
6487
|
+
getGitDiffStat: () => getGitDiffStat,
|
|
6488
|
+
getPrioritySuggestion: () => getPrioritySuggestion,
|
|
6489
|
+
getSessionStats: () => getSessionStats,
|
|
6490
|
+
getUncommittedFiles: () => getUncommittedFiles,
|
|
6491
|
+
getUnresolvedCount: () => getUnresolvedCount,
|
|
6492
|
+
getUnresolvedDetails: () => getUnresolvedDetails,
|
|
6493
|
+
hasVerificationEvidence: () => hasVerificationEvidence,
|
|
6494
|
+
isBashTool: () => isBashTool,
|
|
6495
|
+
isEditTool: () => isEditTool,
|
|
6496
|
+
isReadTool: () => isReadTool,
|
|
6497
|
+
isTestTool: () => isTestTool
|
|
6498
|
+
});
|
|
6499
|
+
import { execSync as execSync5 } from "child_process";
|
|
6500
|
+
import fs10 from "fs";
|
|
6501
|
+
import path12 from "path";
|
|
6249
6502
|
function isEditTool(toolName) {
|
|
6250
6503
|
const n = (toolName || "").toLowerCase();
|
|
6251
6504
|
return n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("apply") || n.includes("replace");
|
|
@@ -6283,7 +6536,7 @@ function getSessionStats(inputGoal) {
|
|
|
6283
6536
|
function getGitDiffStat(timeout = 3e3) {
|
|
6284
6537
|
try {
|
|
6285
6538
|
const root = getProjectRoot();
|
|
6286
|
-
return
|
|
6539
|
+
return execSync5("git diff --stat", {
|
|
6287
6540
|
cwd: root,
|
|
6288
6541
|
encoding: "utf-8",
|
|
6289
6542
|
timeout
|
|
@@ -6292,6 +6545,83 @@ function getGitDiffStat(timeout = 3e3) {
|
|
|
6292
6545
|
return "";
|
|
6293
6546
|
}
|
|
6294
6547
|
}
|
|
6548
|
+
function getUncommittedFiles(timeout = 3e3) {
|
|
6549
|
+
const files = /* @__PURE__ */ new Set();
|
|
6550
|
+
try {
|
|
6551
|
+
const root = getProjectRoot();
|
|
6552
|
+
const porcelain = execSync5("git status --porcelain", {
|
|
6553
|
+
cwd: root,
|
|
6554
|
+
encoding: "utf-8",
|
|
6555
|
+
timeout,
|
|
6556
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6557
|
+
}).trim();
|
|
6558
|
+
for (const line of porcelain.split("\n")) {
|
|
6559
|
+
const m = line.match(/^.{3}(.+?)(?:\s+->\s+.+)?$/);
|
|
6560
|
+
if (m && m[1]) files.add(m[1].trim().replace(/^"|"$/g, ""));
|
|
6561
|
+
}
|
|
6562
|
+
} catch {
|
|
6563
|
+
}
|
|
6564
|
+
return files;
|
|
6565
|
+
}
|
|
6566
|
+
function getBranchCommittedFiles(timeout = 5e3) {
|
|
6567
|
+
const files = /* @__PURE__ */ new Set();
|
|
6568
|
+
let base = "";
|
|
6569
|
+
try {
|
|
6570
|
+
const root = getProjectRoot();
|
|
6571
|
+
const branch = execSync5("git rev-parse --abbrev-ref HEAD", {
|
|
6572
|
+
cwd: root,
|
|
6573
|
+
encoding: "utf-8",
|
|
6574
|
+
timeout,
|
|
6575
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6576
|
+
}).trim();
|
|
6577
|
+
const baseRef = execSync5(
|
|
6578
|
+
`git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo origin/${branch === "main" || branch === "master" ? "HEAD" : "main"}`,
|
|
6579
|
+
{ cwd: root, encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
|
|
6580
|
+
).trim().replace("refs/remotes/", "");
|
|
6581
|
+
const mergeBase = execSync5(`git merge-base HEAD ${baseRef} 2>/dev/null || git rev-list --max-parents=0 HEAD`, {
|
|
6582
|
+
cwd: root,
|
|
6583
|
+
encoding: "utf-8",
|
|
6584
|
+
timeout,
|
|
6585
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6586
|
+
}).trim().split("\n")[0];
|
|
6587
|
+
base = baseRef;
|
|
6588
|
+
if (mergeBase) {
|
|
6589
|
+
const out = execSync5(`git diff --name-only ${mergeBase} HEAD`, {
|
|
6590
|
+
cwd: root,
|
|
6591
|
+
encoding: "utf-8",
|
|
6592
|
+
timeout,
|
|
6593
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6594
|
+
}).trim();
|
|
6595
|
+
for (const f of out.split("\n")) if (f.trim()) files.add(f.trim());
|
|
6596
|
+
}
|
|
6597
|
+
} catch {
|
|
6598
|
+
}
|
|
6599
|
+
return { base, files };
|
|
6600
|
+
}
|
|
6601
|
+
function hasVerificationEvidence(toolCalls) {
|
|
6602
|
+
if (toolCalls.some((c) => isTestTool(c.toolName))) return true;
|
|
6603
|
+
return hasVerificationInHookLog();
|
|
6604
|
+
}
|
|
6605
|
+
function hasVerificationInHookLog(hours = 24) {
|
|
6606
|
+
try {
|
|
6607
|
+
const fp = path12.join(getProjectRoot(), ".kuma", "injections.jsonl");
|
|
6608
|
+
if (!fs10.existsSync(fp)) return false;
|
|
6609
|
+
const cutoff = Date.now() - hours * 60 * 60 * 1e3;
|
|
6610
|
+
const lines = fs10.readFileSync(fp, "utf-8").split("\n");
|
|
6611
|
+
for (const line of lines) {
|
|
6612
|
+
if (!line.trim()) continue;
|
|
6613
|
+
try {
|
|
6614
|
+
const entry = JSON.parse(line);
|
|
6615
|
+
if ((entry.at || 0) < cutoff) continue;
|
|
6616
|
+
const cmd = String(entry.command || "");
|
|
6617
|
+
if (cmd && TEST_COMMAND_PATTERN.test(cmd)) return true;
|
|
6618
|
+
} catch {
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
} catch {
|
|
6622
|
+
}
|
|
6623
|
+
return false;
|
|
6624
|
+
}
|
|
6295
6625
|
function getUnresolvedCount(failedFiles) {
|
|
6296
6626
|
let count = 0;
|
|
6297
6627
|
for (const f of failedFiles) {
|
|
@@ -6301,6 +6631,28 @@ function getUnresolvedCount(failedFiles) {
|
|
|
6301
6631
|
}
|
|
6302
6632
|
return count;
|
|
6303
6633
|
}
|
|
6634
|
+
function getUnresolvedDetails(failedFiles) {
|
|
6635
|
+
const result = [];
|
|
6636
|
+
for (const f of failedFiles) {
|
|
6637
|
+
for (const ff of f.failures) {
|
|
6638
|
+
if (!ff.resolved) {
|
|
6639
|
+
result.push({ task: f.task, error: ff.error.substring(0, 200) });
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
return result;
|
|
6644
|
+
}
|
|
6645
|
+
function checkLadderViolations(toolCalls, modifiedFiles, hasRunTests) {
|
|
6646
|
+
const violations = [];
|
|
6647
|
+
const editCalls = toolCalls.filter((c) => isEditTool(c.toolName)).length;
|
|
6648
|
+
if (editCalls > 5) {
|
|
6649
|
+
violations.push(`${editCalls} file ops in a row \u2014 consider if all are needed`);
|
|
6650
|
+
}
|
|
6651
|
+
if (modifiedFiles.length > 5 && !hasRunTests) {
|
|
6652
|
+
violations.push(`${modifiedFiles.length} files modified without verification`);
|
|
6653
|
+
}
|
|
6654
|
+
return violations;
|
|
6655
|
+
}
|
|
6304
6656
|
function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
|
|
6305
6657
|
const drifts = [];
|
|
6306
6658
|
if (modifiedFiles > 0 && !hasRunTests) {
|
|
@@ -6339,7 +6691,7 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
|
|
|
6339
6691
|
if (!goal) {
|
|
6340
6692
|
return "No goal set \u2014 use goal parameter or setGoal to track intent";
|
|
6341
6693
|
}
|
|
6342
|
-
if (modifiedFiles === 0) {
|
|
6694
|
+
if (modifiedFiles === 0 && !hasRunTests) {
|
|
6343
6695
|
return "Start by exploring what exists before writing code";
|
|
6344
6696
|
}
|
|
6345
6697
|
return "On track \u2014 continue with current approach";
|
|
@@ -6347,11 +6699,43 @@ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifie
|
|
|
6347
6699
|
function countEditCalls(toolCalls) {
|
|
6348
6700
|
return toolCalls.filter((c) => isEditTool(c.toolName)).length;
|
|
6349
6701
|
}
|
|
6702
|
+
function detectTestWeakening(files, root) {
|
|
6703
|
+
const weakened = [];
|
|
6704
|
+
let cwd = "";
|
|
6705
|
+
try {
|
|
6706
|
+
cwd = root || getProjectRoot();
|
|
6707
|
+
} catch {
|
|
6708
|
+
return [];
|
|
6709
|
+
}
|
|
6710
|
+
for (const f of files) {
|
|
6711
|
+
if (!TEST_FILE_PATTERN.test(f)) continue;
|
|
6712
|
+
try {
|
|
6713
|
+
const diff = execSync5(`git diff --unified=0 -- "${f}"`, {
|
|
6714
|
+
cwd,
|
|
6715
|
+
encoding: "utf-8",
|
|
6716
|
+
timeout: 4e3,
|
|
6717
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6718
|
+
}).trim();
|
|
6719
|
+
if (!diff) continue;
|
|
6720
|
+
const removed = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---"));
|
|
6721
|
+
const removedAssertions = removed.filter((l) => ASSERTION_PATTERN.test(l)).length;
|
|
6722
|
+
if (removedAssertions > 0 && removed.length > 0 && removedAssertions / removed.length >= 0.5) {
|
|
6723
|
+
weakened.push(`${f} (\u2212${removedAssertions} assertion(s))`);
|
|
6724
|
+
}
|
|
6725
|
+
} catch {
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
return weakened;
|
|
6729
|
+
}
|
|
6730
|
+
var TEST_COMMAND_PATTERN, TEST_FILE_PATTERN, ASSERTION_PATTERN;
|
|
6350
6731
|
var init_kumaShared = __esm({
|
|
6351
6732
|
"src/utils/kumaShared.ts"() {
|
|
6352
6733
|
"use strict";
|
|
6353
6734
|
init_sessionMemory();
|
|
6354
6735
|
init_pathValidator();
|
|
6736
|
+
TEST_COMMAND_PATTERN = /(npm|pnpm|yarn|bun)\s+(test|run\s+(test|typecheck|check|lint))|pytest|go\s+test|cargo\s+test|jest|vitest|typecheck|tsc\s+--noEmit/i;
|
|
6737
|
+
TEST_FILE_PATTERN = /(\.test\.|\.spec\.|_test\.|__tests__|\/tests?\/)/i;
|
|
6738
|
+
ASSERTION_PATTERN = /\b(expect|assert|toBe|toEqual|toContain|toMatch|ok\(|equal\(|deepEqual\()\b/;
|
|
6355
6739
|
}
|
|
6356
6740
|
});
|
|
6357
6741
|
|
|
@@ -6365,19 +6749,19 @@ __export(kumaMemory_exports, {
|
|
|
6365
6749
|
scoreMemoryRelevance: () => scoreMemoryRelevance,
|
|
6366
6750
|
shouldRecordDecision: () => shouldRecordDecision
|
|
6367
6751
|
});
|
|
6368
|
-
import
|
|
6369
|
-
import
|
|
6752
|
+
import fs11 from "fs";
|
|
6753
|
+
import path13 from "path";
|
|
6370
6754
|
function scoreMemoryRelevance(context, limit = 5) {
|
|
6371
6755
|
const results = [];
|
|
6372
|
-
const kumaDir =
|
|
6373
|
-
if (!
|
|
6756
|
+
const kumaDir = path13.join(getProjectRoot(), MEMORY_DIR);
|
|
6757
|
+
if (!fs11.existsSync(kumaDir)) return [];
|
|
6374
6758
|
const terms = Array.from(new Set(context.toLowerCase().split(/[\s/._-]+/).filter((w) => w.length > 2)));
|
|
6375
6759
|
if (terms.length === 0) return [];
|
|
6376
6760
|
try {
|
|
6377
|
-
const files =
|
|
6761
|
+
const files = fs11.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
|
|
6378
6762
|
for (const file of files) {
|
|
6379
6763
|
try {
|
|
6380
|
-
const content =
|
|
6764
|
+
const content = fs11.readFileSync(path13.join(kumaDir, file), "utf-8");
|
|
6381
6765
|
const lower = content.toLowerCase();
|
|
6382
6766
|
let matchCount = 0;
|
|
6383
6767
|
for (const term of terms) {
|
|
@@ -6548,13 +6932,86 @@ var init_kumaMemory = __esm({
|
|
|
6548
6932
|
}
|
|
6549
6933
|
});
|
|
6550
6934
|
|
|
6935
|
+
// src/engine/cacheFreshness.ts
|
|
6936
|
+
var cacheFreshness_exports = {};
|
|
6937
|
+
__export(cacheFreshness_exports, {
|
|
6938
|
+
MAP_TTL_MS: () => MAP_TTL_MS,
|
|
6939
|
+
RESEARCH_TTL_MS: () => RESEARCH_TTL_MS,
|
|
6940
|
+
clearDirty: () => clearDirty,
|
|
6941
|
+
formatAge: () => formatAge,
|
|
6942
|
+
getDirtyFiles: () => getDirtyFiles,
|
|
6943
|
+
isStale: () => isStale,
|
|
6944
|
+
markDirty: () => markDirty
|
|
6945
|
+
});
|
|
6946
|
+
import fs12 from "fs";
|
|
6947
|
+
import path14 from "path";
|
|
6948
|
+
function isStale(savedAtMs, ttlMs, nowMs = Date.now()) {
|
|
6949
|
+
if (!savedAtMs) return true;
|
|
6950
|
+
return nowMs - savedAtMs > ttlMs;
|
|
6951
|
+
}
|
|
6952
|
+
function formatAge(savedAtMs, nowMs = Date.now()) {
|
|
6953
|
+
const s = Math.max(0, Math.floor((nowMs - savedAtMs) / 1e3));
|
|
6954
|
+
if (s < 60) return `${s}s`;
|
|
6955
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
6956
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
6957
|
+
return `${Math.floor(s / 86400)}d`;
|
|
6958
|
+
}
|
|
6959
|
+
function dirtyPath() {
|
|
6960
|
+
return path14.join(getProjectRoot(), DIRTY_FILE);
|
|
6961
|
+
}
|
|
6962
|
+
function loadDirty() {
|
|
6963
|
+
try {
|
|
6964
|
+
const fp = dirtyPath();
|
|
6965
|
+
if (fs12.existsSync(fp)) {
|
|
6966
|
+
const parsed = JSON.parse(fs12.readFileSync(fp, "utf-8"));
|
|
6967
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
6968
|
+
}
|
|
6969
|
+
} catch {
|
|
6970
|
+
}
|
|
6971
|
+
return {};
|
|
6972
|
+
}
|
|
6973
|
+
function markDirty(filePath) {
|
|
6974
|
+
try {
|
|
6975
|
+
const fp = dirtyPath();
|
|
6976
|
+
const dir = path14.dirname(fp);
|
|
6977
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
6978
|
+
const dirty = loadDirty();
|
|
6979
|
+
dirty[filePath] = Date.now();
|
|
6980
|
+
const keys = Object.keys(dirty).sort((a, b) => dirty[b] - dirty[a]).slice(0, 200);
|
|
6981
|
+
const bounded = {};
|
|
6982
|
+
for (const k of keys) bounded[k] = dirty[k];
|
|
6983
|
+
fs12.writeFileSync(fp, JSON.stringify(bounded), "utf-8");
|
|
6984
|
+
} catch {
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
function getDirtyFiles() {
|
|
6988
|
+
return Object.keys(loadDirty());
|
|
6989
|
+
}
|
|
6990
|
+
function clearDirty() {
|
|
6991
|
+
try {
|
|
6992
|
+
const fp = dirtyPath();
|
|
6993
|
+
if (fs12.existsSync(fp)) fs12.rmSync(fp, { force: true });
|
|
6994
|
+
} catch {
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
var RESEARCH_TTL_MS, MAP_TTL_MS, DIRTY_FILE;
|
|
6998
|
+
var init_cacheFreshness = __esm({
|
|
6999
|
+
"src/engine/cacheFreshness.ts"() {
|
|
7000
|
+
"use strict";
|
|
7001
|
+
init_pathValidator();
|
|
7002
|
+
RESEARCH_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
7003
|
+
MAP_TTL_MS = 24 * 3600 * 1e3;
|
|
7004
|
+
DIRTY_FILE = ".kuma/dirty.json";
|
|
7005
|
+
}
|
|
7006
|
+
});
|
|
7007
|
+
|
|
6551
7008
|
// src/engine/codeReuseEngine.ts
|
|
6552
7009
|
var codeReuseEngine_exports = {};
|
|
6553
7010
|
__export(codeReuseEngine_exports, {
|
|
6554
7011
|
findReusableSymbols: () => findReusableSymbols
|
|
6555
7012
|
});
|
|
6556
|
-
import
|
|
6557
|
-
import
|
|
7013
|
+
import path15 from "path";
|
|
7014
|
+
import fs13 from "fs";
|
|
6558
7015
|
async function findReusableSymbols(query, scope) {
|
|
6559
7016
|
const cleanQuery = query.trim().toLowerCase();
|
|
6560
7017
|
const searchTokens = cleanQuery.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((t) => t.length >= 2);
|
|
@@ -6677,7 +7134,7 @@ function detectRecommendedUtilDirectory() {
|
|
|
6677
7134
|
"utils"
|
|
6678
7135
|
];
|
|
6679
7136
|
for (const c of candidates) {
|
|
6680
|
-
if (
|
|
7137
|
+
if (fs13.existsSync(path15.resolve(root, c))) {
|
|
6681
7138
|
return c;
|
|
6682
7139
|
}
|
|
6683
7140
|
}
|
|
@@ -6696,7 +7153,7 @@ var domainClusterEngine_exports = {};
|
|
|
6696
7153
|
__export(domainClusterEngine_exports, {
|
|
6697
7154
|
buildSubsystemClusters: () => buildSubsystemClusters
|
|
6698
7155
|
});
|
|
6699
|
-
import
|
|
7156
|
+
import path16 from "path";
|
|
6700
7157
|
async function buildSubsystemClusters() {
|
|
6701
7158
|
const db = await getDb();
|
|
6702
7159
|
const fileStmt = db.prepare(`
|
|
@@ -6773,7 +7230,7 @@ async function buildSubsystemClusters() {
|
|
|
6773
7230
|
for (const [cKey, files] of clusterFileMap.entries()) {
|
|
6774
7231
|
for (const f of files) {
|
|
6775
7232
|
fileToCluster.set(f, cKey);
|
|
6776
|
-
fileToCluster.set(
|
|
7233
|
+
fileToCluster.set(path16.normalize(f), cKey);
|
|
6777
7234
|
}
|
|
6778
7235
|
}
|
|
6779
7236
|
const clusterInteractions = /* @__PURE__ */ new Map();
|
|
@@ -6781,8 +7238,8 @@ async function buildSubsystemClusters() {
|
|
|
6781
7238
|
const srcFile = extractFilePathFromNodeId(e.source);
|
|
6782
7239
|
const tgtFile = extractFilePathFromNodeId(e.target);
|
|
6783
7240
|
if (!srcFile || !tgtFile) continue;
|
|
6784
|
-
const srcCluster = fileToCluster.get(srcFile) || fileToCluster.get(
|
|
6785
|
-
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));
|
|
6786
7243
|
if (srcCluster && tgtCluster && srcCluster !== tgtCluster) {
|
|
6787
7244
|
const pairKey = `${srcCluster}->${tgtCluster}`;
|
|
6788
7245
|
const stats = clusterInteractions.get(pairKey) || { calls: 0, imports: 0 };
|
|
@@ -6804,13 +7261,13 @@ async function buildSubsystemClusters() {
|
|
|
6804
7261
|
const clusterId = nodeId("feature_domain", cKey);
|
|
6805
7262
|
const name = formatClusterName(cKey);
|
|
6806
7263
|
const role = inferSubsystemRole(cKey, files);
|
|
6807
|
-
const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(
|
|
7264
|
+
const entrypoints = files.filter((f) => /index|main|server|app|engine|root/i.test(path16.basename(f))).slice(0, 3);
|
|
6808
7265
|
if (entrypoints.length === 0 && files.length > 0) {
|
|
6809
7266
|
entrypoints.push(files[0]);
|
|
6810
7267
|
}
|
|
6811
7268
|
const allSyms = [];
|
|
6812
7269
|
for (const f of files) {
|
|
6813
|
-
const syms = fileSymbols.get(f) || fileSymbols.get(
|
|
7270
|
+
const syms = fileSymbols.get(f) || fileSymbols.get(path16.normalize(f)) || [];
|
|
6814
7271
|
for (const s of syms) {
|
|
6815
7272
|
if (!allSyms.includes(s)) allSyms.push(s);
|
|
6816
7273
|
}
|
|
@@ -6989,7 +7446,7 @@ function inferSubsystemRole(key, files) {
|
|
|
6989
7446
|
if (k.includes("util") || k.includes("common")) {
|
|
6990
7447
|
return "Shared path validation, configuration loading, and platform detection helpers.";
|
|
6991
7448
|
}
|
|
6992
|
-
return `Core subsystem handling ${files.slice(0, 3).map((f) =>
|
|
7449
|
+
return `Core subsystem handling ${files.slice(0, 3).map((f) => path16.basename(f)).join(", ")}.`;
|
|
6993
7450
|
}
|
|
6994
7451
|
function inferTypedVerb(sourceKey, targetKey, _stats) {
|
|
6995
7452
|
const src = sourceKey.toLowerCase();
|
|
@@ -7068,18 +7525,18 @@ __export(domainRules_exports, {
|
|
|
7068
7525
|
readLayer: () => readLayer,
|
|
7069
7526
|
writeLayer: () => writeLayer
|
|
7070
7527
|
});
|
|
7071
|
-
import
|
|
7072
|
-
import
|
|
7528
|
+
import fs14 from "fs";
|
|
7529
|
+
import path17 from "path";
|
|
7073
7530
|
function layerFilePath(layer) {
|
|
7074
|
-
return
|
|
7531
|
+
return path17.join(getProjectRoot(), ".kuma", LAYER_FILES[layer].filename);
|
|
7075
7532
|
}
|
|
7076
7533
|
function ensureLayerFile(layer) {
|
|
7077
7534
|
const fp = layerFilePath(layer);
|
|
7078
|
-
if (!
|
|
7535
|
+
if (!fs14.existsSync(fp)) {
|
|
7079
7536
|
const template = layer === "arch_flow" ? ARCH_FLOW_TEMPLATE : GOTCHAS_TEMPLATE;
|
|
7080
|
-
const dir =
|
|
7081
|
-
if (!
|
|
7082
|
-
|
|
7537
|
+
const dir = path17.dirname(fp);
|
|
7538
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
7539
|
+
fs14.writeFileSync(fp, template, "utf-8");
|
|
7083
7540
|
console.error(`[DomainRules] Auto-created ${LAYER_FILES[layer].filename}`);
|
|
7084
7541
|
}
|
|
7085
7542
|
return fp;
|
|
@@ -7087,7 +7544,7 @@ function ensureLayerFile(layer) {
|
|
|
7087
7544
|
function readLayer(layer) {
|
|
7088
7545
|
try {
|
|
7089
7546
|
const fp = ensureLayerFile(layer);
|
|
7090
|
-
return
|
|
7547
|
+
return fs14.readFileSync(fp, "utf-8");
|
|
7091
7548
|
} catch (err) {
|
|
7092
7549
|
return `Error reading ${layer}: ${err}`;
|
|
7093
7550
|
}
|
|
@@ -7095,7 +7552,7 @@ function readLayer(layer) {
|
|
|
7095
7552
|
function writeLayer(layer, content) {
|
|
7096
7553
|
try {
|
|
7097
7554
|
const fp = ensureLayerFile(layer);
|
|
7098
|
-
|
|
7555
|
+
fs14.writeFileSync(fp, content, "utf-8");
|
|
7099
7556
|
const label = LAYER_FILES[layer].label;
|
|
7100
7557
|
sessionMemory.recordToolCall("kuma_memory", { action: `write_${layer}` });
|
|
7101
7558
|
return `\u2705 **${label}** updated successfully.`;
|
|
@@ -7106,9 +7563,9 @@ function writeLayer(layer, content) {
|
|
|
7106
7563
|
function appendToLayer(layer, entry) {
|
|
7107
7564
|
try {
|
|
7108
7565
|
const fp = ensureLayerFile(layer);
|
|
7109
|
-
const existing =
|
|
7566
|
+
const existing = fs14.readFileSync(fp, "utf-8");
|
|
7110
7567
|
const newContent = existing.trimEnd() + "\n\n" + entry.trim() + "\n";
|
|
7111
|
-
|
|
7568
|
+
fs14.writeFileSync(fp, newContent, "utf-8");
|
|
7112
7569
|
return `\u2705 Entry added to **${LAYER_FILES[layer].label}**.`;
|
|
7113
7570
|
} catch (err) {
|
|
7114
7571
|
return `\u274C Failed to append: ${err}`;
|
|
@@ -7118,7 +7575,7 @@ function getActiveGotchas() {
|
|
|
7118
7575
|
const results = [];
|
|
7119
7576
|
try {
|
|
7120
7577
|
const fp = ensureLayerFile("gotcha");
|
|
7121
|
-
const content =
|
|
7578
|
+
const content = fs14.readFileSync(fp, "utf-8");
|
|
7122
7579
|
const lines = content.split("\n");
|
|
7123
7580
|
let currentSection = "";
|
|
7124
7581
|
let currentFilePath = "";
|
|
@@ -7154,9 +7611,9 @@ function getActiveGotchas() {
|
|
|
7154
7611
|
function checkFileGotchas(filePath) {
|
|
7155
7612
|
const gotchas = getActiveGotchas();
|
|
7156
7613
|
const warnings = [];
|
|
7157
|
-
const base =
|
|
7614
|
+
const base = path17.basename(filePath);
|
|
7158
7615
|
for (const g of gotchas) {
|
|
7159
|
-
const gBase =
|
|
7616
|
+
const gBase = path17.basename(g.filePath);
|
|
7160
7617
|
if (filePath.includes(g.filePath) || g.filePath.includes(filePath) || base && gBase && base === gBase) {
|
|
7161
7618
|
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
7162
7619
|
warnings.push(`${icon} **Gotcha**: ${g.description} (${g.severity})`);
|
|
@@ -7256,8 +7713,8 @@ __export(kumaInject_exports, {
|
|
|
7256
7713
|
parseHookInput: () => parseHookInput,
|
|
7257
7714
|
trackFileEditLoop: () => trackFileEditLoop
|
|
7258
7715
|
});
|
|
7259
|
-
import
|
|
7260
|
-
import
|
|
7716
|
+
import fs15 from "fs";
|
|
7717
|
+
import path18 from "path";
|
|
7261
7718
|
function hashFileContent(filePath) {
|
|
7262
7719
|
return hashFile(filePath);
|
|
7263
7720
|
}
|
|
@@ -7268,16 +7725,17 @@ function isGotchaFresh(storedHash, currentHash) {
|
|
|
7268
7725
|
}
|
|
7269
7726
|
async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false) {
|
|
7270
7727
|
const root = getProjectRoot();
|
|
7271
|
-
const base =
|
|
7728
|
+
const base = path18.basename(filePath);
|
|
7272
7729
|
try {
|
|
7273
|
-
const dbFile =
|
|
7274
|
-
if (
|
|
7730
|
+
const dbFile = path18.join(root, ".kuma", "kuma.db");
|
|
7731
|
+
if (fs15.existsSync(dbFile)) {
|
|
7275
7732
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
7276
7733
|
const db = await getDb2();
|
|
7277
7734
|
const stmt = db.prepare(`
|
|
7278
7735
|
SELECT id, file_path, description, severity, workaround, content_hash
|
|
7279
7736
|
FROM known_gotchas
|
|
7280
7737
|
WHERE status IN ('active', 'verified')
|
|
7738
|
+
AND (quarantined IS NULL OR quarantined = 0)
|
|
7281
7739
|
AND (
|
|
7282
7740
|
file_path = ?
|
|
7283
7741
|
OR file_path LIKE ?
|
|
@@ -7311,11 +7769,11 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
|
|
|
7311
7769
|
} catch {
|
|
7312
7770
|
}
|
|
7313
7771
|
try {
|
|
7314
|
-
if (!
|
|
7772
|
+
if (!fs15.existsSync(path18.join(root, ".kuma"))) return [];
|
|
7315
7773
|
const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
|
|
7316
7774
|
const allGotchas = getActiveGotchas2();
|
|
7317
7775
|
const matched = allGotchas.filter(
|
|
7318
|
-
(g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base &&
|
|
7776
|
+
(g) => g.filePath.includes(filePath) || filePath.includes(g.filePath) || base && path18.basename(g.filePath) === base
|
|
7319
7777
|
);
|
|
7320
7778
|
if (matched.length > 0) {
|
|
7321
7779
|
return matched.slice(0, limit).map((g) => ({
|
|
@@ -7334,8 +7792,8 @@ async function getFreshGotchasForFile(filePath, limit = 5, includeStale = false)
|
|
|
7334
7792
|
async function getFileTrace(filePath, limit = 8) {
|
|
7335
7793
|
try {
|
|
7336
7794
|
const root = getProjectRoot();
|
|
7337
|
-
const dbFile =
|
|
7338
|
-
if (!
|
|
7795
|
+
const dbFile = path18.join(root, ".kuma", "kuma.db");
|
|
7796
|
+
if (!fs15.existsSync(dbFile)) return [];
|
|
7339
7797
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
7340
7798
|
const db = await getDb2();
|
|
7341
7799
|
const stmt = db.prepare(`
|
|
@@ -7345,7 +7803,7 @@ async function getFileTrace(filePath, limit = 8) {
|
|
|
7345
7803
|
ORDER BY cl.created_at DESC
|
|
7346
7804
|
LIMIT ?
|
|
7347
7805
|
`);
|
|
7348
|
-
stmt.bind([`%${
|
|
7806
|
+
stmt.bind([`%${path18.basename(filePath)}%`, limit]);
|
|
7349
7807
|
const rows = [];
|
|
7350
7808
|
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
7351
7809
|
stmt.free();
|
|
@@ -7363,7 +7821,7 @@ async function getFileTrace(filePath, limit = 8) {
|
|
|
7363
7821
|
function formatFileTrace(entries, filePath) {
|
|
7364
7822
|
if (entries.length === 0) return "";
|
|
7365
7823
|
const lines = [
|
|
7366
|
-
`\u{1F570}\uFE0F **Why is ${
|
|
7824
|
+
`\u{1F570}\uFE0F **Why is ${path18.basename(filePath)} written this way?** \u2014 ${entries.length} change(s) recorded:`
|
|
7367
7825
|
];
|
|
7368
7826
|
for (const e of entries) {
|
|
7369
7827
|
const date = new Date((e.createdAt || 0) * 1e3).toLocaleDateString("en-US", {
|
|
@@ -7379,12 +7837,12 @@ function formatFileTrace(entries, filePath) {
|
|
|
7379
7837
|
async function getDecisionsForFile(filePath, limit = 3) {
|
|
7380
7838
|
const decisions = [];
|
|
7381
7839
|
const root = getProjectRoot();
|
|
7382
|
-
const base =
|
|
7840
|
+
const base = path18.basename(filePath);
|
|
7383
7841
|
const baseWithoutExt = base.replace(/\.[^.]+$/, "");
|
|
7384
7842
|
try {
|
|
7385
|
-
const decisionsMdPath =
|
|
7386
|
-
if (
|
|
7387
|
-
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");
|
|
7388
7846
|
const sections = content.split(/^##\s+/m);
|
|
7389
7847
|
for (const section of sections) {
|
|
7390
7848
|
if (!section.trim() || section.startsWith("# ")) continue;
|
|
@@ -7455,7 +7913,17 @@ async function getDecisionsForFile(filePath, limit = 3) {
|
|
|
7455
7913
|
}
|
|
7456
7914
|
async function getRelevantContext(filePath, goal) {
|
|
7457
7915
|
const parts = [];
|
|
7458
|
-
const
|
|
7916
|
+
const { sessionMemory: sessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
|
|
7917
|
+
const gotchaPool = await getFreshGotchasForFile(filePath, 4);
|
|
7918
|
+
const gotchas = gotchaPool.filter((g) => {
|
|
7919
|
+
const key = `${g.filePath}::${g.description.substring(0, 60)}`;
|
|
7920
|
+
if (sessionMemory2.wasShown("gotcha", key)) return false;
|
|
7921
|
+
return true;
|
|
7922
|
+
});
|
|
7923
|
+
sessionMemory2.markShown(
|
|
7924
|
+
"gotcha",
|
|
7925
|
+
gotchaPool.map((g) => `${g.filePath}::${g.description.substring(0, 60)}`)
|
|
7926
|
+
);
|
|
7459
7927
|
if (gotchas.length > 0) {
|
|
7460
7928
|
const lines = ["\u26A0\uFE0F ACTIVE GOTCHAS (fresh):"];
|
|
7461
7929
|
for (const g of gotchas) {
|
|
@@ -7465,7 +7933,12 @@ async function getRelevantContext(filePath, goal) {
|
|
|
7465
7933
|
}
|
|
7466
7934
|
parts.push(lines.join("\n"));
|
|
7467
7935
|
}
|
|
7468
|
-
const
|
|
7936
|
+
const decisionPool = await getDecisionsForFile(filePath, goal ? 3 : 2);
|
|
7937
|
+
const decisions = decisionPool.filter((d) => {
|
|
7938
|
+
if (sessionMemory2.wasShown("decision", d.substring(0, 80))) return false;
|
|
7939
|
+
return true;
|
|
7940
|
+
});
|
|
7941
|
+
sessionMemory2.markShown("decision", decisionPool.map((d) => d.substring(0, 80)));
|
|
7469
7942
|
if (decisions.length > 0) {
|
|
7470
7943
|
parts.push("\u{1F4CC} RELEVANT DECISIONS:\n " + decisions.join("\n "));
|
|
7471
7944
|
}
|
|
@@ -7474,13 +7947,13 @@ async function getRelevantContext(filePath, goal) {
|
|
|
7474
7947
|
if (traceStr) parts.push(traceStr);
|
|
7475
7948
|
try {
|
|
7476
7949
|
const root = getProjectRoot();
|
|
7477
|
-
if (
|
|
7950
|
+
if (fs15.existsSync(path18.join(root, ".kuma", "kuma.db"))) {
|
|
7478
7951
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
7479
7952
|
const db = await getDb2();
|
|
7480
7953
|
const stmt = db.prepare(
|
|
7481
7954
|
`SELECT COUNT(*) FROM known_gotchas WHERE status = 'resolved' AND file_path LIKE ?`
|
|
7482
7955
|
);
|
|
7483
|
-
stmt.bind([`%${
|
|
7956
|
+
stmt.bind([`%${path18.basename(filePath)}%`]);
|
|
7484
7957
|
const found = stmt.step();
|
|
7485
7958
|
const count = found ? Number(stmt.getAsObject()["COUNT(*)"]) : 0;
|
|
7486
7959
|
stmt.free();
|
|
@@ -7491,7 +7964,7 @@ async function getRelevantContext(filePath, goal) {
|
|
|
7491
7964
|
} catch {
|
|
7492
7965
|
}
|
|
7493
7966
|
if (gotchas.length > 0) {
|
|
7494
|
-
const scopeHint =
|
|
7967
|
+
const scopeHint = path18.basename(path18.dirname(filePath)) || "current change";
|
|
7495
7968
|
parts.push(
|
|
7496
7969
|
`\u2705 AFTER editing, verify: kuma_safety({ action: "verify", scope: "${scopeHint}" })`
|
|
7497
7970
|
);
|
|
@@ -7507,11 +7980,11 @@ async function getRelevantContext(filePath, goal) {
|
|
|
7507
7980
|
async function getFileSummary2(filePath) {
|
|
7508
7981
|
try {
|
|
7509
7982
|
const root = getProjectRoot();
|
|
7510
|
-
const dbFile =
|
|
7511
|
-
if (!
|
|
7983
|
+
const dbFile = path18.join(root, ".kuma", "kuma.db");
|
|
7984
|
+
if (!fs15.existsSync(dbFile)) return null;
|
|
7512
7985
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
7513
7986
|
const db = await getDb2();
|
|
7514
|
-
const base =
|
|
7987
|
+
const base = path18.basename(filePath);
|
|
7515
7988
|
const stmt = db.prepare(`
|
|
7516
7989
|
SELECT
|
|
7517
7990
|
(SELECT COUNT(*) FROM known_gotchas WHERE status = 'active' AND (file_path = ? OR file_path LIKE ?)) as active_gotchas,
|
|
@@ -7603,9 +8076,9 @@ async function getCommandContext(command) {
|
|
|
7603
8076
|
}
|
|
7604
8077
|
function loadHookState(root) {
|
|
7605
8078
|
try {
|
|
7606
|
-
const fp =
|
|
7607
|
-
if (
|
|
7608
|
-
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"));
|
|
7609
8082
|
}
|
|
7610
8083
|
} catch {
|
|
7611
8084
|
}
|
|
@@ -7613,10 +8086,10 @@ function loadHookState(root) {
|
|
|
7613
8086
|
}
|
|
7614
8087
|
function saveHookState(root, state) {
|
|
7615
8088
|
try {
|
|
7616
|
-
const fp =
|
|
7617
|
-
const dir =
|
|
7618
|
-
if (!
|
|
7619
|
-
|
|
8089
|
+
const fp = path18.join(root, HOOK_STATE_FILE);
|
|
8090
|
+
const dir = path18.dirname(fp);
|
|
8091
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
8092
|
+
fs15.writeFileSync(fp, JSON.stringify(state), "utf-8");
|
|
7620
8093
|
} catch {
|
|
7621
8094
|
}
|
|
7622
8095
|
}
|
|
@@ -7650,7 +8123,7 @@ async function trackFileEditLoop(filePath) {
|
|
|
7650
8123
|
saveHookState(root, state);
|
|
7651
8124
|
if (recorded) {
|
|
7652
8125
|
try {
|
|
7653
|
-
if (!
|
|
8126
|
+
if (!fs15.existsSync(path18.join(root, ".kuma", "kuma.db"))) return entry.count;
|
|
7654
8127
|
const { addGotcha: addGotcha2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
|
|
7655
8128
|
await addGotcha2({
|
|
7656
8129
|
filePath,
|
|
@@ -7688,20 +8161,24 @@ __export(kumaGotchas_exports, {
|
|
|
7688
8161
|
addGotcha: () => addGotcha,
|
|
7689
8162
|
checkGotchasForFile: () => checkGotchasForFile,
|
|
7690
8163
|
deprecateGotcha: () => deprecateGotcha,
|
|
8164
|
+
findSimilarGotchaId: () => findSimilarGotchaId,
|
|
7691
8165
|
getActiveGotchasForCommand: () => getActiveGotchasForCommand,
|
|
7692
8166
|
getGotchasByStatus: () => getGotchasByStatus,
|
|
7693
8167
|
getInjectionStats: () => getInjectionStats,
|
|
8168
|
+
jaccardSimilarity: () => jaccardSimilarity,
|
|
7694
8169
|
listGotchas: () => listGotchas,
|
|
7695
8170
|
recordInjection: () => recordInjection,
|
|
7696
8171
|
resolveGotcha: () => resolveGotcha,
|
|
7697
8172
|
resolveGotchasForScope: () => resolveGotchasForScope,
|
|
8173
|
+
screenForInjection: () => screenForInjection,
|
|
7698
8174
|
setGotchaStatus: () => setGotchaStatus,
|
|
7699
8175
|
syncGotchaStatusToMarkdown: () => syncGotchaStatusToMarkdown,
|
|
7700
8176
|
syncGotchasGraph: () => syncGotchasGraph,
|
|
7701
|
-
syncGotchasToDb: () => syncGotchasToDb
|
|
8177
|
+
syncGotchasToDb: () => syncGotchasToDb,
|
|
8178
|
+
tokenizeForSimilarity: () => tokenizeForSimilarity
|
|
7702
8179
|
});
|
|
7703
|
-
import
|
|
7704
|
-
import
|
|
8180
|
+
import fs16 from "fs";
|
|
8181
|
+
import path19 from "path";
|
|
7705
8182
|
async function ensureGotchasSchema() {
|
|
7706
8183
|
const db = await getDb();
|
|
7707
8184
|
db.exec(`
|
|
@@ -7721,7 +8198,11 @@ async function ensureGotchasSchema() {
|
|
|
7721
8198
|
CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
|
|
7722
8199
|
last_verified_at INTEGER,
|
|
7723
8200
|
scope_package TEXT,
|
|
7724
|
-
verified_by TEXT
|
|
8201
|
+
verified_by TEXT,
|
|
8202
|
+
provenance TEXT,
|
|
8203
|
+
tier TEXT NOT NULL DEFAULT 'single'
|
|
8204
|
+
CHECK(tier IN ('human-confirmed','corroborated','single','external')),
|
|
8205
|
+
quarantined INTEGER NOT NULL DEFAULT 0
|
|
7725
8206
|
);
|
|
7726
8207
|
CREATE INDEX IF NOT EXISTS idx_gotchas_file ON known_gotchas(file_path);
|
|
7727
8208
|
CREATE INDEX IF NOT EXISTS idx_gotchas_severity ON known_gotchas(severity);
|
|
@@ -7747,6 +8228,15 @@ async function ensureGotchasSchema() {
|
|
|
7747
8228
|
if (!cols.includes("verified_by")) {
|
|
7748
8229
|
db.run(`ALTER TABLE known_gotchas ADD COLUMN verified_by TEXT`);
|
|
7749
8230
|
}
|
|
8231
|
+
if (!cols.includes("provenance")) {
|
|
8232
|
+
db.run(`ALTER TABLE known_gotchas ADD COLUMN provenance TEXT`);
|
|
8233
|
+
}
|
|
8234
|
+
if (!cols.includes("tier")) {
|
|
8235
|
+
db.run(`ALTER TABLE known_gotchas ADD COLUMN tier TEXT NOT NULL DEFAULT 'single'`);
|
|
8236
|
+
}
|
|
8237
|
+
if (!cols.includes("quarantined")) {
|
|
8238
|
+
db.run(`ALTER TABLE known_gotchas ADD COLUMN quarantined INTEGER NOT NULL DEFAULT 0`);
|
|
8239
|
+
}
|
|
7750
8240
|
const schema = db.exec(`SELECT sql FROM sqlite_master WHERE type='table' AND name='known_gotchas'`);
|
|
7751
8241
|
const gotchaSql = schema[0]?.values?.[0]?.[0] || "";
|
|
7752
8242
|
if (gotchaSql && !gotchaSql.includes("candidate")) {
|
|
@@ -7767,7 +8257,11 @@ async function ensureGotchasSchema() {
|
|
|
7767
8257
|
CHECK(status IN ('candidate','active','verified','resolved','deprecated')),
|
|
7768
8258
|
last_verified_at INTEGER,
|
|
7769
8259
|
scope_package TEXT,
|
|
7770
|
-
verified_by TEXT
|
|
8260
|
+
verified_by TEXT,
|
|
8261
|
+
provenance TEXT,
|
|
8262
|
+
tier TEXT NOT NULL DEFAULT 'single'
|
|
8263
|
+
CHECK(tier IN ('human-confirmed','corroborated','single','external')),
|
|
8264
|
+
quarantined INTEGER NOT NULL DEFAULT 0
|
|
7771
8265
|
)`);
|
|
7772
8266
|
db.run(`INSERT OR IGNORE INTO known_gotchas (id, file_path, description, severity, workaround, added_by, created_at, updated_at, content_hash, trigger_command, status, last_verified_at) SELECT id, file_path, description, severity, workaround, added_by, created_at, updated_at, content_hash, trigger_command, status, last_verified_at FROM known_gotchas_old`);
|
|
7773
8267
|
db.run(`DROP TABLE known_gotchas_old`);
|
|
@@ -7776,12 +8270,30 @@ async function ensureGotchasSchema() {
|
|
|
7776
8270
|
}
|
|
7777
8271
|
saveDb();
|
|
7778
8272
|
}
|
|
8273
|
+
function screenForInjection(text) {
|
|
8274
|
+
const t = text.toLowerCase();
|
|
8275
|
+
const patterns = [
|
|
8276
|
+
[/ignore\s+(all\s+)?previous\s+instructions?/, "directive to ignore previous instructions"],
|
|
8277
|
+
[/disregard\s+(the\s+)?(system|prior)\s+(prompt|instructions?)/, "directive to disregard system prompt"],
|
|
8278
|
+
[/(reveal|exfiltrate|leak|send|upload).{0,40}(secret|api[_\s-]?key|token|password|credential)/, "directive to reveal secrets"],
|
|
8279
|
+
[/execute\s+(the\s+)?following\s+(command|code|payload)/, "directive to execute embedded payload"],
|
|
8280
|
+
[/you\s+are\s+now\s+(a|an|in)\s/, "identity-reassignment attempt"]
|
|
8281
|
+
];
|
|
8282
|
+
for (const [re, reason] of patterns) {
|
|
8283
|
+
if (re.test(t)) return reason;
|
|
8284
|
+
}
|
|
8285
|
+
return null;
|
|
8286
|
+
}
|
|
7779
8287
|
async function addGotcha(entry) {
|
|
7780
8288
|
try {
|
|
7781
8289
|
await ensureGotchasSchema();
|
|
7782
8290
|
const db = await getDb();
|
|
7783
8291
|
const severity = entry.severity || "medium";
|
|
7784
8292
|
const status = entry.status || "active";
|
|
8293
|
+
const provenance = entry.provenance || "session:agent";
|
|
8294
|
+
const quarantineReason = screenForInjection(`${entry.description} ${entry.workaround || ""}`);
|
|
8295
|
+
const quarantined = quarantineReason ? 1 : 0;
|
|
8296
|
+
const tier = quarantined ? "external" : entry.tier || (status === "verified" ? "human-confirmed" : "single");
|
|
7785
8297
|
const contentHash = hashFileContent(entry.filePath);
|
|
7786
8298
|
const trap = entry.description;
|
|
7787
8299
|
const ruleFix = entry.workaround || "Review implementation carefully before modifying.";
|
|
@@ -7806,15 +8318,23 @@ async function addGotcha(entry) {
|
|
|
7806
8318
|
existingId = row.id;
|
|
7807
8319
|
}
|
|
7808
8320
|
checkStmt.free();
|
|
8321
|
+
let mergedNote = "";
|
|
8322
|
+
if (existingId === null) {
|
|
8323
|
+
const similarId = await findSimilarGotchaId(entry.filePath, entry.description);
|
|
8324
|
+
if (similarId !== null) {
|
|
8325
|
+
existingId = similarId;
|
|
8326
|
+
mergedNote = " (merged: near-duplicate of an existing gotcha \u2014 refreshed instead of appended)";
|
|
8327
|
+
}
|
|
8328
|
+
}
|
|
7809
8329
|
if (existingId !== null) {
|
|
7810
8330
|
db.run(
|
|
7811
|
-
`UPDATE known_gotchas SET severity = ?, workaround = ?, content_hash = ?, trigger_command = ?, status = ?, scope_package = coalesce(?, scope_package), verified_by = coalesce(?, verified_by), updated_at = strftime('%s','now') WHERE id = ?`,
|
|
7812
|
-
[severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, existingId]
|
|
8331
|
+
`UPDATE known_gotchas SET severity = ?, workaround = ?, content_hash = ?, trigger_command = ?, status = ?, scope_package = coalesce(?, scope_package), verified_by = coalesce(?, verified_by), provenance = coalesce(?, provenance), tier = CASE WHEN quarantined = 1 THEN tier WHEN ? = 'human-confirmed' THEN 'human-confirmed' ELSE 'corroborated' END, updated_at = strftime('%s','now') WHERE id = ?`,
|
|
8332
|
+
[severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, provenance, tier, existingId]
|
|
7813
8333
|
);
|
|
7814
8334
|
} else {
|
|
7815
8335
|
db.run(
|
|
7816
|
-
`INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7817
|
-
[entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null]
|
|
8336
|
+
`INSERT INTO known_gotchas (file_path, description, severity, workaround, content_hash, trigger_command, status, scope_package, verified_by, provenance, tier, quarantined) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8337
|
+
[entry.filePath, entry.description, severity, entry.workaround || null, contentHash, entry.triggerCommand || null, status, entry.scopePackage || null, entry.verifiedBy || null, provenance, tier, quarantined]
|
|
7818
8338
|
);
|
|
7819
8339
|
}
|
|
7820
8340
|
const mdResult = appendToLayer("gotcha", formatted);
|
|
@@ -7846,12 +8366,50 @@ async function addGotcha(entry) {
|
|
|
7846
8366
|
} catch {
|
|
7847
8367
|
}
|
|
7848
8368
|
const action = existingId !== null ? "updated" : "recorded";
|
|
7849
|
-
|
|
8369
|
+
const quarantineNote = quarantined ? `
|
|
8370
|
+
\u{1F512} **Quarantined** (${quarantineReason}) \u2014 excluded from auto-inject until human-confirmed. Promote via verify + resolve/re-record.` : "";
|
|
8371
|
+
return `\u2705 **Gotcha ${action}**: ${entry.filePath} \u2014 ${entry.description}${mergedNote}${quarantineNote}${linkedCount > 0 ? ` (linked to ${linkedCount} flow(s))` : ""} [tier: ${tier}]
|
|
7850
8372
|
${mdResult}`;
|
|
7851
8373
|
} catch (err) {
|
|
7852
8374
|
return `\u274C Failed to add gotcha: ${err}`;
|
|
7853
8375
|
}
|
|
7854
8376
|
}
|
|
8377
|
+
function tokenizeForSimilarity(text) {
|
|
8378
|
+
return new Set(
|
|
8379
|
+
text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)
|
|
8380
|
+
);
|
|
8381
|
+
}
|
|
8382
|
+
function jaccardSimilarity(a, b) {
|
|
8383
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
8384
|
+
let intersection = 0;
|
|
8385
|
+
for (const t of a) if (b.has(t)) intersection++;
|
|
8386
|
+
return intersection / (a.size + b.size - intersection);
|
|
8387
|
+
}
|
|
8388
|
+
async function findSimilarGotchaId(filePath, description) {
|
|
8389
|
+
try {
|
|
8390
|
+
const db = await getDb();
|
|
8391
|
+
const stmt = db.prepare(
|
|
8392
|
+
`SELECT id, description FROM known_gotchas WHERE file_path = ? AND status IN ('active','verified','candidate') LIMIT 50`
|
|
8393
|
+
);
|
|
8394
|
+
stmt.bind([filePath]);
|
|
8395
|
+
const rows = [];
|
|
8396
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
8397
|
+
stmt.free();
|
|
8398
|
+
const incoming = tokenizeForSimilarity(description);
|
|
8399
|
+
let best = null;
|
|
8400
|
+
let bestScore = 0;
|
|
8401
|
+
for (const r of rows) {
|
|
8402
|
+
const score = jaccardSimilarity(incoming, tokenizeForSimilarity(r.description));
|
|
8403
|
+
if (score >= GOTCHA_MERGE_THRESHOLD && score > bestScore) {
|
|
8404
|
+
bestScore = score;
|
|
8405
|
+
best = r.id;
|
|
8406
|
+
}
|
|
8407
|
+
}
|
|
8408
|
+
return best;
|
|
8409
|
+
} catch {
|
|
8410
|
+
return null;
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
7855
8413
|
async function listGotchas(params) {
|
|
7856
8414
|
try {
|
|
7857
8415
|
await ensureGotchasSchema();
|
|
@@ -7917,8 +8475,12 @@ async function listGotchas(params) {
|
|
|
7917
8475
|
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
7918
8476
|
const ageSec = nowSec - (Number(g.created_at) || nowSec);
|
|
7919
8477
|
const isNew = ageSec < 24 * 3600 ? " \u{1F195}" : "";
|
|
8478
|
+
const ageDays = Math.floor(ageSec / 86400);
|
|
8479
|
+
const expired = ageDays > 90 ? " \u23F3expired" : "";
|
|
8480
|
+
const tier = String(g.tier || "single");
|
|
8481
|
+
const quar = Number(g.quarantined) === 1 ? " \u{1F512}quarantined" : "";
|
|
7920
8482
|
const updated = g.updated_at ? ` \xB7 lastSeen ${new Date(Number(g.updated_at) * 1e3).toISOString().split("T")[0]}` : "";
|
|
7921
|
-
lines.push(`${icon} [${g.severity}]${isNew} ${g.file_path}${updated}`);
|
|
8483
|
+
lines.push(`${icon} [${g.severity}]${isNew}${expired}${quar} ${g.file_path}${updated} (${ageDays}d old, tier: ${tier})`);
|
|
7922
8484
|
lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
|
|
7923
8485
|
if (g.trigger_command) lines.push(` \u2328\uFE0F when running: \`${g.trigger_command.substring(0, 80)}\``);
|
|
7924
8486
|
if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
|
|
@@ -8013,7 +8575,7 @@ async function getActiveGotchasForCommand(command) {
|
|
|
8013
8575
|
const stmt = db.prepare(
|
|
8014
8576
|
`SELECT file_path, description, severity, workaround, trigger_command
|
|
8015
8577
|
FROM known_gotchas
|
|
8016
|
-
WHERE status IN ('active', 'verified') AND trigger_command IS NOT NULL AND length(trigger_command) > 0
|
|
8578
|
+
WHERE status IN ('active', 'verified') AND (quarantined IS NULL OR quarantined = 0) AND trigger_command IS NOT NULL AND length(trigger_command) > 0
|
|
8017
8579
|
ORDER BY
|
|
8018
8580
|
CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END
|
|
8019
8581
|
LIMIT 30`
|
|
@@ -8038,8 +8600,8 @@ async function getActiveGotchasForCommand(command) {
|
|
|
8038
8600
|
}
|
|
8039
8601
|
async function recordInjection(params) {
|
|
8040
8602
|
try {
|
|
8041
|
-
const fp =
|
|
8042
|
-
if (!
|
|
8603
|
+
const fp = path19.join(getProjectRoot(), ".kuma", "injections.jsonl");
|
|
8604
|
+
if (!fs16.existsSync(path19.dirname(fp))) fs16.mkdirSync(path19.dirname(fp), { recursive: true });
|
|
8043
8605
|
const line = JSON.stringify({
|
|
8044
8606
|
filePath: params.filePath || null,
|
|
8045
8607
|
command: params.command || null,
|
|
@@ -8048,16 +8610,16 @@ async function recordInjection(params) {
|
|
|
8048
8610
|
// conservative estimate: avoided re-research / re-discovery
|
|
8049
8611
|
at: Date.now()
|
|
8050
8612
|
});
|
|
8051
|
-
|
|
8613
|
+
fs16.appendFileSync(fp, line + "\n", "utf-8");
|
|
8052
8614
|
} catch {
|
|
8053
8615
|
}
|
|
8054
8616
|
}
|
|
8055
8617
|
function getInjectionStats(hours = 24) {
|
|
8056
8618
|
try {
|
|
8057
|
-
const fp =
|
|
8058
|
-
if (!
|
|
8619
|
+
const fp = path19.join(getProjectRoot(), ".kuma", "injections.jsonl");
|
|
8620
|
+
if (!fs16.existsSync(fp)) return { count: 0, savedMs: 0 };
|
|
8059
8621
|
const cutoff = Date.now() - hours * 60 * 60 * 1e3;
|
|
8060
|
-
const lines =
|
|
8622
|
+
const lines = fs16.readFileSync(fp, "utf-8").split("\n");
|
|
8061
8623
|
let count = 0;
|
|
8062
8624
|
let savedMs = 0;
|
|
8063
8625
|
for (const line of lines) {
|
|
@@ -8079,11 +8641,18 @@ function getInjectionStats(hours = 24) {
|
|
|
8079
8641
|
async function setGotchaStatus(id, status, verifiedBy) {
|
|
8080
8642
|
try {
|
|
8081
8643
|
const db = await getDb();
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8644
|
+
if (status === "verified") {
|
|
8645
|
+
db.run(
|
|
8646
|
+
`UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), tier = 'human-confirmed', quarantined = 0, last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
|
|
8647
|
+
[status, verifiedBy || null, id]
|
|
8648
|
+
);
|
|
8649
|
+
} else {
|
|
8650
|
+
db.run(
|
|
8651
|
+
`UPDATE known_gotchas SET status = ?, verified_by = coalesce(?, verified_by), last_verified_at = strftime('%s','now'), updated_at = strftime('%s','now') WHERE id = ?`,
|
|
8652
|
+
[status, verifiedBy || null, id]
|
|
8653
|
+
);
|
|
8654
|
+
}
|
|
8655
|
+
saveDb();
|
|
8087
8656
|
return true;
|
|
8088
8657
|
} catch {
|
|
8089
8658
|
return false;
|
|
@@ -8108,9 +8677,9 @@ async function getGotchasByStatus(status, limit = 20) {
|
|
|
8108
8677
|
}
|
|
8109
8678
|
function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
|
|
8110
8679
|
try {
|
|
8111
|
-
const mdPath =
|
|
8112
|
-
if (!
|
|
8113
|
-
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");
|
|
8114
8683
|
const lines = content.split("\n");
|
|
8115
8684
|
const normalizedTargets = targetPaths.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
8116
8685
|
if (normalizedTargets.length === 0) return false;
|
|
@@ -8127,9 +8696,9 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
|
|
|
8127
8696
|
modified = true;
|
|
8128
8697
|
}
|
|
8129
8698
|
const headingFile = headingMatch[1].trim().toLowerCase();
|
|
8130
|
-
const headingBase =
|
|
8699
|
+
const headingBase = path19.basename(headingFile);
|
|
8131
8700
|
inMatchingSection = normalizedTargets.some(
|
|
8132
|
-
(t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase ===
|
|
8701
|
+
(t) => headingFile.includes(t) || t.includes(headingFile) || headingBase === t || headingBase === path19.basename(t)
|
|
8133
8702
|
);
|
|
8134
8703
|
sectionHasStatus = false;
|
|
8135
8704
|
outputLines.push(line);
|
|
@@ -8151,7 +8720,7 @@ function syncGotchaStatusToMarkdown(targetPaths, newStatus, reason) {
|
|
|
8151
8720
|
modified = true;
|
|
8152
8721
|
}
|
|
8153
8722
|
if (modified) {
|
|
8154
|
-
|
|
8723
|
+
fs16.writeFileSync(mdPath, outputLines.join("\n"), "utf-8");
|
|
8155
8724
|
}
|
|
8156
8725
|
return modified;
|
|
8157
8726
|
} catch {
|
|
@@ -8274,6 +8843,7 @@ async function deprecateGotcha(idOrScope, reason) {
|
|
|
8274
8843
|
return { deprecated: 0, message: `\u274C Failed to deprecate gotcha: ${err}` };
|
|
8275
8844
|
}
|
|
8276
8845
|
}
|
|
8846
|
+
var GOTCHA_MERGE_THRESHOLD;
|
|
8277
8847
|
var init_kumaGotchas = __esm({
|
|
8278
8848
|
"src/engine/kumaGotchas.ts"() {
|
|
8279
8849
|
"use strict";
|
|
@@ -8282,6 +8852,7 @@ var init_kumaGotchas = __esm({
|
|
|
8282
8852
|
init_pathValidator();
|
|
8283
8853
|
init_domainRules();
|
|
8284
8854
|
init_kumaInject();
|
|
8855
|
+
GOTCHA_MERGE_THRESHOLD = 0.6;
|
|
8285
8856
|
}
|
|
8286
8857
|
});
|
|
8287
8858
|
|
|
@@ -8290,24 +8861,24 @@ var kumaMaintenance_exports = {};
|
|
|
8290
8861
|
__export(kumaMaintenance_exports, {
|
|
8291
8862
|
maybeRunAutoCleanup: () => maybeRunAutoCleanup
|
|
8292
8863
|
});
|
|
8293
|
-
import
|
|
8294
|
-
import
|
|
8864
|
+
import fs17 from "fs";
|
|
8865
|
+
import path20 from "path";
|
|
8295
8866
|
async function maybeRunAutoCleanup() {
|
|
8296
8867
|
try {
|
|
8297
8868
|
const root = getProjectRoot();
|
|
8298
|
-
const stateFile =
|
|
8869
|
+
const stateFile = path20.join(root, GC_STATE_FILE);
|
|
8299
8870
|
const now = Date.now();
|
|
8300
8871
|
let lastRun = 0;
|
|
8301
|
-
if (
|
|
8872
|
+
if (fs17.existsSync(stateFile)) {
|
|
8302
8873
|
try {
|
|
8303
|
-
lastRun = JSON.parse(
|
|
8874
|
+
lastRun = JSON.parse(fs17.readFileSync(stateFile, "utf-8")).lastRun || 0;
|
|
8304
8875
|
} catch {
|
|
8305
8876
|
lastRun = 0;
|
|
8306
8877
|
}
|
|
8307
8878
|
}
|
|
8308
8879
|
if (now - lastRun < GC_INTERVAL_MS) return "";
|
|
8309
|
-
|
|
8310
|
-
|
|
8880
|
+
fs17.mkdirSync(path20.dirname(stateFile), { recursive: true });
|
|
8881
|
+
fs17.writeFileSync(stateFile, JSON.stringify({ lastRun: now }), "utf-8");
|
|
8311
8882
|
const { syncGotchasToDb: syncGotchasToDb2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
|
|
8312
8883
|
await syncGotchasToDb2();
|
|
8313
8884
|
const { runGarbageCollection: runGarbageCollection2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
@@ -8343,22 +8914,22 @@ __export(kumaPathRules_exports, {
|
|
|
8343
8914
|
loadPathRules: () => loadPathRules,
|
|
8344
8915
|
matchRulePath: () => matchRulePath
|
|
8345
8916
|
});
|
|
8346
|
-
import
|
|
8347
|
-
import
|
|
8917
|
+
import fs18 from "fs";
|
|
8918
|
+
import path21 from "path";
|
|
8348
8919
|
function rulesDir() {
|
|
8349
|
-
return
|
|
8920
|
+
return path21.join(getProjectRoot(), RULES_DIR);
|
|
8350
8921
|
}
|
|
8351
8922
|
function ensureRulesDir() {
|
|
8352
8923
|
try {
|
|
8353
8924
|
const dir = rulesDir();
|
|
8354
|
-
if (!
|
|
8925
|
+
if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
|
|
8355
8926
|
} catch {
|
|
8356
8927
|
}
|
|
8357
8928
|
}
|
|
8358
8929
|
function parseRuleFile(filePath) {
|
|
8359
8930
|
try {
|
|
8360
|
-
const raw =
|
|
8361
|
-
const id =
|
|
8931
|
+
const raw = fs18.readFileSync(filePath, "utf-8");
|
|
8932
|
+
const id = path21.basename(filePath, ".md");
|
|
8362
8933
|
const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
8363
8934
|
if (!fm) {
|
|
8364
8935
|
return { id, description: id, paths: ["*"], content: raw.trim(), filePath };
|
|
@@ -8383,7 +8954,7 @@ function loadPathRules() {
|
|
|
8383
8954
|
ensureRulesDir();
|
|
8384
8955
|
try {
|
|
8385
8956
|
const dir = rulesDir();
|
|
8386
|
-
return
|
|
8957
|
+
return fs18.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseRuleFile(path21.join(dir, f))).filter((r) => r !== null);
|
|
8387
8958
|
} catch {
|
|
8388
8959
|
return [];
|
|
8389
8960
|
}
|
|
@@ -8471,8 +9042,8 @@ __export(kumaFlowCache_exports, {
|
|
|
8471
9042
|
getFreshDomainFlow: () => getFreshDomainFlow,
|
|
8472
9043
|
getHopsForDomain: () => getHopsForDomain
|
|
8473
9044
|
});
|
|
8474
|
-
import
|
|
8475
|
-
import
|
|
9045
|
+
import fs19 from "fs";
|
|
9046
|
+
import path22 from "path";
|
|
8476
9047
|
async function getFlowFreshness(domain) {
|
|
8477
9048
|
try {
|
|
8478
9049
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
@@ -8507,26 +9078,26 @@ async function getFlowFreshness(domain) {
|
|
|
8507
9078
|
}
|
|
8508
9079
|
function resolveImport(fromFile, spec) {
|
|
8509
9080
|
const root = getProjectRoot();
|
|
8510
|
-
const fromDir =
|
|
9081
|
+
const fromDir = path22.dirname(fromFile);
|
|
8511
9082
|
const candidates = [];
|
|
8512
9083
|
if (spec.startsWith(".")) {
|
|
8513
|
-
const abs =
|
|
9084
|
+
const abs = path22.resolve(fromDir, spec);
|
|
8514
9085
|
candidates.push(abs);
|
|
8515
9086
|
} else {
|
|
8516
|
-
candidates.push(
|
|
9087
|
+
candidates.push(path22.join(root, spec));
|
|
8517
9088
|
}
|
|
8518
9089
|
for (const c of candidates) {
|
|
8519
9090
|
for (const ext of SOURCE_EXTENSIONS) {
|
|
8520
9091
|
const withExt = `${c}${ext}`;
|
|
8521
|
-
if (
|
|
8522
|
-
if (
|
|
8523
|
-
const idx =
|
|
8524
|
-
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);
|
|
8525
9096
|
}
|
|
8526
9097
|
}
|
|
8527
|
-
if (
|
|
9098
|
+
if (fs19.existsSync(c)) {
|
|
8528
9099
|
try {
|
|
8529
|
-
if (
|
|
9100
|
+
if (fs19.statSync(c).isFile()) return path22.relative(root, c);
|
|
8530
9101
|
} catch {
|
|
8531
9102
|
}
|
|
8532
9103
|
}
|
|
@@ -8535,13 +9106,13 @@ function resolveImport(fromFile, spec) {
|
|
|
8535
9106
|
}
|
|
8536
9107
|
function deriveHopsFromImports(entryFile, maxHops = 6) {
|
|
8537
9108
|
const root = getProjectRoot();
|
|
8538
|
-
const absEntry =
|
|
8539
|
-
if (!
|
|
9109
|
+
const absEntry = path22.isAbsolute(entryFile) ? entryFile : path22.resolve(root, entryFile);
|
|
9110
|
+
if (!fs19.existsSync(absEntry)) return [];
|
|
8540
9111
|
const hops = [];
|
|
8541
9112
|
const seen = /* @__PURE__ */ new Set([entryFile]);
|
|
8542
|
-
const fromName =
|
|
9113
|
+
const fromName = path22.basename(absEntry);
|
|
8543
9114
|
try {
|
|
8544
|
-
const lines =
|
|
9115
|
+
const lines = fs19.readFileSync(absEntry, "utf-8").split("\n");
|
|
8545
9116
|
for (const line of lines) {
|
|
8546
9117
|
if (hops.length >= maxHops) break;
|
|
8547
9118
|
const spec = matchImportPath(line);
|
|
@@ -8551,9 +9122,9 @@ function deriveHopsFromImports(entryFile, maxHops = 6) {
|
|
|
8551
9122
|
seen.add(resolved);
|
|
8552
9123
|
hops.push({
|
|
8553
9124
|
from: fromName,
|
|
8554
|
-
to:
|
|
9125
|
+
to: path22.basename(resolved),
|
|
8555
9126
|
relation: "imports",
|
|
8556
|
-
description: `${fromName} imports ${
|
|
9127
|
+
description: `${fromName} imports ${path22.basename(resolved)}`
|
|
8557
9128
|
});
|
|
8558
9129
|
}
|
|
8559
9130
|
} catch {
|
|
@@ -8616,9 +9187,9 @@ async function getHopsForDomain(domain) {
|
|
|
8616
9187
|
}
|
|
8617
9188
|
try {
|
|
8618
9189
|
const root = getProjectRoot();
|
|
8619
|
-
const archPath =
|
|
8620
|
-
if (
|
|
8621
|
-
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");
|
|
8622
9193
|
const lines = content.split("\n");
|
|
8623
9194
|
for (const line of lines) {
|
|
8624
9195
|
const domMatch = line.match(new RegExp(`domain:\\s*${escapeRegex(domain)}\\s*\\|\\s*hops:\\s*(.+)`, "i"));
|
|
@@ -8663,7 +9234,7 @@ Record one with kuma_memory({ action: 'arch_flow', content: 'domain: ${domain} |
|
|
|
8663
9234
|
hops: derivedHops,
|
|
8664
9235
|
filePaths: freshness.filePaths
|
|
8665
9236
|
});
|
|
8666
|
-
lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${
|
|
9237
|
+
lines.push(`\u2705 Re-derived ${derivedHops.length} hop(s) from ${path22.basename(entry)} \u2014 cache refreshed.`);
|
|
8667
9238
|
hops.splice(0, hops.length, ...derivedHops);
|
|
8668
9239
|
} else {
|
|
8669
9240
|
lines.push("\u26A0\uFE0F No imports to follow \u2014 cache left as-is (record hops manually if needed).");
|
|
@@ -8707,6 +9278,127 @@ var init_kumaFlowCache = __esm({
|
|
|
8707
9278
|
}
|
|
8708
9279
|
});
|
|
8709
9280
|
|
|
9281
|
+
// src/engine/packageBriefs.ts
|
|
9282
|
+
var packageBriefs_exports = {};
|
|
9283
|
+
__export(packageBriefs_exports, {
|
|
9284
|
+
BRIEFS_DIR: () => BRIEFS_DIR,
|
|
9285
|
+
briefFreshness: () => briefFreshness,
|
|
9286
|
+
buildPackageBrief: () => buildPackageBrief,
|
|
9287
|
+
readBrief: () => readBrief,
|
|
9288
|
+
refreshPackageBriefs: () => refreshPackageBriefs
|
|
9289
|
+
});
|
|
9290
|
+
import fs20 from "fs";
|
|
9291
|
+
import path23 from "path";
|
|
9292
|
+
function briefsDir(root) {
|
|
9293
|
+
return path23.join(root || getProjectRoot(), BRIEFS_DIR);
|
|
9294
|
+
}
|
|
9295
|
+
function slug(name) {
|
|
9296
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60) || "root";
|
|
9297
|
+
}
|
|
9298
|
+
async function buildPackageBrief(pkgName, pkgPath, _root) {
|
|
9299
|
+
const lines = [`# \u{1F4E6} ${pkgName}`, "", `_Auto-generated by Kuma \u2014 do not edit. Regenerate via map refresh._`, ""];
|
|
9300
|
+
if (pkgPath && pkgPath !== ".") lines.push(`path: \`${pkgPath}\``, "");
|
|
9301
|
+
try {
|
|
9302
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
9303
|
+
const db = await getDb2();
|
|
9304
|
+
const like = pkgPath && pkgPath !== "." ? `%${pkgPath}%` : `%`;
|
|
9305
|
+
const stmt = db.prepare(`
|
|
9306
|
+
SELECT n.file_path as fp, COUNT(*) as cnt FROM edges e
|
|
9307
|
+
JOIN nodes n ON n.id = e.target_id
|
|
9308
|
+
WHERE n.file_path LIKE ? AND n.file_path NOT LIKE '%test%'
|
|
9309
|
+
GROUP BY n.file_path ORDER BY cnt DESC LIMIT 8
|
|
9310
|
+
`);
|
|
9311
|
+
stmt.bind([like]);
|
|
9312
|
+
const hubs = [];
|
|
9313
|
+
while (stmt.step()) {
|
|
9314
|
+
const r = stmt.getAsObject();
|
|
9315
|
+
if (r.fp) hubs.push(`- \`${r.fp}\` (${r.cnt} refs)`);
|
|
9316
|
+
}
|
|
9317
|
+
stmt.free();
|
|
9318
|
+
lines.push(`## \u{1F3AF} Key files`, ...hubs.length > 0 ? hubs : ["- (no graph data yet \u2014 run a scan)"], "");
|
|
9319
|
+
try {
|
|
9320
|
+
const g = db.prepare(
|
|
9321
|
+
`SELECT file_path, description, severity FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0) AND file_path LIKE ? ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 ELSE 2 END LIMIT 5`
|
|
9322
|
+
);
|
|
9323
|
+
g.bind([like]);
|
|
9324
|
+
const rows = [];
|
|
9325
|
+
while (g.step()) {
|
|
9326
|
+
const r = g.getAsObject();
|
|
9327
|
+
rows.push(`- [${r.severity}] \`${r.file_path}\` \u2014 ${String(r.description).substring(0, 100)}`);
|
|
9328
|
+
}
|
|
9329
|
+
g.free();
|
|
9330
|
+
lines.push(`## \u26A0\uFE0F Gotchas`, ...rows.length > 0 ? rows : ["- none"], "");
|
|
9331
|
+
} catch {
|
|
9332
|
+
}
|
|
9333
|
+
try {
|
|
9334
|
+
const t = db.prepare(
|
|
9335
|
+
`SELECT DISTINCT file_path FROM nodes WHERE type = 'test' AND file_path LIKE ? LIMIT 10`
|
|
9336
|
+
);
|
|
9337
|
+
t.bind([like]);
|
|
9338
|
+
const tests = [];
|
|
9339
|
+
while (t.step()) {
|
|
9340
|
+
const r = t.getAsObject();
|
|
9341
|
+
if (r.file_path) tests.push(`- \`${r.file_path}\``);
|
|
9342
|
+
}
|
|
9343
|
+
t.free();
|
|
9344
|
+
lines.push(`## \u{1F9EA} Tests`, ...tests.length > 0 ? tests : ["- (run a scan to map tests)"], "");
|
|
9345
|
+
} catch {
|
|
9346
|
+
}
|
|
9347
|
+
} catch {
|
|
9348
|
+
lines.push("_(graph unavailable \u2014 brief will fill in after a scan)_", "");
|
|
9349
|
+
}
|
|
9350
|
+
return lines.join("\n");
|
|
9351
|
+
}
|
|
9352
|
+
async function refreshPackageBriefs() {
|
|
9353
|
+
const root = getProjectRoot();
|
|
9354
|
+
const dir = briefsDir(root);
|
|
9355
|
+
try {
|
|
9356
|
+
const ws = await getWorkspaceInfo(root);
|
|
9357
|
+
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
9358
|
+
let count = 0;
|
|
9359
|
+
const pkgs = ws.isWorkspace ? ws.packages : [{ name: "root", path: ".", isRoot: true }];
|
|
9360
|
+
for (const p of pkgs.slice(0, 20)) {
|
|
9361
|
+
const pkg = p;
|
|
9362
|
+
const content = await buildPackageBrief(pkg.name, pkg.path, root);
|
|
9363
|
+
fs20.writeFileSync(path23.join(dir, `${slug(pkg.name)}.md`), content, "utf-8");
|
|
9364
|
+
count++;
|
|
9365
|
+
}
|
|
9366
|
+
return { count, dir };
|
|
9367
|
+
} catch {
|
|
9368
|
+
return { count: 0, dir };
|
|
9369
|
+
}
|
|
9370
|
+
}
|
|
9371
|
+
function briefFreshness(pkgName) {
|
|
9372
|
+
try {
|
|
9373
|
+
const fp = path23.join(briefsDir(), `${slug(pkgName)}.md`);
|
|
9374
|
+
if (!fs20.existsSync(fp)) return { exists: false, fresh: false, age: "missing" };
|
|
9375
|
+
const ageMs = Date.now() - fs20.statSync(fp).mtimeMs;
|
|
9376
|
+
return { exists: true, fresh: !isStale(Date.now() - ageMs, MAP_TTL_MS), age: formatAge(Date.now() - ageMs) };
|
|
9377
|
+
} catch {
|
|
9378
|
+
return { exists: false, fresh: false, age: "missing" };
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
function readBrief(pkgName, maxChars = 1200) {
|
|
9382
|
+
try {
|
|
9383
|
+
const fp = path23.join(briefsDir(), `${slug(pkgName)}.md`);
|
|
9384
|
+
if (!fs20.existsSync(fp)) return null;
|
|
9385
|
+
const content = fs20.readFileSync(fp, "utf-8");
|
|
9386
|
+
return content.length > maxChars ? content.slice(0, maxChars) + "\n\u2026(truncated)" : content;
|
|
9387
|
+
} catch {
|
|
9388
|
+
return null;
|
|
9389
|
+
}
|
|
9390
|
+
}
|
|
9391
|
+
var BRIEFS_DIR;
|
|
9392
|
+
var init_packageBriefs = __esm({
|
|
9393
|
+
"src/engine/packageBriefs.ts"() {
|
|
9394
|
+
"use strict";
|
|
9395
|
+
init_pathValidator();
|
|
9396
|
+
init_workspaceIntelligence();
|
|
9397
|
+
init_cacheFreshness();
|
|
9398
|
+
BRIEFS_DIR = ".kuma/briefs";
|
|
9399
|
+
}
|
|
9400
|
+
});
|
|
9401
|
+
|
|
8710
9402
|
// src/engine/kumaSearch.ts
|
|
8711
9403
|
var kumaSearch_exports = {};
|
|
8712
9404
|
__export(kumaSearch_exports, {
|
|
@@ -8719,8 +9411,8 @@ __export(kumaSearch_exports, {
|
|
|
8719
9411
|
formatHybridResults: () => formatHybridResults,
|
|
8720
9412
|
hybridSearch: () => hybridSearch
|
|
8721
9413
|
});
|
|
8722
|
-
import
|
|
8723
|
-
import
|
|
9414
|
+
import fs22 from "fs";
|
|
9415
|
+
import path25 from "path";
|
|
8724
9416
|
function expandQueryTerms(query) {
|
|
8725
9417
|
const terms = query.toLowerCase().split(/[\s_-]+/).filter((t) => t.length > 2);
|
|
8726
9418
|
const expanded = /* @__PURE__ */ new Map();
|
|
@@ -8783,13 +9475,13 @@ async function buildSearchVectors() {
|
|
|
8783
9475
|
}
|
|
8784
9476
|
try {
|
|
8785
9477
|
const root = getProjectRoot();
|
|
8786
|
-
const memDir =
|
|
8787
|
-
if (
|
|
8788
|
-
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);
|
|
8789
9481
|
for (const file of files) {
|
|
8790
9482
|
try {
|
|
8791
|
-
const fullPath =
|
|
8792
|
-
const content =
|
|
9483
|
+
const fullPath = path25.join(memDir, file);
|
|
9484
|
+
const content = fs22.readFileSync(fullPath, "utf-8");
|
|
8793
9485
|
const text = content.toLowerCase();
|
|
8794
9486
|
const rawTerms = extractTerms(text);
|
|
8795
9487
|
const termCounts = /* @__PURE__ */ new Map();
|
|
@@ -8800,7 +9492,7 @@ async function buildSearchVectors() {
|
|
|
8800
9492
|
terms: termCounts,
|
|
8801
9493
|
source: file.replace(/\.md$/, ""),
|
|
8802
9494
|
sourceType: "memory",
|
|
8803
|
-
recency: recencyScore(Math.floor(
|
|
9495
|
+
recency: recencyScore(Math.floor(fs22.statSync(fullPath).mtimeMs / 1e3))
|
|
8804
9496
|
});
|
|
8805
9497
|
} catch {
|
|
8806
9498
|
}
|
|
@@ -9117,14 +9809,14 @@ __export(architectureGuard_exports, {
|
|
|
9117
9809
|
detectWorkspaceCircularDeps: () => detectWorkspaceCircularDeps,
|
|
9118
9810
|
loadArchitectureRules: () => loadArchitectureRules
|
|
9119
9811
|
});
|
|
9120
|
-
import
|
|
9121
|
-
import
|
|
9812
|
+
import fs26 from "fs";
|
|
9813
|
+
import path29 from "path";
|
|
9122
9814
|
import fastGlob4 from "fast-glob";
|
|
9123
9815
|
function loadArchitectureRules(root) {
|
|
9124
|
-
const rulesPath =
|
|
9125
|
-
if (
|
|
9816
|
+
const rulesPath = path29.join(root, ".kuma", "architecture.json");
|
|
9817
|
+
if (fs26.existsSync(rulesPath)) {
|
|
9126
9818
|
try {
|
|
9127
|
-
const data = JSON.parse(
|
|
9819
|
+
const data = JSON.parse(fs26.readFileSync(rulesPath, "utf-8"));
|
|
9128
9820
|
if (Array.isArray(data.rules)) {
|
|
9129
9821
|
return data.rules;
|
|
9130
9822
|
}
|
|
@@ -9186,11 +9878,11 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
|
|
|
9186
9878
|
const customRules = loadArchitectureRules(root);
|
|
9187
9879
|
const importRegex = /(?:import|export\s+.*from|require\s*\()\s*['"]([^'"]+)['"]/g;
|
|
9188
9880
|
for (const file of targetFiles) {
|
|
9189
|
-
const fullPath =
|
|
9190
|
-
if (!
|
|
9881
|
+
const fullPath = path29.isAbsolute(file) ? file : path29.join(root, file);
|
|
9882
|
+
if (!fs26.existsSync(fullPath)) continue;
|
|
9191
9883
|
let content = "";
|
|
9192
9884
|
try {
|
|
9193
|
-
content =
|
|
9885
|
+
content = fs26.readFileSync(fullPath, "utf-8");
|
|
9194
9886
|
} catch {
|
|
9195
9887
|
continue;
|
|
9196
9888
|
}
|
|
@@ -9200,7 +9892,7 @@ async function checkArchitectureBoundaries(filesToCheck, customRoot) {
|
|
|
9200
9892
|
const importPath = match[1];
|
|
9201
9893
|
if (wsInfo.isWorkspace && currentPkg && !currentPkg.isRoot) {
|
|
9202
9894
|
if (importPath.startsWith("../") && (importPath.includes("/src/") || importPath.includes("/dist/"))) {
|
|
9203
|
-
const resolvedTarget =
|
|
9895
|
+
const resolvedTarget = path29.normalize(path29.resolve(path29.dirname(fullPath), importPath));
|
|
9204
9896
|
const targetPkg = findPackageForFile(resolvedTarget, wsInfo);
|
|
9205
9897
|
if (targetPkg && targetPkg.name !== currentPkg.name) {
|
|
9206
9898
|
violations.push({
|
|
@@ -9241,6 +9933,256 @@ var init_architectureGuard = __esm({
|
|
|
9241
9933
|
}
|
|
9242
9934
|
});
|
|
9243
9935
|
|
|
9936
|
+
// src/engine/docDrift.ts
|
|
9937
|
+
var docDrift_exports = {};
|
|
9938
|
+
__export(docDrift_exports, {
|
|
9939
|
+
collectDocFiles: () => collectDocFiles,
|
|
9940
|
+
extractClaims: () => extractClaims,
|
|
9941
|
+
formatDocDrift: () => formatDocDrift,
|
|
9942
|
+
scanDocDrift: () => scanDocDrift
|
|
9943
|
+
});
|
|
9944
|
+
import fs27 from "fs";
|
|
9945
|
+
import path30 from "path";
|
|
9946
|
+
import { execSync as execSync8 } from "child_process";
|
|
9947
|
+
function collectDocFiles(root) {
|
|
9948
|
+
const cwd = root || getProjectRoot();
|
|
9949
|
+
const out = [];
|
|
9950
|
+
const pushIfMd = (dir, rel) => {
|
|
9951
|
+
try {
|
|
9952
|
+
for (const f of fs27.readdirSync(dir)) {
|
|
9953
|
+
if (out.length >= MAX_DOCS) return;
|
|
9954
|
+
const full = path30.join(dir, f);
|
|
9955
|
+
try {
|
|
9956
|
+
if (fs27.statSync(full).isFile() && /\.md$/i.test(f)) out.push(rel(f));
|
|
9957
|
+
} catch {
|
|
9958
|
+
}
|
|
9959
|
+
}
|
|
9960
|
+
} catch {
|
|
9961
|
+
}
|
|
9962
|
+
};
|
|
9963
|
+
pushIfMd(path30.join(cwd, "docs"), (f) => path30.join("docs", f));
|
|
9964
|
+
pushIfMd(cwd, (f) => f);
|
|
9965
|
+
return out;
|
|
9966
|
+
}
|
|
9967
|
+
function isExemptDoc(relPath, content) {
|
|
9968
|
+
if (EXEMPT_PATTERN.test(relPath)) return true;
|
|
9969
|
+
const head = content.split("\n").slice(0, 15).join("\n");
|
|
9970
|
+
return EXEMPT_PATTERN.test(head);
|
|
9971
|
+
}
|
|
9972
|
+
function extractClaims(relPath, content) {
|
|
9973
|
+
const claims = [];
|
|
9974
|
+
const lines = content.split("\n");
|
|
9975
|
+
lines.forEach((line, idx) => {
|
|
9976
|
+
const lineNo = idx + 1;
|
|
9977
|
+
for (const m of line.matchAll(ENV_PATTERN)) {
|
|
9978
|
+
const name = m[1];
|
|
9979
|
+
if (ENV_ALLOWLIST_HINTS.some((h) => name.toLowerCase().includes(h))) {
|
|
9980
|
+
claims.push({ kind: "env", text: name, docFile: relPath, docLine: lineNo });
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
for (const m of line.matchAll(ENDPOINT_PATTERN)) {
|
|
9984
|
+
const ep = m[1];
|
|
9985
|
+
if (ep.length > 60) continue;
|
|
9986
|
+
claims.push({ kind: "endpoint", text: ep, docFile: relPath, docLine: lineNo });
|
|
9987
|
+
}
|
|
9988
|
+
for (const m of line.matchAll(ABSENCE_PATTERN)) {
|
|
9989
|
+
const thing = (m[1] || "").trim();
|
|
9990
|
+
if (thing.length >= 3 && thing.length <= 40) {
|
|
9991
|
+
claims.push({ kind: "absence", text: thing, docFile: relPath, docLine: lineNo });
|
|
9992
|
+
}
|
|
9993
|
+
}
|
|
9994
|
+
});
|
|
9995
|
+
return claims;
|
|
9996
|
+
}
|
|
9997
|
+
function codeContainsBatch(root, needles) {
|
|
9998
|
+
const found = /* @__PURE__ */ new Set();
|
|
9999
|
+
const clean = [...new Set(needles.map((n) => n.replace(/"/g, "").trim()).filter((n) => n.length >= 3))];
|
|
10000
|
+
if (clean.length === 0) return found;
|
|
10001
|
+
for (let i = 0; i < clean.length; i += 50) {
|
|
10002
|
+
const chunk = clean.slice(i, i + 50);
|
|
10003
|
+
try {
|
|
10004
|
+
const args = chunk.map((n) => `-e "${n}"`).join(" ");
|
|
10005
|
+
const out = execSync8(
|
|
10006
|
+
`grep -rn --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.py" --include="*.go" --include="*.rs" --include="*.json" --include="*.yml" --include="*.yaml" --include="*.toml" --include=".env*" -F ${args} . 2>/dev/null | grep -v node_modules | grep -v "/dist/" | grep -v "/.git/" | grep -v "/.kuma/" | head -100`,
|
|
10007
|
+
{ cwd: root, encoding: "utf-8", timeout: 8e3, maxBuffer: 256 * 1024 }
|
|
10008
|
+
).trim();
|
|
10009
|
+
for (const n of chunk) {
|
|
10010
|
+
const isIdent = /^[A-Za-z0-9_]+$/.test(n);
|
|
10011
|
+
const hit = isIdent ? new RegExp(`\\b${n}\\b`).test(out) : out.includes(n);
|
|
10012
|
+
if (hit) found.add(n);
|
|
10013
|
+
}
|
|
10014
|
+
} catch {
|
|
10015
|
+
}
|
|
10016
|
+
}
|
|
10017
|
+
return found;
|
|
10018
|
+
}
|
|
10019
|
+
function scanDocDrift(root) {
|
|
10020
|
+
const cwd = root || getProjectRoot();
|
|
10021
|
+
const skipped = [];
|
|
10022
|
+
let claims = [];
|
|
10023
|
+
for (const rel of collectDocFiles(cwd)) {
|
|
10024
|
+
try {
|
|
10025
|
+
const content = fs27.readFileSync(path30.join(cwd, rel), "utf-8");
|
|
10026
|
+
if (isExemptDoc(rel, content)) {
|
|
10027
|
+
skipped.push(rel);
|
|
10028
|
+
continue;
|
|
10029
|
+
}
|
|
10030
|
+
claims.push(...extractClaims(rel, content));
|
|
10031
|
+
if (claims.length >= MAX_CLAIMS) break;
|
|
10032
|
+
} catch {
|
|
10033
|
+
}
|
|
10034
|
+
}
|
|
10035
|
+
claims = claims.slice(0, MAX_CLAIMS);
|
|
10036
|
+
const present = codeContainsBatch(cwd, claims.map((c) => c.text));
|
|
10037
|
+
const drifted = [];
|
|
10038
|
+
for (const c of claims) {
|
|
10039
|
+
if (c.kind === "absence") {
|
|
10040
|
+
if (present.has(c.text)) {
|
|
10041
|
+
drifted.push({ ...c, codeHint: `code references "${c.text}" but doc claims absence` });
|
|
10042
|
+
}
|
|
10043
|
+
} else {
|
|
10044
|
+
if (!present.has(c.text)) {
|
|
10045
|
+
drifted.push({ ...c, codeHint: `"${c.text}" found in no code/config file` });
|
|
10046
|
+
}
|
|
10047
|
+
}
|
|
10048
|
+
if (drifted.length >= 10) break;
|
|
10049
|
+
}
|
|
10050
|
+
return { checked: claims.length, drifted, skipped };
|
|
10051
|
+
}
|
|
10052
|
+
function formatDocDrift(checked, drifted, skipped) {
|
|
10053
|
+
if (drifted.length === 0) {
|
|
10054
|
+
return `\u{1F4C4} **Doc drift**: ${checked} claim(s) checked, no drift detected.`;
|
|
10055
|
+
}
|
|
10056
|
+
const lines = [
|
|
10057
|
+
`\u{1F4C4} **Doc drift detected** \u2014 ${drifted.length} stale claim(s) of ${checked} checked:`
|
|
10058
|
+
];
|
|
10059
|
+
for (const d of drifted) {
|
|
10060
|
+
lines.push(` \u26A0\uFE0F [${d.kind}] "${d.text}" \u2014 ${d.docFile}:${d.docLine} vs ${d.codeHint}`);
|
|
10061
|
+
}
|
|
10062
|
+
if (skipped.length > 0) lines.push(` \u2139\uFE0F exempt (RFC/aspirational): ${skipped.slice(0, 3).join(", ")}`);
|
|
10063
|
+
lines.push(` \u{1F4A1} Confirm with a human, then patch the doc or the code.`);
|
|
10064
|
+
return lines.join("\n");
|
|
10065
|
+
}
|
|
10066
|
+
var MAX_DOCS, MAX_CLAIMS, EXEMPT_PATTERN, ENV_PATTERN, ENDPOINT_PATTERN, ABSENCE_PATTERN, ENV_ALLOWLIST_HINTS;
|
|
10067
|
+
var init_docDrift = __esm({
|
|
10068
|
+
"src/engine/docDrift.ts"() {
|
|
10069
|
+
"use strict";
|
|
10070
|
+
init_pathValidator();
|
|
10071
|
+
MAX_DOCS = 20;
|
|
10072
|
+
MAX_CLAIMS = 200;
|
|
10073
|
+
EXEMPT_PATTERN = /(rfc|proposal|draft|roadmap|aspirational|vision|wishlist)/i;
|
|
10074
|
+
ENV_PATTERN = /\b([A-Z][A-Z0-9_]{3,})\b/g;
|
|
10075
|
+
ENDPOINT_PATTERN = /(https?:\/\/[^\s)"']+|\/[a-z][a-z0-9/_.-]{2,})/g;
|
|
10076
|
+
ABSENCE_PATTERN = /\b(?:no|without|does not use|doesn't use|never)\s+([A-Za-z][A-Za-z0-9_. -]{2,40}?)(?:\.|,|;|$)/gi;
|
|
10077
|
+
ENV_ALLOWLIST_HINTS = ["key", "secret", "token", "url", "dsn", "endpoint", "host", "port", "id", "name", "path", "dir", "debug"];
|
|
10078
|
+
}
|
|
10079
|
+
});
|
|
10080
|
+
|
|
10081
|
+
// src/engine/guardLedger.ts
|
|
10082
|
+
var guardLedger_exports = {};
|
|
10083
|
+
__export(guardLedger_exports, {
|
|
10084
|
+
GUARD_FP_BUDGET_RATE: () => GUARD_FP_BUDGET_RATE,
|
|
10085
|
+
GUARD_FP_MIN_FEEDBACK: () => GUARD_FP_MIN_FEEDBACK,
|
|
10086
|
+
demoteSeverity: () => demoteSeverity,
|
|
10087
|
+
formatPrecisionLedger: () => formatPrecisionLedger,
|
|
10088
|
+
getDowntieredRules: () => getDowntieredRules,
|
|
10089
|
+
getRulePrecision: () => getRulePrecision,
|
|
10090
|
+
recordGuardFeedback: () => recordGuardFeedback,
|
|
10091
|
+
recordGuardFlag: () => recordGuardFlag
|
|
10092
|
+
});
|
|
10093
|
+
async function ensureLedgerSchema() {
|
|
10094
|
+
const db = await getDb();
|
|
10095
|
+
db.exec(`
|
|
10096
|
+
CREATE TABLE IF NOT EXISTS guard_ledger (
|
|
10097
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10098
|
+
rule TEXT NOT NULL,
|
|
10099
|
+
verdict TEXT NOT NULL DEFAULT 'flag',
|
|
10100
|
+
false_positive INTEGER NOT NULL DEFAULT 0,
|
|
10101
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
10102
|
+
);
|
|
10103
|
+
CREATE INDEX IF NOT EXISTS idx_ledger_rule ON guard_ledger(rule);
|
|
10104
|
+
`);
|
|
10105
|
+
saveDb();
|
|
10106
|
+
}
|
|
10107
|
+
async function recordGuardFlag(rule) {
|
|
10108
|
+
try {
|
|
10109
|
+
await ensureLedgerSchema();
|
|
10110
|
+
const db = await getDb();
|
|
10111
|
+
db.run(`INSERT INTO guard_ledger (rule, verdict) VALUES (?, 'flag')`, [rule]);
|
|
10112
|
+
saveDb();
|
|
10113
|
+
} catch {
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
async function recordGuardFeedback(rule, wasFalsePositive) {
|
|
10117
|
+
try {
|
|
10118
|
+
await ensureLedgerSchema();
|
|
10119
|
+
const db = await getDb();
|
|
10120
|
+
db.run(
|
|
10121
|
+
`INSERT INTO guard_ledger (rule, verdict, false_positive) VALUES (?, 'feedback', ?)`,
|
|
10122
|
+
[rule, wasFalsePositive ? 1 : 0]
|
|
10123
|
+
);
|
|
10124
|
+
saveDb();
|
|
10125
|
+
} catch {
|
|
10126
|
+
}
|
|
10127
|
+
}
|
|
10128
|
+
async function getRulePrecision() {
|
|
10129
|
+
try {
|
|
10130
|
+
await ensureLedgerSchema();
|
|
10131
|
+
const db = await getDb();
|
|
10132
|
+
const stmt = db.prepare(`
|
|
10133
|
+
SELECT rule,
|
|
10134
|
+
SUM(CASE WHEN verdict = 'flag' THEN 1 ELSE 0 END) as flags,
|
|
10135
|
+
SUM(CASE WHEN verdict = 'feedback' THEN 1 ELSE 0 END) as feedbacks,
|
|
10136
|
+
SUM(CASE WHEN verdict = 'feedback' AND false_positive = 1 THEN 1 ELSE 0 END) as fps
|
|
10137
|
+
FROM guard_ledger GROUP BY rule
|
|
10138
|
+
`);
|
|
10139
|
+
const out = [];
|
|
10140
|
+
while (stmt.step()) {
|
|
10141
|
+
const r = stmt.getAsObject();
|
|
10142
|
+
const fpRate = r.feedbacks >= GUARD_FP_MIN_FEEDBACK ? r.fps / r.feedbacks : null;
|
|
10143
|
+
out.push({
|
|
10144
|
+
rule: r.rule,
|
|
10145
|
+
flags: Number(r.flags) || 0,
|
|
10146
|
+
feedbacks: Number(r.feedbacks) || 0,
|
|
10147
|
+
falsePositives: Number(r.fps) || 0,
|
|
10148
|
+
fpRate,
|
|
10149
|
+
overBudget: fpRate !== null && fpRate > GUARD_FP_BUDGET_RATE
|
|
10150
|
+
});
|
|
10151
|
+
}
|
|
10152
|
+
stmt.free();
|
|
10153
|
+
return out;
|
|
10154
|
+
} catch {
|
|
10155
|
+
return [];
|
|
10156
|
+
}
|
|
10157
|
+
}
|
|
10158
|
+
async function getDowntieredRules() {
|
|
10159
|
+
const precision = await getRulePrecision();
|
|
10160
|
+
return new Set(precision.filter((p) => p.overBudget).map((p) => p.rule));
|
|
10161
|
+
}
|
|
10162
|
+
function demoteSeverity(sev) {
|
|
10163
|
+
if (sev === "high") return "medium";
|
|
10164
|
+
if (sev === "medium") return "low";
|
|
10165
|
+
return "low";
|
|
10166
|
+
}
|
|
10167
|
+
function formatPrecisionLedger(rows) {
|
|
10168
|
+
if (rows.length === 0) return "\u{1F4CF} **Guard precision ledger** \u2014 no flags recorded yet.";
|
|
10169
|
+
const lines = ["\u{1F4CF} **Guard precision ledger** (per-rule, FP budget 50% / \u22655 feedbacks)", ""];
|
|
10170
|
+
for (const r of rows) {
|
|
10171
|
+
const fp = r.fpRate === null ? "n/a" : `${Math.round(r.fpRate * 100)}%`;
|
|
10172
|
+
lines.push(` ${r.overBudget ? "\u{1F53B}" : "\u2705"} \`${r.rule}\` \u2014 ${r.flags} flag(s), FP ${fp}${r.overBudget ? " OVER BUDGET (auto-downtiered)" : ""}`);
|
|
10173
|
+
}
|
|
10174
|
+
return lines.join("\n");
|
|
10175
|
+
}
|
|
10176
|
+
var GUARD_FP_BUDGET_RATE, GUARD_FP_MIN_FEEDBACK;
|
|
10177
|
+
var init_guardLedger = __esm({
|
|
10178
|
+
"src/engine/guardLedger.ts"() {
|
|
10179
|
+
"use strict";
|
|
10180
|
+
init_kumaDb();
|
|
10181
|
+
GUARD_FP_BUDGET_RATE = 0.5;
|
|
10182
|
+
GUARD_FP_MIN_FEEDBACK = 5;
|
|
10183
|
+
}
|
|
10184
|
+
});
|
|
10185
|
+
|
|
9244
10186
|
// src/engine/kumaCheckpoint.ts
|
|
9245
10187
|
var kumaCheckpoint_exports = {};
|
|
9246
10188
|
__export(kumaCheckpoint_exports, {
|
|
@@ -9251,27 +10193,27 @@ __export(kumaCheckpoint_exports, {
|
|
|
9251
10193
|
pruneCheckpoints: () => pruneCheckpoints,
|
|
9252
10194
|
rollbackToCheckpoint: () => rollbackToCheckpoint
|
|
9253
10195
|
});
|
|
9254
|
-
import
|
|
9255
|
-
import
|
|
10196
|
+
import fs28 from "fs";
|
|
10197
|
+
import path31 from "path";
|
|
9256
10198
|
async function createCheckpoint(label, description) {
|
|
9257
10199
|
try {
|
|
9258
10200
|
const root = getProjectRoot();
|
|
9259
|
-
const cpDir =
|
|
9260
|
-
if (
|
|
10201
|
+
const cpDir = path31.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
|
|
10202
|
+
if (fs28.existsSync(cpDir)) {
|
|
9261
10203
|
return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
|
|
9262
10204
|
}
|
|
9263
|
-
|
|
10205
|
+
fs28.mkdirSync(cpDir, { recursive: true });
|
|
9264
10206
|
let gitHead;
|
|
9265
10207
|
let isGitClean = false;
|
|
9266
10208
|
try {
|
|
9267
|
-
const { execSync:
|
|
9268
|
-
gitHead =
|
|
10209
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
10210
|
+
gitHead = execSync9("git rev-parse HEAD", {
|
|
9269
10211
|
cwd: root,
|
|
9270
10212
|
encoding: "utf-8",
|
|
9271
10213
|
timeout: 3e3,
|
|
9272
10214
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9273
10215
|
}).trim();
|
|
9274
|
-
const statusOut =
|
|
10216
|
+
const statusOut = execSync9("git status --porcelain", {
|
|
9275
10217
|
cwd: root,
|
|
9276
10218
|
encoding: "utf-8",
|
|
9277
10219
|
timeout: 3e3,
|
|
@@ -9282,8 +10224,8 @@ async function createCheckpoint(label, description) {
|
|
|
9282
10224
|
}
|
|
9283
10225
|
const candidatePaths = /* @__PURE__ */ new Set();
|
|
9284
10226
|
try {
|
|
9285
|
-
const { execSync:
|
|
9286
|
-
const gitOut =
|
|
10227
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
10228
|
+
const gitOut = execSync9("git status --porcelain", {
|
|
9287
10229
|
cwd: root,
|
|
9288
10230
|
encoding: "utf-8",
|
|
9289
10231
|
timeout: 4e3,
|
|
@@ -9326,15 +10268,15 @@ async function createCheckpoint(label, description) {
|
|
|
9326
10268
|
}
|
|
9327
10269
|
const files = [];
|
|
9328
10270
|
for (const fp of candidatePaths) {
|
|
9329
|
-
const fullPath =
|
|
10271
|
+
const fullPath = path31.resolve(root, fp);
|
|
9330
10272
|
try {
|
|
9331
|
-
if (
|
|
9332
|
-
const content =
|
|
10273
|
+
if (fs28.existsSync(fullPath) && fs28.statSync(fullPath).isFile()) {
|
|
10274
|
+
const content = fs28.readFileSync(fullPath, "utf-8");
|
|
9333
10275
|
const hash = simpleHash2(content);
|
|
9334
|
-
const snapFile =
|
|
9335
|
-
const fileDir =
|
|
9336
|
-
if (!
|
|
9337
|
-
|
|
10276
|
+
const snapFile = path31.join(cpDir, "files", `${fp}.kuma-snap`);
|
|
10277
|
+
const fileDir = path31.dirname(snapFile);
|
|
10278
|
+
if (!fs28.existsSync(fileDir)) fs28.mkdirSync(fileDir, { recursive: true });
|
|
10279
|
+
fs28.writeFileSync(snapFile, content, "utf-8");
|
|
9338
10280
|
files.push({ path: fp, hash });
|
|
9339
10281
|
}
|
|
9340
10282
|
} catch {
|
|
@@ -9342,7 +10284,7 @@ async function createCheckpoint(label, description) {
|
|
|
9342
10284
|
}
|
|
9343
10285
|
const db = await getDb();
|
|
9344
10286
|
const dbData = db.export();
|
|
9345
|
-
|
|
10287
|
+
fs28.writeFileSync(path31.join(cpDir, "kuma.db"), Buffer.from(dbData));
|
|
9346
10288
|
const manifest = {
|
|
9347
10289
|
label,
|
|
9348
10290
|
timestamp: Date.now(),
|
|
@@ -9352,8 +10294,8 @@ async function createCheckpoint(label, description) {
|
|
|
9352
10294
|
dbSnapshot: true,
|
|
9353
10295
|
description
|
|
9354
10296
|
};
|
|
9355
|
-
|
|
9356
|
-
|
|
10297
|
+
fs28.writeFileSync(
|
|
10298
|
+
path31.join(cpDir, "manifest.json"),
|
|
9357
10299
|
JSON.stringify(manifest, null, 2),
|
|
9358
10300
|
"utf-8"
|
|
9359
10301
|
);
|
|
@@ -9381,29 +10323,29 @@ async function createCheckpoint(label, description) {
|
|
|
9381
10323
|
async function rollbackToCheckpoint(label) {
|
|
9382
10324
|
try {
|
|
9383
10325
|
const root = getProjectRoot();
|
|
9384
|
-
const cpDir =
|
|
9385
|
-
const manifestPath =
|
|
9386
|
-
if (!
|
|
10326
|
+
const cpDir = path31.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
|
|
10327
|
+
const manifestPath = path31.join(cpDir, "manifest.json");
|
|
10328
|
+
if (!fs28.existsSync(manifestPath)) {
|
|
9387
10329
|
return `\u274C Checkpoint "${label}" not found. Available checkpoints:
|
|
9388
10330
|
|
|
9389
10331
|
${listCheckpoints()}`;
|
|
9390
10332
|
}
|
|
9391
10333
|
const manifest = JSON.parse(
|
|
9392
|
-
|
|
10334
|
+
fs28.readFileSync(manifestPath, "utf-8")
|
|
9393
10335
|
);
|
|
9394
10336
|
let restored = 0;
|
|
9395
10337
|
let failed = 0;
|
|
9396
10338
|
for (const f of manifest.files) {
|
|
9397
|
-
const snapPath =
|
|
9398
|
-
const legacyPath =
|
|
9399
|
-
const sourcePath =
|
|
9400
|
-
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)) {
|
|
9401
10343
|
try {
|
|
9402
|
-
const content =
|
|
9403
|
-
const targetPath =
|
|
9404
|
-
const targetDir =
|
|
9405
|
-
if (!
|
|
9406
|
-
|
|
10344
|
+
const content = fs28.readFileSync(sourcePath, "utf-8");
|
|
10345
|
+
const targetPath = path31.resolve(root, f.path);
|
|
10346
|
+
const targetDir = path31.dirname(targetPath);
|
|
10347
|
+
if (!fs28.existsSync(targetDir)) fs28.mkdirSync(targetDir, { recursive: true });
|
|
10348
|
+
fs28.writeFileSync(targetPath, content, "utf-8");
|
|
9407
10349
|
restored++;
|
|
9408
10350
|
} catch {
|
|
9409
10351
|
failed++;
|
|
@@ -9412,8 +10354,8 @@ ${listCheckpoints()}`;
|
|
|
9412
10354
|
}
|
|
9413
10355
|
if (manifest.files.length === 0 && manifest.gitHead) {
|
|
9414
10356
|
try {
|
|
9415
|
-
const { execSync:
|
|
9416
|
-
|
|
10357
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
10358
|
+
execSync9(`git checkout ${manifest.gitHead} -- .`, {
|
|
9417
10359
|
cwd: root,
|
|
9418
10360
|
encoding: "utf-8",
|
|
9419
10361
|
timeout: 8e3,
|
|
@@ -9424,12 +10366,12 @@ ${listCheckpoints()}`;
|
|
|
9424
10366
|
failed++;
|
|
9425
10367
|
}
|
|
9426
10368
|
}
|
|
9427
|
-
const dbSnapshotPath =
|
|
9428
|
-
if (
|
|
9429
|
-
const kumaDir =
|
|
9430
|
-
const dbPath =
|
|
9431
|
-
const snapshotData =
|
|
9432
|
-
|
|
10369
|
+
const dbSnapshotPath = path31.join(cpDir, "kuma.db");
|
|
10370
|
+
if (fs28.existsSync(dbSnapshotPath)) {
|
|
10371
|
+
const kumaDir = path31.join(root, ".kuma");
|
|
10372
|
+
const dbPath = path31.join(kumaDir, "kuma.db");
|
|
10373
|
+
const snapshotData = fs28.readFileSync(dbSnapshotPath);
|
|
10374
|
+
fs28.writeFileSync(dbPath, snapshotData);
|
|
9433
10375
|
resetDbInstance();
|
|
9434
10376
|
}
|
|
9435
10377
|
sessionMemory.recordToolCall("kuma_checkpoint_rollback", {
|
|
@@ -9454,18 +10396,18 @@ ${listCheckpoints()}`;
|
|
|
9454
10396
|
function listCheckpoints() {
|
|
9455
10397
|
try {
|
|
9456
10398
|
const root = getProjectRoot();
|
|
9457
|
-
const cpDir =
|
|
9458
|
-
if (!
|
|
10399
|
+
const cpDir = path31.join(root, CHECKPOINT_DIR);
|
|
10400
|
+
if (!fs28.existsSync(cpDir)) {
|
|
9459
10401
|
return "\u{1F4ED} No checkpoints found. Use kuma_safety({ action: 'checkpoint', label: 'pre-feature-x' }) to create one.";
|
|
9460
10402
|
}
|
|
9461
|
-
const entries =
|
|
10403
|
+
const entries = fs28.readdirSync(cpDir);
|
|
9462
10404
|
const checkpoints = [];
|
|
9463
10405
|
for (const entry of entries) {
|
|
9464
|
-
const manifestPath =
|
|
9465
|
-
if (
|
|
10406
|
+
const manifestPath = path31.join(cpDir, entry, "manifest.json");
|
|
10407
|
+
if (fs28.existsSync(manifestPath)) {
|
|
9466
10408
|
try {
|
|
9467
10409
|
const manifest = JSON.parse(
|
|
9468
|
-
|
|
10410
|
+
fs28.readFileSync(manifestPath, "utf-8")
|
|
9469
10411
|
);
|
|
9470
10412
|
checkpoints.push({ label: entry, manifest });
|
|
9471
10413
|
} catch {
|
|
@@ -9497,12 +10439,12 @@ function listCheckpoints() {
|
|
|
9497
10439
|
function pruneCheckpoints(keep = 5) {
|
|
9498
10440
|
try {
|
|
9499
10441
|
const root = getProjectRoot();
|
|
9500
|
-
const cpDir =
|
|
9501
|
-
if (!
|
|
9502
|
-
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);
|
|
9503
10445
|
let removed = 0;
|
|
9504
10446
|
for (let i = keep; i < entries.length; i++) {
|
|
9505
|
-
|
|
10447
|
+
fs28.rmSync(path31.join(cpDir, entries[i].name), { recursive: true, force: true });
|
|
9506
10448
|
removed++;
|
|
9507
10449
|
}
|
|
9508
10450
|
return removed > 0 ? `\u{1F9F9} Pruned **${removed} old checkpoint(s)**. Kept ${Math.min(keep, entries.length)} most recent.` : "\u2705 No checkpoints needed pruning.";
|
|
@@ -9527,9 +10469,9 @@ function isRiskyEditPath(filePath) {
|
|
|
9527
10469
|
}
|
|
9528
10470
|
function loadAutoCpDone(root) {
|
|
9529
10471
|
try {
|
|
9530
|
-
const fp =
|
|
9531
|
-
if (
|
|
9532
|
-
const parsed = JSON.parse(
|
|
10472
|
+
const fp = path31.join(root, AUTO_CP_STATE);
|
|
10473
|
+
if (fs28.existsSync(fp)) {
|
|
10474
|
+
const parsed = JSON.parse(fs28.readFileSync(fp, "utf-8"));
|
|
9533
10475
|
return parsed?.autoCheckpoints ?? {};
|
|
9534
10476
|
}
|
|
9535
10477
|
} catch {
|
|
@@ -9538,10 +10480,10 @@ function loadAutoCpDone(root) {
|
|
|
9538
10480
|
}
|
|
9539
10481
|
function saveAutoCpDone(root, done) {
|
|
9540
10482
|
try {
|
|
9541
|
-
const fp =
|
|
9542
|
-
const parsed =
|
|
10483
|
+
const fp = path31.join(root, AUTO_CP_STATE);
|
|
10484
|
+
const parsed = fs28.existsSync(fp) ? JSON.parse(fs28.readFileSync(fp, "utf-8")) : {};
|
|
9543
10485
|
parsed.autoCheckpoints = done;
|
|
9544
|
-
|
|
10486
|
+
fs28.writeFileSync(fp, JSON.stringify(parsed), "utf-8");
|
|
9545
10487
|
} catch {
|
|
9546
10488
|
}
|
|
9547
10489
|
}
|
|
@@ -9551,14 +10493,14 @@ function maybeAutoCheckpoint(filePath) {
|
|
|
9551
10493
|
const root = getProjectRoot();
|
|
9552
10494
|
const done = loadAutoCpDone(root);
|
|
9553
10495
|
if (done[filePath]) return null;
|
|
9554
|
-
const abs =
|
|
9555
|
-
if (!
|
|
9556
|
-
const label = `auto-pre-${sanitizeLabel(
|
|
9557
|
-
const cpDir =
|
|
9558
|
-
if (!
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
10496
|
+
const abs = path31.isAbsolute(filePath) ? filePath : path31.join(root, filePath);
|
|
10497
|
+
if (!fs28.existsSync(abs) || !fs28.statSync(abs).isFile()) return null;
|
|
10498
|
+
const label = `auto-pre-${sanitizeLabel(path31.basename(filePath))}`;
|
|
10499
|
+
const cpDir = path31.join(root, CHECKPOINT_DIR, label);
|
|
10500
|
+
if (!fs28.existsSync(cpDir)) fs28.mkdirSync(cpDir, { recursive: true });
|
|
10501
|
+
fs28.copyFileSync(abs, path31.join(cpDir, path31.basename(abs)));
|
|
10502
|
+
fs28.writeFileSync(
|
|
10503
|
+
path31.join(cpDir, "manifest.json"),
|
|
9562
10504
|
JSON.stringify({
|
|
9563
10505
|
label,
|
|
9564
10506
|
timestamp: Date.now(),
|
|
@@ -9598,8 +10540,8 @@ __export(init_exports, {
|
|
|
9598
10540
|
generateInitMdContent: () => generateInitMdContent,
|
|
9599
10541
|
runInit: () => runInit
|
|
9600
10542
|
});
|
|
9601
|
-
import
|
|
9602
|
-
import
|
|
10543
|
+
import fs29 from "fs";
|
|
10544
|
+
import path32 from "path";
|
|
9603
10545
|
function configFilePath(type) {
|
|
9604
10546
|
switch (type) {
|
|
9605
10547
|
case "claude":
|
|
@@ -9701,11 +10643,10 @@ function opencodeAgentsMdTemplate() {
|
|
|
9701
10643
|
"Record what matters, skip what doesn't.",
|
|
9702
10644
|
"",
|
|
9703
10645
|
"<kuma_obedience>",
|
|
9704
|
-
"## \u{1F512} Obedience (
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
"- MUST NOT call actions outside the core actions.",
|
|
10646
|
+
"## \u{1F512} Obedience \u2014 honest model (issue #39)",
|
|
10647
|
+
"- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
|
|
10648
|
+
'- AGENT-EXPECTED (~70%): call `kuma_kuma_context({ action: "init" })` (or `kuma_context`) first; record gotchas IMMEDIATELY; verify after edits.',
|
|
10649
|
+
"- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
|
|
9709
10650
|
"</kuma_obedience>",
|
|
9710
10651
|
"",
|
|
9711
10652
|
"<kuma_workflow>",
|
|
@@ -9869,6 +10810,7 @@ function geminiTemplate() {
|
|
|
9869
10810
|
'- **Gotchas:** Call `kuma_memory({ action: "gotcha", scope: "<file>", content: "<description>" })` IMMEDIATELY when any quirk/bug is discovered.',
|
|
9870
10811
|
'- **Decisions:** Call `kuma_memory({ action: "decision", title: "...", rationale: "..." })` when choosing between options.',
|
|
9871
10812
|
'- **Post-Edits:** Call `kuma_safety({ action: "verify" })` after modifying code to run affected package/unit tests.',
|
|
10813
|
+
"- **Enforcement (honest model, issue #39):** schema + hooks enforce what they can; the rest is best-effort \u2014 see `.kuma/init.md` for mechanisms and fallbacks.",
|
|
9872
10814
|
"</kuma_obedience>",
|
|
9873
10815
|
"",
|
|
9874
10816
|
"### \u{1F375} Matcha & Kuma Harmony",
|
|
@@ -9948,6 +10890,16 @@ function opencodePluginTemplate() {
|
|
|
9948
10890
|
' const fp = input.filePath || input.path || (input.file && input.file.path) || "";',
|
|
9949
10891
|
' if (fp) console.warn(`\u{1F43B} [Kuma] ${fp} edited \u2014 consider kuma_safety({ action: "verify", scope: "${fp}" })`);',
|
|
9950
10892
|
" },",
|
|
10893
|
+
' "experimental.session.compacting": async (input, output) => {',
|
|
10894
|
+
" // Compaction survival set via the native channel (docs: plugins#compaction).",
|
|
10895
|
+
" try {",
|
|
10896
|
+
' const { execSync } = await import("node:child_process");',
|
|
10897
|
+
' const ctx = execSync("kuma hook pre-compact", { encoding: "utf-8", timeout: 8000 });',
|
|
10898
|
+
' const parsed = JSON.parse(ctx || "{}");',
|
|
10899
|
+
' const add = parsed?.hookSpecificOutput?.hookSpecificOutput?.additionalContext || "";',
|
|
10900
|
+
" if (add && output && Array.isArray(output.context)) output.context.push(add);",
|
|
10901
|
+
" } catch { /* compacting must never fail the session */ }",
|
|
10902
|
+
" },",
|
|
9951
10903
|
" };",
|
|
9952
10904
|
"};",
|
|
9953
10905
|
""
|
|
@@ -10007,15 +10959,26 @@ function generateInitMdContent() {
|
|
|
10007
10959
|
"---",
|
|
10008
10960
|
"",
|
|
10009
10961
|
"<kuma_obedience>",
|
|
10010
|
-
"## \u{1F512} OBEDIENCE (
|
|
10962
|
+
"## \u{1F512} OBEDIENCE \u2014 honest model (issue #39)",
|
|
10011
10963
|
"",
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
"
|
|
10016
|
-
"
|
|
10964
|
+
"Rules are split by what actually guarantees them. A rule written as",
|
|
10965
|
+
"mandatory but silently skippable is worse than an explicit best-effort \u2014",
|
|
10966
|
+
"so every rule below names its mechanism or its fallback.",
|
|
10967
|
+
"",
|
|
10968
|
+
"### HARNESS-ENFORCED (guaranteed by mechanism, zero agent choice)",
|
|
10969
|
+
"",
|
|
10970
|
+
"- **Unknown actions rejected** \u2014 enforced by the MCP schema (invalid action = error, not silence).",
|
|
10971
|
+
'- **Pre-edit gotcha inject** \u2014 enforced by hooks: Claude `PreToolUse` (`kuma hook pre-edit`), OpenCode `tool.execute.before`, Cursor/Windsurf/Copilot/Codex native hooks from `kuma init`. Fallback if hooks missing: call `kuma_context({ action: "history", target: "<file>" })` before editing.',
|
|
10972
|
+
"- **Session brief** \u2014 enforced by `SessionStart` hook (`kuma hook session-start`). Fallback if skipped: the brief lazy-loads on the first unfamiliar edit (history/research include it).",
|
|
10973
|
+
'- **Post-edit map patch** \u2014 enforced by dirty-flags: the pre-edit hook marks files dirty, the next context call syncs + clears. Fallback: `kuma_context({ action: "map" })` rebuilds on demand.',
|
|
10017
10974
|
"",
|
|
10018
|
-
"
|
|
10975
|
+
"### AGENT-EXPECTED (best-effort, designed for ~70% compliance)",
|
|
10976
|
+
"",
|
|
10977
|
+
'1. **Call `kuma_context({ action: "init" })` at session start.** Nudged (non-blocking warning) on any other call first. Fallback if skipped: lazy brief on first unfamiliar edit.',
|
|
10978
|
+
'2. **Record gotchas IMMEDIATELY** (`kuma_memory({ action: "gotcha" })`). Nudged by guard (`error-without-gotcha`). Fallback if skipped: auto-gotcha records after repeated verify failures; CI/evals catch the rest.',
|
|
10979
|
+
'3. **Call `kuma_safety({ action: "verify" })` after edits.** Nudged post-edit by guard + hooks. Fallback if skipped: CI gate is the backstop \u2014 never treat skipped verify as verified.',
|
|
10980
|
+
"4. **Record decisions/arch_flows/research_saves as you go.** No enforcement; value compounds with compliance. Fallback: grep/glob still work \u2014 memory is acceleration, not a gate.",
|
|
10981
|
+
"5. If a Kuma call fails or returns an error, **re-read this file and retry** \u2014 do not silently continue.",
|
|
10019
10982
|
"</kuma_obedience>",
|
|
10020
10983
|
"",
|
|
10021
10984
|
"---",
|
|
@@ -10192,11 +11155,10 @@ function handleOpencodeSecondary(root, results) {
|
|
|
10192
11155
|
"Record what matters, skip what doesn't.",
|
|
10193
11156
|
"",
|
|
10194
11157
|
"<kuma_obedience>",
|
|
10195
|
-
"## \u{1F512} Obedience (
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
"- MUST NOT call actions outside the core actions.",
|
|
11158
|
+
"## \u{1F512} Obedience \u2014 honest model (issue #39)",
|
|
11159
|
+
"- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
|
|
11160
|
+
'- AGENT-EXPECTED (~70%): call `kuma_kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; verify after edits.',
|
|
11161
|
+
"- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
|
|
10200
11162
|
"</kuma_obedience>",
|
|
10201
11163
|
"",
|
|
10202
11164
|
"<kuma_workflow>",
|
|
@@ -10213,21 +11175,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
10213
11175
|
"",
|
|
10214
11176
|
"\u{1F4D6} Full rules: `.kuma/init.md`"
|
|
10215
11177
|
].join("\n");
|
|
10216
|
-
const skillPath =
|
|
11178
|
+
const skillPath = path32.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
|
|
10217
11179
|
if (!results.some((r) => r.filePath === ".agents/skills/kuma/SKILL.md")) {
|
|
10218
11180
|
try {
|
|
10219
|
-
const dir =
|
|
10220
|
-
if (
|
|
10221
|
-
const existing =
|
|
11181
|
+
const dir = path32.dirname(skillPath);
|
|
11182
|
+
if (fs29.existsSync(skillPath)) {
|
|
11183
|
+
const existing = fs29.readFileSync(skillPath, "utf-8");
|
|
10222
11184
|
if (existing.includes("kuma-mcp")) {
|
|
10223
11185
|
results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "skipped" });
|
|
10224
11186
|
} else {
|
|
10225
|
-
|
|
11187
|
+
fs29.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
|
|
10226
11188
|
results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "appended" });
|
|
10227
11189
|
}
|
|
10228
11190
|
} else {
|
|
10229
|
-
if (!
|
|
10230
|
-
|
|
11191
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11192
|
+
fs29.writeFileSync(skillPath, opencodeContent, "utf-8");
|
|
10231
11193
|
results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "created" });
|
|
10232
11194
|
}
|
|
10233
11195
|
} catch (err) {
|
|
@@ -10239,21 +11201,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
10239
11201
|
});
|
|
10240
11202
|
}
|
|
10241
11203
|
}
|
|
10242
|
-
const opencodeSkillPath =
|
|
11204
|
+
const opencodeSkillPath = path32.resolve(root, ".opencode", "skills", "kuma", "SKILL.md");
|
|
10243
11205
|
if (!results.some((r) => r.filePath === ".opencode/skills/kuma/SKILL.md")) {
|
|
10244
11206
|
try {
|
|
10245
|
-
const dir =
|
|
10246
|
-
if (
|
|
10247
|
-
const existing =
|
|
11207
|
+
const dir = path32.dirname(opencodeSkillPath);
|
|
11208
|
+
if (fs29.existsSync(opencodeSkillPath)) {
|
|
11209
|
+
const existing = fs29.readFileSync(opencodeSkillPath, "utf-8");
|
|
10248
11210
|
if (existing.includes("kuma-mcp")) {
|
|
10249
11211
|
results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "skipped" });
|
|
10250
11212
|
} else {
|
|
10251
|
-
|
|
11213
|
+
fs29.writeFileSync(opencodeSkillPath, existing.trimEnd() + "\n\n---\n\n" + opencodeContent, "utf-8");
|
|
10252
11214
|
results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "appended" });
|
|
10253
11215
|
}
|
|
10254
11216
|
} else {
|
|
10255
|
-
if (!
|
|
10256
|
-
|
|
11217
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11218
|
+
fs29.writeFileSync(opencodeSkillPath, opencodeContent, "utf-8");
|
|
10257
11219
|
results.push({ type: "opencode", filePath: ".opencode/skills/kuma/SKILL.md", action: "created" });
|
|
10258
11220
|
}
|
|
10259
11221
|
} catch (err) {
|
|
@@ -10265,21 +11227,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
10265
11227
|
});
|
|
10266
11228
|
}
|
|
10267
11229
|
}
|
|
10268
|
-
const pluginPath =
|
|
11230
|
+
const pluginPath = path32.resolve(root, ".opencode", "plugins", "kuma.js");
|
|
10269
11231
|
if (!results.some((r) => r.filePath === ".opencode/plugins/kuma.js")) {
|
|
10270
11232
|
try {
|
|
10271
|
-
const dir =
|
|
10272
|
-
if (
|
|
10273
|
-
const existing =
|
|
11233
|
+
const dir = path32.dirname(pluginPath);
|
|
11234
|
+
if (fs29.existsSync(pluginPath)) {
|
|
11235
|
+
const existing = fs29.readFileSync(pluginPath, "utf-8");
|
|
10274
11236
|
if (existing.includes("KumaPlugin") || existing.includes("kuma")) {
|
|
10275
11237
|
results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "skipped" });
|
|
10276
11238
|
} else {
|
|
10277
|
-
|
|
11239
|
+
fs29.writeFileSync(pluginPath, existing.trimEnd() + "\n\n" + opencodePluginTemplate(), "utf-8");
|
|
10278
11240
|
results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "appended" });
|
|
10279
11241
|
}
|
|
10280
11242
|
} else {
|
|
10281
|
-
if (!
|
|
10282
|
-
|
|
11243
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11244
|
+
fs29.writeFileSync(pluginPath, opencodePluginTemplate(), "utf-8");
|
|
10283
11245
|
results.push({ type: "opencode", filePath: ".opencode/plugins/kuma.js", action: "created" });
|
|
10284
11246
|
}
|
|
10285
11247
|
} catch (err) {
|
|
@@ -10293,21 +11255,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
10293
11255
|
}
|
|
10294
11256
|
}
|
|
10295
11257
|
function handleCodexSecondary(root, results) {
|
|
10296
|
-
const tomlPath =
|
|
11258
|
+
const tomlPath = path32.resolve(root, ".codex/config.toml");
|
|
10297
11259
|
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
10298
11260
|
try {
|
|
10299
|
-
const dir =
|
|
10300
|
-
if (!
|
|
10301
|
-
if (
|
|
10302
|
-
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");
|
|
10303
11265
|
if (existingContent.includes("kuma")) {
|
|
10304
11266
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
10305
11267
|
return;
|
|
10306
11268
|
}
|
|
10307
|
-
|
|
11269
|
+
fs29.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
10308
11270
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
10309
11271
|
} else {
|
|
10310
|
-
|
|
11272
|
+
fs29.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
10311
11273
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
10312
11274
|
}
|
|
10313
11275
|
} catch (err) {
|
|
@@ -10320,11 +11282,11 @@ function handleCodexSecondary(root, results) {
|
|
|
10320
11282
|
}
|
|
10321
11283
|
}
|
|
10322
11284
|
function handleQwenSecondary(root, results) {
|
|
10323
|
-
const settingsPath =
|
|
11285
|
+
const settingsPath = path32.resolve(root, "settings.json");
|
|
10324
11286
|
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
10325
11287
|
try {
|
|
10326
|
-
if (
|
|
10327
|
-
const existingContent =
|
|
11288
|
+
if (fs29.existsSync(settingsPath)) {
|
|
11289
|
+
const existingContent = fs29.readFileSync(settingsPath, "utf-8");
|
|
10328
11290
|
if (existingContent.includes("kuma")) {
|
|
10329
11291
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
10330
11292
|
try {
|
|
@@ -10332,7 +11294,7 @@ function handleQwenSecondary(root, results) {
|
|
|
10332
11294
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
10333
11295
|
if (!parsed.mcpServers.kuma) {
|
|
10334
11296
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
10335
|
-
|
|
11297
|
+
fs29.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
10336
11298
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
10337
11299
|
return;
|
|
10338
11300
|
}
|
|
@@ -10347,13 +11309,13 @@ function handleQwenSecondary(root, results) {
|
|
|
10347
11309
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
10348
11310
|
if (!parsed.mcpServers.kuma) {
|
|
10349
11311
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
10350
|
-
|
|
11312
|
+
fs29.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
10351
11313
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
10352
11314
|
}
|
|
10353
11315
|
} catch {
|
|
10354
11316
|
}
|
|
10355
11317
|
} else {
|
|
10356
|
-
|
|
11318
|
+
fs29.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
10357
11319
|
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
10358
11320
|
}
|
|
10359
11321
|
} catch (err) {
|
|
@@ -10376,11 +11338,10 @@ function getCombinedAgentsMd(selectedTypes) {
|
|
|
10376
11338
|
"## SESSION WORKFLOW",
|
|
10377
11339
|
"",
|
|
10378
11340
|
"<kuma_obedience>",
|
|
10379
|
-
"## \u{1F512} Obedience (
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
"- MUST NOT call actions outside the 13 core actions.",
|
|
11341
|
+
"## \u{1F512} Obedience \u2014 honest model (issue #39)",
|
|
11342
|
+
"- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
|
|
11343
|
+
'- AGENT-EXPECTED (~70%): call `[kuma_]context({ action: "init" })` first; record gotchas IMMEDIATELY; verify after edits.',
|
|
11344
|
+
"- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
|
|
10384
11345
|
"</kuma_obedience>",
|
|
10385
11346
|
"",
|
|
10386
11347
|
"<kuma_workflow>",
|
|
@@ -10456,12 +11417,12 @@ function getCombinedAgentsMd(selectedTypes) {
|
|
|
10456
11417
|
return lines.join("\n");
|
|
10457
11418
|
}
|
|
10458
11419
|
function handleAntigravityMcpConfig(root, results) {
|
|
10459
|
-
const mcpPath =
|
|
11420
|
+
const mcpPath = path32.resolve(root, ".agents/mcp_config.json");
|
|
10460
11421
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
10461
11422
|
try {
|
|
10462
|
-
const mcpDir =
|
|
10463
|
-
if (
|
|
10464
|
-
const existingContent =
|
|
11423
|
+
const mcpDir = path32.dirname(mcpPath);
|
|
11424
|
+
if (fs29.existsSync(mcpPath)) {
|
|
11425
|
+
const existingContent = fs29.readFileSync(mcpPath, "utf-8");
|
|
10465
11426
|
if (existingContent.includes("kuma")) {
|
|
10466
11427
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "skipped" });
|
|
10467
11428
|
return;
|
|
@@ -10470,14 +11431,14 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
10470
11431
|
const parsed = JSON.parse(existingContent);
|
|
10471
11432
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
10472
11433
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
10473
|
-
|
|
11434
|
+
fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
10474
11435
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
10475
11436
|
} catch {
|
|
10476
11437
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "error", error: "Invalid JSON in existing mcp_config.json" });
|
|
10477
11438
|
}
|
|
10478
11439
|
} else {
|
|
10479
|
-
if (!
|
|
10480
|
-
|
|
11440
|
+
if (!fs29.existsSync(mcpDir)) fs29.mkdirSync(mcpDir, { recursive: true });
|
|
11441
|
+
fs29.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
10481
11442
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
10482
11443
|
}
|
|
10483
11444
|
} catch (err) {
|
|
@@ -10490,21 +11451,21 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
10490
11451
|
}
|
|
10491
11452
|
}
|
|
10492
11453
|
function handleAntigravityRules(root, results) {
|
|
10493
|
-
const rulePath =
|
|
11454
|
+
const rulePath = path32.resolve(root, ".agents", "rules", "kuma.md");
|
|
10494
11455
|
if (results.some((r) => r.filePath === ".agents/rules/kuma.md")) return;
|
|
10495
11456
|
try {
|
|
10496
|
-
const dir =
|
|
10497
|
-
if (!
|
|
10498
|
-
if (
|
|
10499
|
-
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");
|
|
10500
11461
|
if (existing.includes("Kuma MCP") || existing.includes("kuma-mcp")) {
|
|
10501
11462
|
results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "skipped" });
|
|
10502
11463
|
return;
|
|
10503
11464
|
}
|
|
10504
|
-
|
|
11465
|
+
fs29.writeFileSync(rulePath, existing.trimEnd() + "\n\n---\n\n" + antigravityRuleTemplate(), "utf-8");
|
|
10505
11466
|
results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "appended" });
|
|
10506
11467
|
} else {
|
|
10507
|
-
|
|
11468
|
+
fs29.writeFileSync(rulePath, antigravityRuleTemplate(), "utf-8");
|
|
10508
11469
|
results.push({ type: "antigravity", filePath: ".agents/rules/kuma.md", action: "created" });
|
|
10509
11470
|
}
|
|
10510
11471
|
} catch (err) {
|
|
@@ -10517,19 +11478,19 @@ function handleAntigravityRules(root, results) {
|
|
|
10517
11478
|
}
|
|
10518
11479
|
}
|
|
10519
11480
|
function handleGeminiMd(root, results) {
|
|
10520
|
-
const geminiPath =
|
|
11481
|
+
const geminiPath = path32.resolve(root, "GEMINI.md");
|
|
10521
11482
|
if (results.some((r) => r.filePath === "GEMINI.md")) return;
|
|
10522
11483
|
try {
|
|
10523
|
-
if (
|
|
10524
|
-
const existing =
|
|
11484
|
+
if (fs29.existsSync(geminiPath)) {
|
|
11485
|
+
const existing = fs29.readFileSync(geminiPath, "utf-8");
|
|
10525
11486
|
if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
|
|
10526
11487
|
results.push({ type: "antigravity", filePath: "GEMINI.md", action: "skipped" });
|
|
10527
11488
|
return;
|
|
10528
11489
|
}
|
|
10529
|
-
|
|
11490
|
+
fs29.writeFileSync(geminiPath, existing.trimEnd() + "\n\n---\n\n" + geminiTemplate(), "utf-8");
|
|
10530
11491
|
results.push({ type: "antigravity", filePath: "GEMINI.md", action: "appended" });
|
|
10531
11492
|
} else {
|
|
10532
|
-
|
|
11493
|
+
fs29.writeFileSync(geminiPath, geminiTemplate(), "utf-8");
|
|
10533
11494
|
results.push({ type: "antigravity", filePath: "GEMINI.md", action: "created" });
|
|
10534
11495
|
}
|
|
10535
11496
|
} catch (err) {
|
|
@@ -10547,19 +11508,19 @@ function handleAntigravitySecondary(root, results) {
|
|
|
10547
11508
|
handleGeminiMd(root, results);
|
|
10548
11509
|
}
|
|
10549
11510
|
function handleWindsurfSecondary(root, results) {
|
|
10550
|
-
const rulesPath =
|
|
11511
|
+
const rulesPath = path32.resolve(root, ".windsurfrules");
|
|
10551
11512
|
if (results.some((r) => r.filePath === ".windsurfrules")) return;
|
|
10552
11513
|
try {
|
|
10553
|
-
if (
|
|
10554
|
-
const existing =
|
|
11514
|
+
if (fs29.existsSync(rulesPath)) {
|
|
11515
|
+
const existing = fs29.readFileSync(rulesPath, "utf-8");
|
|
10555
11516
|
if (existing.includes("Kuma MCP") || existing.includes("kuma_context")) {
|
|
10556
11517
|
results.push({ type: "windsurf", filePath: ".windsurfrules", action: "skipped" });
|
|
10557
11518
|
return;
|
|
10558
11519
|
}
|
|
10559
|
-
|
|
11520
|
+
fs29.writeFileSync(rulesPath, existing.trimEnd() + "\n\n---\n\n" + windsurfrulesTemplate(), "utf-8");
|
|
10560
11521
|
results.push({ type: "windsurf", filePath: ".windsurfrules", action: "appended" });
|
|
10561
11522
|
} else {
|
|
10562
|
-
|
|
11523
|
+
fs29.writeFileSync(rulesPath, windsurfrulesTemplate(), "utf-8");
|
|
10563
11524
|
results.push({ type: "windsurf", filePath: ".windsurfrules", action: "created" });
|
|
10564
11525
|
}
|
|
10565
11526
|
} catch (err) {
|
|
@@ -10572,21 +11533,21 @@ function handleWindsurfSecondary(root, results) {
|
|
|
10572
11533
|
}
|
|
10573
11534
|
}
|
|
10574
11535
|
function handleClineSecondary(root, results) {
|
|
10575
|
-
const rooPath =
|
|
11536
|
+
const rooPath = path32.resolve(root, ".roo", "rules", "kuma.md");
|
|
10576
11537
|
if (!results.some((r) => r.filePath === ".roo/rules/kuma.md")) {
|
|
10577
11538
|
try {
|
|
10578
|
-
const dir =
|
|
10579
|
-
if (
|
|
10580
|
-
const existing =
|
|
11539
|
+
const dir = path32.dirname(rooPath);
|
|
11540
|
+
if (fs29.existsSync(rooPath)) {
|
|
11541
|
+
const existing = fs29.readFileSync(rooPath, "utf-8");
|
|
10581
11542
|
if (existing.includes("kuma-mcp") || existing.includes("Kuma MCP")) {
|
|
10582
11543
|
results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "skipped" });
|
|
10583
11544
|
} else {
|
|
10584
|
-
|
|
11545
|
+
fs29.writeFileSync(rooPath, existing.trimEnd() + "\n\n---\n\n" + rooRulesTemplate(), "utf-8");
|
|
10585
11546
|
results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "appended" });
|
|
10586
11547
|
}
|
|
10587
11548
|
} else {
|
|
10588
|
-
if (!
|
|
10589
|
-
|
|
11549
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11550
|
+
fs29.writeFileSync(rooPath, rooRulesTemplate(), "utf-8");
|
|
10590
11551
|
results.push({ type: "cline", filePath: ".roo/rules/kuma.md", action: "created" });
|
|
10591
11552
|
}
|
|
10592
11553
|
} catch (err) {
|
|
@@ -10598,21 +11559,21 @@ function handleClineSecondary(root, results) {
|
|
|
10598
11559
|
});
|
|
10599
11560
|
}
|
|
10600
11561
|
}
|
|
10601
|
-
const traePath =
|
|
11562
|
+
const traePath = path32.resolve(root, ".trae", "rules", "kuma.md");
|
|
10602
11563
|
if (!results.some((r) => r.filePath === ".trae/rules/kuma.md")) {
|
|
10603
11564
|
try {
|
|
10604
|
-
const dir =
|
|
10605
|
-
if (
|
|
10606
|
-
const existing =
|
|
11565
|
+
const dir = path32.dirname(traePath);
|
|
11566
|
+
if (fs29.existsSync(traePath)) {
|
|
11567
|
+
const existing = fs29.readFileSync(traePath, "utf-8");
|
|
10607
11568
|
if (existing.includes("Kuma MCP")) {
|
|
10608
11569
|
results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "skipped" });
|
|
10609
11570
|
} else {
|
|
10610
|
-
|
|
11571
|
+
fs29.writeFileSync(traePath, existing.trimEnd() + "\n\n---\n\n" + traeRulesTemplate(), "utf-8");
|
|
10611
11572
|
results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "appended" });
|
|
10612
11573
|
}
|
|
10613
11574
|
} else {
|
|
10614
|
-
if (!
|
|
10615
|
-
|
|
11575
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11576
|
+
fs29.writeFileSync(traePath, traeRulesTemplate(), "utf-8");
|
|
10616
11577
|
results.push({ type: "cline", filePath: ".trae/rules/kuma.md", action: "created" });
|
|
10617
11578
|
}
|
|
10618
11579
|
} catch (err) {
|
|
@@ -10624,21 +11585,21 @@ function handleClineSecondary(root, results) {
|
|
|
10624
11585
|
});
|
|
10625
11586
|
}
|
|
10626
11587
|
}
|
|
10627
|
-
const qoderPath =
|
|
11588
|
+
const qoderPath = path32.resolve(root, ".qoder", "rules", "kuma.md");
|
|
10628
11589
|
if (!results.some((r) => r.filePath === ".qoder/rules/kuma.md")) {
|
|
10629
11590
|
try {
|
|
10630
|
-
const dir =
|
|
10631
|
-
if (
|
|
10632
|
-
const existing =
|
|
11591
|
+
const dir = path32.dirname(qoderPath);
|
|
11592
|
+
if (fs29.existsSync(qoderPath)) {
|
|
11593
|
+
const existing = fs29.readFileSync(qoderPath, "utf-8");
|
|
10633
11594
|
if (existing.includes("Kuma MCP")) {
|
|
10634
11595
|
results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "skipped" });
|
|
10635
11596
|
} else {
|
|
10636
|
-
|
|
11597
|
+
fs29.writeFileSync(qoderPath, existing.trimEnd() + "\n\n---\n\n" + qoderRulesTemplate(), "utf-8");
|
|
10637
11598
|
results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "appended" });
|
|
10638
11599
|
}
|
|
10639
11600
|
} else {
|
|
10640
|
-
if (!
|
|
10641
|
-
|
|
11601
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11602
|
+
fs29.writeFileSync(qoderPath, qoderRulesTemplate(), "utf-8");
|
|
10642
11603
|
results.push({ type: "cline", filePath: ".qoder/rules/kuma.md", action: "created" });
|
|
10643
11604
|
}
|
|
10644
11605
|
} catch (err) {
|
|
@@ -10652,18 +11613,18 @@ function handleClineSecondary(root, results) {
|
|
|
10652
11613
|
}
|
|
10653
11614
|
}
|
|
10654
11615
|
function handleAiderSecondary(root, results) {
|
|
10655
|
-
const ymlPath =
|
|
11616
|
+
const ymlPath = path32.resolve(root, ".aider.conf.yml");
|
|
10656
11617
|
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
10657
11618
|
try {
|
|
10658
11619
|
const conventionsRef = "read: CONVENTIONS.md";
|
|
10659
|
-
if (
|
|
10660
|
-
const existingContent =
|
|
11620
|
+
if (fs29.existsSync(ymlPath)) {
|
|
11621
|
+
const existingContent = fs29.readFileSync(ymlPath, "utf-8");
|
|
10661
11622
|
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
10662
11623
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
10663
11624
|
return;
|
|
10664
11625
|
}
|
|
10665
11626
|
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
10666
|
-
|
|
11627
|
+
fs29.writeFileSync(ymlPath, newContent, "utf-8");
|
|
10667
11628
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
10668
11629
|
} else {
|
|
10669
11630
|
const content = [
|
|
@@ -10673,7 +11634,7 @@ function handleAiderSecondary(root, results) {
|
|
|
10673
11634
|
conventionsRef,
|
|
10674
11635
|
""
|
|
10675
11636
|
].join("\n");
|
|
10676
|
-
|
|
11637
|
+
fs29.writeFileSync(ymlPath, content, "utf-8");
|
|
10677
11638
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
10678
11639
|
}
|
|
10679
11640
|
} catch (err) {
|
|
@@ -10686,10 +11647,10 @@ function handleAiderSecondary(root, results) {
|
|
|
10686
11647
|
}
|
|
10687
11648
|
}
|
|
10688
11649
|
function handleCopilotSecondary(root, results) {
|
|
10689
|
-
const skillPath =
|
|
11650
|
+
const skillPath = path32.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
10690
11651
|
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
10691
11652
|
try {
|
|
10692
|
-
const dir =
|
|
11653
|
+
const dir = path32.dirname(skillPath);
|
|
10693
11654
|
const content = [
|
|
10694
11655
|
"---",
|
|
10695
11656
|
"name: kuma-mcp",
|
|
@@ -10700,17 +11661,17 @@ function handleCopilotSecondary(root, results) {
|
|
|
10700
11661
|
"",
|
|
10701
11662
|
"\u{1F4D6} Read `.kuma/init.md` for detailed rules."
|
|
10702
11663
|
].join("\n");
|
|
10703
|
-
if (
|
|
10704
|
-
const existingContent =
|
|
11664
|
+
if (fs29.existsSync(skillPath)) {
|
|
11665
|
+
const existingContent = fs29.readFileSync(skillPath, "utf-8");
|
|
10705
11666
|
if (existingContent.includes("kuma")) {
|
|
10706
11667
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
10707
11668
|
return;
|
|
10708
11669
|
}
|
|
10709
|
-
|
|
11670
|
+
fs29.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
10710
11671
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
10711
11672
|
} else {
|
|
10712
|
-
if (!
|
|
10713
|
-
|
|
11673
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11674
|
+
fs29.writeFileSync(skillPath, content, "utf-8");
|
|
10714
11675
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
10715
11676
|
}
|
|
10716
11677
|
} catch (err) {
|
|
@@ -10723,21 +11684,21 @@ function handleCopilotSecondary(root, results) {
|
|
|
10723
11684
|
}
|
|
10724
11685
|
}
|
|
10725
11686
|
function handleOpenclawSecondary(root, results) {
|
|
10726
|
-
const mcpPath =
|
|
11687
|
+
const mcpPath = path32.resolve(root, ".agents/mcp_config.json");
|
|
10727
11688
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
10728
11689
|
try {
|
|
10729
|
-
const dir =
|
|
10730
|
-
if (
|
|
10731
|
-
const existingContent =
|
|
11690
|
+
const dir = path32.dirname(mcpPath);
|
|
11691
|
+
if (fs29.existsSync(mcpPath)) {
|
|
11692
|
+
const existingContent = fs29.readFileSync(mcpPath, "utf-8");
|
|
10732
11693
|
if (existingContent.includes("kuma")) return;
|
|
10733
11694
|
const parsed = JSON.parse(existingContent);
|
|
10734
11695
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
10735
11696
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
10736
|
-
|
|
11697
|
+
fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
10737
11698
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
10738
11699
|
} else {
|
|
10739
|
-
if (!
|
|
10740
|
-
|
|
11700
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11701
|
+
fs29.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
10741
11702
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
10742
11703
|
}
|
|
10743
11704
|
} catch (err) {
|
|
@@ -10750,20 +11711,20 @@ function handleOpenclawSecondary(root, results) {
|
|
|
10750
11711
|
}
|
|
10751
11712
|
}
|
|
10752
11713
|
function handleInitMdGeneration(root, results) {
|
|
10753
|
-
const initMdPath =
|
|
11714
|
+
const initMdPath = path32.resolve(root, ".kuma/init.md");
|
|
10754
11715
|
try {
|
|
10755
|
-
const kumaDir =
|
|
10756
|
-
if (!
|
|
10757
|
-
if (
|
|
10758
|
-
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");
|
|
10759
11720
|
if (existing.includes("_Generated by Kuma MCP_")) {
|
|
10760
11721
|
results.push({ type: "claude", filePath: ".kuma/init.md", action: "skipped" });
|
|
10761
11722
|
return;
|
|
10762
11723
|
}
|
|
10763
|
-
|
|
11724
|
+
fs29.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
|
|
10764
11725
|
results.push({ type: "claude", filePath: ".kuma/init.md", action: "appended" });
|
|
10765
11726
|
} else {
|
|
10766
|
-
|
|
11727
|
+
fs29.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
|
|
10767
11728
|
results.push({ type: "claude", filePath: ".kuma/init.md", action: "created" });
|
|
10768
11729
|
}
|
|
10769
11730
|
} catch (err) {
|
|
@@ -10776,20 +11737,20 @@ function handleInitMdGeneration(root, results) {
|
|
|
10776
11737
|
}
|
|
10777
11738
|
}
|
|
10778
11739
|
function handleCodewhaleSecondary(root, results) {
|
|
10779
|
-
const mcpPath =
|
|
11740
|
+
const mcpPath = path32.resolve(root, ".codewhale/mcp.json");
|
|
10780
11741
|
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
10781
11742
|
try {
|
|
10782
|
-
const dir =
|
|
10783
|
-
if (
|
|
10784
|
-
const existingContent =
|
|
11743
|
+
const dir = path32.dirname(mcpPath);
|
|
11744
|
+
if (fs29.existsSync(mcpPath)) {
|
|
11745
|
+
const existingContent = fs29.readFileSync(mcpPath, "utf-8");
|
|
10785
11746
|
if (existingContent.includes("kuma")) return;
|
|
10786
11747
|
const parsed = JSON.parse(existingContent);
|
|
10787
11748
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
10788
11749
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
10789
|
-
|
|
11750
|
+
fs29.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
10790
11751
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
10791
11752
|
} else {
|
|
10792
|
-
if (!
|
|
11753
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
10793
11754
|
const config = {
|
|
10794
11755
|
mcpServers: {
|
|
10795
11756
|
kuma: {
|
|
@@ -10799,7 +11760,7 @@ function handleCodewhaleSecondary(root, results) {
|
|
|
10799
11760
|
}
|
|
10800
11761
|
}
|
|
10801
11762
|
};
|
|
10802
|
-
|
|
11763
|
+
fs29.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
10803
11764
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
10804
11765
|
}
|
|
10805
11766
|
} catch (err) {
|
|
@@ -10812,7 +11773,7 @@ function handleCodewhaleSecondary(root, results) {
|
|
|
10812
11773
|
}
|
|
10813
11774
|
}
|
|
10814
11775
|
function handleClaudeSecondary(root, results) {
|
|
10815
|
-
const settingsPath =
|
|
11776
|
+
const settingsPath = path32.resolve(root, ".claude", "settings.json");
|
|
10816
11777
|
const hookBlock = {
|
|
10817
11778
|
hooks: {
|
|
10818
11779
|
PreToolUse: [
|
|
@@ -10824,32 +11785,56 @@ function handleClaudeSecondary(root, results) {
|
|
|
10824
11785
|
matcher: "Bash",
|
|
10825
11786
|
hooks: [{ type: "command", command: "kuma hook pre-bash" }]
|
|
10826
11787
|
}
|
|
11788
|
+
],
|
|
11789
|
+
// Issue #38: SessionStart serves the cached repo brief before the
|
|
11790
|
+
// first agent turn — zero agent choice involved.
|
|
11791
|
+
SessionStart: [
|
|
11792
|
+
{
|
|
11793
|
+
matcher: "startup",
|
|
11794
|
+
hooks: [{ type: "command", command: "kuma hook session-start" }]
|
|
11795
|
+
}
|
|
11796
|
+
],
|
|
11797
|
+
// Compaction survival set (goal + critical gotchas + dirty files).
|
|
11798
|
+
PreCompact: [
|
|
11799
|
+
{
|
|
11800
|
+
matcher: "",
|
|
11801
|
+
hooks: [{ type: "command", command: "kuma hook pre-compact" }]
|
|
11802
|
+
}
|
|
10827
11803
|
]
|
|
10828
11804
|
}
|
|
10829
11805
|
};
|
|
10830
11806
|
try {
|
|
10831
|
-
if (
|
|
10832
|
-
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
|
+
}
|
|
10833
11828
|
const merged = {
|
|
10834
11829
|
...existing,
|
|
10835
|
-
hooks: {
|
|
10836
|
-
...existing.hooks || {},
|
|
10837
|
-
...hookBlock.hooks
|
|
10838
|
-
}
|
|
11830
|
+
hooks: { ...existingHooks, PreToolUse: mergedPre, SessionStart: mergedStart, PreCompact: mergedCompact }
|
|
10839
11831
|
};
|
|
10840
|
-
|
|
10841
|
-
for (const entry of hookBlock.hooks.PreToolUse) {
|
|
10842
|
-
if (!matchers.has(entry.matcher)) {
|
|
10843
|
-
merged.hooks.PreToolUse = [...merged.hooks.PreToolUse || [], entry];
|
|
10844
|
-
matchers.add(entry.matcher);
|
|
10845
|
-
}
|
|
10846
|
-
}
|
|
10847
|
-
fs25.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
11832
|
+
fs29.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
10848
11833
|
results.push({ type: "claude", filePath: ".claude/settings.json", action: "appended" });
|
|
10849
11834
|
} else {
|
|
10850
|
-
const dir =
|
|
10851
|
-
if (!
|
|
10852
|
-
|
|
11835
|
+
const dir = path32.dirname(settingsPath);
|
|
11836
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11837
|
+
fs29.writeFileSync(settingsPath, JSON.stringify(hookBlock, null, 2) + "\n", "utf-8");
|
|
10853
11838
|
results.push({ type: "claude", filePath: ".claude/settings.json", action: "created" });
|
|
10854
11839
|
}
|
|
10855
11840
|
} catch (err) {
|
|
@@ -10860,10 +11845,10 @@ function handleClaudeSecondary(root, results) {
|
|
|
10860
11845
|
error: err instanceof Error ? err.message : String(err)
|
|
10861
11846
|
});
|
|
10862
11847
|
}
|
|
10863
|
-
const skillPath =
|
|
11848
|
+
const skillPath = path32.resolve(root, ".claude", "skills", "kuma", "SKILL.md");
|
|
10864
11849
|
if (!results.some((r) => r.filePath === ".claude/skills/kuma/SKILL.md")) {
|
|
10865
11850
|
try {
|
|
10866
|
-
const dir =
|
|
11851
|
+
const dir = path32.dirname(skillPath);
|
|
10867
11852
|
const content = [
|
|
10868
11853
|
"---",
|
|
10869
11854
|
"name: kuma-mcp",
|
|
@@ -10875,17 +11860,17 @@ function handleClaudeSecondary(root, results) {
|
|
|
10875
11860
|
"\u{1F4D6} Read `.kuma/init.md` for detailed rules.",
|
|
10876
11861
|
"\u{1F4C2} Readable state: `.kuma/KNOWN_GOTCHAS.md` (gotchas) \xB7 `.kuma/memories/decisions.md` (decisions)"
|
|
10877
11862
|
].join("\n");
|
|
10878
|
-
if (
|
|
10879
|
-
const existing =
|
|
11863
|
+
if (fs29.existsSync(skillPath)) {
|
|
11864
|
+
const existing = fs29.readFileSync(skillPath, "utf-8");
|
|
10880
11865
|
if (existing.includes("kuma-mcp")) {
|
|
10881
11866
|
results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "skipped" });
|
|
10882
11867
|
} else {
|
|
10883
|
-
|
|
11868
|
+
fs29.writeFileSync(skillPath, existing.trimEnd() + "\n\n---\n\n" + content, "utf-8");
|
|
10884
11869
|
results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "appended" });
|
|
10885
11870
|
}
|
|
10886
11871
|
} else {
|
|
10887
|
-
if (!
|
|
10888
|
-
|
|
11872
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
11873
|
+
fs29.writeFileSync(skillPath, content, "utf-8");
|
|
10889
11874
|
results.push({ type: "claude", filePath: ".claude/skills/kuma/SKILL.md", action: "created" });
|
|
10890
11875
|
}
|
|
10891
11876
|
} catch (err) {
|
|
@@ -10899,27 +11884,27 @@ function handleClaudeSecondary(root, results) {
|
|
|
10899
11884
|
}
|
|
10900
11885
|
}
|
|
10901
11886
|
function writeHooksFile(root, relPath, type, results, build) {
|
|
10902
|
-
const fullPath =
|
|
11887
|
+
const fullPath = path32.resolve(root, relPath);
|
|
10903
11888
|
try {
|
|
10904
11889
|
let existing = {};
|
|
10905
11890
|
let existed = false;
|
|
10906
|
-
if (
|
|
11891
|
+
if (fs29.existsSync(fullPath)) {
|
|
10907
11892
|
existed = true;
|
|
10908
11893
|
try {
|
|
10909
|
-
existing = JSON.parse(
|
|
11894
|
+
existing = JSON.parse(fs29.readFileSync(fullPath, "utf-8"));
|
|
10910
11895
|
} catch {
|
|
10911
11896
|
existing = {};
|
|
10912
11897
|
}
|
|
10913
11898
|
} else {
|
|
10914
|
-
const dir =
|
|
10915
|
-
if (!
|
|
11899
|
+
const dir = path32.dirname(fullPath);
|
|
11900
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
10916
11901
|
}
|
|
10917
11902
|
const { doc, changed } = build(existing);
|
|
10918
11903
|
if (!changed) {
|
|
10919
11904
|
results.push({ type, filePath: relPath, action: "skipped" });
|
|
10920
11905
|
return;
|
|
10921
11906
|
}
|
|
10922
|
-
|
|
11907
|
+
fs29.writeFileSync(fullPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
10923
11908
|
results.push({ type, filePath: relPath, action: existed ? "appended" : "created" });
|
|
10924
11909
|
} catch (err) {
|
|
10925
11910
|
results.push({ type, filePath: relPath, action: "error", error: err instanceof Error ? err.message : String(err) });
|
|
@@ -11000,18 +11985,18 @@ function handleCodexHooks(root, results) {
|
|
|
11000
11985
|
});
|
|
11001
11986
|
}
|
|
11002
11987
|
function handleCursorGotchaRules(root, results) {
|
|
11003
|
-
const rulesDir2 =
|
|
11988
|
+
const rulesDir2 = path32.resolve(root, ".cursor", "rules", "kuma-gotchas");
|
|
11004
11989
|
try {
|
|
11005
11990
|
let gotchas = [];
|
|
11006
11991
|
try {
|
|
11007
11992
|
gotchas = getActiveGotchas().filter((g) => g.severity === "high" || g.severity === "critical");
|
|
11008
11993
|
} catch {
|
|
11009
11994
|
}
|
|
11010
|
-
if (
|
|
11011
|
-
for (const f of
|
|
11995
|
+
if (fs29.existsSync(rulesDir2)) {
|
|
11996
|
+
for (const f of fs29.readdirSync(rulesDir2)) {
|
|
11012
11997
|
if (f.endsWith(".mdc")) {
|
|
11013
11998
|
try {
|
|
11014
|
-
|
|
11999
|
+
fs29.rmSync(path32.join(rulesDir2, f), { force: true });
|
|
11015
12000
|
} catch {
|
|
11016
12001
|
}
|
|
11017
12002
|
}
|
|
@@ -11021,13 +12006,13 @@ function handleCursorGotchaRules(root, results) {
|
|
|
11021
12006
|
results.push({ type: "cursor", filePath: ".cursor/rules/kuma-gotchas/", action: "skipped" });
|
|
11022
12007
|
return;
|
|
11023
12008
|
}
|
|
11024
|
-
|
|
12009
|
+
fs29.mkdirSync(rulesDir2, { recursive: true });
|
|
11025
12010
|
const seen = /* @__PURE__ */ new Set();
|
|
11026
12011
|
let created = 0;
|
|
11027
12012
|
for (const g of gotchas) {
|
|
11028
|
-
const
|
|
11029
|
-
if (!
|
|
11030
|
-
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);
|
|
11031
12016
|
const md = [
|
|
11032
12017
|
"---",
|
|
11033
12018
|
`description: "KUMA gotcha \u2014 ${g.description.substring(0, 80)}"`,
|
|
@@ -11042,7 +12027,7 @@ function handleCursorGotchaRules(root, results) {
|
|
|
11042
12027
|
"`kuma_context({ action: 'history', target: '<file>' })`",
|
|
11043
12028
|
""
|
|
11044
12029
|
].join("\n");
|
|
11045
|
-
|
|
12030
|
+
fs29.writeFileSync(path32.join(rulesDir2, `${slug2}.mdc`), md, "utf-8");
|
|
11046
12031
|
created++;
|
|
11047
12032
|
}
|
|
11048
12033
|
results.push({
|
|
@@ -11069,28 +12054,28 @@ function runInit(options) {
|
|
|
11069
12054
|
let agentsMdHandled = false;
|
|
11070
12055
|
for (const type of selected) {
|
|
11071
12056
|
const relativePath = configFilePath(type);
|
|
11072
|
-
const fullPath =
|
|
12057
|
+
const fullPath = path32.resolve(root, relativePath);
|
|
11073
12058
|
const getTemplate = TEMPLATES[type];
|
|
11074
12059
|
try {
|
|
11075
12060
|
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
11076
12061
|
agentsMdHandled = true;
|
|
11077
12062
|
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
11078
|
-
if (
|
|
12063
|
+
if (fs29.existsSync(fullPath)) {
|
|
11079
12064
|
if (options.skipExisting) {
|
|
11080
12065
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
11081
12066
|
} else {
|
|
11082
|
-
const existingContent =
|
|
12067
|
+
const existingContent = fs29.readFileSync(fullPath, "utf-8");
|
|
11083
12068
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
11084
12069
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
11085
12070
|
} else {
|
|
11086
|
-
|
|
12071
|
+
fs29.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
11087
12072
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
11088
12073
|
}
|
|
11089
12074
|
}
|
|
11090
12075
|
} else {
|
|
11091
|
-
const dir =
|
|
11092
|
-
if (!
|
|
11093
|
-
|
|
12076
|
+
const dir = path32.dirname(fullPath);
|
|
12077
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
12078
|
+
fs29.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
11094
12079
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
11095
12080
|
}
|
|
11096
12081
|
if (selectedSet.has("opencode")) handleOpencodeSecondary(root, results);
|
|
@@ -11102,12 +12087,12 @@ function runInit(options) {
|
|
|
11102
12087
|
continue;
|
|
11103
12088
|
} else {
|
|
11104
12089
|
const template = getTemplate();
|
|
11105
|
-
if (
|
|
12090
|
+
if (fs29.existsSync(fullPath)) {
|
|
11106
12091
|
if (options.skipExisting) {
|
|
11107
12092
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
11108
12093
|
continue;
|
|
11109
12094
|
}
|
|
11110
|
-
const existingContent =
|
|
12095
|
+
const existingContent = fs29.readFileSync(fullPath, "utf-8");
|
|
11111
12096
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
11112
12097
|
if (type === "antigravity") {
|
|
11113
12098
|
handleAntigravitySecondary(root, results);
|
|
@@ -11124,14 +12109,14 @@ function runInit(options) {
|
|
|
11124
12109
|
continue;
|
|
11125
12110
|
}
|
|
11126
12111
|
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
11127
|
-
|
|
12112
|
+
fs29.writeFileSync(fullPath, newContent, "utf-8");
|
|
11128
12113
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
11129
12114
|
} else {
|
|
11130
|
-
const dir =
|
|
11131
|
-
if (!
|
|
11132
|
-
|
|
12115
|
+
const dir = path32.dirname(fullPath);
|
|
12116
|
+
if (!fs29.existsSync(dir)) {
|
|
12117
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
11133
12118
|
}
|
|
11134
|
-
|
|
12119
|
+
fs29.writeFileSync(fullPath, template, "utf-8");
|
|
11135
12120
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
11136
12121
|
}
|
|
11137
12122
|
if (type === "antigravity") {
|
|
@@ -11256,11 +12241,10 @@ var init_init = __esm({
|
|
|
11256
12241
|
"Record what matters, skip what doesn't.",
|
|
11257
12242
|
"",
|
|
11258
12243
|
"<kuma_obedience>",
|
|
11259
|
-
"## \u{1F512} Obedience (
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
"- MUST NOT call actions outside the core actions.",
|
|
12244
|
+
"## \u{1F512} Obedience \u2014 honest model (issue #39)",
|
|
12245
|
+
"- HARNESS-ENFORCED (no choice): MCP schema rejects unknown actions; hooks inject gotchas pre-edit (`kuma hook pre-edit`); SessionStart serves the brief; dirty-flags patch the map post-edit.",
|
|
12246
|
+
'- AGENT-EXPECTED (best-effort ~70%): call `kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; `kuma_safety({ action: "verify" })` after edits.',
|
|
12247
|
+
"- FALLBACKS: init skipped \u2192 lazy brief on first unfamiliar edit; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop (never treat as verified).",
|
|
11264
12248
|
"</kuma_obedience>",
|
|
11265
12249
|
"",
|
|
11266
12250
|
"<kuma_workflow>",
|
|
@@ -11299,6 +12283,72 @@ var init_init = __esm({
|
|
|
11299
12283
|
}
|
|
11300
12284
|
});
|
|
11301
12285
|
|
|
12286
|
+
// src/engine/compactionContext.ts
|
|
12287
|
+
var compactionContext_exports = {};
|
|
12288
|
+
__export(compactionContext_exports, {
|
|
12289
|
+
getCompactionContext: () => getCompactionContext
|
|
12290
|
+
});
|
|
12291
|
+
import fs30 from "fs";
|
|
12292
|
+
import path33 from "path";
|
|
12293
|
+
async function getCompactionContext() {
|
|
12294
|
+
const parts = [];
|
|
12295
|
+
try {
|
|
12296
|
+
const { sessionMemory: sessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
|
|
12297
|
+
const summary = sessionMemory2.getSummary();
|
|
12298
|
+
const goal = String(summary.currentGoal || "");
|
|
12299
|
+
if (goal) parts.push(`Goal: ${goal.substring(0, 120)}`);
|
|
12300
|
+
const modified = (summary.modifiedFiles || []).length;
|
|
12301
|
+
if (modified > 0) parts.push(`Modified: ${modified} file(s)`);
|
|
12302
|
+
} catch {
|
|
12303
|
+
}
|
|
12304
|
+
try {
|
|
12305
|
+
const dbFile = path33.join(getProjectRoot(), ".kuma", "kuma.db");
|
|
12306
|
+
if (fs30.existsSync(dbFile)) {
|
|
12307
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
12308
|
+
const db = await getDb2();
|
|
12309
|
+
const stmt = db.prepare(
|
|
12310
|
+
`SELECT file_path, description FROM known_gotchas
|
|
12311
|
+
WHERE status IN ('active','verified') AND severity IN ('critical','high')
|
|
12312
|
+
AND (quarantined IS NULL OR quarantined = 0)
|
|
12313
|
+
ORDER BY updated_at DESC LIMIT 3`
|
|
12314
|
+
);
|
|
12315
|
+
const rows = [];
|
|
12316
|
+
while (stmt.step()) {
|
|
12317
|
+
const r = stmt.getAsObject();
|
|
12318
|
+
rows.push(`[${r.file_path}] ${String(r.description).substring(0, 80)}`);
|
|
12319
|
+
}
|
|
12320
|
+
stmt.free();
|
|
12321
|
+
if (rows.length > 0) parts.push(`Gotchas: ${rows.join(" | ")}`);
|
|
12322
|
+
}
|
|
12323
|
+
} catch {
|
|
12324
|
+
}
|
|
12325
|
+
try {
|
|
12326
|
+
const { getDirtyFiles: getDirtyFiles2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
|
|
12327
|
+
const dirty = getDirtyFiles2().slice(0, 5);
|
|
12328
|
+
if (dirty.length > 0) parts.push(`Dirty: ${dirty.join(", ")}`);
|
|
12329
|
+
} catch {
|
|
12330
|
+
}
|
|
12331
|
+
try {
|
|
12332
|
+
const cpDir = path33.join(getProjectRoot(), ".kuma", "checkpoints");
|
|
12333
|
+
if (fs30.existsSync(cpDir)) {
|
|
12334
|
+
const cps = fs30.readdirSync(cpDir).slice(0, 3);
|
|
12335
|
+
if (cps.length > 0) parts.push(`Checkpoints: ${cps.join(", ")}`);
|
|
12336
|
+
}
|
|
12337
|
+
} catch {
|
|
12338
|
+
}
|
|
12339
|
+
let out = parts.join("\n");
|
|
12340
|
+
if (out.length > BUDGET) out = out.slice(0, BUDGET) + "\u2026";
|
|
12341
|
+
return out;
|
|
12342
|
+
}
|
|
12343
|
+
var BUDGET;
|
|
12344
|
+
var init_compactionContext = __esm({
|
|
12345
|
+
"src/engine/compactionContext.ts"() {
|
|
12346
|
+
"use strict";
|
|
12347
|
+
init_pathValidator();
|
|
12348
|
+
BUDGET = 600;
|
|
12349
|
+
}
|
|
12350
|
+
});
|
|
12351
|
+
|
|
11302
12352
|
// src/utils/agentDetector.ts
|
|
11303
12353
|
var agentDetector_exports = {};
|
|
11304
12354
|
__export(agentDetector_exports, {
|
|
@@ -11307,19 +12357,19 @@ __export(agentDetector_exports, {
|
|
|
11307
12357
|
getSkillPath: () => getSkillPath,
|
|
11308
12358
|
skillExists: () => skillExists
|
|
11309
12359
|
});
|
|
11310
|
-
import
|
|
11311
|
-
import
|
|
12360
|
+
import fs31 from "fs";
|
|
12361
|
+
import path34 from "path";
|
|
11312
12362
|
function checkFile(root, filePath) {
|
|
11313
12363
|
try {
|
|
11314
|
-
return
|
|
12364
|
+
return fs31.existsSync(path34.join(root, filePath));
|
|
11315
12365
|
} catch {
|
|
11316
12366
|
return false;
|
|
11317
12367
|
}
|
|
11318
12368
|
}
|
|
11319
12369
|
function checkDir(root, dirPath) {
|
|
11320
12370
|
try {
|
|
11321
|
-
const fullPath =
|
|
11322
|
-
return
|
|
12371
|
+
const fullPath = path34.join(root, dirPath);
|
|
12372
|
+
return fs31.existsSync(fullPath) && fs31.statSync(fullPath).isDirectory();
|
|
11323
12373
|
} catch {
|
|
11324
12374
|
return false;
|
|
11325
12375
|
}
|
|
@@ -11387,7 +12437,7 @@ function getAgentLabel(type) {
|
|
|
11387
12437
|
function skillExists(type, projectRoot) {
|
|
11388
12438
|
const root = projectRoot ?? getProjectRoot();
|
|
11389
12439
|
const skillPath = getSkillPath(type);
|
|
11390
|
-
return
|
|
12440
|
+
return fs31.existsSync(path34.join(root, skillPath));
|
|
11391
12441
|
}
|
|
11392
12442
|
var AGENT_DETECTORS;
|
|
11393
12443
|
var init_agentDetector = __esm({
|
|
@@ -11928,11 +12978,10 @@ var init_skillGenerator = __esm({
|
|
|
11928
12978
|
"use strict";
|
|
11929
12979
|
CORE_ACTIONS = [
|
|
11930
12980
|
"<kuma_obedience>",
|
|
11931
|
-
"## \u{1F512} Obedience (
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
"- MUST NOT call actions outside the core actions.",
|
|
12981
|
+
"## \u{1F512} Obedience \u2014 honest model (issue #39)",
|
|
12982
|
+
"- HARNESS-ENFORCED: MCP schema rejects unknown actions; hooks inject gotchas pre-edit; SessionStart serves the brief; dirty-flags patch the map.",
|
|
12983
|
+
'- AGENT-EXPECTED (~70%): `kuma_context({ action: "init" })` first; record gotchas IMMEDIATELY; `kuma_safety({ action: "verify" })` after edits.',
|
|
12984
|
+
"- FALLBACKS: init skipped \u2192 lazy brief; gotcha skipped \u2192 auto-gotcha on repeated failures; verify skipped \u2192 CI is the backstop.",
|
|
11936
12985
|
"</kuma_obedience>",
|
|
11937
12986
|
"",
|
|
11938
12987
|
"<kuma_workflow>",
|
|
@@ -11984,8 +13033,8 @@ init_kumaGraph();
|
|
|
11984
13033
|
init_kumaMemory();
|
|
11985
13034
|
init_kumaDriftDetector();
|
|
11986
13035
|
init_pathValidator();
|
|
11987
|
-
import
|
|
11988
|
-
import
|
|
13036
|
+
import fs21 from "fs";
|
|
13037
|
+
import path24 from "path";
|
|
11989
13038
|
var CONTEXT_ALIASES = {
|
|
11990
13039
|
"research": "research",
|
|
11991
13040
|
"search": "research",
|
|
@@ -12031,9 +13080,13 @@ var CONTEXT_ALIASES = {
|
|
|
12031
13080
|
"anti-duplication": "reuse"
|
|
12032
13081
|
};
|
|
12033
13082
|
async function handleContext(params) {
|
|
13083
|
+
let dirtySynced = 0;
|
|
12034
13084
|
try {
|
|
13085
|
+
const { getDirtyFiles: getDirtyFiles2, clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
|
|
12035
13086
|
const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
|
|
13087
|
+
dirtySynced = getDirtyFiles2().length;
|
|
12036
13088
|
await syncModifiedFiles2(30);
|
|
13089
|
+
if (dirtySynced > 0) clearDirty2();
|
|
12037
13090
|
} catch {
|
|
12038
13091
|
}
|
|
12039
13092
|
const rawAction = params.action || "init";
|
|
@@ -12049,7 +13102,7 @@ async function handleContext(params) {
|
|
|
12049
13102
|
case "init":
|
|
12050
13103
|
return handleInit(params);
|
|
12051
13104
|
case "research":
|
|
12052
|
-
return obediencePrefix + await handleResearch(params);
|
|
13105
|
+
return obediencePrefix + await handleResearch(params, dirtySynced);
|
|
12053
13106
|
case "history":
|
|
12054
13107
|
return obediencePrefix + await handleHistory(params);
|
|
12055
13108
|
case "flow":
|
|
@@ -12256,11 +13309,15 @@ function sessionFocusAdvice(focus) {
|
|
|
12256
13309
|
return "Record gotchas & decisions as they happen \u2014 skip what isn't worth it.";
|
|
12257
13310
|
}
|
|
12258
13311
|
}
|
|
12259
|
-
async function handleResearch(params) {
|
|
13312
|
+
async function handleResearch(params, dirtySynced = 0) {
|
|
12260
13313
|
const scope = params.scope || "project";
|
|
12261
13314
|
sessionMemory.setGoal(`Researching: ${scope}`);
|
|
12262
13315
|
sessionMemory.recordToolCall("kuma_context_research", { scope });
|
|
12263
13316
|
const lines = [`\u{1F52C} **Research: ${scope}**`, `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`, ""];
|
|
13317
|
+
if (dirtySynced > 0) {
|
|
13318
|
+
lines.push(`\u{1F5FA}\uFE0F Map patched post-edit: ${dirtySynced} dirty file(s) synced before this research (no rebuild asked).`);
|
|
13319
|
+
lines.push("");
|
|
13320
|
+
}
|
|
12264
13321
|
lines.push("**Step 1/5: Loading Research Cache**");
|
|
12265
13322
|
const cached = await getResearchCache(scope);
|
|
12266
13323
|
let record = null;
|
|
@@ -12274,7 +13331,18 @@ async function handleResearch(params) {
|
|
|
12274
13331
|
const ageSeconds = Math.floor((Date.now() - (record.validatedAt ? new Date(record.validatedAt).getTime() : 0)) / 1e3);
|
|
12275
13332
|
const ageStr = ageSeconds > 86400 ? `${Math.floor(ageSeconds / 86400)}d` : ageSeconds > 3600 ? `${Math.floor(ageSeconds / 3600)}h` : `${Math.floor(ageSeconds / 60)}m`;
|
|
12276
13333
|
lines.push(` \u2705 Found cached research (${ageStr} old)`);
|
|
12277
|
-
|
|
13334
|
+
try {
|
|
13335
|
+
const { isStale: isStale2, RESEARCH_TTL_MS: RESEARCH_TTL_MS2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
|
|
13336
|
+
const validatedMs = record.validatedAt ? new Date(record.validatedAt).getTime() : 0;
|
|
13337
|
+
if (isStale2(validatedMs, RESEARCH_TTL_MS2)) {
|
|
13338
|
+
lines.push(` \u23F3 **STALE**: older than TTL 7d \u2014 re-researching fresh below`);
|
|
13339
|
+
record = null;
|
|
13340
|
+
} else {
|
|
13341
|
+
lines.push(` \u2705 Fresh (TTL 7d)`);
|
|
13342
|
+
}
|
|
13343
|
+
} catch {
|
|
13344
|
+
}
|
|
13345
|
+
if (ageSeconds > 86400 && record) lines.push(` ${ageSeconds > 604800 ? "\u{1F534}" : "\u{1F7E1}"} **Staleness:** Cache is ${ageStr} old \u2014 may be stale`);
|
|
12278
13346
|
} else lines.push(" \u23F3 No cached research \u2014 starting fresh");
|
|
12279
13347
|
lines.push("");
|
|
12280
13348
|
lines.push("**Step 2/5: Staleness Check**");
|
|
@@ -12383,10 +13451,10 @@ async function handleHistory(params) {
|
|
|
12383
13451
|
lines.push(traceFormatted);
|
|
12384
13452
|
} else {
|
|
12385
13453
|
try {
|
|
12386
|
-
const { execSync:
|
|
13454
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
12387
13455
|
const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
|
|
12388
13456
|
const root = getProjectRoot2();
|
|
12389
|
-
const gitHistory =
|
|
13457
|
+
const gitHistory = execSync9(
|
|
12390
13458
|
`git log -n 5 --pretty=format:" \u2022 %h (%ad) %s" --date=short -- "${target}"`,
|
|
12391
13459
|
{ cwd: root, encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
12392
13460
|
).trim();
|
|
@@ -12418,7 +13486,7 @@ ${gitHistory}`);
|
|
|
12418
13486
|
try {
|
|
12419
13487
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
12420
13488
|
const db = await getDb2();
|
|
12421
|
-
const base =
|
|
13489
|
+
const base = path24.basename(target);
|
|
12422
13490
|
const stmt = db.prepare(`
|
|
12423
13491
|
SELECT description, last_verified_at FROM known_gotchas
|
|
12424
13492
|
WHERE status = 'resolved' AND (file_path LIKE ? OR file_path LIKE ?)
|
|
@@ -12431,9 +13499,9 @@ ${gitHistory}`);
|
|
|
12431
13499
|
if (resolved.length === 0) {
|
|
12432
13500
|
try {
|
|
12433
13501
|
const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
|
|
12434
|
-
const mdPath =
|
|
12435
|
-
if (
|
|
12436
|
-
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");
|
|
12437
13505
|
const sections = mdContent.split(/^###\s+/m);
|
|
12438
13506
|
for (const sec of sections) {
|
|
12439
13507
|
const secLower = sec.toLowerCase();
|
|
@@ -12478,7 +13546,18 @@ async function handleMap(_params) {
|
|
|
12478
13546
|
sessionMemory.recordToolCall("kuma_context_map", {});
|
|
12479
13547
|
const { getWorkspaceInfo: getWorkspaceInfo2, formatWorkspaceMap: formatWorkspaceMap2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
|
|
12480
13548
|
const wsInfo = await getWorkspaceInfo2();
|
|
12481
|
-
|
|
13549
|
+
const base = formatWorkspaceMap2(wsInfo);
|
|
13550
|
+
try {
|
|
13551
|
+
const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
|
|
13552
|
+
const { count } = await refreshPackageBriefs2();
|
|
13553
|
+
if (count > 0) {
|
|
13554
|
+
return base + `
|
|
13555
|
+
|
|
13556
|
+
\u{1F4DA} **Pre-computed briefs**: ${count} package brief(s) refreshed in \`.kuma/briefs/\` \u2014 read instead of grep-walking (saves ~5x discovery tokens).`;
|
|
13557
|
+
}
|
|
13558
|
+
} catch {
|
|
13559
|
+
}
|
|
13560
|
+
return base;
|
|
12482
13561
|
}
|
|
12483
13562
|
async function handleImpact(params) {
|
|
12484
13563
|
const target = params.target || params.scope || "project";
|
|
@@ -12494,8 +13573,8 @@ init_kumaDb();
|
|
|
12494
13573
|
init_kumaMemory();
|
|
12495
13574
|
init_pathValidator();
|
|
12496
13575
|
init_pathValidator();
|
|
12497
|
-
import
|
|
12498
|
-
import
|
|
13576
|
+
import fs23 from "fs";
|
|
13577
|
+
import path26 from "path";
|
|
12499
13578
|
var MEMORY_ALIASES = {
|
|
12500
13579
|
"decision": "decision",
|
|
12501
13580
|
"adr": "decision",
|
|
@@ -12519,7 +13598,11 @@ var MEMORY_ALIASES = {
|
|
|
12519
13598
|
"gotcha-resolve": "gotcha",
|
|
12520
13599
|
"deprecate_gotcha": "gotcha",
|
|
12521
13600
|
"deprecate-gotcha": "gotcha",
|
|
12522
|
-
"deprecate": "gotcha"
|
|
13601
|
+
"deprecate": "gotcha",
|
|
13602
|
+
"test_outcome": "test_outcome",
|
|
13603
|
+
"test-outcome": "test_outcome",
|
|
13604
|
+
"testoutcome": "test_outcome",
|
|
13605
|
+
"test-record": "test_outcome"
|
|
12523
13606
|
};
|
|
12524
13607
|
async function handleMemory(params) {
|
|
12525
13608
|
const rawAction = params.action || "search";
|
|
@@ -12537,10 +13620,24 @@ async function handleMemory(params) {
|
|
|
12537
13620
|
return handleArchFlow(params);
|
|
12538
13621
|
case "gotcha":
|
|
12539
13622
|
return handleGotchaAction(params);
|
|
13623
|
+
case "test_outcome":
|
|
13624
|
+
return handleTestOutcome(params);
|
|
12540
13625
|
default:
|
|
12541
|
-
return `Unknown action "${action}". Use: gotcha, decision, arch_flow, research_save, search`;
|
|
13626
|
+
return `Unknown action "${action}". Use: gotcha, decision, arch_flow, research_save, search, test_outcome`;
|
|
12542
13627
|
}
|
|
12543
13628
|
}
|
|
13629
|
+
async function handleTestOutcome(params) {
|
|
13630
|
+
const changedRaw = params.scope || params.target || "";
|
|
13631
|
+
const failedRaw = params.content || params.description || "";
|
|
13632
|
+
if (!changedRaw.trim()) {
|
|
13633
|
+
return '\u274C **Test outcome NOT saved** \u2014 missing `scope` (changed file(s), comma-separated).\n\u2705 Use: kuma_memory({ action: "test_outcome", scope: "<changed files>", content: "<failed tests, empty when green>" })\nFuture impact queries rank history-backed tests first (\u{1F525}) and flaky ones last.';
|
|
13634
|
+
}
|
|
13635
|
+
const changed = changedRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13636
|
+
const failed = failedRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13637
|
+
const { recordTestOutcome: recordTestOutcome2 } = await Promise.resolve().then(() => (init_testHistory(), testHistory_exports));
|
|
13638
|
+
const { recorded } = await recordTestOutcome2(changed, failed);
|
|
13639
|
+
return failed.length === 0 ? `\u2705 Green run recorded for ${changed.length} file(s) (${recorded} row(s)). Future impact queries trust these areas more.` : `\u2705 Recorded ${failed.length} failing test(s) for ${changed.length} file(s). Future impact queries rank them first (\u{1F525}). If a retry flips one to green, re-record with it absent from content and it stays signal; use flaky marking via retry instead.`;
|
|
13640
|
+
}
|
|
12544
13641
|
async function handleDecision(params) {
|
|
12545
13642
|
const title = params.title || params.content || (params.target ? `Decision regarding ${params.target}` : params.scope ? `Decision for ${params.scope}` : "");
|
|
12546
13643
|
const rationale = params.rationale || params.description || (params.content && params.content !== title ? params.content : "");
|
|
@@ -12588,9 +13685,9 @@ async function handleResearchSave(params) {
|
|
|
12588
13685
|
} catch {
|
|
12589
13686
|
}
|
|
12590
13687
|
try {
|
|
12591
|
-
const researchDir =
|
|
12592
|
-
if (!
|
|
12593
|
-
|
|
13688
|
+
const researchDir = path26.join(getProjectRoot(), ".kuma", "research");
|
|
13689
|
+
if (!fs23.existsSync(researchDir)) fs23.mkdirSync(researchDir, { recursive: true });
|
|
13690
|
+
fs23.writeFileSync(path26.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
|
|
12594
13691
|
} catch {
|
|
12595
13692
|
}
|
|
12596
13693
|
sessionMemory.recordMemoryAction("research_save");
|
|
@@ -12786,9 +13883,9 @@ init_sessionMemory();
|
|
|
12786
13883
|
init_pathValidator();
|
|
12787
13884
|
init_sessionMemory();
|
|
12788
13885
|
init_kumaShared();
|
|
12789
|
-
import
|
|
12790
|
-
import
|
|
12791
|
-
import { execSync as
|
|
13886
|
+
import fs24 from "fs";
|
|
13887
|
+
import path27 from "path";
|
|
13888
|
+
import { execSync as execSync6 } from "child_process";
|
|
12792
13889
|
var SCRIPT_PATCH_PATTERNS = [
|
|
12793
13890
|
"writeFileSync",
|
|
12794
13891
|
"writeFile",
|
|
@@ -12814,20 +13911,20 @@ function findPatchScripts(projectRoot) {
|
|
|
12814
13911
|
const warnings = [];
|
|
12815
13912
|
const recentFiles = scanRecentFiles(projectRoot);
|
|
12816
13913
|
for (const file of recentFiles) {
|
|
12817
|
-
const ext =
|
|
13914
|
+
const ext = path27.extname(file).toLowerCase();
|
|
12818
13915
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
12819
|
-
const relativePath =
|
|
13916
|
+
const relativePath = path27.relative(projectRoot, file);
|
|
12820
13917
|
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".") || relativePath.startsWith(".kuma/scratch")) {
|
|
12821
13918
|
continue;
|
|
12822
13919
|
}
|
|
12823
13920
|
try {
|
|
12824
|
-
const content =
|
|
13921
|
+
const content = fs24.readFileSync(file, "utf-8").toLowerCase();
|
|
12825
13922
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
12826
13923
|
if (matchedPattern) {
|
|
12827
13924
|
warnings.push({
|
|
12828
13925
|
severity: "high",
|
|
12829
13926
|
pattern: "script-patching",
|
|
12830
|
-
message: `Created script file that modifies other files: ${
|
|
13927
|
+
message: `Created script file that modifies other files: ${path27.basename(file)}`,
|
|
12831
13928
|
suggestion: "Use your native edit tools instead \u2014 targeted edits with auto-backup and rollback",
|
|
12832
13929
|
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
12833
13930
|
filePath: relativePath
|
|
@@ -12861,15 +13958,15 @@ function detectBashGrepUsage() {
|
|
|
12861
13958
|
function scanRecentFiles(projectRoot) {
|
|
12862
13959
|
const recent = [];
|
|
12863
13960
|
try {
|
|
12864
|
-
const entries =
|
|
13961
|
+
const entries = fs24.readdirSync(projectRoot, { withFileTypes: true });
|
|
12865
13962
|
const now = Date.now();
|
|
12866
13963
|
const recentThreshold = 30 * 60 * 1e3;
|
|
12867
13964
|
for (const entry of entries) {
|
|
12868
13965
|
if (!entry.isFile()) continue;
|
|
12869
13966
|
try {
|
|
12870
|
-
const stat =
|
|
13967
|
+
const stat = fs24.statSync(path27.join(projectRoot, entry.name));
|
|
12871
13968
|
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
12872
|
-
recent.push(
|
|
13969
|
+
recent.push(path27.join(projectRoot, entry.name));
|
|
12873
13970
|
}
|
|
12874
13971
|
} catch {
|
|
12875
13972
|
continue;
|
|
@@ -12882,7 +13979,7 @@ function scanRecentFiles(projectRoot) {
|
|
|
12882
13979
|
function detectGitPatchScripts(projectRoot) {
|
|
12883
13980
|
const warnings = [];
|
|
12884
13981
|
try {
|
|
12885
|
-
const statusStdout =
|
|
13982
|
+
const statusStdout = execSync6("git status --porcelain", {
|
|
12886
13983
|
cwd: projectRoot,
|
|
12887
13984
|
encoding: "utf-8",
|
|
12888
13985
|
timeout: 3e3
|
|
@@ -12893,14 +13990,14 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
12893
13990
|
const prefix = line.substring(0, 2);
|
|
12894
13991
|
if (prefix !== "??" && prefix !== "A ") continue;
|
|
12895
13992
|
const file = line.substring(3).trim();
|
|
12896
|
-
const ext =
|
|
13993
|
+
const ext = path27.extname(file).toLowerCase();
|
|
12897
13994
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
12898
13995
|
const isRootLevel = !file.includes("/");
|
|
12899
13996
|
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
12900
13997
|
if (!isRootLevel && !isScriptsDir) continue;
|
|
12901
|
-
const fullPath =
|
|
12902
|
-
if (
|
|
12903
|
-
const content =
|
|
13998
|
+
const fullPath = path27.join(projectRoot, file);
|
|
13999
|
+
if (fs24.existsSync(fullPath)) {
|
|
14000
|
+
const content = fs24.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
12904
14001
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
12905
14002
|
if (matchedPattern) {
|
|
12906
14003
|
warnings.push({
|
|
@@ -12915,7 +14012,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
12915
14012
|
}
|
|
12916
14013
|
}
|
|
12917
14014
|
}
|
|
12918
|
-
const deletedStdout =
|
|
14015
|
+
const deletedStdout = execSync6("git diff --name-only --diff-filter=D HEAD", {
|
|
12919
14016
|
cwd: projectRoot,
|
|
12920
14017
|
encoding: "utf-8",
|
|
12921
14018
|
timeout: 3e3
|
|
@@ -12923,7 +14020,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
12923
14020
|
if (deletedStdout) {
|
|
12924
14021
|
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
12925
14022
|
for (const file of deletedFiles) {
|
|
12926
|
-
const ext =
|
|
14023
|
+
const ext = path27.extname(file).toLowerCase();
|
|
12927
14024
|
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
12928
14025
|
warnings.push({
|
|
12929
14026
|
severity: "high",
|
|
@@ -12985,21 +14082,21 @@ function detectAllAntiPatterns() {
|
|
|
12985
14082
|
// src/engine/contextSnapshot.ts
|
|
12986
14083
|
init_sessionMemory();
|
|
12987
14084
|
init_pathValidator();
|
|
12988
|
-
import
|
|
12989
|
-
import
|
|
12990
|
-
import { execSync as
|
|
14085
|
+
import fs25 from "fs";
|
|
14086
|
+
import path28 from "path";
|
|
14087
|
+
import { execSync as execSync7 } from "child_process";
|
|
12991
14088
|
var SNAPSHOT_DIR = "context-snapshots";
|
|
12992
14089
|
function snapshotDir() {
|
|
12993
|
-
return
|
|
14090
|
+
return path28.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
12994
14091
|
}
|
|
12995
14092
|
function ensureSnapshotDir() {
|
|
12996
14093
|
const dir = snapshotDir();
|
|
12997
|
-
if (!
|
|
12998
|
-
|
|
14094
|
+
if (!fs25.existsSync(dir)) {
|
|
14095
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
12999
14096
|
}
|
|
13000
14097
|
}
|
|
13001
14098
|
function snapshotFilePath(timestamp) {
|
|
13002
|
-
return
|
|
14099
|
+
return path28.join(snapshotDir(), `${timestamp}.json`);
|
|
13003
14100
|
}
|
|
13004
14101
|
function saveSnapshot(goal) {
|
|
13005
14102
|
try {
|
|
@@ -13020,7 +14117,7 @@ function saveSnapshot(goal) {
|
|
|
13020
14117
|
hasConventions: !!summary.hasConventions
|
|
13021
14118
|
};
|
|
13022
14119
|
try {
|
|
13023
|
-
|
|
14120
|
+
fs25.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
13024
14121
|
} catch {
|
|
13025
14122
|
}
|
|
13026
14123
|
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
@@ -13059,7 +14156,7 @@ function formatSnapshot(snapshot) {
|
|
|
13059
14156
|
function getGitDiffStat2() {
|
|
13060
14157
|
try {
|
|
13061
14158
|
const root = getProjectRoot();
|
|
13062
|
-
const stdout =
|
|
14159
|
+
const stdout = execSync7("git diff --stat", {
|
|
13063
14160
|
cwd: root,
|
|
13064
14161
|
encoding: "utf-8",
|
|
13065
14162
|
timeout: 3e3
|
|
@@ -13106,24 +14203,62 @@ async function handleKumaGuard(params) {
|
|
|
13106
14203
|
});
|
|
13107
14204
|
}
|
|
13108
14205
|
const drifts = [];
|
|
14206
|
+
const info = [];
|
|
14207
|
+
let verified = stats.hasRunTests;
|
|
14208
|
+
let uncommittedModifiedCount = stats.modifiedFiles.length;
|
|
13109
14209
|
if (check === "all" || check === "drift") {
|
|
13110
14210
|
const unresolvedCount = getUnresolvedCount(stats.failedFiles);
|
|
13111
14211
|
const gitStat = getGitDiffStat();
|
|
13112
14212
|
const editCalls = stats.toolCalls.filter((c) => isEditTool(c.toolName)).length;
|
|
14213
|
+
verified = stats.hasRunTests || hasVerificationEvidence(stats.toolCalls);
|
|
14214
|
+
const uncommitted = getUncommittedFiles();
|
|
14215
|
+
const modifiedPaths = stats.modifiedFiles.map((f) => String(f.filePath || f.path || ""));
|
|
14216
|
+
const uncommittedModified = modifiedPaths.filter(
|
|
14217
|
+
(p) => [...uncommitted].some((u) => u === p || u.endsWith(p) || p.endsWith(u))
|
|
14218
|
+
);
|
|
14219
|
+
const committedOnly = modifiedPaths.length - uncommittedModified.length;
|
|
14220
|
+
uncommittedModifiedCount = uncommittedModified.length;
|
|
13113
14221
|
drifts.push(...buildDriftMessages(
|
|
13114
|
-
|
|
13115
|
-
|
|
14222
|
+
uncommittedModified.length,
|
|
14223
|
+
verified,
|
|
13116
14224
|
unresolvedCount,
|
|
13117
14225
|
gitStat
|
|
13118
14226
|
));
|
|
13119
|
-
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) {
|
|
13120
14234
|
warnings.push({
|
|
13121
14235
|
severity: "medium",
|
|
13122
14236
|
pattern: "no-test-after-edit",
|
|
13123
|
-
message: `${
|
|
14237
|
+
message: `${uncommittedModified.length} uncommitted file(s) modified without running tests`,
|
|
13124
14238
|
suggestion: "Run the project's test/typecheck command to verify changes"
|
|
13125
14239
|
});
|
|
13126
14240
|
}
|
|
14241
|
+
try {
|
|
14242
|
+
const { detectTestWeakening: detectTestWeakening2 } = await Promise.resolve().then(() => (init_kumaShared(), kumaShared_exports));
|
|
14243
|
+
const weakened = detectTestWeakening2(modifiedPaths);
|
|
14244
|
+
for (const w of weakened) {
|
|
14245
|
+
warnings.push({
|
|
14246
|
+
severity: "high",
|
|
14247
|
+
pattern: "test-weakening",
|
|
14248
|
+
message: `Test-weakening diff detected: ${w} \u2014 assertions removed without replacement`,
|
|
14249
|
+
suggestion: "Restore removed assertions or justify why the weakened test still guards the behavior"
|
|
14250
|
+
});
|
|
14251
|
+
}
|
|
14252
|
+
} catch {
|
|
14253
|
+
}
|
|
14254
|
+
try {
|
|
14255
|
+
const { scanDocDrift: scanDocDrift2, formatDocDrift: formatDocDrift2 } = await Promise.resolve().then(() => (init_docDrift(), docDrift_exports));
|
|
14256
|
+
const { checked, drifted, skipped } = scanDocDrift2();
|
|
14257
|
+
if (drifted.length > 0) {
|
|
14258
|
+
info.push(formatDocDrift2(checked, drifted.slice(0, 5), skipped));
|
|
14259
|
+
}
|
|
14260
|
+
} catch {
|
|
14261
|
+
}
|
|
13127
14262
|
if (editCalls > 5) {
|
|
13128
14263
|
warnings.push({
|
|
13129
14264
|
severity: "low",
|
|
@@ -13195,6 +14330,25 @@ async function handleKumaGuard(params) {
|
|
|
13195
14330
|
}
|
|
13196
14331
|
return formatSnapshot(snapshot);
|
|
13197
14332
|
}
|
|
14333
|
+
try {
|
|
14334
|
+
const { recordGuardFlag: recordGuardFlag2, getDowntieredRules: getDowntieredRules2, demoteSeverity: demoteSeverity2 } = await Promise.resolve().then(() => (init_guardLedger(), guardLedger_exports));
|
|
14335
|
+
for (const w of warnings) {
|
|
14336
|
+
try {
|
|
14337
|
+
await recordGuardFlag2(w.pattern);
|
|
14338
|
+
} catch {
|
|
14339
|
+
}
|
|
14340
|
+
}
|
|
14341
|
+
const downtiered = await getDowntieredRules2();
|
|
14342
|
+
if (downtiered.size > 0) {
|
|
14343
|
+
for (const w of warnings) {
|
|
14344
|
+
if (downtiered.has(w.pattern) && (w.severity === "high" || w.severity === "medium")) {
|
|
14345
|
+
w.severity = demoteSeverity2(w.severity);
|
|
14346
|
+
w.message = `\u{1F53B} [auto-downtiered: rule over FP budget] ${w.message}`;
|
|
14347
|
+
}
|
|
14348
|
+
}
|
|
14349
|
+
}
|
|
14350
|
+
} catch {
|
|
14351
|
+
}
|
|
13198
14352
|
const hasWarnings = warnings.length > 0;
|
|
13199
14353
|
const hasDrifts = drifts.length > 0;
|
|
13200
14354
|
const onTrack = !hasWarnings && !hasDrifts;
|
|
@@ -13203,8 +14357,8 @@ async function handleKumaGuard(params) {
|
|
|
13203
14357
|
warnings,
|
|
13204
14358
|
loop.isLooping,
|
|
13205
14359
|
getUnresolvedCount(stats.failedFiles),
|
|
13206
|
-
|
|
13207
|
-
|
|
14360
|
+
uncommittedModifiedCount,
|
|
14361
|
+
verified,
|
|
13208
14362
|
countEditCalls(stats.toolCalls)
|
|
13209
14363
|
);
|
|
13210
14364
|
const report = {
|
|
@@ -13212,6 +14366,7 @@ async function handleKumaGuard(params) {
|
|
|
13212
14366
|
onTrack,
|
|
13213
14367
|
warnings,
|
|
13214
14368
|
drifts,
|
|
14369
|
+
info,
|
|
13215
14370
|
suggestion,
|
|
13216
14371
|
stats: {
|
|
13217
14372
|
goal: stats.goal,
|
|
@@ -13242,6 +14397,90 @@ async function handleKumaGuard(params) {
|
|
|
13242
14397
|
return JSON.stringify(reportWithRecordings, null, 2);
|
|
13243
14398
|
}
|
|
13244
14399
|
|
|
14400
|
+
// src/engine/costLedger.ts
|
|
14401
|
+
init_kumaDb();
|
|
14402
|
+
async function ensureCostSchema() {
|
|
14403
|
+
const db = await getDb();
|
|
14404
|
+
db.exec(`
|
|
14405
|
+
CREATE TABLE IF NOT EXISTS cost_ledger (
|
|
14406
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14407
|
+
tool TEXT NOT NULL,
|
|
14408
|
+
action TEXT,
|
|
14409
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
14410
|
+
output_chars INTEGER NOT NULL DEFAULT 0,
|
|
14411
|
+
outcome TEXT NOT NULL DEFAULT 'ok',
|
|
14412
|
+
tokens INTEGER,
|
|
14413
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
14414
|
+
);
|
|
14415
|
+
CREATE INDEX IF NOT EXISTS idx_cost_tool ON cost_ledger(tool);
|
|
14416
|
+
`);
|
|
14417
|
+
saveDb();
|
|
14418
|
+
}
|
|
14419
|
+
async function recordCost(r) {
|
|
14420
|
+
try {
|
|
14421
|
+
await ensureCostSchema();
|
|
14422
|
+
const db = await getDb();
|
|
14423
|
+
db.run(
|
|
14424
|
+
`INSERT INTO cost_ledger (tool, action, duration_ms, output_chars, outcome, tokens) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
14425
|
+
[r.tool, r.action || null, Math.round(r.durationMs), r.outputChars, r.outcome, r.tokens ?? null]
|
|
14426
|
+
);
|
|
14427
|
+
saveDb();
|
|
14428
|
+
} catch {
|
|
14429
|
+
}
|
|
14430
|
+
}
|
|
14431
|
+
async function getCostStats(limit = 15) {
|
|
14432
|
+
try {
|
|
14433
|
+
await ensureCostSchema();
|
|
14434
|
+
const db = await getDb();
|
|
14435
|
+
const stmt = db.prepare(`
|
|
14436
|
+
SELECT tool, COUNT(*) as calls,
|
|
14437
|
+
SUM(CASE WHEN outcome = 'error' THEN 1 ELSE 0 END) as errors,
|
|
14438
|
+
AVG(duration_ms) as avgMs, AVG(output_chars) as avgChars,
|
|
14439
|
+
SUM(tokens) as totalTokens
|
|
14440
|
+
FROM cost_ledger GROUP BY tool ORDER BY calls DESC LIMIT ?
|
|
14441
|
+
`);
|
|
14442
|
+
stmt.bind([limit]);
|
|
14443
|
+
const out = [];
|
|
14444
|
+
while (stmt.step()) {
|
|
14445
|
+
const r = stmt.getAsObject();
|
|
14446
|
+
out.push({
|
|
14447
|
+
tool: String(r.tool),
|
|
14448
|
+
calls: Number(r.calls) || 0,
|
|
14449
|
+
errors: Number(r.errors) || 0,
|
|
14450
|
+
avgMs: Math.round(Number(r.avgMs) || 0),
|
|
14451
|
+
avgChars: Math.round(Number(r.avgChars) || 0),
|
|
14452
|
+
totalTokens: r.totalTokens === null ? null : Number(r.totalTokens)
|
|
14453
|
+
});
|
|
14454
|
+
}
|
|
14455
|
+
stmt.free();
|
|
14456
|
+
return out;
|
|
14457
|
+
} catch {
|
|
14458
|
+
return [];
|
|
14459
|
+
}
|
|
14460
|
+
}
|
|
14461
|
+
function formatCostStats(rows) {
|
|
14462
|
+
if (rows.length === 0) return "\u{1F4B0} **Cost ledger** \u2014 no tool calls recorded yet.";
|
|
14463
|
+
const lines = ["\u{1F4B0} **Cost per tool** (duration \xD7 output size \u2192 cost-per-success signal)", ""];
|
|
14464
|
+
for (const r of rows) {
|
|
14465
|
+
const err = r.errors > 0 ? `, \u274C${r.errors}` : "";
|
|
14466
|
+
const tok = r.totalTokens !== null ? `, ${r.totalTokens} tokens` : "";
|
|
14467
|
+
lines.push(` \u2022 \`${r.tool}\` \u2014 ${r.calls} call(s), ~${r.avgMs}ms, ~${r.avgChars} chars${err}${tok}`);
|
|
14468
|
+
}
|
|
14469
|
+
return lines.join("\n");
|
|
14470
|
+
}
|
|
14471
|
+
async function withCost(tool, action, fn) {
|
|
14472
|
+
const start = Date.now();
|
|
14473
|
+
try {
|
|
14474
|
+
const result = await fn();
|
|
14475
|
+
const chars = typeof result === "string" ? result.length : JSON.stringify(result ?? "").length;
|
|
14476
|
+
await recordCost({ tool, action, durationMs: Date.now() - start, outputChars: chars, outcome: "ok" });
|
|
14477
|
+
return { result, outcome: "ok" };
|
|
14478
|
+
} catch (err) {
|
|
14479
|
+
await recordCost({ tool, action, durationMs: Date.now() - start, outputChars: String(err).length, outcome: "error" });
|
|
14480
|
+
throw err;
|
|
14481
|
+
}
|
|
14482
|
+
}
|
|
14483
|
+
|
|
13245
14484
|
// src/tools/kumaSafetyTool.ts
|
|
13246
14485
|
async function handleSafety(params) {
|
|
13247
14486
|
const { action } = params;
|
|
@@ -13261,10 +14500,17 @@ async function handleSafety(params) {
|
|
|
13261
14500
|
return prefix + await handleCheckpoint(params);
|
|
13262
14501
|
case "rollback_label":
|
|
13263
14502
|
return prefix + await handleRollbackLabel(params);
|
|
14503
|
+
case "cost":
|
|
14504
|
+
return prefix + await handleCost();
|
|
13264
14505
|
default:
|
|
13265
|
-
return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label`;
|
|
14506
|
+
return `Unknown action "${action}". Use: guard, verify, checkpoint, rollback_label, cost`;
|
|
13266
14507
|
}
|
|
13267
14508
|
}
|
|
14509
|
+
async function handleCost() {
|
|
14510
|
+
sessionMemory.recordToolCall("kuma_safety_cost", {});
|
|
14511
|
+
const stats = await getCostStats();
|
|
14512
|
+
return formatCostStats(stats);
|
|
14513
|
+
}
|
|
13268
14514
|
async function handleGuard(params) {
|
|
13269
14515
|
const base = await handleKumaGuard({
|
|
13270
14516
|
goal: params.guardGoal,
|
|
@@ -13437,12 +14683,12 @@ function registerAllTools(server) {
|
|
|
13437
14683
|
const contextHandler = async (params) => {
|
|
13438
14684
|
try {
|
|
13439
14685
|
await ensureInitialized();
|
|
13440
|
-
const text = await handleContext({
|
|
14686
|
+
const { result: text } = await withCost("kuma_context", params.action, () => handleContext({
|
|
13441
14687
|
action: params.action,
|
|
13442
14688
|
scope: params.scope,
|
|
13443
14689
|
target: params.target,
|
|
13444
14690
|
goal: params.goal
|
|
13445
|
-
});
|
|
14691
|
+
}));
|
|
13446
14692
|
return { content: [{ type: "text", text }] };
|
|
13447
14693
|
} catch (err) {
|
|
13448
14694
|
return { content: [{ type: "text", text: `Error in kuma_context: ${err}` }], isError: true };
|
|
@@ -13450,10 +14696,10 @@ function registerAllTools(server) {
|
|
|
13450
14696
|
};
|
|
13451
14697
|
server.tool("kuma_context", contextDesc, contextSchema, contextHandler);
|
|
13452
14698
|
server.tool("kuma_kuma_context", contextDesc, contextSchema, contextHandler);
|
|
13453
|
-
const memoryDesc = "Persistent knowledge. Record what matters, skip what doesn't. This is what saves time in future sessions. Don't record what grep/glob answers faster (functions, imports, types, components)." + CORE_NOTE + "`gotcha` (IMMEDIATELY when you find a bug/quirk), `decision` (when choosing between options), `arch_flow` (after tracing a complete flow, max 5 core files), `research_save` (after exploring an area), `search` (quick lookup of memory + knowledge graph), `gotcha_resolve` (archive fixed bugs).";
|
|
14699
|
+
const memoryDesc = "Persistent knowledge. Record what matters, skip what doesn't. This is what saves time in future sessions. Don't record what grep/glob answers faster (functions, imports, types, components)." + CORE_NOTE + "`gotcha` (IMMEDIATELY when you find a bug/quirk), `decision` (when choosing between options), `arch_flow` (after tracing a complete flow, max 5 core files), `research_save` (after exploring an area), `search` (quick lookup of memory + knowledge graph), `gotcha_resolve` (archive fixed bugs), `test_outcome` (record changed files + failed tests to train test selection).";
|
|
13454
14700
|
const memorySchema = {
|
|
13455
|
-
action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve"]).describe(
|
|
13456
|
-
"gotcha=record bug/quirk, decision=ADR, arch_flow=record architecture flow, research_save=save findings, search=quick memory+graph lookup, gotcha_resolve=archive fixed bug"
|
|
14701
|
+
action: z.enum(["gotcha", "decision", "arch_flow", "research_save", "search", "gotcha_resolve", "test_outcome"]).describe(
|
|
14702
|
+
"gotcha=record bug/quirk, decision=ADR, arch_flow=record architecture flow, research_save=save findings, search=quick memory+graph lookup, gotcha_resolve=archive fixed bug, test_outcome=record test results for predictive selection"
|
|
13457
14703
|
),
|
|
13458
14704
|
scope: z.string().optional().describe("File path for gotcha \u2014 or scope for research_save/search"),
|
|
13459
14705
|
target: z.string().optional().describe("Target file, gotcha ID, or component"),
|
|
@@ -13475,7 +14721,7 @@ function registerAllTools(server) {
|
|
|
13475
14721
|
const memoryHandler = async (params) => {
|
|
13476
14722
|
try {
|
|
13477
14723
|
await ensureInitialized();
|
|
13478
|
-
const text = await handleMemory({
|
|
14724
|
+
const { result: text } = await withCost("kuma_memory", params.action, () => handleMemory({
|
|
13479
14725
|
action: params.action,
|
|
13480
14726
|
scope: params.scope,
|
|
13481
14727
|
target: params.target,
|
|
@@ -13493,7 +14739,7 @@ function registerAllTools(server) {
|
|
|
13493
14739
|
description: params.description,
|
|
13494
14740
|
trigger_command: params.trigger_command,
|
|
13495
14741
|
id: params.id
|
|
13496
|
-
});
|
|
14742
|
+
}));
|
|
13497
14743
|
return { content: [{ type: "text", text }] };
|
|
13498
14744
|
} catch (err) {
|
|
13499
14745
|
return { content: [{ type: "text", text: `Error in kuma_memory: ${err}` }], isError: true };
|
|
@@ -13501,10 +14747,10 @@ function registerAllTools(server) {
|
|
|
13501
14747
|
};
|
|
13502
14748
|
server.tool("kuma_memory", memoryDesc, memorySchema, memoryHandler);
|
|
13503
14749
|
server.tool("kuma_kuma_memory", memoryDesc, memorySchema, memoryHandler);
|
|
13504
|
-
const safetyDesc = "Safety & verification. Use at task boundaries, not on every edit." + CORE_NOTE + "`guard` (before risky work: anti-patterns, drift, loops), `verify` (after edits: scoped tests + validation), `checkpoint` + `rollback_label` (the one rollback mechanism: snapshot before risky work, restore after).";
|
|
14750
|
+
const safetyDesc = "Safety & verification. Use at task boundaries, not on every edit." + CORE_NOTE + "`guard` (before risky work: anti-patterns, drift, loops), `verify` (after edits: scoped tests + validation), `checkpoint` + `rollback_label` (the one rollback mechanism: snapshot before risky work, restore after), `cost` (cost-per-tool ledger).";
|
|
13505
14751
|
const safetySchema = {
|
|
13506
|
-
action: z.enum(["guard", "verify", "checkpoint", "rollback_label"]).describe(
|
|
13507
|
-
"guard=anti-patterns/drift/loops before risky work, verify=scoped tests after edits, checkpoint=labeled snapshot before risky work, rollback_label=restore a labeled snapshot"
|
|
14752
|
+
action: z.enum(["guard", "verify", "checkpoint", "rollback_label", "cost"]).describe(
|
|
14753
|
+
"guard=anti-patterns/drift/loops before risky work, verify=scoped tests after edits, checkpoint=labeled snapshot before risky work, rollback_label=restore a labeled snapshot, cost=cost-per-tool ledger"
|
|
13508
14754
|
),
|
|
13509
14755
|
scope: z.string().optional().describe("Scope for verify (e.g. 'auth', file path)"),
|
|
13510
14756
|
target: z.string().optional().describe("Target test file or scope for verify"),
|
|
@@ -13520,7 +14766,7 @@ function registerAllTools(server) {
|
|
|
13520
14766
|
const safetyHandler = async (params) => {
|
|
13521
14767
|
try {
|
|
13522
14768
|
await ensureInitialized();
|
|
13523
|
-
const text = await handleSafety({
|
|
14769
|
+
const { result: text } = await withCost("kuma_safety", params.action, () => handleSafety({
|
|
13524
14770
|
action: params.action,
|
|
13525
14771
|
guardGoal: params.guardGoal,
|
|
13526
14772
|
guardCheck: params.guardCheck,
|
|
@@ -13532,7 +14778,7 @@ function registerAllTools(server) {
|
|
|
13532
14778
|
force: params.force,
|
|
13533
14779
|
label: params.label,
|
|
13534
14780
|
description: params.description
|
|
13535
|
-
});
|
|
14781
|
+
}));
|
|
13536
14782
|
return { content: [{ type: "text", text }] };
|
|
13537
14783
|
} catch (err) {
|
|
13538
14784
|
return { content: [{ type: "text", text: `Error in kuma_safety: ${err}` }], isError: true };
|
|
@@ -13567,6 +14813,10 @@ Usage:
|
|
|
13567
14813
|
npx @plumpslabs/kuma init --help Show this help
|
|
13568
14814
|
npx @plumpslabs/kuma hook pre-edit Claude Code PreToolUse hook \u2014 inject gotcha/decision/history before edits
|
|
13569
14815
|
npx @plumpslabs/kuma hook pre-bash Claude Code PreToolUse hook \u2014 inject command-triggered gotchas before Bash
|
|
14816
|
+
npx @plumpslabs/kuma hook session-start SessionStart hook \u2014 cached repo brief before the first turn
|
|
14817
|
+
npx @plumpslabs/kuma hook pre-compact PreCompact hook \u2014 survival set (goal + critical gotchas + dirty)
|
|
14818
|
+
npx @plumpslabs/kuma hook post-commit Git post-commit \u2014 periodic incremental map sync (no rebuild)
|
|
14819
|
+
npx @plumpslabs/kuma hook install-git Install the git post-commit hook (idempotent)
|
|
13570
14820
|
|
|
13571
14821
|
Available config files:
|
|
13572
14822
|
--claude CLAUDE.md (Claude Code)
|
|
@@ -13659,14 +14909,14 @@ async function main() {
|
|
|
13659
14909
|
console.error("");
|
|
13660
14910
|
let killedCount = 0;
|
|
13661
14911
|
try {
|
|
13662
|
-
const
|
|
13663
|
-
const
|
|
13664
|
-
const lockDir =
|
|
13665
|
-
if (
|
|
13666
|
-
const pidFile =
|
|
13667
|
-
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)) {
|
|
13668
14918
|
try {
|
|
13669
|
-
const pid = parseInt(
|
|
14919
|
+
const pid = parseInt(fs32.readFileSync(pidFile, "utf-8"), 10);
|
|
13670
14920
|
if (pid && pid !== process.pid) {
|
|
13671
14921
|
try {
|
|
13672
14922
|
process.kill(-pid, "SIGKILL");
|
|
@@ -13682,7 +14932,7 @@ async function main() {
|
|
|
13682
14932
|
} catch {
|
|
13683
14933
|
}
|
|
13684
14934
|
}
|
|
13685
|
-
|
|
14935
|
+
fs32.rmSync(lockDir, { recursive: true, force: true });
|
|
13686
14936
|
}
|
|
13687
14937
|
if (killedCount === 0) {
|
|
13688
14938
|
console.error("\u2705 No running verifications found to kill.");
|
|
@@ -13690,25 +14940,25 @@ async function main() {
|
|
|
13690
14940
|
} catch {
|
|
13691
14941
|
}
|
|
13692
14942
|
try {
|
|
13693
|
-
const { execSync:
|
|
14943
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
13694
14944
|
try {
|
|
13695
|
-
|
|
14945
|
+
execSync9("pkill -f 'jest' 2>/dev/null || true");
|
|
13696
14946
|
} catch {
|
|
13697
14947
|
}
|
|
13698
14948
|
try {
|
|
13699
|
-
|
|
14949
|
+
execSync9("pkill -f 'jest-worker' 2>/dev/null || true");
|
|
13700
14950
|
} catch {
|
|
13701
14951
|
}
|
|
13702
14952
|
try {
|
|
13703
|
-
|
|
14953
|
+
execSync9("pkill -f 'pnpm test' 2>/dev/null || true");
|
|
13704
14954
|
} catch {
|
|
13705
14955
|
}
|
|
13706
14956
|
try {
|
|
13707
|
-
|
|
14957
|
+
execSync9("pkill -f 'npm test' 2>/dev/null || true");
|
|
13708
14958
|
} catch {
|
|
13709
14959
|
}
|
|
13710
14960
|
try {
|
|
13711
|
-
|
|
14961
|
+
execSync9("pkill -f 'yarn test' 2>/dev/null || true");
|
|
13712
14962
|
} catch {
|
|
13713
14963
|
}
|
|
13714
14964
|
console.error("\u2705 Killed orphaned test processes");
|
|
@@ -13776,6 +15026,11 @@ async function main() {
|
|
|
13776
15026
|
}
|
|
13777
15027
|
const target = hook.filePaths[0];
|
|
13778
15028
|
await trackFileEditLoop2(target);
|
|
15029
|
+
try {
|
|
15030
|
+
const { markDirty: markDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
|
|
15031
|
+
markDirty2(target);
|
|
15032
|
+
} catch {
|
|
15033
|
+
}
|
|
13779
15034
|
try {
|
|
13780
15035
|
const { maybeAutoCheckpoint: maybeAutoCheckpoint2 } = await Promise.resolve().then(() => (init_kumaCheckpoint(), kumaCheckpoint_exports));
|
|
13781
15036
|
const autoCp = maybeAutoCheckpoint2(target);
|
|
@@ -13830,9 +15085,138 @@ async function main() {
|
|
|
13830
15085
|
}
|
|
13831
15086
|
process.exit(0);
|
|
13832
15087
|
}
|
|
15088
|
+
if (args[0] === "hook" && args[1] === "pre-compact") {
|
|
15089
|
+
try {
|
|
15090
|
+
const { getCompactionContext: getCompactionContext2 } = await Promise.resolve().then(() => (init_compactionContext(), compactionContext_exports));
|
|
15091
|
+
const { buildHookResponse: buildHookResponse2 } = await Promise.resolve().then(() => (init_kumaInject(), kumaInject_exports));
|
|
15092
|
+
const context = await getCompactionContext2();
|
|
15093
|
+
const body = context.trim() ? `\u{1F4E6} [KUMA survival set \u2014 preserve across compaction]
|
|
15094
|
+
${context}` : "";
|
|
15095
|
+
process.stdout.write(buildHookResponse2(body));
|
|
15096
|
+
} catch {
|
|
15097
|
+
process.stdout.write("{}");
|
|
15098
|
+
}
|
|
15099
|
+
process.exit(0);
|
|
15100
|
+
}
|
|
15101
|
+
if (args[0] === "hook" && args[1] === "post-commit") {
|
|
15102
|
+
try {
|
|
15103
|
+
const { syncModifiedFiles: syncModifiedFiles2 } = await Promise.resolve().then(() => (init_kumaCodeScanner(), kumaCodeScanner_exports));
|
|
15104
|
+
const result = await syncModifiedFiles2(50);
|
|
15105
|
+
try {
|
|
15106
|
+
const { refreshPackageBriefs: refreshPackageBriefs2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
|
|
15107
|
+
await refreshPackageBriefs2();
|
|
15108
|
+
} catch {
|
|
15109
|
+
}
|
|
15110
|
+
try {
|
|
15111
|
+
const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
|
|
15112
|
+
clearDirty2();
|
|
15113
|
+
} catch {
|
|
15114
|
+
}
|
|
15115
|
+
process.stdout.write(`\u{1F43B} [Kuma] post-commit map sync: ${result.filesScanned} file(s), ${result.nodeCount} nodes, ${result.edgeCount} edges.`);
|
|
15116
|
+
} catch (err) {
|
|
15117
|
+
process.stdout.write(`\u{1F43B} [Kuma] post-commit sync skipped: ${err}`);
|
|
15118
|
+
}
|
|
15119
|
+
process.exit(0);
|
|
15120
|
+
}
|
|
15121
|
+
if (args[0] === "hook" && args[1] === "install-git") {
|
|
15122
|
+
try {
|
|
15123
|
+
const fsMod = await import("fs");
|
|
15124
|
+
const pathMod = await import("path");
|
|
15125
|
+
const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
|
|
15126
|
+
const hookPath = pathMod.join(getProjectRoot2(), ".git", "hooks", "post-commit");
|
|
15127
|
+
const line = "kuma hook post-commit >/dev/null 2>&1 || true";
|
|
15128
|
+
if (fsMod.existsSync(hookPath)) {
|
|
15129
|
+
const existing = fsMod.readFileSync(hookPath, "utf-8");
|
|
15130
|
+
if (existing.includes("kuma hook post-commit")) {
|
|
15131
|
+
process.stdout.write("\u{1F43B} [Kuma] git post-commit hook already installed.");
|
|
15132
|
+
} else {
|
|
15133
|
+
fsMod.appendFileSync(hookPath, `
|
|
15134
|
+
# Kuma periodic mapping (industry auto-map)
|
|
15135
|
+
${line}
|
|
15136
|
+
`, "utf-8");
|
|
15137
|
+
process.stdout.write("\u{1F43B} [Kuma] appended to existing git post-commit hook.");
|
|
15138
|
+
}
|
|
15139
|
+
} else {
|
|
15140
|
+
fsMod.writeFileSync(hookPath, `#!/bin/sh
|
|
15141
|
+
# Kuma periodic mapping (industry auto-map)
|
|
15142
|
+
${line}
|
|
15143
|
+
`, { mode: 493 });
|
|
15144
|
+
process.stdout.write("\u{1F43B} [Kuma] git post-commit hook installed.");
|
|
15145
|
+
}
|
|
15146
|
+
} catch (err) {
|
|
15147
|
+
process.stdout.write(`\u{1F43B} [Kuma] install-git failed: ${err}`);
|
|
15148
|
+
}
|
|
15149
|
+
process.exit(0);
|
|
15150
|
+
}
|
|
13833
15151
|
if (args[0] === "--hook") {
|
|
13834
15152
|
process.exit(0);
|
|
13835
15153
|
}
|
|
15154
|
+
if (args[0] === "hook" && args[1] === "session-start") {
|
|
15155
|
+
try {
|
|
15156
|
+
const { getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_pathValidator(), pathValidator_exports));
|
|
15157
|
+
const root = getProjectRoot2();
|
|
15158
|
+
const proj = root.split("/").pop() || "unknown";
|
|
15159
|
+
const lines = [`\u{1F43B} [Kuma session brief] ${proj}`];
|
|
15160
|
+
try {
|
|
15161
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
15162
|
+
const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
|
|
15163
|
+
cwd: root,
|
|
15164
|
+
encoding: "utf-8",
|
|
15165
|
+
timeout: 2e3,
|
|
15166
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
15167
|
+
}).trim();
|
|
15168
|
+
if (branch) lines.push(`git: ${branch}`);
|
|
15169
|
+
} catch {
|
|
15170
|
+
}
|
|
15171
|
+
try {
|
|
15172
|
+
const fsMod = await import("fs");
|
|
15173
|
+
const pathMod = await import("path");
|
|
15174
|
+
const dbFile = pathMod.join(root, ".kuma", "kuma.db");
|
|
15175
|
+
if (fsMod.existsSync(dbFile)) {
|
|
15176
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
15177
|
+
const db = await getDb2();
|
|
15178
|
+
const stmt = db.prepare(
|
|
15179
|
+
`SELECT COUNT(*) as cnt FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0)`
|
|
15180
|
+
);
|
|
15181
|
+
if (stmt.step()) lines.push(`active gotchas: ${stmt.getAsObject().cnt}`);
|
|
15182
|
+
stmt.free();
|
|
15183
|
+
const top = db.prepare(
|
|
15184
|
+
`SELECT file_path, description, severity FROM known_gotchas WHERE status IN ('active','verified') AND (quarantined IS NULL OR quarantined = 0) ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 ELSE 2 END LIMIT 3`
|
|
15185
|
+
);
|
|
15186
|
+
while (top.step()) {
|
|
15187
|
+
const r = top.getAsObject();
|
|
15188
|
+
lines.push(` [${r.severity}] ${r.file_path} \u2014 ${String(r.description).substring(0, 100)}`);
|
|
15189
|
+
}
|
|
15190
|
+
top.free();
|
|
15191
|
+
} else {
|
|
15192
|
+
const { getActiveGotchas: getActiveGotchas2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
|
|
15193
|
+
const gotchas = getActiveGotchas2().filter((g) => g.severity === "critical" || g.severity === "high").slice(0, 3);
|
|
15194
|
+
lines.push(`active gotchas: ${getActiveGotchas2().length} (markdown layer)`);
|
|
15195
|
+
for (const g of gotchas) lines.push(` [${g.severity}] ${g.filePath} \u2014 ${g.description.substring(0, 100)}`);
|
|
15196
|
+
}
|
|
15197
|
+
} catch {
|
|
15198
|
+
}
|
|
15199
|
+
lines.push(`MUST: kuma_context({ action: "init" }) first; record gotchas immediately; verify after edits.`);
|
|
15200
|
+
try {
|
|
15201
|
+
const { readBrief: readBrief2, briefFreshness: briefFreshness2 } = await Promise.resolve().then(() => (init_packageBriefs(), packageBriefs_exports));
|
|
15202
|
+
const { getWorkspaceInfo: getWorkspaceInfo2 } = await Promise.resolve().then(() => (init_workspaceIntelligence(), workspaceIntelligence_exports));
|
|
15203
|
+
const ws = await getWorkspaceInfo2(root);
|
|
15204
|
+
const firstPkg = ws.isWorkspace && ws.packages.length > 0 ? ws.packages[0].name : "root";
|
|
15205
|
+
const fresh = briefFreshness2(firstPkg);
|
|
15206
|
+
if (fresh.exists && fresh.fresh) {
|
|
15207
|
+
const brief = readBrief2(firstPkg, 800);
|
|
15208
|
+
if (brief) lines.push("", "\u{1F4DA} [cached brief \u2014 read, don't re-discover]", brief);
|
|
15209
|
+
} else {
|
|
15210
|
+
lines.push(`\u{1F4DA} No fresh brief (age: ${fresh.age}) \u2014 run kuma_context({ action: "map" }) once to pre-compute.`);
|
|
15211
|
+
}
|
|
15212
|
+
} catch {
|
|
15213
|
+
}
|
|
15214
|
+
process.stdout.write(lines.join("\n"));
|
|
15215
|
+
} catch {
|
|
15216
|
+
process.stdout.write('\u{1F43B} [Kuma session brief] unavailable \u2014 run kuma_context({ action: "init" }).');
|
|
15217
|
+
}
|
|
15218
|
+
process.exit(0);
|
|
15219
|
+
}
|
|
13836
15220
|
if (args[0] === "init") {
|
|
13837
15221
|
const flags = args.slice(1);
|
|
13838
15222
|
if (flags.includes("--help") || flags.includes("-h")) {
|
|
@@ -13893,23 +15277,23 @@ async function main() {
|
|
|
13893
15277
|
});
|
|
13894
15278
|
const output = formatInitResults(results);
|
|
13895
15279
|
console.log(output);
|
|
13896
|
-
const
|
|
13897
|
-
const
|
|
13898
|
-
const matchaSkills =
|
|
13899
|
-
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(
|
|
13900
15284
|
process.cwd(),
|
|
13901
15285
|
".agents/skills/matcha/SKILL.md"
|
|
13902
15286
|
);
|
|
13903
|
-
const matchaRootSkills =
|
|
15287
|
+
const matchaRootSkills = path35.resolve(
|
|
13904
15288
|
process.cwd(),
|
|
13905
15289
|
"skills/matcha/SKILL.md"
|
|
13906
15290
|
);
|
|
13907
|
-
const matchaAgentsMd =
|
|
13908
|
-
const matchaWindsurfRules =
|
|
15291
|
+
const matchaAgentsMd = path35.resolve(process.cwd(), "AGENTS.md");
|
|
15292
|
+
const matchaWindsurfRules = path35.resolve(
|
|
13909
15293
|
process.cwd(),
|
|
13910
15294
|
".windsurf"
|
|
13911
15295
|
);
|
|
13912
|
-
if (
|
|
15296
|
+
if (fs32.existsSync(matchaSkills) || fs32.existsSync(matchaAgents) || fs32.existsSync(matchaRootSkills) || fs32.existsSync(matchaAgentsMd) || fs32.existsSync(matchaWindsurfRules)) {
|
|
13913
15297
|
console.error(
|
|
13914
15298
|
"\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
|
|
13915
15299
|
);
|
|
@@ -13923,13 +15307,13 @@ async function main() {
|
|
|
13923
15307
|
(async () => {
|
|
13924
15308
|
try {
|
|
13925
15309
|
const { generateInitMdContent: generateInitMdContent2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
13926
|
-
const
|
|
13927
|
-
const
|
|
13928
|
-
const initMdPath =
|
|
13929
|
-
if (!
|
|
13930
|
-
const kumaDir =
|
|
13931
|
-
if (!
|
|
13932
|
-
|
|
15310
|
+
const fs32 = await import("fs");
|
|
15311
|
+
const path35 = await import("path");
|
|
15312
|
+
const initMdPath = path35.resolve(process.cwd(), ".kuma/init.md");
|
|
15313
|
+
if (!fs32.existsSync(initMdPath)) {
|
|
15314
|
+
const kumaDir = path35.dirname(initMdPath);
|
|
15315
|
+
if (!fs32.existsSync(kumaDir)) fs32.mkdirSync(kumaDir, { recursive: true });
|
|
15316
|
+
fs32.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
|
|
13933
15317
|
console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
|
|
13934
15318
|
}
|
|
13935
15319
|
} catch (err) {
|
|
@@ -13940,8 +15324,8 @@ async function main() {
|
|
|
13940
15324
|
try {
|
|
13941
15325
|
const { detectAgent: detectAgent2, getSkillPath: getSkillPath2, getAgentLabel: getAgentLabel2 } = await Promise.resolve().then(() => (init_agentDetector(), agentDetector_exports));
|
|
13942
15326
|
const { generateSkill: generateSkill2, getSecondaryFiles: getSecondaryFiles2 } = await Promise.resolve().then(() => (init_skillGenerator(), skillGenerator_exports));
|
|
13943
|
-
const
|
|
13944
|
-
const
|
|
15327
|
+
const fs32 = await import("fs");
|
|
15328
|
+
const path35 = await import("path");
|
|
13945
15329
|
const detection = detectAgent2();
|
|
13946
15330
|
if (!detection.primary) {
|
|
13947
15331
|
console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
|
|
@@ -13949,22 +15333,22 @@ async function main() {
|
|
|
13949
15333
|
}
|
|
13950
15334
|
const agentType = detection.primary;
|
|
13951
15335
|
const skillPath = getSkillPath2(agentType);
|
|
13952
|
-
const fullPath =
|
|
13953
|
-
if (
|
|
15336
|
+
const fullPath = path35.resolve(process.cwd(), skillPath);
|
|
15337
|
+
if (fs32.existsSync(fullPath)) {
|
|
13954
15338
|
console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel2(agentType)} \u2014 skipping`);
|
|
13955
15339
|
return;
|
|
13956
15340
|
}
|
|
13957
|
-
const dir =
|
|
13958
|
-
if (!
|
|
13959
|
-
|
|
15341
|
+
const dir = path35.dirname(fullPath);
|
|
15342
|
+
if (!fs32.existsSync(dir)) fs32.mkdirSync(dir, { recursive: true });
|
|
15343
|
+
fs32.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
|
|
13960
15344
|
console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel2(agentType)}`);
|
|
13961
15345
|
const secondaryFiles = getSecondaryFiles2(agentType);
|
|
13962
15346
|
for (const sf of secondaryFiles) {
|
|
13963
|
-
const sfPath =
|
|
13964
|
-
const sfDir =
|
|
13965
|
-
if (!
|
|
13966
|
-
if (!
|
|
13967
|
-
|
|
15347
|
+
const sfPath = path35.resolve(process.cwd(), sf.path);
|
|
15348
|
+
const sfDir = path35.dirname(sfPath);
|
|
15349
|
+
if (!fs32.existsSync(sfPath)) {
|
|
15350
|
+
if (!fs32.existsSync(sfDir)) fs32.mkdirSync(sfDir, { recursive: true });
|
|
15351
|
+
fs32.writeFileSync(sfPath, sf.content, "utf-8");
|
|
13968
15352
|
console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
|
|
13969
15353
|
}
|
|
13970
15354
|
}
|