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