@theglitchking/semantic-memory 1.6.1 → 1.7.0

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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +61 -0
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +19 -2
  6. package/THIRD_PARTY_NOTICES.md +17 -0
  7. package/commands/jev-mode.md +18 -0
  8. package/commands/skill-loop.md +23 -0
  9. package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
  10. package/dist/chunk-3FPUBAEW.js +113 -0
  11. package/dist/chunk-3FPUBAEW.js.map +1 -0
  12. package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
  13. package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
  14. package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
  15. package/dist/chunk-BEACL25M.js +224 -0
  16. package/dist/chunk-BEACL25M.js.map +1 -0
  17. package/dist/chunk-CATPGINC.js +186 -0
  18. package/dist/chunk-CATPGINC.js.map +1 -0
  19. package/dist/chunk-DBFD3MTT.js +130 -0
  20. package/dist/chunk-DBFD3MTT.js.map +1 -0
  21. package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
  22. package/dist/chunk-GYR7U6KA.js +80 -0
  23. package/dist/chunk-GYR7U6KA.js.map +1 -0
  24. package/dist/chunk-IEZVFKUT.js +157 -0
  25. package/dist/chunk-IEZVFKUT.js.map +1 -0
  26. package/dist/chunk-MQS7JEBD.js +137 -0
  27. package/dist/chunk-MQS7JEBD.js.map +1 -0
  28. package/dist/chunk-NCGM36NX.js +118 -0
  29. package/dist/chunk-NCGM36NX.js.map +1 -0
  30. package/dist/chunk-NIZLBNH4.js +165 -0
  31. package/dist/chunk-NIZLBNH4.js.map +1 -0
  32. package/dist/chunk-OT3MV7XF.js +123 -0
  33. package/dist/chunk-OT3MV7XF.js.map +1 -0
  34. package/dist/chunk-P3GLY42T.js +291 -0
  35. package/dist/chunk-P3GLY42T.js.map +1 -0
  36. package/dist/chunk-PJXYCCI2.js +177 -0
  37. package/dist/chunk-PJXYCCI2.js.map +1 -0
  38. package/dist/chunk-R6GNP7DS.js +87 -0
  39. package/dist/chunk-R6GNP7DS.js.map +1 -0
  40. package/dist/chunk-TKHWVISV.js +71 -0
  41. package/dist/chunk-TKHWVISV.js.map +1 -0
  42. package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
  43. package/dist/chunk-VBLCUWT4.js.map +1 -0
  44. package/dist/chunk-VQCNWRTL.js +121 -0
  45. package/dist/chunk-VQCNWRTL.js.map +1 -0
  46. package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
  47. package/dist/chunk-Y7H76Q7F.js.map +1 -0
  48. package/dist/chunk-ZSWI3Y4Y.js +232 -0
  49. package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
  50. package/dist/cli/index.js +381 -52
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/core/features.d.ts +38 -0
  53. package/dist/core/features.js +28 -0
  54. package/dist/core/index.d.ts +285 -2
  55. package/dist/core/index.js +170 -21
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/jev.d.ts +35 -0
  58. package/dist/core/jev.js +20 -0
  59. package/dist/core/skill-curator.d.ts +1 -0
  60. package/dist/core/skill-curator.js +41 -0
  61. package/dist/core/skill-evidence.d.ts +25 -0
  62. package/dist/core/skill-evidence.js +23 -0
  63. package/dist/core/skill-queue.d.ts +40 -0
  64. package/dist/core/skill-queue.js +38 -0
  65. package/dist/core/skill-review.d.ts +3 -0
  66. package/dist/core/skill-review.js +29 -0
  67. package/dist/core/skill-worker.d.ts +21 -0
  68. package/dist/core/skill-worker.js +70 -0
  69. package/dist/core/skill-worker.js.map +1 -0
  70. package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
  71. package/dist/failure-eval-IUJO47UC.js +18 -0
  72. package/dist/failure-eval-IUJO47UC.js.map +1 -0
  73. package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
  74. package/dist/healthcheck-S7D6YNTA.js.map +1 -0
  75. package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
  76. package/dist/lexicon-SEJSLBHO.js.map +1 -0
  77. package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
  78. package/dist/lint-3JNLRM5U.js.map +1 -0
  79. package/dist/mcp/server.js +19 -17
  80. package/dist/mcp/server.js.map +1 -1
  81. package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
  82. package/dist/profile-SNULTJ27.js.map +1 -0
  83. package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
  84. package/dist/schema-RDYUK6CC.js.map +1 -0
  85. package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
  86. package/dist/skill-land-XU2APPBJ.js +31 -0
  87. package/dist/skill-land-XU2APPBJ.js.map +1 -0
  88. package/dist/skill-ledger-AYXVNFX7.js +18 -0
  89. package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
  90. package/dist/skill-merge-GXBQLBZA.js +24 -0
  91. package/dist/skill-merge-GXBQLBZA.js.map +1 -0
  92. package/dist/skill-review-BaW5n0Zg.d.ts +179 -0
  93. package/dist/skill-schema-RVB3EHEF.js +30 -0
  94. package/dist/skill-schema-RVB3EHEF.js.map +1 -0
  95. package/hooks/hooks.json +26 -1
  96. package/hooks/jev-bridge.py +109 -0
  97. package/hooks/session-start.js +5 -0
  98. package/hooks/skill-loop.js +65 -0
  99. package/hooks/vault-context.js +104 -6
  100. package/package.json +4 -2
  101. package/dist/chunk-5F5ZIAW2.js +0 -79
  102. package/dist/chunk-5F5ZIAW2.js.map +0 -1
  103. package/dist/chunk-GYBUBJYM.js.map +0 -1
  104. package/dist/chunk-HUG7E25D.js.map +0 -1
  105. package/dist/failure-eval-37JRNTIW.js +0 -10
  106. /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
  107. /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
  108. /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
  109. /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
  110. /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
  111. /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
  112. /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
  113. /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
  114. /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
  115. /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
  116. /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
  117. /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
