@theglitchking/semantic-memory 1.6.1 → 1.7.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +19 -2
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/chunk-2BFG4IBC.js +295 -0
- package/dist/chunk-2BFG4IBC.js.map +1 -0
- package/dist/chunk-2OMDSTSS.js +118 -0
- package/dist/chunk-2OMDSTSS.js.map +1 -0
- package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3ML5ESXI.js +224 -0
- package/dist/chunk-3ML5ESXI.js.map +1 -0
- package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
- package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
- package/dist/chunk-ACZKDSMP.js +123 -0
- package/dist/chunk-ACZKDSMP.js.map +1 -0
- package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
- package/dist/chunk-BSXIGPDY.js +137 -0
- package/dist/chunk-BSXIGPDY.js.map +1 -0
- package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/chunk-M2PC64DB.js +195 -0
- package/dist/chunk-M2PC64DB.js.map +1 -0
- package/dist/chunk-MTLYF357.js +130 -0
- package/dist/chunk-MTLYF357.js.map +1 -0
- package/dist/chunk-N77YCMSX.js +113 -0
- package/dist/chunk-N77YCMSX.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.js.map +1 -0
- package/dist/chunk-PMOZKUEM.js +170 -0
- package/dist/chunk-PMOZKUEM.js.map +1 -0
- package/dist/chunk-R6GNP7DS.js +87 -0
- package/dist/chunk-R6GNP7DS.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/chunk-V6PSK4Y4.js +186 -0
- package/dist/chunk-V6PSK4Y4.js.map +1 -0
- package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
- package/dist/chunk-VBLCUWT4.js.map +1 -0
- package/dist/chunk-VQCNWRTL.js +121 -0
- package/dist/chunk-VQCNWRTL.js.map +1 -0
- package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
- package/dist/chunk-Y7H76Q7F.js.map +1 -0
- package/dist/chunk-ZSWI3Y4Y.js +232 -0
- package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
- package/dist/cli/index.js +381 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/core/features.d.ts +38 -0
- package/dist/core/features.js +28 -0
- package/dist/core/index.d.ts +285 -2
- package/dist/core/index.js +170 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/jev.d.ts +35 -0
- package/dist/core/jev.js +20 -0
- package/dist/core/skill-curator.d.ts +1 -0
- package/dist/core/skill-curator.js +41 -0
- package/dist/core/skill-evidence.d.ts +25 -0
- package/dist/core/skill-evidence.js +23 -0
- package/dist/core/skill-queue.d.ts +46 -0
- package/dist/core/skill-queue.js +42 -0
- package/dist/core/skill-review.d.ts +3 -0
- package/dist/core/skill-review.js +29 -0
- package/dist/core/skill-worker.d.ts +21 -0
- package/dist/core/skill-worker.js +70 -0
- package/dist/core/skill-worker.js.map +1 -0
- package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/failure-eval-IUJO47UC.js.map +1 -0
- package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
- package/dist/healthcheck-S7D6YNTA.js.map +1 -0
- package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/mcp/server.js +19 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
- package/dist/skill-land-EHXZW47F.js +31 -0
- package/dist/skill-land-EHXZW47F.js.map +1 -0
- package/dist/skill-ledger-AYXVNFX7.js +18 -0
- package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
- package/dist/skill-merge-HBZBALZX.js +24 -0
- package/dist/skill-merge-HBZBALZX.js.map +1 -0
- package/dist/skill-review-BNvHipn9.d.ts +182 -0
- package/dist/skill-schema-RVB3EHEF.js +30 -0
- package/dist/skill-schema-RVB3EHEF.js.map +1 -0
- package/hooks/hooks.json +26 -1
- package/hooks/jev-bridge.py +109 -0
- package/hooks/session-start.js +5 -0
- package/hooks/skill-loop.js +65 -0
- package/hooks/vault-context.js +104 -6
- package/package.json +4 -2
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-GYBUBJYM.js.map +0 -1
- package/dist/chunk-HUG7E25D.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
- /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
- /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
- /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
- /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-eval/sets.ts","../src/core/skill-eval/trigger.ts"],"sourcesContent":["/**\n * Trigger eval sets (v1.7): positives are prompts from the skill's own provenance sessions, near-miss\n * negatives are prompts from other sessions that share terms with it. The caller supplies the prompts\n * (this module reads no transcripts). Split ~60/40 into train / held-out by hashing the prompt text.\n */\nimport { createHash } from \"node:crypto\";\nimport type { ParsedSkill } from \"../skill-schema.js\";\n\nexport interface CandidatePrompt {\n session: string;\n text: string;\n}\n\nexport interface EvalPrompt {\n text: string;\n should_trigger: boolean;\n split: \"train\" | \"heldout\";\n}\n\nconst STOP = new Set([\"the\", \"and\", \"for\", \"with\", \"when\", \"this\", \"that\", \"use\", \"run\", \"you\", \"are\", \"not\", \"can\", \"how\", \"what\", \"from\", \"into\", \"your\"]);\nconst terms = (s: string) => new Set(s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !STOP.has(t)));\n\n/** Same prompt, same side of the split, on every machine. */\nexport const splitOf = (text: string): EvalPrompt[\"split\"] => (createHash(\"sha256\").update(text).digest().readUInt32BE(0) % 100 < 60 ? \"train\" : \"heldout\");\n\nexport function buildEvalSet(skill: ParsedSkill, candidates: CandidatePrompt[], opts: { maxNegatives?: number } = {}): EvalPrompt[] {\n const sessions = new Set(skill.meta?.provenance.sessions ?? []);\n const own = terms([skill.name, skill.description, ...(skill.meta?.triggers ?? [])].join(\" \"));\n const seen = new Set<string>();\n const positives: string[] = [];\n const near: { text: string; overlap: number }[] = [];\n for (const c of candidates) {\n const text = c.text.trim();\n if (!text || seen.has(text)) continue;\n seen.add(text);\n if (sessions.has(c.session)) positives.push(text);\n else {\n const overlap = [...terms(text)].filter((t) => own.has(t)).length;\n if (overlap > 0) near.push({ text, overlap });\n }\n }\n near.sort((a, b) => b.overlap - a.overlap || a.text.localeCompare(b.text));\n const negatives = near.slice(0, opts.maxNegatives ?? Math.max(positives.length, 4)).map((n) => n.text);\n return [...positives.map((text) => ({ text, should_trigger: true })), ...negatives.map((text) => ({ text, should_trigger: false }))].map((p) => ({ ...p, split: splitOf(p.text) }));\n}\n","/**\n * Trigger eval (v1.7): does Claude reach for the skill when it should, and stay away when it should not?\n * Each prompt runs in a scratch directory (outside the repo) holding the skill under test next to copies of\n * the repo's other skills, so it competes like it would for real. Up to 3 runs per prompt, stopping once the\n * first two agree. Results are cached by (sha256 of the skill file, prompt): an unchanged skill costs no runs.\n */\nimport { createHash } from \"node:crypto\";\nimport { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { runClaude, skillsCalled } from \"../claude-run.js\";\nimport { parseSkill } from \"../skill-schema.js\";\nimport type { EvalPrompt } from \"./sets.js\";\n\nexport const MAX_TRIGGER_RUNS = 3;\n\nexport const evalCachePath = (env = process.env) =>\n env.SEMANTIC_MEMORY_SKILL_EVAL_CACHE || join(env.HOME || homedir(), \".semantic-memory\", \"skill-eval-cache.jsonl\");\n\nexport const sha256 = (s: string) => createHash(\"sha256\").update(s).digest(\"hex\");\n\nexport interface PromptTrigger extends EvalPrompt {\n triggers: number;\n runs: number;\n /** a majority of the runs called the skill */\n triggered: boolean;\n cached: boolean;\n error?: string;\n}\n\nexport interface Rates {\n n: number;\n /** share of should-trigger prompts that triggered; null when there are none */\n positiveRate: number | null;\n /** share of near-miss prompts that triggered; null when there are none */\n falseTriggerRate: number | null;\n /** prompts that behaved as expected / n */\n accuracy: number | null;\n}\n\nexport interface TriggerReport {\n skill: string;\n results: PromptTrigger[];\n train: Rates;\n heldout: Rates;\n /** claude runs started (0 when everything was cached) */\n runs: number;\n}\n\nconst rate = (xs: boolean[]) => (xs.length ? xs.filter(Boolean).length / xs.length : null);\n\nexport function ratesOf(rs: PromptTrigger[]): Rates {\n return {\n n: rs.length,\n positiveRate: rate(rs.filter((r) => r.should_trigger).map((r) => r.triggered)),\n falseTriggerRate: rate(rs.filter((r) => !r.should_trigger).map((r) => r.triggered)),\n accuracy: rate(rs.map((r) => r.triggered === r.should_trigger)),\n };\n}\n\n/** A scratch dir with the skill under test and copies of the repo's other project skills. The caller removes it. */\nexport function makeScratch(repoRoot: string, name: string, skillRaw: string): string {\n const dir = mkdtempSync(join(tmpdir(), \"sm-skill-eval-\"));\n const skills = join(dir, \".claude\", \"skills\");\n const src = join(repoRoot, \".claude\", \"skills\");\n if (existsSync(src)) {\n for (const e of readdirSync(src)) if (e !== name) cpSync(join(src, e), join(skills, e), { recursive: true });\n }\n mkdirSync(join(skills, name), { recursive: true });\n writeFileSync(join(skills, name, \"SKILL.md\"), skillRaw);\n return dir;\n}\n\ntype Cache = Map<string, { triggers: number; runs: number }>;\n// prompts come from real sessions: the cache (in $HOME, shared by every repo) keeps only their hash\nconst cacheKey = (hash: string, prompt: string) => `${hash}\\n${sha256(prompt)}`;\n\nfunction loadCache(path: string): Cache {\n const m: Cache = new Map();\n let raw = \"\";\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {}\n for (const line of raw.split(\"\\n\")) {\n try {\n const r = JSON.parse(line);\n m.set(`${r.skill}\\n${r.prompt}`, { triggers: r.triggers, runs: r.runs });\n } catch {}\n }\n return m;\n}\n\nexport interface TriggerOptions {\n repoRoot: string;\n /** the skill file as it is on disk (or a candidate version of it) */\n skillRaw: string;\n env?: NodeJS.ProcessEnv;\n timeoutMs?: number;\n}\n\nexport async function runTrigger(prompts: EvalPrompt[], o: TriggerOptions): Promise<TriggerReport> {\n const env = o.env ?? process.env;\n const skill = parseSkill(o.skillRaw);\n const hash = sha256(o.skillRaw);\n const cachePath = evalCachePath(env);\n const cache = loadCache(cachePath);\n const scratch = makeScratch(o.repoRoot, skill.name, o.skillRaw);\n let started = 0;\n const callOnce = async (text: string): Promise<boolean | string> => {\n started++;\n const r = await runClaude({ prompt: text, cwd: scratch, tools: [\"Skill\", \"Read\", \"Glob\", \"Grep\"], maxTurns: 2, output: \"stream-json\", env, timeoutMs: o.timeoutMs });\n return r.ok ? skillsCalled(r.events).includes(skill.name) : (r.error ?? \"claude failed\");\n };\n try {\n const results = await Promise.all(\n prompts.map(async (p): Promise<PromptTrigger> => {\n const hit = cache.get(cacheKey(hash, p.text));\n if (hit) return { ...p, ...hit, triggered: hit.triggers * 2 > hit.runs, cached: true };\n const votes: boolean[] = [];\n let error: string | undefined;\n // errored runs are retried inside the same budget of attempts, never counted as a vote\n for (let attempt = 0; attempt < MAX_TRIGGER_RUNS + 1 && votes.length < MAX_TRIGGER_RUNS; attempt++) {\n if (votes.length === 2 && votes[0] === votes[1]) break;\n const v = await callOnce(p.text);\n if (typeof v === \"string\") error = v;\n else votes.push(v);\n }\n const triggers = votes.filter(Boolean).length;\n if (votes.length) {\n mkdirSync(dirname(cachePath), { recursive: true });\n appendFileSync(cachePath, JSON.stringify({ skill: hash, prompt: sha256(p.text), triggers, runs: votes.length }) + \"\\n\");\n }\n return { ...p, triggers, runs: votes.length, triggered: triggers * 2 > votes.length, cached: false, ...(votes.length ? {} : { error }) };\n }),\n );\n return {\n skill: skill.name,\n results,\n train: ratesOf(results.filter((r) => r.split === \"train\")),\n heldout: ratesOf(results.filter((r) => r.split === \"heldout\")),\n runs: started,\n };\n } finally {\n rmSync(scratch, { recursive: true, force: true });\n }\n}\n"],"mappings":"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","names":["createHash","createHash"]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveSessionDir
|
|
3
3
|
} from "./chunk-EYVFC745.js";
|
|
4
|
-
import {
|
|
5
|
-
redact
|
|
6
|
-
} from "./chunk-H2BES6FU.js";
|
|
7
4
|
import {
|
|
8
5
|
matter_default
|
|
9
6
|
} from "./chunk-J7LYDRML.js";
|
|
7
|
+
import {
|
|
8
|
+
redact
|
|
9
|
+
} from "./chunk-H2BES6FU.js";
|
|
10
10
|
|
|
11
11
|
// src/core/dossier.ts
|
|
12
12
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
@@ -316,4 +316,4 @@ export {
|
|
|
316
316
|
appendToSection,
|
|
317
317
|
replaceSectionBody
|
|
318
318
|
};
|
|
319
|
-
//# sourceMappingURL=chunk-
|
|
319
|
+
//# sourceMappingURL=chunk-EXAUVIGW.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/core/jev.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var JEV_BAR = 0.95;
|
|
7
|
+
var BRIDGE = ["../../hooks/jev-bridge.py", "../hooks/jev-bridge.py"].map((p) => fileURLToPath(new URL(p, import.meta.url))).find((p) => existsSync(p)) ?? fileURLToPath(new URL("../../hooks/jev-bridge.py", import.meta.url));
|
|
8
|
+
function askJev(input, opts = {}) {
|
|
9
|
+
const bridge = opts.bridge ?? BRIDGE;
|
|
10
|
+
if (!existsSync(bridge)) return Promise.resolve({ scores: {}, error: `bridge missing at ${bridge}` });
|
|
11
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
12
|
+
return new Promise((done) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const finish = (a) => {
|
|
15
|
+
if (settled) return;
|
|
16
|
+
settled = true;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
done(a);
|
|
19
|
+
};
|
|
20
|
+
const child = spawn("python3", [bridge], { env: opts.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
child.kill("SIGKILL");
|
|
23
|
+
finish({ scores: {}, error: `timed out after ${timeoutMs} ms` });
|
|
24
|
+
}, timeoutMs);
|
|
25
|
+
let stdout = "";
|
|
26
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
27
|
+
child.on("error", (e) => finish({ scores: {}, error: `bridge failed to start: ${e.message}` }));
|
|
28
|
+
child.on("close", () => {
|
|
29
|
+
try {
|
|
30
|
+
const line = stdout.trim().split("\n").pop() || "{}";
|
|
31
|
+
const a = JSON.parse(line);
|
|
32
|
+
finish({ ...a, scores: a.scores && typeof a.scores === "object" ? a.scores : {} });
|
|
33
|
+
} catch {
|
|
34
|
+
finish({ scores: {}, error: "bridge gave no JSON" });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
child.stdin.on("error", () => {
|
|
38
|
+
});
|
|
39
|
+
const live = input.caller === "semantic-memory-hook" ? { timeout_seconds: Math.max(0.2, (timeoutMs - 250) / 1e3) } : {};
|
|
40
|
+
child.stdin.end(JSON.stringify({ ...input, ...live }));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function jevSpendToday(jevDir, now = /* @__PURE__ */ new Date()) {
|
|
44
|
+
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
45
|
+
const out = {};
|
|
46
|
+
const logs = join(jevDir, "logs");
|
|
47
|
+
let files = [];
|
|
48
|
+
try {
|
|
49
|
+
files = readdirSync(logs).filter((f) => f.startsWith("jev-usage-") && f.endsWith(".jsonl"));
|
|
50
|
+
} catch {
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
for (const line of readFileSync(join(logs, f), "utf8").split("\n")) {
|
|
55
|
+
try {
|
|
56
|
+
const r = JSON.parse(line);
|
|
57
|
+
if (String(r.caller).startsWith("semantic-memory-") && String(r.time).startsWith(day)) {
|
|
58
|
+
out[r.caller] = (out[r.caller] ?? 0) + (r.input_tokens ?? 0) + (r.output_tokens ?? 0);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
var jevVerdict = (p, bar = JEV_BAR) => p === void 0 ? null : p >= bar ? true : p <= 1 - bar + 1e-9 ? false : null;
|
|
67
|
+
var relevanceQuestion = (title, excerpt) => `Does this document explain the cause of this failure, or how to fix or avoid it \u2014 the same specific problem, not just the same tool or topic? Document: ${title} \u2014 ${excerpt.replace(/\s+/g, " ").trim().slice(0, 600)}`;
|
|
68
|
+
var failureState = (command, error) => `Command: ${command}
|
|
69
|
+
Error: ${error}`.slice(0, 2e3);
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
JEV_BAR,
|
|
73
|
+
BRIDGE,
|
|
74
|
+
askJev,
|
|
75
|
+
jevSpendToday,
|
|
76
|
+
jevVerdict,
|
|
77
|
+
relevanceQuestion,
|
|
78
|
+
failureState
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=chunk-GYR7U6KA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/jev.ts"],"sourcesContent":["/**\n * Ask Jev yes/no questions through hooks/jev-bridge.py → jev-loop's `router.ask` (v1.7 jev mode).\n * The bridge redacts (jev-loop's redact.py), caches, keeps the ledger and enforces budgets; this side\n * only spawns it, with a hard timeout. Every failure is `{scores: {}, error}` — callers treat a missing\n * score as \"no\" (the safe default), never as permission.\n */\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type JevCaller = \"semantic-memory-hook\" | \"semantic-memory-judge\" | \"semantic-memory-skill-loop\";\n\nexport interface JevQuestion {\n key: string;\n instructions: string;\n}\n\nexport interface JevAnswer {\n scores: Record<string, number>;\n tokens?: number;\n error?: string;\n /** The live caller's daily ceiling was reached: nothing was sent. */\n ceiling?: boolean;\n}\n\n/** jev-loop's bar: act on a yes only at or above it, on a no only at or below 1 - it. */\nexport const JEV_BAR = 0.95;\n\n// src/core/jev.ts sits two levels below the package root; built, this code can land in a shared dist/ chunk one level below\nexport const BRIDGE =\n [\"../../hooks/jev-bridge.py\", \"../hooks/jev-bridge.py\"].map((p) => fileURLToPath(new URL(p, import.meta.url))).find((p) => existsSync(p)) ??\n fileURLToPath(new URL(\"../../hooks/jev-bridge.py\", import.meta.url));\n\nexport function askJev(\n input: { cwd: string; caller: JevCaller; state: string; questions: JevQuestion[] },\n opts: { timeoutMs?: number; env?: NodeJS.ProcessEnv; bridge?: string } = {},\n): Promise<JevAnswer> {\n const bridge = opts.bridge ?? BRIDGE;\n if (!existsSync(bridge)) return Promise.resolve({ scores: {}, error: `bridge missing at ${bridge}` });\n const timeoutMs = opts.timeoutMs ?? 60_000;\n return new Promise((done) => {\n let settled = false;\n const finish = (a: JevAnswer) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n done(a);\n };\n const child = spawn(\"python3\", [bridge], { env: opts.env ?? process.env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n finish({ scores: {}, error: `timed out after ${timeoutMs} ms` });\n }, timeoutMs);\n let stdout = \"\";\n child.stdout.on(\"data\", (d) => (stdout += d));\n child.on(\"error\", (e) => finish({ scores: {}, error: `bridge failed to start: ${e.message}` }));\n child.on(\"close\", () => {\n try {\n const line = stdout.trim().split(\"\\n\").pop() || \"{}\";\n const a = JSON.parse(line) as JevAnswer;\n finish({ ...a, scores: a.scores && typeof a.scores === \"object\" ? a.scores : {} });\n } catch {\n finish({ scores: {}, error: \"bridge gave no JSON\" });\n }\n });\n child.stdin.on(\"error\", () => {});\n // live: the HTTP call must end inside the hook's budget (python start-up and import take ~150 ms)\n const live = input.caller === \"semantic-memory-hook\" ? { timeout_seconds: Math.max(0.2, (timeoutMs - 250) / 1000) } : {};\n child.stdin.end(JSON.stringify({ ...input, ...live }));\n });\n}\n\n/** Tokens each semantic-memory caller spent with Jev today, from jev-loop's usage ledger (numbers only). */\nexport function jevSpendToday(jevDir: string, now = new Date()): Record<string, number> {\n const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}`;\n const out: Record<string, number> = {};\n const logs = join(jevDir, \"logs\");\n let files: string[] = [];\n try {\n files = readdirSync(logs).filter((f) => f.startsWith(\"jev-usage-\") && f.endsWith(\".jsonl\"));\n } catch {\n return out;\n }\n for (const f of files) {\n for (const line of readFileSync(join(logs, f), \"utf8\").split(\"\\n\")) {\n try {\n const r = JSON.parse(line);\n if (String(r.caller).startsWith(\"semantic-memory-\") && String(r.time).startsWith(day)) {\n out[r.caller] = (out[r.caller] ?? 0) + (r.input_tokens ?? 0) + (r.output_tokens ?? 0);\n }\n } catch {}\n }\n }\n return out;\n}\n\n/** A Jev probability as a verdict: yes at or above the bar, no at or below 1 − bar, otherwise unsure (null). */\nexport const jevVerdict = (p: number | undefined, bar = JEV_BAR): boolean | null =>\n p === undefined ? null : p >= bar ? true : p <= 1 - bar + 1e-9 ? false : null;\n\n/** The failure → document relevance question (judge, gate and the failure hook ask the same thing). */\nexport const relevanceQuestion = (title: string, excerpt: string) =>\n `Does this document explain the cause of this failure, or how to fix or avoid it — the same specific problem, ` +\n `not just the same tool or topic? Document: ${title} — ${excerpt.replace(/\\s+/g, \" \").trim().slice(0, 600)}`;\n\n/** The state Jev sees for a failed tool call: the command and its error, capped. */\nexport const failureState = (command: string, error: string) => `Command: ${command}\\nError: ${error}`.slice(0, 2000);\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readEvidence,
|
|
3
|
+
stateDir
|
|
4
|
+
} from "./chunk-R6GNP7DS.js";
|
|
5
|
+
|
|
6
|
+
// src/core/skill-queue.ts
|
|
7
|
+
import { appendFileSync, mkdirSync, openSync, writeSync, closeSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, statSync } from "fs";
|
|
8
|
+
import { homedir, tmpdir } from "os";
|
|
9
|
+
import { join, sep } from "path";
|
|
10
|
+
var smHome = (env = process.env) => env.SEMANTIC_MEMORY_STATE_HOME || join(env.HOME || homedir(), ".semantic-memory");
|
|
11
|
+
var queueDir = (env = process.env) => join(smHome(env), "review-queue");
|
|
12
|
+
var runsPath = (env = process.env) => join(smHome(env), "skill-loop-runs.jsonl");
|
|
13
|
+
var lockPath = (repo) => join(stateDir(repo), "skill-loop.lock");
|
|
14
|
+
var logPath = (repo) => join(stateDir(repo), "skill-loop.log");
|
|
15
|
+
var dailyRunCap = (env = process.env) => Math.max(0, Number(env.SEMANTIC_MEMORY_SKILL_LOOP_DAILY_RUNS ?? 10) || 0);
|
|
16
|
+
var minToolCalls = (env = process.env) => Number(env.SEMANTIC_MEMORY_SKILL_LOOP_MIN_TOOL_CALLS) || 20;
|
|
17
|
+
var TRANSCRIPT_SCAN_MAX = 50 * 1024 * 1024;
|
|
18
|
+
var safeName = (s) => s.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
|
|
19
|
+
var real = (p) => {
|
|
20
|
+
try {
|
|
21
|
+
return realpathSync(p);
|
|
22
|
+
} catch {
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function inTempDir(repo, env = process.env) {
|
|
27
|
+
if (env.SEMANTIC_MEMORY_SKILL_LOOP_ALLOW_TMP === "1") return false;
|
|
28
|
+
const r = real(repo) + sep;
|
|
29
|
+
return [tmpdir(), "/tmp", "/var/tmp"].some((d) => r.startsWith(real(d) + sep));
|
|
30
|
+
}
|
|
31
|
+
function reviewedAt(session_id, env = process.env) {
|
|
32
|
+
let last = "";
|
|
33
|
+
for (const d of ["done", "failed"]) {
|
|
34
|
+
try {
|
|
35
|
+
const j = JSON.parse(readFileSync(join(queueDir(env), d, `${safeName(session_id)}.json`), "utf8"));
|
|
36
|
+
const at = String(j.finished || j.ts || "");
|
|
37
|
+
if (at > last) last = at;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return last || null;
|
|
42
|
+
}
|
|
43
|
+
function countToolCalls(transcript) {
|
|
44
|
+
if (!transcript) return 0;
|
|
45
|
+
try {
|
|
46
|
+
if (statSync(transcript).size > TRANSCRIPT_SCAN_MAX) return 0;
|
|
47
|
+
return readFileSync(transcript, "utf8").split('"type":"tool_use"').length - 1;
|
|
48
|
+
} catch {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function sessionJob(repo, input, env = process.env) {
|
|
53
|
+
const session_id = typeof input.session_id === "string" ? input.session_id : "";
|
|
54
|
+
if (!session_id || inTempDir(repo, env)) return null;
|
|
55
|
+
const since = reviewedAt(session_id, env) ?? void 0;
|
|
56
|
+
const mine = (r) => r.session_id === session_id && (!since || String(r.ts ?? "") > since);
|
|
57
|
+
const failures = readEvidence(repo, "failures").filter(mine);
|
|
58
|
+
const corrections = readEvidence(repo, "corrections").filter(mine).length;
|
|
59
|
+
const skills_used = [...new Set(readEvidence(repo, "usage").filter(mine).map((u) => String(u.skill)))];
|
|
60
|
+
const transcript_path = typeof input.transcript_path === "string" ? input.transcript_path : null;
|
|
61
|
+
const tool_calls = failures.length || corrections || since ? 0 : countToolCalls(transcript_path);
|
|
62
|
+
const why = failures.length ? `${failures.length} failure(s)` : corrections ? `${corrections} correction(s)` : tool_calls >= minToolCalls(env) ? `${tool_calls} tool calls` : "";
|
|
63
|
+
if (!why) return null;
|
|
64
|
+
return {
|
|
65
|
+
...since ? { since } : {},
|
|
66
|
+
session_id,
|
|
67
|
+
repo,
|
|
68
|
+
transcript_path,
|
|
69
|
+
reason: `${why}${since ? `; resumed since its review at ${since}` : ""}; ended: ${input.reason ?? "unknown"}`,
|
|
70
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71
|
+
evidence: { failures: failures.map((f) => String(f.id ?? "")).filter(Boolean), corrections, skills_used, tool_calls }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function enqueue(job, env = process.env) {
|
|
75
|
+
mkdirSync(queueDir(env), { recursive: true });
|
|
76
|
+
const path = join(queueDir(env), `${safeName(job.session_id)}.json`);
|
|
77
|
+
const last = reviewedAt(job.session_id, env);
|
|
78
|
+
if (last && (!job.since || job.since < last)) return path;
|
|
79
|
+
writeFileSync(path, JSON.stringify(job, null, 1));
|
|
80
|
+
return path;
|
|
81
|
+
}
|
|
82
|
+
function pendingJobs(repo, env = process.env) {
|
|
83
|
+
let files = [];
|
|
84
|
+
try {
|
|
85
|
+
files = readdirSync(queueDir(env)).filter((f) => f.endsWith(".json"));
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const jobs = [];
|
|
90
|
+
for (const f of files) {
|
|
91
|
+
try {
|
|
92
|
+
const path = join(queueDir(env), f);
|
|
93
|
+
const job = JSON.parse(readFileSync(path, "utf8"));
|
|
94
|
+
if (job.repo === repo) jobs.push({ path, job });
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return jobs.sort((a, b) => a.job.ts.localeCompare(b.job.ts));
|
|
99
|
+
}
|
|
100
|
+
function finishJob(path, outcome, result, env = process.env) {
|
|
101
|
+
const dir = join(queueDir(env), outcome);
|
|
102
|
+
mkdirSync(dir, { recursive: true });
|
|
103
|
+
const job = JSON.parse(readFileSync(path, "utf8"));
|
|
104
|
+
writeFileSync(path, JSON.stringify({ ...job, finished: (/* @__PURE__ */ new Date()).toISOString(), result }, null, 1));
|
|
105
|
+
renameSync(path, join(dir, path.split("/").pop()));
|
|
106
|
+
}
|
|
107
|
+
function bumpAttempts(path) {
|
|
108
|
+
const job = JSON.parse(readFileSync(path, "utf8"));
|
|
109
|
+
job.attempts = (job.attempts ?? 0) + 1;
|
|
110
|
+
writeFileSync(path, JSON.stringify(job, null, 1));
|
|
111
|
+
return job.attempts;
|
|
112
|
+
}
|
|
113
|
+
var localDay = (d = /* @__PURE__ */ new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
114
|
+
function runsToday(env = process.env, now = /* @__PURE__ */ new Date()) {
|
|
115
|
+
try {
|
|
116
|
+
const day = localDay(now);
|
|
117
|
+
return readFileSync(runsPath(env), "utf8").split("\n").filter((l) => l.includes(`"day":"${day}"`)).length;
|
|
118
|
+
} catch {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function recordRun(repo, session_id, env = process.env, now = /* @__PURE__ */ new Date()) {
|
|
123
|
+
mkdirSync(smHome(env), { recursive: true });
|
|
124
|
+
appendFileSync(runsPath(env), JSON.stringify({ day: localDay(now), ts: now.toISOString(), repo, session_id }) + "\n");
|
|
125
|
+
}
|
|
126
|
+
var alive = (pid) => {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(pid, 0);
|
|
129
|
+
return true;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return e.code === "EPERM";
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
function lockHolder(repo) {
|
|
135
|
+
try {
|
|
136
|
+
const pid = Number(readFileSync(lockPath(repo), "utf8").trim());
|
|
137
|
+
return pid && alive(pid) ? pid : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function takeLock(repo) {
|
|
143
|
+
mkdirSync(stateDir(repo), { recursive: true });
|
|
144
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
145
|
+
try {
|
|
146
|
+
const fd = openSync(lockPath(repo), "wx");
|
|
147
|
+
writeSync(fd, `${process.pid}
|
|
148
|
+
`);
|
|
149
|
+
closeSync(fd);
|
|
150
|
+
return () => {
|
|
151
|
+
try {
|
|
152
|
+
if (Number(readFileSync(lockPath(repo), "utf8").trim()) === process.pid) unlinkSync(lockPath(repo));
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (e.code !== "EEXIST") throw e;
|
|
158
|
+
if (lockHolder(repo) !== null) return null;
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(lockPath(repo));
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function log(repo, line) {
|
|
168
|
+
try {
|
|
169
|
+
mkdirSync(stateDir(repo), { recursive: true });
|
|
170
|
+
appendFileSync(logPath(repo), `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
171
|
+
`);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
smHome,
|
|
178
|
+
queueDir,
|
|
179
|
+
lockPath,
|
|
180
|
+
logPath,
|
|
181
|
+
dailyRunCap,
|
|
182
|
+
inTempDir,
|
|
183
|
+
reviewedAt,
|
|
184
|
+
sessionJob,
|
|
185
|
+
enqueue,
|
|
186
|
+
pendingJobs,
|
|
187
|
+
finishJob,
|
|
188
|
+
bumpAttempts,
|
|
189
|
+
runsToday,
|
|
190
|
+
recordRun,
|
|
191
|
+
lockHolder,
|
|
192
|
+
takeLock,
|
|
193
|
+
log
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=chunk-M2PC64DB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-queue.ts"],"sourcesContent":["/**\n * The skill loop's review queue (v1.7). SessionEnd writes one job per session that has signal and starts\n * the worker detached; the worker drains this repo's jobs oldest first, inside a global daily cap. The\n * session never waits: SessionEnd hooks get 1.5 s by default, so the hook only writes and spawns.\n *\n * ~/.semantic-memory/review-queue/<session>.json pending jobs (all repos)\n * ~/.semantic-memory/review-queue/{done,failed}/ finished jobs; a resumed session is reviewed again only\n * for the failures / corrections recorded after that (`since`)\n * ~/.semantic-memory/skill-loop-runs.jsonl one line per review run (the daily cap)\n * <repo>/.claude/.semantic-memory/skill-loop.lock the repo's worker (pid)\n * <repo>/.claude/.semantic-memory/skill-loop.log what the worker did\n *\n * Standalone for hooks (no embedder).\n */\nimport { appendFileSync, mkdirSync, openSync, writeSync, closeSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, statSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { join, sep } from \"node:path\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\n\nexport interface ReviewJob {\n session_id: string;\n repo: string;\n transcript_path: string | null;\n reason: string;\n ts: string;\n evidence: { failures: string[]; corrections: number; skills_used: string[]; tool_calls: number };\n attempts?: number;\n /** a follow-up review of a resumed session: only evidence recorded after this time (the last review's end) */\n since?: string;\n}\n\nexport const smHome = (env = process.env) => env.SEMANTIC_MEMORY_STATE_HOME || join(env.HOME || homedir(), \".semantic-memory\");\nexport const queueDir = (env = process.env) => join(smHome(env), \"review-queue\");\nconst runsPath = (env = process.env) => join(smHome(env), \"skill-loop-runs.jsonl\");\nexport const lockPath = (repo: string) => join(stateDir(repo), \"skill-loop.lock\");\nexport const logPath = (repo: string) => join(stateDir(repo), \"skill-loop.log\");\n\nexport const dailyRunCap = (env = process.env) => Math.max(0, Number(env.SEMANTIC_MEMORY_SKILL_LOOP_DAILY_RUNS ?? 10) || 0);\nconst minToolCalls = (env = process.env) => Number(env.SEMANTIC_MEMORY_SKILL_LOOP_MIN_TOOL_CALLS) || 20;\n// shortcut: counts tool calls by scanning the transcript text; above this size the count is skipped (signal then\n// needs a failure or a correction). Upgrade if long tool-only sessions should qualify.\nconst TRANSCRIPT_SCAN_MAX = 50 * 1024 * 1024;\n\nconst safeName = (s: string) => s.replace(/[^A-Za-z0-9_.-]/g, \"_\").slice(0, 120);\n\nconst real = (p: string) => {\n try {\n return realpathSync(p);\n } catch {\n return p;\n }\n};\n/** Throwaway repos (test runs, benchmarks in mkdtemp dirs) are never reviewed: they would use the daily cap. */\nexport function inTempDir(repo: string, env = process.env): boolean {\n if (env.SEMANTIC_MEMORY_SKILL_LOOP_ALLOW_TMP === \"1\") return false;\n const r = real(repo) + sep;\n return [tmpdir(), \"/tmp\", \"/var/tmp\"].some((d) => r.startsWith(real(d) + sep));\n}\n\n/** When this session's last review finished (done or failed record), or null if it was never reviewed. */\nexport function reviewedAt(session_id: string, env = process.env): string | null {\n let last = \"\";\n for (const d of [\"done\", \"failed\"]) {\n try {\n const j = JSON.parse(readFileSync(join(queueDir(env), d, `${safeName(session_id)}.json`), \"utf8\"));\n const at = String(j.finished || j.ts || \"\");\n if (at > last) last = at;\n } catch {}\n }\n return last || null;\n}\n\nfunction countToolCalls(transcript: string | null): number {\n if (!transcript) return 0;\n try {\n if (statSync(transcript).size > TRANSCRIPT_SCAN_MAX) return 0;\n return readFileSync(transcript, \"utf8\").split('\"type\":\"tool_use\"').length - 1;\n } catch {\n return 0;\n }\n}\n\n/** The job SessionEnd would write for this session, or null when the session has nothing to learn from. */\nexport function sessionJob(repo: string, input: { session_id?: string; transcript_path?: string; reason?: string }, env = process.env): ReviewJob | null {\n const session_id = typeof input.session_id === \"string\" ? input.session_id : \"\";\n if (!session_id || inTempDir(repo, env)) return null;\n const since = reviewedAt(session_id, env) ?? undefined;\n const mine = (r: Record<string, any>) => r.session_id === session_id && (!since || String(r.ts ?? \"\") > since);\n const failures = readEvidence(repo, \"failures\").filter(mine);\n const corrections = readEvidence(repo, \"corrections\").filter(mine).length;\n const skills_used = [...new Set(readEvidence(repo, \"usage\").filter(mine).map((u) => String(u.skill)))];\n const transcript_path = typeof input.transcript_path === \"string\" ? input.transcript_path : null;\n // a resumed session needs new failures or corrections: its tool-call count is mostly the reviewed part\n const tool_calls = failures.length || corrections || since ? 0 : countToolCalls(transcript_path);\n const why = failures.length ? `${failures.length} failure(s)` : corrections ? `${corrections} correction(s)` : tool_calls >= minToolCalls(env) ? `${tool_calls} tool calls` : \"\";\n if (!why) return null;\n return {\n ...(since ? { since } : {}),\n session_id,\n repo,\n transcript_path,\n reason: `${why}${since ? `; resumed since its review at ${since}` : \"\"}; ended: ${input.reason ?? \"unknown\"}`,\n ts: new Date().toISOString(),\n evidence: { failures: failures.map((f) => String(f.id ?? \"\")).filter(Boolean), corrections, skills_used, tool_calls },\n };\n}\n\nexport function enqueue(job: ReviewJob, env = process.env): string {\n mkdirSync(queueDir(env), { recursive: true });\n const path = join(queueDir(env), `${safeName(job.session_id)}.json`);\n // a session that ends twice (e.g. /clear then exit) is one job; once reviewed, only a follow-up job made after\n // that review (since = its end) goes in\n const last = reviewedAt(job.session_id, env);\n if (last && (!job.since || job.since < last)) return path;\n writeFileSync(path, JSON.stringify(job, null, 1));\n return path;\n}\n\nexport function pendingJobs(repo: string, env = process.env): { path: string; job: ReviewJob }[] {\n let files: string[] = [];\n try {\n files = readdirSync(queueDir(env)).filter((f) => f.endsWith(\".json\"));\n } catch {\n return [];\n }\n const jobs: { path: string; job: ReviewJob }[] = [];\n for (const f of files) {\n try {\n const path = join(queueDir(env), f);\n const job = JSON.parse(readFileSync(path, \"utf8\")) as ReviewJob;\n if (job.repo === repo) jobs.push({ path, job });\n } catch {}\n }\n return jobs.sort((a, b) => a.job.ts.localeCompare(b.job.ts));\n}\n\nexport function finishJob(path: string, outcome: \"done\" | \"failed\", result: Record<string, unknown>, env = process.env): void {\n const dir = join(queueDir(env), outcome);\n mkdirSync(dir, { recursive: true });\n const job = JSON.parse(readFileSync(path, \"utf8\"));\n writeFileSync(path, JSON.stringify({ ...job, finished: new Date().toISOString(), result }, null, 1));\n renameSync(path, join(dir, path.split(\"/\").pop()!));\n}\n\nexport function bumpAttempts(path: string): number {\n const job = JSON.parse(readFileSync(path, \"utf8\")) as ReviewJob;\n job.attempts = (job.attempts ?? 0) + 1;\n writeFileSync(path, JSON.stringify(job, null, 1));\n return job.attempts;\n}\n\nconst localDay = (d = new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n\nexport function runsToday(env = process.env, now = new Date()): number {\n try {\n const day = localDay(now);\n return readFileSync(runsPath(env), \"utf8\").split(\"\\n\").filter((l) => l.includes(`\"day\":\"${day}\"`)).length;\n } catch {\n return 0;\n }\n}\n\nexport function recordRun(repo: string, session_id: string, env = process.env, now = new Date()): void {\n mkdirSync(smHome(env), { recursive: true });\n appendFileSync(runsPath(env), JSON.stringify({ day: localDay(now), ts: now.toISOString(), repo, session_id }) + \"\\n\");\n}\n\nconst alive = (pid: number) => {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === \"EPERM\";\n }\n};\n\nexport function lockHolder(repo: string): number | null {\n try {\n const pid = Number(readFileSync(lockPath(repo), \"utf8\").trim());\n return pid && alive(pid) ? pid : null;\n } catch {\n return null;\n }\n}\n\n/** Take the repo's worker lock (reclaiming a dead holder's). Returns a release function, or null if held. */\nexport function takeLock(repo: string): (() => void) | null {\n mkdirSync(stateDir(repo), { recursive: true });\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const fd = openSync(lockPath(repo), \"wx\");\n writeSync(fd, `${process.pid}\\n`);\n closeSync(fd);\n return () => {\n try {\n if (Number(readFileSync(lockPath(repo), \"utf8\").trim()) === process.pid) unlinkSync(lockPath(repo));\n } catch {}\n };\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"EEXIST\") throw e;\n if (lockHolder(repo) !== null) return null;\n try {\n unlinkSync(lockPath(repo));\n } catch {}\n }\n }\n return null;\n}\n\nexport function log(repo: string, line: string): void {\n try {\n mkdirSync(stateDir(repo), { recursive: true });\n appendFileSync(logPath(repo), `${new Date().toISOString()} ${line}\\n`);\n } catch {}\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClaude
|
|
3
|
+
} from "./chunk-PMOZKUEM.js";
|
|
4
|
+
import {
|
|
5
|
+
parseSkill
|
|
6
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
7
|
+
import {
|
|
8
|
+
featureOn
|
|
9
|
+
} from "./chunk-VQCNWRTL.js";
|
|
10
|
+
import {
|
|
11
|
+
askJev,
|
|
12
|
+
jevVerdict
|
|
13
|
+
} from "./chunk-GYR7U6KA.js";
|
|
14
|
+
|
|
15
|
+
// src/core/skill-eval/task.ts
|
|
16
|
+
import { execFile, execFileSync } from "child_process";
|
|
17
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { promisify } from "util";
|
|
21
|
+
|
|
22
|
+
// src/core/skill-eval/prompts/grader.ts
|
|
23
|
+
var graderPrompt = (verify, outcome) => `You are a grader. Decide whether the outcome below satisfies the expectation.
|
|
24
|
+
|
|
25
|
+
PASS: there is clear evidence the expectation is true AND the evidence reflects genuine task completion, not surface-level compliance.
|
|
26
|
+
FAIL: there is no evidence, or the evidence contradicts the expectation, or it is superficial (a claim of success with nothing to back it).
|
|
27
|
+
Judge only from the outcome text; do not assume anything it does not say.
|
|
28
|
+
|
|
29
|
+
<expectation>
|
|
30
|
+
${verify}
|
|
31
|
+
</expectation>
|
|
32
|
+
|
|
33
|
+
<outcome>
|
|
34
|
+
${outcome}
|
|
35
|
+
</outcome>
|
|
36
|
+
|
|
37
|
+
Answer with only JSON: {"passed": true|false, "evidence": "<one sentence quoting or describing what you found>"}`;
|
|
38
|
+
function parseGrade(text) {
|
|
39
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
40
|
+
try {
|
|
41
|
+
const j = JSON.parse(m ? m[0] : "");
|
|
42
|
+
return { passed: j.passed === true, evidence: String(j.evidence ?? "") };
|
|
43
|
+
} catch {
|
|
44
|
+
return { passed: false, evidence: "grader gave no JSON" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/skill-eval/task.ts
|
|
49
|
+
var execFileP = promisify(execFile);
|
|
50
|
+
function parseVerify(body) {
|
|
51
|
+
const m = body.match(/^##\s+Verify\s*$([\s\S]*?)(?=^##\s|(?![\s\S]))/m);
|
|
52
|
+
const text = m?.[1].trim() ?? "";
|
|
53
|
+
if (!text) return { kind: "none" };
|
|
54
|
+
const fenced = text.match(/```[^\n]*\n([\s\S]*?)```/);
|
|
55
|
+
if (fenced?.[1].trim()) return { kind: "command", command: fenced[1].trim() };
|
|
56
|
+
const inline = text.replace(/^(?:[-*]|\d+\.)\s+/, "").match(/^`([^`\n]+)`/);
|
|
57
|
+
return inline ? { kind: "command", command: inline[1].trim() } : { kind: "prose", text };
|
|
58
|
+
}
|
|
59
|
+
var OUTCOME_CAP = 2e3;
|
|
60
|
+
async function runArm(o, withSkill) {
|
|
61
|
+
const env = o.env ?? process.env;
|
|
62
|
+
const skill = parseSkill(o.skillRaw);
|
|
63
|
+
const verify = parseVerify(skill.body);
|
|
64
|
+
const holder = mkdtempSync(join(tmpdir(), "sm-skill-task-"));
|
|
65
|
+
const dir = join(holder, "repo");
|
|
66
|
+
const git = (...a) => execFileSync("git", ["-C", o.repoRoot, ...a], { stdio: "pipe" });
|
|
67
|
+
try {
|
|
68
|
+
git("worktree", "add", "--detach", dir, "HEAD");
|
|
69
|
+
const skillDir = join(dir, ".claude", "skills", skill.name);
|
|
70
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
71
|
+
if (withSkill) {
|
|
72
|
+
mkdirSync(skillDir, { recursive: true });
|
|
73
|
+
writeFileSync(join(skillDir, "SKILL.md"), o.skillRaw);
|
|
74
|
+
}
|
|
75
|
+
const allowedTools = ["Edit", "Write", ...(skill.meta?.applies_to.commands ?? []).map((c) => `Bash(${c}:*)`)];
|
|
76
|
+
const run = await runClaude({
|
|
77
|
+
prompt: o.prompt,
|
|
78
|
+
cwd: dir,
|
|
79
|
+
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
|
|
80
|
+
allowedTools,
|
|
81
|
+
maxTurns: o.maxTurns ?? 12,
|
|
82
|
+
env,
|
|
83
|
+
timeoutMs: o.timeoutMs
|
|
84
|
+
});
|
|
85
|
+
const finalText = run.result.slice(0, OUTCOME_CAP);
|
|
86
|
+
if (!run.ok) return { passed: false, evidence: "the run failed", finalText, error: run.error };
|
|
87
|
+
if (verify.kind === "none") return { passed: false, evidence: "the skill has no ## Verify to check", finalText };
|
|
88
|
+
if (verify.kind === "command") {
|
|
89
|
+
try {
|
|
90
|
+
await execFileP("bash", ["-c", verify.command], { cwd: dir, timeout: o.verifyTimeoutMs ?? 12e4, env });
|
|
91
|
+
return { passed: true, evidence: `\`${verify.command}\` exited 0`, finalText };
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return { passed: false, evidence: `\`${verify.command}\` failed: ${String(e.message).slice(0, 200)}`, finalText };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (featureOn("jev", o.repoRoot, env)) {
|
|
97
|
+
const a = await askJev(
|
|
98
|
+
{ cwd: o.repoRoot, caller: "semantic-memory-skill-loop", state: finalText, questions: [{ key: "verify", instructions: `Does this outcome satisfy: ${verify.text}?` }] },
|
|
99
|
+
{ env }
|
|
100
|
+
);
|
|
101
|
+
const p = a.scores.verify;
|
|
102
|
+
return { passed: jevVerdict(p) === true, evidence: a.error ? `jev: ${a.error}` : `jev p=${p}`, finalText };
|
|
103
|
+
}
|
|
104
|
+
const g = await runClaude({ prompt: graderPrompt(verify.text, finalText), cwd: dir, tools: [], maxTurns: 1, env, timeoutMs: o.timeoutMs });
|
|
105
|
+
return { ...g.ok ? parseGrade(g.result) : { passed: false, evidence: `grader failed: ${g.error}` }, finalText };
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { passed: false, evidence: "could not set up the scratch copy", finalText: "", error: e.message };
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
git("worktree", "remove", "--force", dir);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
rmSync(holder, { recursive: true, force: true });
|
|
114
|
+
try {
|
|
115
|
+
git("worktree", "prune");
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function runTaskEval(o) {
|
|
121
|
+
const skill = parseSkill(o.skillRaw);
|
|
122
|
+
const [w, wo] = await Promise.all([runArm(o, true), runArm(o, false)]);
|
|
123
|
+
return { skill: skill.name, verify: parseVerify(skill.body), with: w, without: wo };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
parseVerify,
|
|
128
|
+
runTaskEval
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=chunk-MTLYF357.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-eval/task.ts","../src/core/skill-eval/prompts/grader.ts"],"sourcesContent":["/**\n * Task eval (v1.7): replay a task prompt with and without the skill, each in its own throwaway\n * `git worktree` of HEAD (the real working tree is never touched), then check the outcome with the skill's\n * `## Verify`: a command is run here, in the scratch copy; prose is graded — by Jev in jev mode, else by\n * the grader prompt through `claude -p`.\n */\nimport { execFile, execFileSync } from \"node:child_process\";\nimport { mkdirSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { runClaude } from \"../claude-run.js\";\nimport { featureOn } from \"../features.js\";\nimport { askJev, jevVerdict } from \"../jev.js\";\nimport { parseSkill } from \"../skill-schema.js\";\nimport { graderPrompt, parseGrade } from \"./prompts/grader.js\";\n\nconst execFileP = promisify(execFile);\n\nexport type Verify = { kind: \"command\"; command: string } | { kind: \"prose\"; text: string } | { kind: \"none\" };\n\n/** The `## Verify` section as a command (fenced block, or a leading inline-backtick span) or as prose. */\nexport function parseVerify(body: string): Verify {\n const m = body.match(/^##\\s+Verify\\s*$([\\s\\S]*?)(?=^##\\s|(?![\\s\\S]))/m);\n const text = m?.[1].trim() ?? \"\";\n if (!text) return { kind: \"none\" };\n const fenced = text.match(/```[^\\n]*\\n([\\s\\S]*?)```/);\n if (fenced?.[1].trim()) return { kind: \"command\", command: fenced[1].trim() };\n const inline = text.replace(/^(?:[-*]|\\d+\\.)\\s+/, \"\").match(/^`([^`\\n]+)`/);\n return inline ? { kind: \"command\", command: inline[1].trim() } : { kind: \"prose\", text };\n}\n\nexport interface TaskArm {\n passed: boolean;\n evidence: string;\n /** the run's final text */\n finalText: string;\n error?: string;\n}\n\nexport interface TaskReport {\n skill: string;\n verify: Verify;\n with: TaskArm;\n without: TaskArm;\n}\n\nexport interface TaskOptions {\n repoRoot: string;\n skillRaw: string;\n prompt: string;\n env?: NodeJS.ProcessEnv;\n maxTurns?: number;\n timeoutMs?: number;\n verifyTimeoutMs?: number;\n}\n\nconst OUTCOME_CAP = 2000;\n\nasync function runArm(o: TaskOptions, withSkill: boolean): Promise<TaskArm> {\n const env = o.env ?? process.env;\n const skill = parseSkill(o.skillRaw);\n const verify = parseVerify(skill.body);\n const holder = mkdtempSync(join(tmpdir(), \"sm-skill-task-\"));\n const dir = join(holder, \"repo\");\n const git = (...a: string[]) => execFileSync(\"git\", [\"-C\", o.repoRoot, ...a], { stdio: \"pipe\" });\n try {\n git(\"worktree\", \"add\", \"--detach\", dir, \"HEAD\");\n const skillDir = join(dir, \".claude\", \"skills\", skill.name);\n rmSync(skillDir, { recursive: true, force: true }); // the baseline must not have it, the other arm gets this exact text\n if (withSkill) {\n mkdirSync(skillDir, { recursive: true });\n writeFileSync(join(skillDir, \"SKILL.md\"), o.skillRaw);\n }\n // dontAsk denies whatever is not allowed: edits, and only the commands the skill says it applies to\n const allowedTools = [\"Edit\", \"Write\", ...(skill.meta?.applies_to.commands ?? []).map((c) => `Bash(${c}:*)`)];\n const run = await runClaude({\n prompt: o.prompt,\n cwd: dir,\n tools: [\"Read\", \"Grep\", \"Glob\", \"Edit\", \"Write\", \"Bash\"],\n allowedTools,\n maxTurns: o.maxTurns ?? 12,\n env,\n timeoutMs: o.timeoutMs,\n });\n const finalText = run.result.slice(0, OUTCOME_CAP);\n if (!run.ok) return { passed: false, evidence: \"the run failed\", finalText, error: run.error };\n if (verify.kind === \"none\") return { passed: false, evidence: \"the skill has no ## Verify to check\", finalText };\n if (verify.kind === \"command\") {\n try {\n await execFileP(\"bash\", [\"-c\", verify.command], { cwd: dir, timeout: o.verifyTimeoutMs ?? 120_000, env });\n return { passed: true, evidence: `\\`${verify.command}\\` exited 0`, finalText };\n } catch (e) {\n return { passed: false, evidence: `\\`${verify.command}\\` failed: ${String((e as Error).message).slice(0, 200)}`, finalText };\n }\n }\n if (featureOn(\"jev\", o.repoRoot, env)) {\n const a = await askJev(\n { cwd: o.repoRoot, caller: \"semantic-memory-skill-loop\", state: finalText, questions: [{ key: \"verify\", instructions: `Does this outcome satisfy: ${verify.text}?` }] },\n { env },\n );\n const p = a.scores.verify;\n return { passed: jevVerdict(p) === true, evidence: a.error ? `jev: ${a.error}` : `jev p=${p}`, finalText };\n }\n const g = await runClaude({ prompt: graderPrompt(verify.text, finalText), cwd: dir, tools: [], maxTurns: 1, env, timeoutMs: o.timeoutMs });\n return { ...(g.ok ? parseGrade(g.result) : { passed: false, evidence: `grader failed: ${g.error}` }), finalText };\n } catch (e) {\n return { passed: false, evidence: \"could not set up the scratch copy\", finalText: \"\", error: (e as Error).message };\n } finally {\n try {\n git(\"worktree\", \"remove\", \"--force\", dir);\n } catch {}\n rmSync(holder, { recursive: true, force: true });\n try {\n git(\"worktree\", \"prune\");\n } catch {}\n }\n}\n\nexport async function runTaskEval(o: TaskOptions): Promise<TaskReport> {\n const skill = parseSkill(o.skillRaw);\n const [w, wo] = await Promise.all([runArm(o, true), runArm(o, false)]);\n return { skill: skill.name, verify: parseVerify(skill.body), with: w, without: wo };\n}\n","/**\n * Adapted from Anthropic's skill-creator (https://github.com/anthropics/claude-plugins-official, Apache License 2.0); modified for semantic-memory.\n */\n\n/**\n * Grade one outcome against a skill's `## Verify` text (jev mode off). Skill-creator's grader rules: a pass needs\n * clear evidence of real completion, not surface compliance; no evidence, or superficial evidence, is a fail.\n */\nexport const graderPrompt = (verify: string, outcome: string) =>\n `You are a grader. Decide whether the outcome below satisfies the expectation.\n\nPASS: there is clear evidence the expectation is true AND the evidence reflects genuine task completion, not surface-level compliance.\nFAIL: there is no evidence, or the evidence contradicts the expectation, or it is superficial (a claim of success with nothing to back it).\nJudge only from the outcome text; do not assume anything it does not say.\n\n<expectation>\n${verify}\n</expectation>\n\n<outcome>\n${outcome}\n</outcome>\n\nAnswer with only JSON: {\"passed\": true|false, \"evidence\": \"<one sentence quoting or describing what you found>\"}`;\n\n/** Parse the grader's reply; anything unparseable is a fail (the safe default). */\nexport function parseGrade(text: string): { passed: boolean; evidence: string } {\n const m = text.match(/\\{[\\s\\S]*\\}/);\n try {\n const j = JSON.parse(m ? m[0] : \"\");\n return { passed: j.passed === true, evidence: String(j.evidence ?? \"\") };\n } catch {\n return { passed: false, evidence: \"grader gave no JSON\" };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAMA,SAAS,UAAU,oBAAoB;AACvC,SAAS,WAAW,aAAa,QAAQ,qBAAqB;AAC9D,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,iBAAiB;;;ACFnB,IAAM,eAAe,CAAC,QAAgB,YAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM;AAAA;AAAA;AAAA;AAAA,EAIN,OAAO;AAAA;AAAA;AAAA;AAMF,SAAS,WAAW,MAAqD;AAC9E,QAAM,IAAI,KAAK,MAAM,aAAa;AAClC,MAAI;AACF,UAAM,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE;AAClC,WAAO,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,OAAO,EAAE,YAAY,EAAE,EAAE;AAAA,EACzE,QAAQ;AACN,WAAO,EAAE,QAAQ,OAAO,UAAU,sBAAsB;AAAA,EAC1D;AACF;;;ADjBA,IAAM,YAAY,UAAU,QAAQ;AAK7B,SAAS,YAAY,MAAsB;AAChD,QAAM,IAAI,KAAK,MAAM,iDAAiD;AACtE,QAAM,OAAO,IAAI,CAAC,EAAE,KAAK,KAAK;AAC9B,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,OAAO;AACjC,QAAM,SAAS,KAAK,MAAM,0BAA0B;AACpD,MAAI,SAAS,CAAC,EAAE,KAAK,EAAG,QAAO,EAAE,MAAM,WAAW,SAAS,OAAO,CAAC,EAAE,KAAK,EAAE;AAC5E,QAAM,SAAS,KAAK,QAAQ,sBAAsB,EAAE,EAAE,MAAM,cAAc;AAC1E,SAAO,SAAS,EAAE,MAAM,WAAW,SAAS,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,KAAK;AACzF;AA2BA,IAAM,cAAc;AAEpB,eAAe,OAAO,GAAgB,WAAsC;AAC1E,QAAM,MAAM,EAAE,OAAO,QAAQ;AAC7B,QAAM,QAAQ,WAAW,EAAE,QAAQ;AACnC,QAAM,SAAS,YAAY,MAAM,IAAI;AACrC,QAAM,SAAS,YAAY,KAAK,OAAO,GAAG,gBAAgB,CAAC;AAC3D,QAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,QAAM,MAAM,IAAI,MAAgB,aAAa,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,OAAO,CAAC;AAC/F,MAAI;AACF,QAAI,YAAY,OAAO,YAAY,KAAK,MAAM;AAC9C,UAAM,WAAW,KAAK,KAAK,WAAW,UAAU,MAAM,IAAI;AAC1D,WAAO,UAAU,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACjD,QAAI,WAAW;AACb,gBAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AACvC,oBAAc,KAAK,UAAU,UAAU,GAAG,EAAE,QAAQ;AAAA,IACtD;AAEA,UAAM,eAAe,CAAC,QAAQ,SAAS,IAAI,MAAM,MAAM,WAAW,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC;AAC5G,UAAM,MAAM,MAAM,UAAU;AAAA,MAC1B,QAAQ,EAAE;AAAA,MACV,KAAK;AAAA,MACL,OAAO,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,SAAS,MAAM;AAAA,MACvD;AAAA,MACA,UAAU,EAAE,YAAY;AAAA,MACxB;AAAA,MACA,WAAW,EAAE;AAAA,IACf,CAAC;AACD,UAAM,YAAY,IAAI,OAAO,MAAM,GAAG,WAAW;AACjD,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,QAAQ,OAAO,UAAU,kBAAkB,WAAW,OAAO,IAAI,MAAM;AAC7F,QAAI,OAAO,SAAS,OAAQ,QAAO,EAAE,QAAQ,OAAO,UAAU,uCAAuC,UAAU;AAC/G,QAAI,OAAO,SAAS,WAAW;AAC7B,UAAI;AACF,cAAM,UAAU,QAAQ,CAAC,MAAM,OAAO,OAAO,GAAG,EAAE,KAAK,KAAK,SAAS,EAAE,mBAAmB,MAAS,IAAI,CAAC;AACxG,eAAO,EAAE,QAAQ,MAAM,UAAU,KAAK,OAAO,OAAO,eAAe,UAAU;AAAA,MAC/E,SAAS,GAAG;AACV,eAAO,EAAE,QAAQ,OAAO,UAAU,KAAK,OAAO,OAAO,cAAc,OAAQ,EAAY,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,IAAI,UAAU;AAAA,MAC7H;AAAA,IACF;AACA,QAAI,UAAU,OAAO,EAAE,UAAU,GAAG,GAAG;AACrC,YAAM,IAAI,MAAM;AAAA,QACd,EAAE,KAAK,EAAE,UAAU,QAAQ,8BAA8B,OAAO,WAAW,WAAW,CAAC,EAAE,KAAK,UAAU,cAAc,8BAA8B,OAAO,IAAI,IAAI,CAAC,EAAE;AAAA,QACtK,EAAE,IAAI;AAAA,MACR;AACA,YAAM,IAAI,EAAE,OAAO;AACnB,aAAO,EAAE,QAAQ,WAAW,CAAC,MAAM,MAAM,UAAU,EAAE,QAAQ,QAAQ,EAAE,KAAK,KAAK,SAAS,CAAC,IAAI,UAAU;AAAA,IAC3G;AACA,UAAM,IAAI,MAAM,UAAU,EAAE,QAAQ,aAAa,OAAO,MAAM,SAAS,GAAG,KAAK,KAAK,OAAO,CAAC,GAAG,UAAU,GAAG,KAAK,WAAW,EAAE,UAAU,CAAC;AACzI,WAAO,EAAE,GAAI,EAAE,KAAK,WAAW,EAAE,MAAM,IAAI,EAAE,QAAQ,OAAO,UAAU,kBAAkB,EAAE,KAAK,GAAG,GAAI,UAAU;AAAA,EAClH,SAAS,GAAG;AACV,WAAO,EAAE,QAAQ,OAAO,UAAU,qCAAqC,WAAW,IAAI,OAAQ,EAAY,QAAQ;AAAA,EACpH,UAAE;AACA,QAAI;AACF,UAAI,YAAY,UAAU,WAAW,GAAG;AAAA,IAC1C,QAAQ;AAAA,IAAC;AACT,WAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC/C,QAAI;AACF,UAAI,YAAY,OAAO;AAAA,IACzB,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;AAEA,eAAsB,YAAY,GAAqC;AACrE,QAAM,QAAQ,WAAW,EAAE,QAAQ;AACnC,QAAM,CAAC,GAAG,EAAE,IAAI,MAAM,QAAQ,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC;AACrE,SAAO,EAAE,OAAO,MAAM,MAAM,QAAQ,YAAY,MAAM,IAAI,GAAG,MAAM,GAAG,SAAS,GAAG;AACpF;","names":[]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
featureOn
|
|
3
|
+
} from "./chunk-VQCNWRTL.js";
|
|
4
|
+
import {
|
|
5
|
+
askJev,
|
|
6
|
+
jevVerdict
|
|
7
|
+
} from "./chunk-GYR7U6KA.js";
|
|
8
|
+
import {
|
|
9
|
+
smHome
|
|
10
|
+
} from "./chunk-M2PC64DB.js";
|
|
11
|
+
|
|
12
|
+
// src/core/skill-scope.ts
|
|
13
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join, sep } from "path";
|
|
16
|
+
function privatePaths(env = process.env) {
|
|
17
|
+
const home = env.HOME || homedir();
|
|
18
|
+
return (env.SEMANTIC_MEMORY_PRIVATE_PATHS ?? "~/workspace/kentro").split(",").map((p) => p.trim().replace(/^~(?=$|\/)/, home)).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
var isPrivateRepo = (repo, env = process.env) => privatePaths(env).some((p) => repo === p || repo.startsWith(p.endsWith(sep) ? p : p + sep));
|
|
21
|
+
function repoReferences(repo, text, paths = []) {
|
|
22
|
+
const tokens = new Set(paths);
|
|
23
|
+
for (const m of text.matchAll(/(?:^|[\s`'"(])((?:\.\/)?[\w.@-]+(?:\/[\w.@-]+)+\/?|[\w-]+\.(?:sh|js|mjs|ts|py|json|toml|ya?ml|md))(?=[\s`'"),:;]|$)/gm)) {
|
|
24
|
+
tokens.add(m[1].replace(/^\.\//, "").replace(/\/$/, ""));
|
|
25
|
+
}
|
|
26
|
+
return [...tokens].filter((p) => p && !p.startsWith("/") && !p.startsWith("~") && !p.includes("..") && existsSync(join(repo, p)));
|
|
27
|
+
}
|
|
28
|
+
function commonSubdir(repo, refs) {
|
|
29
|
+
const dirs = refs.map((r) => {
|
|
30
|
+
let isDir = false;
|
|
31
|
+
try {
|
|
32
|
+
isDir = statSync(join(repo, r)).isDirectory();
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
const parts = r.split("/");
|
|
36
|
+
return (isDir ? parts : parts.slice(0, -1)).slice(0, 2).join("/");
|
|
37
|
+
});
|
|
38
|
+
if (!dirs.length || dirs.some((d) => !d)) return null;
|
|
39
|
+
const first = dirs[0];
|
|
40
|
+
return dirs.every((d) => d === first || d.startsWith(first + "/")) ? first : null;
|
|
41
|
+
}
|
|
42
|
+
var SCOPE_QUESTION = "Would these steps work unchanged in any software repository \u2014 they name no file, script, package, service or setting that belongs to one particular repository?";
|
|
43
|
+
async function decideScope(repo, draft, env = process.env) {
|
|
44
|
+
const refs = repoReferences(repo, `${draft.description}
|
|
45
|
+
${draft.body}`, draft.applies_to?.paths ?? []);
|
|
46
|
+
if (refs.length) {
|
|
47
|
+
const sub = commonSubdir(repo, refs);
|
|
48
|
+
if (sub && (existsSync(join(repo, sub, "package.json")) || existsSync(join(repo, sub, ".claude")) || existsSync(join(repo, sub, "pyproject.toml")))) {
|
|
49
|
+
return { scope: "directory", directory: sub, globalCandidate: false, why: `every path it names is under ${sub}/` };
|
|
50
|
+
}
|
|
51
|
+
return { scope: "project", globalCandidate: false, why: `names this repo's ${refs.slice(0, 3).join(", ")}` };
|
|
52
|
+
}
|
|
53
|
+
if (isPrivateRepo(repo, env)) return { scope: "project", globalCandidate: false, why: "private repo: stays here" };
|
|
54
|
+
if (!featureOn("jev", repo, env)) return { scope: "project", globalCandidate: false, why: "names nothing in the repo; jev mode off \u2192 project" };
|
|
55
|
+
const a = await askJev(
|
|
56
|
+
{
|
|
57
|
+
cwd: repo,
|
|
58
|
+
caller: "semantic-memory-skill-loop",
|
|
59
|
+
state: `Skill ${draft.name}: ${draft.description}
|
|
60
|
+
|
|
61
|
+
${draft.body}`.slice(0, 2e3),
|
|
62
|
+
questions: [{ key: "global", instructions: SCOPE_QUESTION }]
|
|
63
|
+
},
|
|
64
|
+
{ env, timeoutMs: 3e4 }
|
|
65
|
+
);
|
|
66
|
+
const yes = jevVerdict(a.scores.global) === true;
|
|
67
|
+
return {
|
|
68
|
+
scope: "project",
|
|
69
|
+
globalCandidate: yes,
|
|
70
|
+
why: yes ? `Jev: holds in any repo (p=${a.scores.global?.toFixed(2)}); promotion needs a second repo` : `names nothing in the repo; Jev not sure it is general (${a.error ?? `p=${a.scores.global?.toFixed(2)}`}) \u2192 project`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var skillIndexPath = (env = process.env) => join(smHome(env), "skill-index.jsonl");
|
|
74
|
+
function indexSkill(e, env = process.env) {
|
|
75
|
+
if (isPrivateRepo(e.repo, env)) return;
|
|
76
|
+
mkdirSync(smHome(env), { recursive: true });
|
|
77
|
+
appendFileSync(skillIndexPath(env), JSON.stringify({ ...e, ts: (/* @__PURE__ */ new Date()).toISOString() }) + "\n");
|
|
78
|
+
}
|
|
79
|
+
function readIndex(env = process.env) {
|
|
80
|
+
const latest = /* @__PURE__ */ new Map();
|
|
81
|
+
let raw = "";
|
|
82
|
+
try {
|
|
83
|
+
raw = readFileSync(skillIndexPath(env), "utf8");
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
for (const line of raw.split("\n")) {
|
|
88
|
+
try {
|
|
89
|
+
const e = JSON.parse(line);
|
|
90
|
+
latest.set(`${e.repo}\0${e.name}\0${e.path}`, e);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return [...latest.values()];
|
|
95
|
+
}
|
|
96
|
+
var nameTokens = (n) => new Set(n.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1));
|
|
97
|
+
function sameLesson(a, b) {
|
|
98
|
+
if (a === b) return true;
|
|
99
|
+
const x = nameTokens(a), y = nameTokens(b);
|
|
100
|
+
const shared = [...x].filter((t) => y.has(t)).length;
|
|
101
|
+
return shared / Math.max(x.size, y.size, 1) >= 0.75;
|
|
102
|
+
}
|
|
103
|
+
function promotionPeers(repo, name, env = process.env) {
|
|
104
|
+
if (isPrivateRepo(repo, env)) return [];
|
|
105
|
+
return readIndex(env).filter((e) => e.repo !== repo && e.status !== "archived" && sameLesson(e.name, name) && !isPrivateRepo(e.repo, env));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
decideScope,
|
|
110
|
+
indexSkill,
|
|
111
|
+
promotionPeers
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=chunk-N77YCMSX.js.map
|