@@ -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":"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","names":[]}
@@ -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-HZ7B5KHY.js.map
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,157 @@
1
+ // src/core/claude-run.ts
2
+ import { spawn } from "child_process";
3
+ import { openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+ var MAX_CLAUDE = 5;
7
+ function claudeConcurrency(env = process.env) {
8
+ const n = Number(env.SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY);
9
+ return Number.isFinite(n) && n >= 1 ? Math.min(MAX_CLAUDE, Math.floor(n)) : MAX_CLAUDE;
10
+ }
11
+ var slotsDir = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_SLOTS_DIR || join(env.HOME || homedir(), ".semantic-memory", "claude-slots");
12
+ var alive = (pid) => {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ } catch (e) {
17
+ return e.code === "EPERM";
18
+ }
19
+ };
20
+ async function acquireSlot(opts = {}) {
21
+ const env = opts.env ?? process.env;
22
+ const dir = slotsDir(env);
23
+ const max = claudeConcurrency(env);
24
+ const deadline = Date.now() + (opts.timeoutMs ?? 30 * 6e4);
25
+ mkdirSync(dir, { recursive: true });
26
+ for (; ; ) {
27
+ for (let i = 0; i < max; i++) {
28
+ const path = join(dir, `slot-${i}`);
29
+ try {
30
+ const fd = openSync(path, "wx");
31
+ writeSync(fd, `${process.pid}
32
+ `);
33
+ closeSync(fd);
34
+ let released = false;
35
+ return () => {
36
+ if (released) return;
37
+ released = true;
38
+ try {
39
+ unlinkSync(path);
40
+ } catch {
41
+ }
42
+ };
43
+ } catch (e) {
44
+ if (e.code !== "EEXIST") throw e;
45
+ try {
46
+ const pid = Number(readFileSync(path, "utf8").trim());
47
+ if (!pid || !alive(pid)) unlinkSync(path);
48
+ } catch {
49
+ }
50
+ }
51
+ }
52
+ if (Date.now() > deadline) throw new Error(`no claude slot free after ${opts.timeoutMs ?? 18e5} ms (${max} in use)`);
53
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 250));
54
+ }
55
+ }
56
+ var claudeBin = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_BIN || "claude";
57
+ var skillLoopModel = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_LOOP_MODEL || "sonnet";
58
+ function fencedArgs(o, env = process.env) {
59
+ const args = [
60
+ "-p",
61
+ "--tools",
62
+ o.tools.join(","),
63
+ "--disallowedTools",
64
+ "Task,Agent",
65
+ "--strict-mcp-config",
66
+ "--settings",
67
+ JSON.stringify({ disableAllHooks: true }),
68
+ "--setting-sources",
69
+ "project,local",
70
+ "--permission-mode",
71
+ "dontAsk",
72
+ "--no-session-persistence",
73
+ "--max-turns",
74
+ String(o.maxTurns),
75
+ "--model",
76
+ o.model ?? skillLoopModel(env),
77
+ "--output-format",
78
+ o.output ?? "json"
79
+ ];
80
+ if (o.allowedTools?.length) args.push("--allowedTools", o.allowedTools.join(","));
81
+ if ((o.output ?? "json") === "stream-json") args.push("--verbose");
82
+ for (const d of o.addDirs ?? []) args.push("--add-dir", d);
83
+ if (o.appendSystemPrompt) args.push("--append-system-prompt", o.appendSystemPrompt);
84
+ return args;
85
+ }
86
+ async function runClaude(o) {
87
+ const env = { ...o.env ?? process.env, SEMANTIC_MEMORY_REVIEWER: "1" };
88
+ const fail = (error) => ({ ok: false, result: "", events: [], costUsd: 0, turns: 0, error });
89
+ let release;
90
+ try {
91
+ release = await acquireSlot({ env });
92
+ } catch (e) {
93
+ return fail(e.message);
94
+ }
95
+ try {
96
+ return await new Promise((done) => {
97
+ const child = spawn(claudeBin(env), fencedArgs(o, env), { cwd: o.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
98
+ let stdout = "";
99
+ let stderr = "";
100
+ const timer = setTimeout(() => {
101
+ child.kill("SIGKILL");
102
+ done(fail(`timed out after ${o.timeoutMs ?? 6e5} ms`));
103
+ }, o.timeoutMs ?? 6e5);
104
+ child.stdout.on("data", (d) => stdout += d);
105
+ child.stderr.on("data", (d) => stderr += d);
106
+ child.on("error", (e) => {
107
+ clearTimeout(timer);
108
+ done(fail(`could not start ${claudeBin(env)}: ${e.message}`));
109
+ });
110
+ child.on("close", (code) => {
111
+ clearTimeout(timer);
112
+ done(parseClaudeOutput(stdout, o.output ?? "json", code, stderr));
113
+ });
114
+ child.stdin.on("error", () => {
115
+ });
116
+ child.stdin.end(o.prompt);
117
+ });
118
+ } finally {
119
+ release();
120
+ }
121
+ }
122
+ function parseClaudeOutput(stdout, output, code, stderr = "") {
123
+ const events = [];
124
+ let final = null;
125
+ if (output === "json") {
126
+ try {
127
+ final = JSON.parse(stdout.trim());
128
+ } catch {
129
+ }
130
+ } else {
131
+ for (const line of stdout.split("\n")) {
132
+ if (!line.trim()) continue;
133
+ try {
134
+ const e = JSON.parse(line);
135
+ events.push(e);
136
+ if (e.type === "result") final = e;
137
+ } catch {
138
+ }
139
+ }
140
+ }
141
+ if (!final) return { ok: false, result: "", events, costUsd: 0, turns: 0, error: `no result from claude (exit ${code}): ${stderr.trim().slice(0, 300)}` };
142
+ return {
143
+ ok: !final.is_error && final.subtype !== "error_during_execution",
144
+ result: typeof final.result === "string" ? final.result : "",
145
+ events,
146
+ costUsd: Number(final.total_cost_usd) || 0,
147
+ turns: Number(final.num_turns) || 0,
148
+ error: final.is_error ? String(final.result ?? final.subtype ?? "error") : final.subtype === "error_max_turns" ? "max turns reached" : void 0
149
+ };
150
+ }
151
+ var skillsCalled = (events) => events.filter((e) => e.type === "assistant").flatMap((e) => Array.isArray(e.message?.content) ? e.message.content : []).filter((c) => c.type === "tool_use" && c.name === "Skill" && typeof c.input?.skill === "string").map((c) => c.input.skill);
152
+
153
+ export {
154
+ runClaude,
155
+ skillsCalled
156
+ };
157
+ //# sourceMappingURL=chunk-IEZVFKUT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/claude-run.ts"],"sourcesContent":["/**\n * Every `claude` process the skill loop starts goes through here (v1.7): the reviewer, eval runs, graders,\n * description tuning. Two guarantees:\n *\n * - **At most 5 at once, machine-wide.** A slot file under ~/.semantic-memory/claude-slots/ is held for the\n * life of each run (SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY, 1–5, higher clamps to 5). A slot whose\n * holder died is reclaimed.\n * - **Fenced.** No hooks, no MCP servers (claude.ai connectors included), no user settings, only the tools\n * asked for, never Agent/Task (no nested subagents), nothing outside the working directory (dontAsk),\n * no session file, and SEMANTIC_MEMORY_REVIEWER=1 so any semantic-memory hook that still runs exits at once.\n * Verified flag by flag on Claude Code 2.1.310 (.planning/v17-skill-loop-and-jev-mode/notes.md, Phase 2).\n */\nimport { spawn } from \"node:child_process\";\nimport { openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const MAX_CLAUDE = 5;\n\nexport function claudeConcurrency(env = process.env): number {\n const n = Number(env.SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY);\n return Number.isFinite(n) && n >= 1 ? Math.min(MAX_CLAUDE, Math.floor(n)) : MAX_CLAUDE;\n}\n\nexport const slotsDir = (env = process.env) =>\n env.SEMANTIC_MEMORY_CLAUDE_SLOTS_DIR || join(env.HOME || homedir(), \".semantic-memory\", \"claude-slots\");\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\n/** Take one of the machine-wide slots, waiting for one to free up. Resolves to its release function. */\nexport async function acquireSlot(opts: { env?: NodeJS.ProcessEnv; timeoutMs?: number; pollMs?: number } = {}): Promise<() => void> {\n const env = opts.env ?? process.env;\n const dir = slotsDir(env);\n const max = claudeConcurrency(env);\n const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);\n mkdirSync(dir, { recursive: true });\n for (;;) {\n for (let i = 0; i < max; i++) {\n const path = join(dir, `slot-${i}`);\n try {\n const fd = openSync(path, \"wx\");\n writeSync(fd, `${process.pid}\\n`);\n closeSync(fd);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n try {\n unlinkSync(path);\n } catch {}\n };\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"EEXIST\") throw e;\n // held: reclaim it when its holder is gone (crashed worker, killed run)\n try {\n const pid = Number(readFileSync(path, \"utf8\").trim());\n if (!pid || !alive(pid)) unlinkSync(path);\n } catch {}\n }\n }\n if (Date.now() > deadline) throw new Error(`no claude slot free after ${opts.timeoutMs ?? 1_800_000} ms (${max} in use)`);\n await new Promise((r) => setTimeout(r, opts.pollMs ?? 250));\n }\n}\n\nexport interface ClaudeRunOptions {\n prompt: string;\n cwd: string;\n /** built-in tools the run may use; [] = none */\n tools: string[];\n /** with dontAsk, only these permission rules run un-prompted, e.g. \"Edit\", \"Bash(npm test:*)\"; the rest is denied */\n allowedTools?: string[];\n maxTurns: number;\n model?: string;\n /** \"json\": one result object; \"stream-json\": every event (to see which tools were called) */\n output?: \"json\" | \"stream-json\";\n timeoutMs?: number;\n /** extra directories the run may read */\n addDirs?: string[];\n appendSystemPrompt?: string;\n env?: NodeJS.ProcessEnv;\n}\n\nexport interface ClaudeRunResult {\n ok: boolean;\n /** final assistant text */\n result: string;\n /** stream-json events (empty for json output) */\n events: any[];\n costUsd: number;\n turns: number;\n error?: string;\n}\n\nexport const claudeBin = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_BIN || \"claude\";\nexport const skillLoopModel = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_LOOP_MODEL || \"sonnet\";\n\n/** The fenced argument list (exported for tests: these flags are the security boundary). */\nexport function fencedArgs(o: Pick<ClaudeRunOptions, \"tools\" | \"allowedTools\" | \"maxTurns\" | \"model\" | \"output\" | \"addDirs\" | \"appendSystemPrompt\">, env = process.env): string[] {\n const args = [\n \"-p\",\n \"--tools\", o.tools.join(\",\"),\n \"--disallowedTools\", \"Task,Agent\",\n \"--strict-mcp-config\",\n \"--settings\", JSON.stringify({ disableAllHooks: true }),\n \"--setting-sources\", \"project,local\",\n \"--permission-mode\", \"dontAsk\",\n \"--no-session-persistence\",\n \"--max-turns\", String(o.maxTurns),\n \"--model\", o.model ?? skillLoopModel(env),\n \"--output-format\", o.output ?? \"json\",\n ];\n if (o.allowedTools?.length) args.push(\"--allowedTools\", o.allowedTools.join(\",\"));\n if ((o.output ?? \"json\") === \"stream-json\") args.push(\"--verbose\");\n for (const d of o.addDirs ?? []) args.push(\"--add-dir\", d);\n if (o.appendSystemPrompt) args.push(\"--append-system-prompt\", o.appendSystemPrompt);\n return args;\n}\n\n/** Run one fenced `claude -p` inside a slot. Never throws: failures come back as `ok: false` with the reason. */\nexport async function runClaude(o: ClaudeRunOptions): Promise<ClaudeRunResult> {\n const env = { ...(o.env ?? process.env), SEMANTIC_MEMORY_REVIEWER: \"1\" };\n const fail = (error: string): ClaudeRunResult => ({ ok: false, result: \"\", events: [], costUsd: 0, turns: 0, error });\n let release: () => void;\n try {\n release = await acquireSlot({ env });\n } catch (e) {\n return fail((e as Error).message);\n }\n try {\n return await new Promise<ClaudeRunResult>((done) => {\n const child = spawn(claudeBin(env), fencedArgs(o, env), { cwd: o.cwd, env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n done(fail(`timed out after ${o.timeoutMs ?? 600_000} ms`));\n }, o.timeoutMs ?? 600_000);\n child.stdout.on(\"data\", (d) => (stdout += d));\n child.stderr.on(\"data\", (d) => (stderr += d));\n child.on(\"error\", (e) => {\n clearTimeout(timer);\n done(fail(`could not start ${claudeBin(env)}: ${e.message}`));\n });\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n done(parseClaudeOutput(stdout, o.output ?? \"json\", code, stderr));\n });\n child.stdin.on(\"error\", () => {});\n child.stdin.end(o.prompt);\n });\n } finally {\n release();\n }\n}\n\nexport function parseClaudeOutput(stdout: string, output: \"json\" | \"stream-json\", code: number | null, stderr = \"\"): ClaudeRunResult {\n const events: any[] = [];\n let final: any = null;\n if (output === \"json\") {\n try {\n final = JSON.parse(stdout.trim());\n } catch {}\n } else {\n for (const line of stdout.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line);\n events.push(e);\n if (e.type === \"result\") final = e;\n } catch {}\n }\n }\n if (!final) return { ok: false, result: \"\", events, costUsd: 0, turns: 0, error: `no result from claude (exit ${code}): ${stderr.trim().slice(0, 300)}` };\n return {\n ok: !final.is_error && final.subtype !== \"error_during_execution\",\n result: typeof final.result === \"string\" ? final.result : \"\",\n events,\n costUsd: Number(final.total_cost_usd) || 0,\n turns: Number(final.num_turns) || 0,\n error: final.is_error ? String(final.result ?? final.subtype ?? \"error\") : final.subtype === \"error_max_turns\" ? \"max turns reached\" : undefined,\n };\n}\n\n/** Names of the skills a stream-json run invoked through the Skill tool, in order. */\nexport const skillsCalled = (events: any[]): string[] =>\n events\n .filter((e) => e.type === \"assistant\")\n .flatMap((e) => (Array.isArray(e.message?.content) ? e.message.content : []))\n .filter((c: any) => c.type === \"tool_use\" && c.name === \"Skill\" && typeof c.input?.skill === \"string\")\n .map((c: any) => c.input.skill);\n"],"mappings":";AAYA,SAAS,aAAa;AACtB,SAAS,UAAU,WAAW,WAAW,cAAc,YAAY,iBAAiB;AACpF,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,IAAM,aAAa;AAEnB,SAAS,kBAAkB,MAAM,QAAQ,KAAa;AAC3D,QAAM,IAAI,OAAO,IAAI,sCAAsC;AAC3D,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,KAAK,IAAI,YAAY,KAAK,MAAM,CAAC,CAAC,IAAI;AAC9E;AAEO,IAAM,WAAW,CAAC,MAAM,QAAQ,QACrC,IAAI,oCAAoC,KAAK,IAAI,QAAQ,QAAQ,GAAG,oBAAoB,cAAc;AAExG,IAAM,QAAQ,CAAC,QAAgB;AAC7B,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,GAAG;AACV,WAAQ,EAA4B,SAAS;AAAA,EAC/C;AACF;AAGA,eAAsB,YAAY,OAAyE,CAAC,GAAwB;AAClI,QAAM,MAAM,KAAK,OAAO,QAAQ;AAChC,QAAM,MAAM,SAAS,GAAG;AACxB,QAAM,MAAM,kBAAkB,GAAG;AACjC,QAAM,WAAW,KAAK,IAAI,KAAK,KAAK,aAAa,KAAK;AACtD,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,aAAS;AACP,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,YAAM,OAAO,KAAK,KAAK,QAAQ,CAAC,EAAE;AAClC,UAAI;AACF,cAAM,KAAK,SAAS,MAAM,IAAI;AAC9B,kBAAU,IAAI,GAAG,QAAQ,GAAG;AAAA,CAAI;AAChC,kBAAU,EAAE;AACZ,YAAI,WAAW;AACf,eAAO,MAAM;AACX,cAAI,SAAU;AACd,qBAAW;AACX,cAAI;AACF,uBAAW,IAAI;AAAA,UACjB,QAAQ;AAAA,UAAC;AAAA,QACX;AAAA,MACF,SAAS,GAAG;AACV,YAAK,EAA4B,SAAS,SAAU,OAAM;AAE1D,YAAI;AACF,gBAAM,MAAM,OAAO,aAAa,MAAM,MAAM,EAAE,KAAK,CAAC;AACpD,cAAI,CAAC,OAAO,CAAC,MAAM,GAAG,EAAG,YAAW,IAAI;AAAA,QAC1C,QAAQ;AAAA,QAAC;AAAA,MACX;AAAA,IACF;AACA,QAAI,KAAK,IAAI,IAAI,SAAU,OAAM,IAAI,MAAM,6BAA6B,KAAK,aAAa,IAAS,QAAQ,GAAG,UAAU;AACxH,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,UAAU,GAAG,CAAC;AAAA,EAC5D;AACF;AA+BO,IAAM,YAAY,CAAC,MAAM,QAAQ,QAAQ,IAAI,8BAA8B;AAC3E,IAAM,iBAAiB,CAAC,MAAM,QAAQ,QAAQ,IAAI,oCAAoC;AAGtF,SAAS,WAAW,GAA0H,MAAM,QAAQ,KAAe;AAChL,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IAAW,EAAE,MAAM,KAAK,GAAG;AAAA,IAC3B;AAAA,IAAqB;AAAA,IACrB;AAAA,IACA;AAAA,IAAc,KAAK,UAAU,EAAE,iBAAiB,KAAK,CAAC;AAAA,IACtD;AAAA,IAAqB;AAAA,IACrB;AAAA,IAAqB;AAAA,IACrB;AAAA,IACA;AAAA,IAAe,OAAO,EAAE,QAAQ;AAAA,IAChC;AAAA,IAAW,EAAE,SAAS,eAAe,GAAG;AAAA,IACxC;AAAA,IAAmB,EAAE,UAAU;AAAA,EACjC;AACA,MAAI,EAAE,cAAc,OAAQ,MAAK,KAAK,kBAAkB,EAAE,aAAa,KAAK,GAAG,CAAC;AAChF,OAAK,EAAE,UAAU,YAAY,cAAe,MAAK,KAAK,WAAW;AACjE,aAAW,KAAK,EAAE,WAAW,CAAC,EAAG,MAAK,KAAK,aAAa,CAAC;AACzD,MAAI,EAAE,mBAAoB,MAAK,KAAK,0BAA0B,EAAE,kBAAkB;AAClF,SAAO;AACT;AAGA,eAAsB,UAAU,GAA+C;AAC7E,QAAM,MAAM,EAAE,GAAI,EAAE,OAAO,QAAQ,KAAM,0BAA0B,IAAI;AACvE,QAAM,OAAO,CAAC,WAAoC,EAAE,IAAI,OAAO,QAAQ,IAAI,QAAQ,CAAC,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM;AACnH,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,YAAY,EAAE,IAAI,CAAC;AAAA,EACrC,SAAS,GAAG;AACV,WAAO,KAAM,EAAY,OAAO;AAAA,EAClC;AACA,MAAI;AACF,WAAO,MAAM,IAAI,QAAyB,CAAC,SAAS;AAClD,YAAM,QAAQ,MAAM,UAAU,GAAG,GAAG,WAAW,GAAG,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAC5G,UAAI,SAAS;AACb,UAAI,SAAS;AACb,YAAM,QAAQ,WAAW,MAAM;AAC7B,cAAM,KAAK,SAAS;AACpB,aAAK,KAAK,mBAAmB,EAAE,aAAa,GAAO,KAAK,CAAC;AAAA,MAC3D,GAAG,EAAE,aAAa,GAAO;AACzB,YAAM,OAAO,GAAG,QAAQ,CAAC,MAAO,UAAU,CAAE;AAC5C,YAAM,OAAO,GAAG,QAAQ,CAAC,MAAO,UAAU,CAAE;AAC5C,YAAM,GAAG,SAAS,CAAC,MAAM;AACvB,qBAAa,KAAK;AAClB,aAAK,KAAK,mBAAmB,UAAU,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;AAAA,MAC9D,CAAC;AACD,YAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,qBAAa,KAAK;AAClB,aAAK,kBAAkB,QAAQ,EAAE,UAAU,QAAQ,MAAM,MAAM,CAAC;AAAA,MAClE,CAAC;AACD,YAAM,MAAM,GAAG,SAAS,MAAM;AAAA,MAAC,CAAC;AAChC,YAAM,MAAM,IAAI,EAAE,MAAM;AAAA,IAC1B,CAAC;AAAA,EACH,UAAE;AACA,YAAQ;AAAA,EACV;AACF;AAEO,SAAS,kBAAkB,QAAgB,QAAgC,MAAqB,SAAS,IAAqB;AACnI,QAAM,SAAgB,CAAC;AACvB,MAAI,QAAa;AACjB,MAAI,WAAW,QAAQ;AACrB,QAAI;AACF,cAAQ,KAAK,MAAM,OAAO,KAAK,CAAC;AAAA,IAClC,QAAQ;AAAA,IAAC;AAAA,EACX,OAAO;AACL,eAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,UAAI,CAAC,KAAK,KAAK,EAAG;AAClB,UAAI;AACF,cAAM,IAAI,KAAK,MAAM,IAAI;AACzB,eAAO,KAAK,CAAC;AACb,YAAI,EAAE,SAAS,SAAU,SAAQ;AAAA,MACnC,QAAQ;AAAA,MAAC;AAAA,IACX;AAAA,EACF;AACA,MAAI,CAAC,MAAO,QAAO,EAAE,IAAI,OAAO,QAAQ,IAAI,QAAQ,SAAS,GAAG,OAAO,GAAG,OAAO,+BAA+B,IAAI,MAAM,OAAO,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,GAAG;AACxJ,SAAO;AAAA,IACL,IAAI,CAAC,MAAM,YAAY,MAAM,YAAY;AAAA,IACzC,QAAQ,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;AAAA,IAC1D;AAAA,IACA,SAAS,OAAO,MAAM,cAAc,KAAK;AAAA,IACzC,OAAO,OAAO,MAAM,SAAS,KAAK;AAAA,IAClC,OAAO,MAAM,WAAW,OAAO,MAAM,UAAU,MAAM,WAAW,OAAO,IAAI,MAAM,YAAY,oBAAoB,sBAAsB;AAAA,EACzI;AACF;AAGO,IAAM,eAAe,CAAC,WAC3B,OACG,OAAO,CAAC,MAAM,EAAE,SAAS,WAAW,EACpC,QAAQ,CAAC,MAAO,MAAM,QAAQ,EAAE,SAAS,OAAO,IAAI,EAAE,QAAQ,UAAU,CAAC,CAAE,EAC3E,OAAO,CAAC,MAAW,EAAE,SAAS,cAAc,EAAE,SAAS,WAAW,OAAO,EAAE,OAAO,UAAU,QAAQ,EACpG,IAAI,CAAC,MAAW,EAAE,MAAM,KAAK;","names":[]}
@@ -0,0 +1,137 @@
1
+ import {
2
+ runClaude,
3
+ skillsCalled
4
+ } from "./chunk-IEZVFKUT.js";
5
+ import {
6
+ parseSkill
7
+ } from "./chunk-ZSWI3Y4Y.js";
8
+
9
+ // src/core/skill-eval/sets.ts
10
+ import { createHash } from "crypto";
11
+ var STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "when", "this", "that", "use", "run", "you", "are", "not", "can", "how", "what", "from", "into", "your"]);
12
+ var terms = (s) => new Set(s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !STOP.has(t)));
13
+ var splitOf = (text) => createHash("sha256").update(text).digest().readUInt32BE(0) % 100 < 60 ? "train" : "heldout";
14
+ function buildEvalSet(skill, candidates, opts = {}) {
15
+ const sessions = new Set(skill.meta?.provenance.sessions ?? []);
16
+ const own = terms([skill.name, skill.description, ...skill.meta?.triggers ?? []].join(" "));
17
+ const seen = /* @__PURE__ */ new Set();
18
+ const positives = [];
19
+ const near = [];
20
+ for (const c of candidates) {
21
+ const text = c.text.trim();
22
+ if (!text || seen.has(text)) continue;
23
+ seen.add(text);
24
+ if (sessions.has(c.session)) positives.push(text);
25
+ else {
26
+ const overlap = [...terms(text)].filter((t) => own.has(t)).length;
27
+ if (overlap > 0) near.push({ text, overlap });
28
+ }
29
+ }
30
+ near.sort((a, b) => b.overlap - a.overlap || a.text.localeCompare(b.text));
31
+ const negatives = near.slice(0, opts.maxNegatives ?? Math.max(positives.length, 4)).map((n) => n.text);
32
+ return [...positives.map((text) => ({ text, should_trigger: true })), ...negatives.map((text) => ({ text, should_trigger: false }))].map((p) => ({ ...p, split: splitOf(p.text) }));
33
+ }
34
+
35
+ // src/core/skill-eval/trigger.ts
36
+ import { createHash as createHash2 } from "crypto";
37
+ import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
38
+ import { homedir, tmpdir } from "os";
39
+ import { dirname, join } from "path";
40
+ var MAX_TRIGGER_RUNS = 3;
41
+ var evalCachePath = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_EVAL_CACHE || join(env.HOME || homedir(), ".semantic-memory", "skill-eval-cache.jsonl");
42
+ var sha256 = (s) => createHash2("sha256").update(s).digest("hex");
43
+ var rate = (xs) => xs.length ? xs.filter(Boolean).length / xs.length : null;
44
+ function ratesOf(rs) {
45
+ return {
46
+ n: rs.length,
47
+ positiveRate: rate(rs.filter((r) => r.should_trigger).map((r) => r.triggered)),
48
+ falseTriggerRate: rate(rs.filter((r) => !r.should_trigger).map((r) => r.triggered)),
49
+ accuracy: rate(rs.map((r) => r.triggered === r.should_trigger))
50
+ };
51
+ }
52
+ function makeScratch(repoRoot, name, skillRaw) {
53
+ const dir = mkdtempSync(join(tmpdir(), "sm-skill-eval-"));
54
+ const skills = join(dir, ".claude", "skills");
55
+ const src = join(repoRoot, ".claude", "skills");
56
+ if (existsSync(src)) {
57
+ for (const e of readdirSync(src)) if (e !== name) cpSync(join(src, e), join(skills, e), { recursive: true });
58
+ }
59
+ mkdirSync(join(skills, name), { recursive: true });
60
+ writeFileSync(join(skills, name, "SKILL.md"), skillRaw);
61
+ return dir;
62
+ }
63
+ var cacheKey = (hash, prompt) => `${hash}
64
+ ${sha256(prompt)}`;
65
+ function loadCache(path) {
66
+ const m = /* @__PURE__ */ new Map();
67
+ let raw = "";
68
+ try {
69
+ raw = readFileSync(path, "utf8");
70
+ } catch {
71
+ }
72
+ for (const line of raw.split("\n")) {
73
+ try {
74
+ const r = JSON.parse(line);
75
+ m.set(`${r.skill}
76
+ ${r.prompt}`, { triggers: r.triggers, runs: r.runs });
77
+ } catch {
78
+ }
79
+ }
80
+ return m;
81
+ }
82
+ async function runTrigger(prompts, o) {
83
+ const env = o.env ?? process.env;
84
+ const skill = parseSkill(o.skillRaw);
85
+ const hash = sha256(o.skillRaw);
86
+ const cachePath = evalCachePath(env);
87
+ const cache = loadCache(cachePath);
88
+ const scratch = makeScratch(o.repoRoot, skill.name, o.skillRaw);
89
+ let started = 0;
90
+ const callOnce = async (text) => {
91
+ started++;
92
+ const r = await runClaude({ prompt: text, cwd: scratch, tools: ["Skill", "Read", "Glob", "Grep"], maxTurns: 2, output: "stream-json", env, timeoutMs: o.timeoutMs });
93
+ return r.ok ? skillsCalled(r.events).includes(skill.name) : r.error ?? "claude failed";
94
+ };
95
+ try {
96
+ const results = await Promise.all(
97
+ prompts.map(async (p) => {
98
+ const hit = cache.get(cacheKey(hash, p.text));
99
+ if (hit) return { ...p, ...hit, triggered: hit.triggers * 2 > hit.runs, cached: true };
100
+ const votes = [];
101
+ let error;
102
+ for (let attempt = 0; attempt < MAX_TRIGGER_RUNS + 1 && votes.length < MAX_TRIGGER_RUNS; attempt++) {
103
+ if (votes.length === 2 && votes[0] === votes[1]) break;
104
+ const v = await callOnce(p.text);
105
+ if (typeof v === "string") error = v;
106
+ else votes.push(v);
107
+ }
108
+ const triggers = votes.filter(Boolean).length;
109
+ if (votes.length) {
110
+ mkdirSync(dirname(cachePath), { recursive: true });
111
+ appendFileSync(cachePath, JSON.stringify({ skill: hash, prompt: sha256(p.text), triggers, runs: votes.length }) + "\n");
112
+ }
113
+ return { ...p, triggers, runs: votes.length, triggered: triggers * 2 > votes.length, cached: false, ...votes.length ? {} : { error } };
114
+ })
115
+ );
116
+ return {
117
+ skill: skill.name,
118
+ results,
119
+ train: ratesOf(results.filter((r) => r.split === "train")),
120
+ heldout: ratesOf(results.filter((r) => r.split === "heldout")),
121
+ runs: started
122
+ };
123
+ } finally {
124
+ rmSync(scratch, { recursive: true, force: true });
125
+ }
126
+ }
127
+
128
+ export {
129
+ splitOf,
130
+ buildEvalSet,
131
+ MAX_TRIGGER_RUNS,
132
+ evalCachePath,
133
+ ratesOf,
134
+ makeScratch,
135
+ runTrigger
136
+ };
137
+ //# sourceMappingURL=chunk-MQS7JEBD.js.map
@@ -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"]}
@@ -0,0 +1,118 @@
1
+ import {
2
+ materializeDraft,
3
+ parseReviewerJson
4
+ } from "./chunk-P3GLY42T.js";
5
+ import {
6
+ runClaude
7
+ } from "./chunk-IEZVFKUT.js";
8
+ import {
9
+ findSkills
10
+ } from "./chunk-ZSWI3Y4Y.js";
11
+ import {
12
+ featureOn,
13
+ readRepoConfig
14
+ } from "./chunk-VQCNWRTL.js";
15
+ import {
16
+ askJev,
17
+ jevVerdict
18
+ } from "./chunk-GYR7U6KA.js";
19
+
20
+ // src/core/skill-merge.ts
21
+ var MERGE_MIN_OVERLAP = 0.5;
22
+ var words = (s) => new Set(
23
+ [s.skill.name, s.skill.description, ...s.skill.meta?.triggers ?? [], ...s.skill.meta?.applies_to.paths ?? [], ...s.skill.meta?.applies_to.commands ?? []].join(" ").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2)
24
+ );
25
+ function mergeCandidates(repo, env = process.env) {
26
+ const learned = findSkills(repo, { home: env.HOME, global: false }).filter((s) => s.learned && s.skill.meta && !s.skill.meta.pinned);
27
+ const out = [];
28
+ for (let i = 0; i < learned.length; i++) {
29
+ for (let j = i + 1; j < learned.length; j++) {
30
+ const x = words(learned[i]), y = words(learned[j]);
31
+ const shared = [...x].filter((w) => y.has(w)).length;
32
+ const overlap = shared / (x.size + y.size - shared || 1);
33
+ if (overlap >= MERGE_MIN_OVERLAP) out.push({ a: learned[i], b: learned[j], overlap });
34
+ }
35
+ }
36
+ return out.sort((p, q) => q.overlap - p.overlap);
37
+ }
38
+ var consolidateOn = (repo) => readRepoConfig(repo).skill_loop_consolidate === true;
39
+ var show = (s) => `### ${s.skill.name}
40
+ ${s.skill.description}
41
+
42
+ ${s.skill.body}`;
43
+ var MERGE_PROMPT = `Two learned skills of this repository may describe the same procedure. If they do (or one contains the
44
+ other), write ONE skill that keeps every step and pitfall that is still true, in the same format as the inputs:
45
+ sections "## When to use", "## Steps", "## Pitfalls", "## Verify". Never invent steps. If they are different
46
+ procedures, say so.
47
+
48
+ Answer with one JSON object and nothing else:
49
+ {"drafts": [{"kind": "new", "name": "<kebab-case>", "description": "<what + Use when ...>", "scope": "project",
50
+ "applies_to": {"paths": [], "commands": []}, "triggers": [], "provenance": {"sessions": [], "failures": [], "corrections": []},
51
+ "cites": [], "body": "..."}]} \u2014 or {"drafts": [], "none_reason": "<why they are different>"}`;
52
+ async function proposeMerges(repo, env = process.env) {
53
+ const drafted = [];
54
+ const skipped = [];
55
+ const jev = featureOn("jev", repo, env);
56
+ const used = /* @__PURE__ */ new Set();
57
+ for (const { a, b } of mergeCandidates(repo, env)) {
58
+ if (used.has(a.path) || used.has(b.path)) continue;
59
+ const pair = `${a.skill.name} + ${b.skill.name}`;
60
+ if (jev) {
61
+ const r2 = await askJev(
62
+ {
63
+ cwd: repo,
64
+ caller: "semantic-memory-skill-loop",
65
+ state: `${show(a)}
66
+
67
+ ${show(b)}`.slice(0, 2e3),
68
+ questions: [
69
+ { key: "same", instructions: "Do these two skills describe the same procedure, so that one could replace the other?" },
70
+ { key: "subset", instructions: "Is every step of one of these skills already contained in the other skill?" }
71
+ ]
72
+ },
73
+ { env, timeoutMs: 3e4 }
74
+ );
75
+ if (jevVerdict(r2.scores.same) !== true && jevVerdict(r2.scores.subset) !== true) {
76
+ skipped.push(`${pair}: Jev did not say duplicate (${r2.error ?? JSON.stringify(r2.scores)})`);
77
+ continue;
78
+ }
79
+ }
80
+ const run = await runClaude({ prompt: `${MERGE_PROMPT}
81
+
82
+ ---
83
+
84
+ ${show(a)}
85
+
86
+ ${show(b)}`, cwd: repo, tools: [], maxTurns: 1, env });
87
+ const answer = run.ok ? parseReviewerJson(run.result) : null;
88
+ const d = answer?.drafts[0];
89
+ if (!d) {
90
+ skipped.push(`${pair}: ${answer?.none_reason ?? run.error ?? "no merge proposed"}`);
91
+ continue;
92
+ }
93
+ const prov = (k) => [.../* @__PURE__ */ new Set([...a.skill.meta.provenance[k] ?? [], ...b.skill.meta.provenance[k] ?? []])];
94
+ const job = { session_id: prov("sessions")[0] ?? "merge", repo, transcript_path: null, reason: `merge of ${pair}`, ts: (/* @__PURE__ */ new Date()).toISOString(), evidence: { failures: prov("failures"), corrections: prov("corrections").length, skills_used: [], tool_calls: 0 } };
95
+ const r = await materializeDraft(
96
+ repo,
97
+ job,
98
+ { ...d, kind: "new", provenance: { sessions: prov("sessions"), failures: prov("failures"), corrections: prov("corrections") } },
99
+ findSkills(repo, { home: env.HOME }).filter((s) => s.path !== a.path && s.path !== b.path),
100
+ async () => ({ scope: "project", globalCandidate: false, why: `merge of ${pair}` }),
101
+ env,
102
+ { supersedes: [a.path, b.path] }
103
+ );
104
+ if (r.id) {
105
+ drafted.push(r.id);
106
+ used.add(a.path).add(b.path);
107
+ } else skipped.push(`${pair}: ${r.rejected}`);
108
+ }
109
+ return { drafted, skipped };
110
+ }
111
+
112
+ export {
113
+ MERGE_MIN_OVERLAP,
114
+ mergeCandidates,
115
+ consolidateOn,
116
+ proposeMerges
117
+ };
118
+ //# sourceMappingURL=chunk-NCGM36NX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/skill-merge.ts"],"sourcesContent":["/**\n * Merging learned skills (v1.7) — opt-in only: `skill_loop_consolidate: true` in the repo config, or\n * `skill-loop curate --merge`. Candidate pairs come from word overlap of name, description, triggers and\n * applies_to. Jev mode: Jev judges each pair (same procedure / one contains the other, >= 0.95) and only a\n * yes goes on to Claude for the merged text; without jev mode Claude judges and drafts in one call. Either\n * way the result is a review draft that `supersedes` both — nothing merges without a person.\n */\nimport { featureOn, readRepoConfig } from \"./features.js\";\nimport { askJev, jevVerdict } from \"./jev.js\";\nimport { runClaude } from \"./claude-run.js\";\nimport { findSkills, type SkillFile } from \"./skill-schema.js\";\nimport { materializeDraft, parseReviewerJson } from \"./skill-review.js\";\n\nexport const MERGE_MIN_OVERLAP = 0.5;\n\nconst words = (s: SkillFile) =>\n new Set(\n [s.skill.name, s.skill.description, ...(s.skill.meta?.triggers ?? []), ...(s.skill.meta?.applies_to.paths ?? []), ...(s.skill.meta?.applies_to.commands ?? [])]\n .join(\" \")\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((w) => w.length > 2),\n );\n\nexport function mergeCandidates(repo: string, env = process.env): { a: SkillFile; b: SkillFile; overlap: number }[] {\n const learned = findSkills(repo, { home: env.HOME, global: false }).filter((s) => s.learned && s.skill.meta && !s.skill.meta.pinned);\n const out: { a: SkillFile; b: SkillFile; overlap: number }[] = [];\n for (let i = 0; i < learned.length; i++) {\n for (let j = i + 1; j < learned.length; j++) {\n const x = words(learned[i]), y = words(learned[j]);\n const shared = [...x].filter((w) => y.has(w)).length;\n const overlap = shared / (x.size + y.size - shared || 1);\n if (overlap >= MERGE_MIN_OVERLAP) out.push({ a: learned[i], b: learned[j], overlap });\n }\n }\n return out.sort((p, q) => q.overlap - p.overlap);\n}\n\nexport const consolidateOn = (repo: string) => readRepoConfig(repo).skill_loop_consolidate === true;\n\nconst show = (s: SkillFile) => `### ${s.skill.name}\\n${s.skill.description}\\n\\n${s.skill.body}`;\n\nconst MERGE_PROMPT = `Two learned skills of this repository may describe the same procedure. If they do (or one contains the\nother), write ONE skill that keeps every step and pitfall that is still true, in the same format as the inputs:\nsections \"## When to use\", \"## Steps\", \"## Pitfalls\", \"## Verify\". Never invent steps. If they are different\nprocedures, say so.\n\nAnswer with one JSON object and nothing else:\n{\"drafts\": [{\"kind\": \"new\", \"name\": \"<kebab-case>\", \"description\": \"<what + Use when ...>\", \"scope\": \"project\",\n \"applies_to\": {\"paths\": [], \"commands\": []}, \"triggers\": [], \"provenance\": {\"sessions\": [], \"failures\": [], \"corrections\": []},\n \"cites\": [], \"body\": \"...\"}]} — or {\"drafts\": [], \"none_reason\": \"<why they are different>\"}`;\n\nexport async function proposeMerges(repo: string, env = process.env): Promise<{ drafted: string[]; skipped: string[] }> {\n const drafted: string[] = [];\n const skipped: string[] = [];\n const jev = featureOn(\"jev\", repo, env);\n const used = new Set<string>();\n for (const { a, b } of mergeCandidates(repo, env)) {\n if (used.has(a.path) || used.has(b.path)) continue;\n const pair = `${a.skill.name} + ${b.skill.name}`;\n if (jev) {\n const r = await askJev(\n {\n cwd: repo,\n caller: \"semantic-memory-skill-loop\",\n state: `${show(a)}\\n\\n${show(b)}`.slice(0, 2000),\n questions: [\n { key: \"same\", instructions: \"Do these two skills describe the same procedure, so that one could replace the other?\" },\n { key: \"subset\", instructions: \"Is every step of one of these skills already contained in the other skill?\" },\n ],\n },\n { env, timeoutMs: 30_000 },\n );\n if (jevVerdict(r.scores.same) !== true && jevVerdict(r.scores.subset) !== true) {\n skipped.push(`${pair}: Jev did not say duplicate (${r.error ?? JSON.stringify(r.scores)})`);\n continue;\n }\n }\n const run = await runClaude({ prompt: `${MERGE_PROMPT}\\n\\n---\\n\\n${show(a)}\\n\\n${show(b)}`, cwd: repo, tools: [], maxTurns: 1, env });\n const answer = run.ok ? parseReviewerJson(run.result) : null;\n const d = answer?.drafts[0];\n if (!d) {\n skipped.push(`${pair}: ${answer?.none_reason ?? run.error ?? \"no merge proposed\"}`);\n continue;\n }\n const prov = (k: \"sessions\" | \"failures\" | \"corrections\") => [...new Set([...(a.skill.meta!.provenance[k] ?? []), ...(b.skill.meta!.provenance[k] ?? [])])];\n const job = { session_id: prov(\"sessions\")[0] ?? \"merge\", repo, transcript_path: null, reason: `merge of ${pair}`, ts: new Date().toISOString(), evidence: { failures: prov(\"failures\"), corrections: prov(\"corrections\").length, skills_used: [], tool_calls: 0 } };\n const r = await materializeDraft(\n repo,\n job,\n { ...d, kind: \"new\", provenance: { sessions: prov(\"sessions\"), failures: prov(\"failures\"), corrections: prov(\"corrections\") } },\n findSkills(repo, { home: env.HOME }).filter((s) => s.path !== a.path && s.path !== b.path),\n async () => ({ scope: \"project\", globalCandidate: false, why: `merge of ${pair}` }),\n env,\n { supersedes: [a.path, b.path] },\n );\n if (r.id) {\n drafted.push(r.id);\n used.add(a.path).add(b.path);\n } else skipped.push(`${pair}: ${r.rejected}`);\n }\n return { drafted, skipped };\n}\n"],"mappings":"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","names":["r"]}