@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.
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 +81 -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-2BFG4IBC.js +295 -0
  10. package/dist/chunk-2BFG4IBC.js.map +1 -0
  11. package/dist/chunk-2OMDSTSS.js +118 -0
  12. package/dist/chunk-2OMDSTSS.js.map +1 -0
  13. package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
  14. package/dist/chunk-3ML5ESXI.js +224 -0
  15. package/dist/chunk-3ML5ESXI.js.map +1 -0
  16. package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
  17. package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
  18. package/dist/chunk-ACZKDSMP.js +123 -0
  19. package/dist/chunk-ACZKDSMP.js.map +1 -0
  20. package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
  21. package/dist/chunk-BSXIGPDY.js +137 -0
  22. package/dist/chunk-BSXIGPDY.js.map +1 -0
  23. package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
  24. package/dist/chunk-GYR7U6KA.js +80 -0
  25. package/dist/chunk-GYR7U6KA.js.map +1 -0
  26. package/dist/chunk-M2PC64DB.js +195 -0
  27. package/dist/chunk-M2PC64DB.js.map +1 -0
  28. package/dist/chunk-MTLYF357.js +130 -0
  29. package/dist/chunk-MTLYF357.js.map +1 -0
  30. package/dist/chunk-N77YCMSX.js +113 -0
  31. package/dist/chunk-N77YCMSX.js.map +1 -0
  32. package/dist/chunk-PJXYCCI2.js +177 -0
  33. package/dist/chunk-PJXYCCI2.js.map +1 -0
  34. package/dist/chunk-PMOZKUEM.js +170 -0
  35. package/dist/chunk-PMOZKUEM.js.map +1 -0
  36. package/dist/chunk-R6GNP7DS.js +87 -0
  37. package/dist/chunk-R6GNP7DS.js.map +1 -0
  38. package/dist/chunk-TKHWVISV.js +71 -0
  39. package/dist/chunk-TKHWVISV.js.map +1 -0
  40. package/dist/chunk-V6PSK4Y4.js +186 -0
  41. package/dist/chunk-V6PSK4Y4.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 +46 -0
  64. package/dist/core/skill-queue.js +42 -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-EHXZW47F.js +31 -0
  87. package/dist/skill-land-EHXZW47F.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-HBZBALZX.js +24 -0
  91. package/dist/skill-merge-HBZBALZX.js.map +1 -0
  92. package/dist/skill-review-BNvHipn9.d.ts +182 -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,295 @@
1
+ import {
2
+ decideScope,
3
+ indexSkill,
4
+ promotionPeers
5
+ } from "./chunk-N77YCMSX.js";
6
+ import {
7
+ runClaude
8
+ } from "./chunk-PMOZKUEM.js";
9
+ import {
10
+ factIds,
11
+ findSkills,
12
+ lintSkill,
13
+ renderSkill
14
+ } from "./chunk-ZSWI3Y4Y.js";
15
+ import {
16
+ readEvidence,
17
+ stateDir
18
+ } from "./chunk-R6GNP7DS.js";
19
+ import {
20
+ redact
21
+ } from "./chunk-H2BES6FU.js";
22
+
23
+ // src/core/skill-review.ts
24
+ import { createHash } from "crypto";
25
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
26
+ import { basename, join } from "path";
27
+
28
+ // src/core/skill-review-prompt.ts
29
+ var REVIEW_PROMPT = `You review one finished Claude Code session in this repository and decide whether it taught a
30
+ reusable procedure worth saving as a skill \u2014 a short, step-by-step playbook a future session should follow.
31
+
32
+ Draft only from the evidence in the brief below: a command that failed and the fix that worked, a correction
33
+ the user gave, or a learned skill that was used and then failed or was corrected. Never invent steps, commands,
34
+ paths or facts that the brief or the repository does not show. Most sessions teach nothing reusable: then
35
+ answer with no drafts. A draft is worth it only when the same situation is likely to come up again here.
36
+
37
+ Kinds of draft:
38
+ - "new": a procedure no existing skill covers. Check the existing skills in the brief first.
39
+ - "patch": an existing learned skill (listed with "learned: true") was used in this session and then failed or
40
+ was corrected: give its full revised body. Never patch a skill that is pinned or not learned.
41
+
42
+ Each draft:
43
+ - name: kebab-case, specific ("restart-vite-dev-server", not "dev-helper").
44
+ - description: what it does and when to use it, one or two sentences, starting with the action; include
45
+ "Use when ..." with the literal error, command or request that should load it. Under 300 characters.
46
+ - scope: "project" (about this repository) or "directory" (only about one subdirectory; give it in applies_to.paths).
47
+ - applies_to: { paths: [repo-relative paths that exist], commands: [program names the steps run] }.
48
+ - triggers: the literal error strings, commands or user phrases from the brief that this skill answers.
49
+ - provenance: { sessions: [the session id], failures: [failure ids from the brief it came from], corrections: [correction
50
+ numbers from the brief, as strings] } \u2014 at least one failure or correction.
51
+ - cites: knowledge-base fact ids from the brief's list that the steps rely on (only ids that are listed).
52
+ - body: markdown with exactly these sections, in order:
53
+ ## When to use \u2014 the situation, in two or three lines.
54
+ ## Steps \u2014 numbered, concrete commands or actions as the session finally did them.
55
+ ## Pitfalls \u2014 what went wrong in the session and why; each line names the failure id or fact id it comes from.
56
+ ## Verify \u2014 one command whose exit code proves it worked, in a fenced code block; or, if none exists, one
57
+ sentence a reviewer can check.
58
+
59
+ Hard rules:
60
+ - No secrets, tokens, passwords, keys, personal data, customer data, names of people or money amounts, even if
61
+ the brief shows them. Use placeholders like <token>.
62
+ - At most 2 drafts. No duplicates of existing skills.
63
+ - Do not write or change any file. You may read files in this repository to get paths and commands right.
64
+
65
+ Answer with one JSON object and nothing else:
66
+ {"drafts": [{"kind": "new" | "patch", "target": "<existing skill name, patch only>", "name": "...",
67
+ "description": "...", "scope": "project" | "directory", "applies_to": {"paths": [], "commands": []},
68
+ "triggers": [], "provenance": {"sessions": [], "failures": [], "corrections": []}, "cites": [], "body": "..."}],
69
+ "none_reason": "<when drafts is empty: one line on why>"}
70
+ `;
71
+
72
+ // src/core/skill-review.ts
73
+ var draftsDir = (repo) => join(stateDir(repo), "skill-drafts");
74
+ var BRIEF_MAX = 4e4;
75
+ var TIMELINE_MAX = 120;
76
+ function transcriptTimeline(path, max = TIMELINE_MAX, since) {
77
+ if (!path) return [];
78
+ let raw = "";
79
+ try {
80
+ if (statSync(path).size > 200 * 1024 * 1024) return ["(transcript too large to read)"];
81
+ raw = readFileSync(path, "utf8");
82
+ } catch {
83
+ return [];
84
+ }
85
+ const out = [];
86
+ const calls = /* @__PURE__ */ new Map();
87
+ for (const line of raw.split("\n")) {
88
+ let e;
89
+ try {
90
+ e = JSON.parse(line);
91
+ } catch {
92
+ continue;
93
+ }
94
+ const content = e?.message?.content;
95
+ const shown = !since || String(e?.timestamp ?? "") > since;
96
+ if (!shown && e?.type !== "assistant") continue;
97
+ if (e?.type === "user" && typeof content === "string" && !e.isMeta) {
98
+ if (!content.trimStart().startsWith("<")) out.push(`USER: ${content.slice(0, 600)}`);
99
+ } else if (e?.type === "assistant" && Array.isArray(content)) {
100
+ for (const c of content) {
101
+ if (c?.type !== "tool_use") continue;
102
+ const input = c.input ?? {};
103
+ const what = typeof input.command === "string" ? input.command : typeof input.file_path === "string" ? input.file_path : typeof input.skill === "string" ? input.skill : JSON.stringify(input);
104
+ calls.set(c.id, `${c.name}: ${String(what).slice(0, 300)}`);
105
+ }
106
+ } else if (e?.type === "user" && Array.isArray(content)) {
107
+ for (const c of content) {
108
+ if (c?.type !== "tool_result") continue;
109
+ const call = calls.get(c.tool_use_id) ?? "tool";
110
+ const text = typeof c.content === "string" ? c.content : Array.isArray(c.content) ? c.content.map((x) => x?.text ?? "").join(" ") : "";
111
+ out.push(c.is_error ? `TOOL ${call}
112
+ \u2192 ERROR (${c.tool_use_id}): ${text.slice(0, 500)}` : `TOOL ${call} \u2192 ok`);
113
+ }
114
+ }
115
+ }
116
+ if (out.length <= max) return out;
117
+ const errors = out.filter((l) => l.includes("\u2192 ERROR")).slice(-Math.floor(max / 2));
118
+ const tail = out.slice(-(max - errors.length)).filter((l) => !errors.includes(l));
119
+ return [`(${out.length - errors.length - tail.length} earlier entries omitted)`, ...errors, ...tail];
120
+ }
121
+ function topLevel(repo) {
122
+ try {
123
+ return readdirSync(repo).filter((f) => !f.startsWith(".") || f === ".claude" || f === ".documentation").slice(0, 60);
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+ function buildBrief(job, skills = findSkills(job.repo)) {
129
+ const repo = job.repo;
130
+ const mine = (r2) => r2.session_id === job.session_id && (!job.since || String(r2.ts ?? "") > job.since);
131
+ const failures = readEvidence(repo, "failures").filter(mine);
132
+ const corrections = readEvidence(repo, "corrections").filter(mine);
133
+ const used = new Set(job.evidence.skills_used);
134
+ const r = (t) => redact(t, "broad").text;
135
+ const lines = [
136
+ `# Session ${job.session_id}`,
137
+ `Repository: ${basename(repo)} \u2014 top level: ${topLevel(repo).join(", ")}`,
138
+ `Why reviewed: ${job.reason}`,
139
+ ...job.since ? [`Follow-up review: this session was reviewed at ${job.since} and then resumed. Below is only what happened after that; judge the new evidence.`] : [],
140
+ "",
141
+ "## Failures (from the evidence log)",
142
+ ...failures.length ? failures.map((f) => `- id ${f.id}: \`${r(String(f.command))}\`
143
+ error: ${r(String(f.error).slice(0, 500))}`) : ["(none)"],
144
+ "",
145
+ "## Corrections the user gave",
146
+ ...corrections.length ? corrections.map((c, i) => `- correction ${i + 1} (${c.cue}): ${r(String(c.excerpt))}`) : ["(none)"],
147
+ "",
148
+ "## Skills this repository loads",
149
+ ...skills.length ? skills.map((s) => `- ${s.skill.name || basename(s.dir)} (${s.scope}; learned: ${s.learned}${s.skill.meta?.pinned ? "; pinned" : ""}${used.has(s.skill.name) ? "; USED this session" : ""}): ${r(s.skill.description.slice(0, 200))}`) : ["(none)"],
150
+ ...skills.filter((s) => s.learned && used.has(s.skill.name)).flatMap((s) => ["", `### Current text of the learned skill ${s.skill.name} (used this session)`, r(s.skill.body.slice(0, 4e3))]),
151
+ "",
152
+ "## Knowledge-base fact ids (for cites)",
153
+ [...factIds(repo)].slice(0, 200).join(", ") || "(none)",
154
+ "",
155
+ "## Timeline (prompts, tool calls, errors)",
156
+ ...transcriptTimeline(job.transcript_path, TIMELINE_MAX, job.since).map(r)
157
+ ];
158
+ let brief = lines.join("\n");
159
+ if (brief.length > BRIEF_MAX) brief = brief.slice(0, BRIEF_MAX) + "\n(brief cut at its size cap)";
160
+ return brief;
161
+ }
162
+ function parseReviewerJson(text) {
163
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
164
+ const candidates = [fenced?.[1], text, text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1)].filter(Boolean);
165
+ for (const c of candidates) {
166
+ try {
167
+ const v = JSON.parse(c.trim());
168
+ if (v && Array.isArray(v.drafts)) return v;
169
+ } catch {
170
+ }
171
+ }
172
+ return null;
173
+ }
174
+ var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && !x.includes("[REDACTED]")) : [];
175
+ var sha256 = (s) => createHash("sha256").update(s).digest("hex");
176
+ async function materializeDraft(repo, job, d, skills, resolveScope = (x) => decideScope(repo, x), env = process.env, merge) {
177
+ if (!d || typeof d.name !== "string" || typeof d.body !== "string" || typeof d.description !== "string") return { rejected: "malformed draft" };
178
+ let target;
179
+ if (d.kind === "patch") {
180
+ target = skills.find((s) => s.skill.name === d.target);
181
+ if (!target) return { rejected: `patch target "${d.target}" not found` };
182
+ if (!target.learned) return { rejected: `"${d.target}" was not written by the skill loop: never patched` };
183
+ if (target.skill.meta?.pinned) return { rejected: `"${d.target}" is pinned` };
184
+ }
185
+ const prov = {
186
+ sessions: [.../* @__PURE__ */ new Set([job.session_id, ...strs(d.provenance?.sessions)])],
187
+ failures: strs(d.provenance?.failures),
188
+ corrections: strs(d.provenance?.corrections)
189
+ };
190
+ if (prov.failures.length + prov.corrections.length === 0 && job.evidence.failures.length + job.evidence.corrections === 0) {
191
+ return { rejected: "no failure or correction behind it" };
192
+ }
193
+ const decision = target ? { scope: target.scope === "directory" ? "directory" : "project", globalCandidate: false, why: "patch: the skill's own scope" } : await resolveScope(d);
194
+ const scope = target?.scope ?? decision.scope;
195
+ const paths = strs(d.applies_to?.paths);
196
+ if (decision.directory && !paths.includes(decision.directory)) paths.unshift(decision.directory);
197
+ const name = d.kind === "patch" ? target.skill.name : d.name;
198
+ const meta = {
199
+ schema: 1,
200
+ scope,
201
+ status: "draft",
202
+ pinned: false,
203
+ triggers: strs(d.triggers),
204
+ applies_to: { paths, commands: strs(d.applies_to?.commands) },
205
+ provenance: prov,
206
+ cites: strs(d.cites),
207
+ supersedes: merge ? merge.supersedes.map((p) => basename(join(p, ".."))) : target?.skill.meta?.supersedes ?? []
208
+ };
209
+ const text = redact(renderSkill({ name, description: d.description, meta, body: d.body }), "precise").text;
210
+ const findings = lintSkill(join(draftsDir(repo), name, "SKILL.md"), text, { projectRoot: repo, others: skills.filter((s) => s.path !== target?.path) }, name);
211
+ const errors = findings.filter((f) => f.severity === "error");
212
+ if (errors.length) return { rejected: `lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join("; ")}` };
213
+ const id = `${name}-${sha256(text).slice(0, 8)}`;
214
+ const dir = join(draftsDir(repo), id);
215
+ mkdirSync(dir, { recursive: true });
216
+ writeFileSync(join(dir, "SKILL.md"), text);
217
+ const record = {
218
+ id,
219
+ kind: merge ? "merge" : d.kind === "patch" ? "patch" : "new",
220
+ name,
221
+ scope,
222
+ ...target ? { target: target.path, base_sha256: sha256(readFileSync(target.path, "utf8")) } : {},
223
+ session_id: job.session_id,
224
+ created: (/* @__PURE__ */ new Date()).toISOString(),
225
+ warnings: findings.filter((f) => f.severity === "warn").map((f) => `${f.rule}: ${f.message}`),
226
+ ...decision.directory ? { directory: decision.directory } : {},
227
+ scope_why: decision.why,
228
+ ...decision.globalCandidate ? { global_candidate: true } : {},
229
+ ...merge ? { supersedes: merge.supersedes } : {}
230
+ };
231
+ if (!target && !merge) {
232
+ const peers = promotionPeers(repo, name, env);
233
+ if (peers.length) record.promotion = peers.map((p) => ({ repo: p.repo, path: p.path }));
234
+ indexSkill({ repo, name, path: join(dir, "SKILL.md"), status: "draft", ...decision.globalCandidate ? { global_candidate: true } : {} }, env);
235
+ }
236
+ writeFileSync(join(dir, "draft.json"), JSON.stringify(record, null, 1));
237
+ return { id };
238
+ }
239
+ function listDrafts(repo) {
240
+ let ids = [];
241
+ try {
242
+ ids = readdirSync(draftsDir(repo));
243
+ } catch {
244
+ return [];
245
+ }
246
+ const out = [];
247
+ for (const id of ids) {
248
+ const dir = join(draftsDir(repo), id);
249
+ try {
250
+ if (existsSync(join(dir, "SKILL.md"))) out.push({ dir, record: JSON.parse(readFileSync(join(dir, "draft.json"), "utf8")) });
251
+ } catch {
252
+ }
253
+ }
254
+ return out.sort((a, b) => a.record.created.localeCompare(b.record.created));
255
+ }
256
+ async function reviewSession(job, opts = {}) {
257
+ const skills = findSkills(job.repo);
258
+ const brief = buildBrief(job, skills);
259
+ const run = await runClaude({
260
+ prompt: `${REVIEW_PROMPT}
261
+
262
+ ---
263
+
264
+ ${brief}`,
265
+ cwd: job.repo,
266
+ tools: ["Read", "Grep", "Glob"],
267
+ maxTurns: opts.maxTurns ?? 8,
268
+ output: "json",
269
+ timeoutMs: 10 * 6e4,
270
+ env: opts.env
271
+ });
272
+ if (!run.ok && !run.result) throw new Error(run.error ?? "reviewer run failed");
273
+ const answer = parseReviewerJson(run.result);
274
+ if (!answer) return { drafts: [], rejected: [{ reason: "reviewer answer was not JSON" }], costUsd: run.costUsd };
275
+ const drafts = [];
276
+ const rejected = [];
277
+ for (const d of answer.drafts.slice(0, 2)) {
278
+ const env = opts.env ?? process.env;
279
+ const r = await materializeDraft(job.repo, job, d, skills, opts.resolveScope ?? ((x) => decideScope(job.repo, x, env)), env);
280
+ if (r.id) drafts.push(r.id);
281
+ else rejected.push({ reason: `${d?.name ?? "?"}: ${r.rejected}` });
282
+ }
283
+ return { drafts, rejected, costUsd: run.costUsd, note: answer.drafts.length ? void 0 : answer.none_reason?.slice(0, 200) };
284
+ }
285
+
286
+ export {
287
+ draftsDir,
288
+ transcriptTimeline,
289
+ buildBrief,
290
+ parseReviewerJson,
291
+ materializeDraft,
292
+ listDrafts,
293
+ reviewSession
294
+ };
295
+ //# sourceMappingURL=chunk-2BFG4IBC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/skill-review.ts","../src/core/skill-review-prompt.ts"],"sourcesContent":["/**\n * The skill loop's reviewer (v1.7): one finished session in, zero to two skill drafts out.\n *\n * 1. A brief: the session's prompts, tool calls and errors (from the transcript, read fail-soft: its format\n * is internal to Claude Code), plus the evidence logs, the skills this repo loads and its KB fact ids.\n * Redacted (broad) and capped before the model sees it.\n * 2. A fenced `claude -p` (Read / Grep / Glob on the repo only; see claude-run.ts) answers JSON.\n * 3. Each draft is rendered, redacted, linted (skill-schema.ts) and written to\n * .claude/.semantic-memory/skill-drafts/<id>/ — or rejected with its reasons. Nothing is written anywhere\n * else; landing a draft is a person's call (`skill-loop review`).\n */\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { redact } from \"./redact.js\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\nimport { factIds, findSkills, lintSkill, renderSkill, type SkillFile, type SkillLoopMeta, type SkillScope } from \"./skill-schema.js\";\nimport { runClaude } from \"./claude-run.js\";\nimport { REVIEW_PROMPT } from \"./skill-review-prompt.js\";\nimport type { ReviewJob } from \"./skill-queue.js\";\nimport type { ReviewOutcome } from \"./skill-worker.js\";\nimport { decideScope, indexSkill, promotionPeers, type ScopeDecision } from \"./skill-scope.js\";\n\nexport const draftsDir = (repo: string) => join(stateDir(repo), \"skill-drafts\");\nconst BRIEF_MAX = 40_000;\nconst TIMELINE_MAX = 120;\n\nexport interface DraftRecord {\n id: string;\n kind: \"new\" | \"patch\" | \"merge\";\n name: string;\n scope: SkillScope;\n /** patch: the SKILL.md it replaces, and that file's sha256 when the draft was made */\n target?: string;\n base_sha256?: string;\n session_id: string;\n created: string;\n warnings: string[];\n /** directory scope: where it lands (repo-relative subdirectory) */\n directory?: string;\n scope_why?: string;\n /** Jev thinks it holds in any repo */\n global_candidate?: boolean;\n /** other repos with the same lesson: review offers one global skill replacing the copies */\n promotion?: { repo: string; path: string }[];\n /** merge: the SKILL.md files the merged skill replaces (archived when it lands) */\n supersedes?: string[];\n /** filled by skill evals (Phase 14) when they run at approval time */\n evals?: Record<string, unknown>;\n}\n\n// --- the brief ---\n\n/**\n * The session's timeline from its transcript: prompts, tool calls, and the errors they returned. Fail-soft.\n * `since`: only entries after it (a follow-up review of a resumed session).\n */\nexport function transcriptTimeline(path: string | null, max = TIMELINE_MAX, since?: string): string[] {\n if (!path) return [];\n let raw = \"\";\n try {\n if (statSync(path).size > 200 * 1024 * 1024) return [\"(transcript too large to read)\"];\n raw = readFileSync(path, \"utf8\");\n } catch {\n return [];\n }\n const out: string[] = [];\n const calls = new Map<string, string>();\n for (const line of raw.split(\"\\n\")) {\n let e: any;\n try {\n e = JSON.parse(line);\n } catch {\n continue;\n }\n const content = e?.message?.content;\n // calls are remembered from the whole transcript so a result after `since` still names its call\n const shown = !since || String(e?.timestamp ?? \"\") > since;\n if (!shown && e?.type !== \"assistant\") continue;\n if (e?.type === \"user\" && typeof content === \"string\" && !e.isMeta) {\n // wrappers (slash-command echoes, task notifications, local command output) are not the user talking\n if (!content.trimStart().startsWith(\"<\")) out.push(`USER: ${content.slice(0, 600)}`);\n } else if (e?.type === \"assistant\" && Array.isArray(content)) {\n for (const c of content) {\n if (c?.type !== \"tool_use\") continue;\n const input = c.input ?? {};\n const what = typeof input.command === \"string\" ? input.command : typeof input.file_path === \"string\" ? input.file_path : typeof input.skill === \"string\" ? input.skill : JSON.stringify(input);\n calls.set(c.id, `${c.name}: ${String(what).slice(0, 300)}`);\n }\n } else if (e?.type === \"user\" && Array.isArray(content)) {\n for (const c of content) {\n if (c?.type !== \"tool_result\") continue;\n const call = calls.get(c.tool_use_id) ?? \"tool\";\n const text = typeof c.content === \"string\" ? c.content : Array.isArray(c.content) ? c.content.map((x: any) => x?.text ?? \"\").join(\" \") : \"\";\n out.push(c.is_error ? `TOOL ${call}\\n → ERROR (${c.tool_use_id}): ${text.slice(0, 500)}` : `TOOL ${call} → ok`);\n }\n }\n }\n // keep the end of long sessions (where the fix landed) and every error\n if (out.length <= max) return out;\n const errors = out.filter((l) => l.includes(\"→ ERROR\")).slice(-Math.floor(max / 2));\n const tail = out.slice(-(max - errors.length)).filter((l) => !errors.includes(l));\n return [`(${out.length - errors.length - tail.length} earlier entries omitted)`, ...errors, ...tail];\n}\n\nfunction topLevel(repo: string): string[] {\n try {\n return readdirSync(repo).filter((f) => !f.startsWith(\".\") || f === \".claude\" || f === \".documentation\").slice(0, 60);\n } catch {\n return [];\n }\n}\n\nexport function buildBrief(job: ReviewJob, skills: SkillFile[] = findSkills(job.repo)): string {\n const repo = job.repo;\n const mine = (r: Record<string, any>) => r.session_id === job.session_id && (!job.since || String(r.ts ?? \"\") > job.since);\n const failures = readEvidence(repo, \"failures\").filter(mine);\n const corrections = readEvidence(repo, \"corrections\").filter(mine);\n const used = new Set(job.evidence.skills_used);\n // free text is redacted (broad); the ids the reviewer must cite back (session, failure ids) are not, or broad\n // redaction would mask them too. The evidence logs were redacted when written.\n const r = (t: string) => redact(t, \"broad\").text;\n const lines = [\n `# Session ${job.session_id}`,\n `Repository: ${basename(repo)} — top level: ${topLevel(repo).join(\", \")}`,\n `Why reviewed: ${job.reason}`,\n ...(job.since ? [`Follow-up review: this session was reviewed at ${job.since} and then resumed. Below is only what happened after that; judge the new evidence.`] : []),\n \"\",\n \"## Failures (from the evidence log)\",\n ...(failures.length ? failures.map((f) => `- id ${f.id}: \\`${r(String(f.command))}\\`\\n error: ${r(String(f.error).slice(0, 500))}`) : [\"(none)\"]),\n \"\",\n \"## Corrections the user gave\",\n ...(corrections.length ? corrections.map((c, i) => `- correction ${i + 1} (${c.cue}): ${r(String(c.excerpt))}`) : [\"(none)\"]),\n \"\",\n \"## Skills this repository loads\",\n ...(skills.length\n ? skills.map((s) => `- ${s.skill.name || basename(s.dir)} (${s.scope}; learned: ${s.learned}${s.skill.meta?.pinned ? \"; pinned\" : \"\"}${used.has(s.skill.name) ? \"; USED this session\" : \"\"}): ${r(s.skill.description.slice(0, 200))}`)\n : [\"(none)\"]),\n ...skills\n .filter((s) => s.learned && used.has(s.skill.name))\n .flatMap((s) => [\"\", `### Current text of the learned skill ${s.skill.name} (used this session)`, r(s.skill.body.slice(0, 4000))]),\n \"\",\n \"## Knowledge-base fact ids (for cites)\",\n [...factIds(repo)].slice(0, 200).join(\", \") || \"(none)\",\n \"\",\n \"## Timeline (prompts, tool calls, errors)\",\n ...transcriptTimeline(job.transcript_path, TIMELINE_MAX, job.since).map(r),\n ];\n let brief = lines.join(\"\\n\");\n if (brief.length > BRIEF_MAX) brief = brief.slice(0, BRIEF_MAX) + \"\\n(brief cut at its size cap)\";\n return brief;\n}\n\n// --- the answer ---\n\nexport interface ReviewerDraft {\n kind: \"new\" | \"patch\";\n target?: string;\n name: string;\n description: string;\n scope: string;\n applies_to?: { paths?: string[]; commands?: string[] };\n triggers?: string[];\n provenance?: { sessions?: string[]; failures?: string[]; corrections?: string[] };\n cites?: string[];\n body: string;\n}\n\n/** The JSON object in the reviewer's answer (bare, or inside a ```json fence). */\nexport function parseReviewerJson(text: string): { drafts: ReviewerDraft[]; none_reason?: string } | null {\n const fenced = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n const candidates = [fenced?.[1], text, text.slice(text.indexOf(\"{\"), text.lastIndexOf(\"}\") + 1)].filter(Boolean) as string[];\n for (const c of candidates) {\n try {\n const v = JSON.parse(c.trim());\n if (v && Array.isArray(v.drafts)) return v;\n } catch {}\n }\n return null;\n}\n\n// a placeholder the model copied from redacted text is not evidence\nconst strs = (v: unknown) => (Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\" && !x.includes(\"[REDACTED]\")) : []);\nconst sha256 = (s: string) => createHash(\"sha256\").update(s).digest(\"hex\");\n\nexport type ScopeResolver = (d: ReviewerDraft) => Promise<ScopeDecision>;\n\n/**\n * Turn one reviewer draft into a draft folder, or say why not. `resolveScope` decides the final scope\n * (skill-scope.ts: deterministic rules, then Jev); the reviewer's own choice is never trusted for it.\n */\nexport async function materializeDraft(\n repo: string,\n job: ReviewJob,\n d: ReviewerDraft,\n skills: SkillFile[],\n resolveScope: ScopeResolver = (x) => decideScope(repo, x),\n env = process.env,\n merge?: { supersedes: string[] },\n): Promise<{ id?: string; rejected?: string }> {\n if (!d || typeof d.name !== \"string\" || typeof d.body !== \"string\" || typeof d.description !== \"string\") return { rejected: \"malformed draft\" };\n let target: SkillFile | undefined;\n if (d.kind === \"patch\") {\n target = skills.find((s) => s.skill.name === d.target);\n if (!target) return { rejected: `patch target \"${d.target}\" not found` };\n if (!target.learned) return { rejected: `\"${d.target}\" was not written by the skill loop: never patched` };\n if (target.skill.meta?.pinned) return { rejected: `\"${d.target}\" is pinned` };\n }\n const prov = {\n sessions: [...new Set([job.session_id, ...strs(d.provenance?.sessions)])],\n failures: strs(d.provenance?.failures),\n corrections: strs(d.provenance?.corrections),\n };\n if (prov.failures.length + prov.corrections.length === 0 && job.evidence.failures.length + job.evidence.corrections === 0) {\n return { rejected: \"no failure or correction behind it\" };\n }\n // a patch keeps its skill's scope; a new skill's comes from the resolver\n const decision: ScopeDecision = target\n ? { scope: target.scope === \"directory\" ? \"directory\" : \"project\", globalCandidate: false, why: \"patch: the skill's own scope\" }\n : await resolveScope(d);\n const scope: SkillScope = target?.scope ?? decision.scope;\n const paths = strs(d.applies_to?.paths);\n if (decision.directory && !paths.includes(decision.directory)) paths.unshift(decision.directory);\n const name = d.kind === \"patch\" ? target!.skill.name : d.name;\n const meta: SkillLoopMeta = {\n schema: 1,\n scope,\n status: \"draft\",\n pinned: false,\n triggers: strs(d.triggers),\n applies_to: { paths, commands: strs(d.applies_to?.commands) },\n provenance: prov,\n cites: strs(d.cites),\n supersedes: merge ? merge.supersedes.map((p) => basename(join(p, \"..\"))) : (target?.skill.meta?.supersedes ?? []),\n };\n // precise: a skill is prose and code, broad would mangle ordinary identifiers\n const text = redact(renderSkill({ name, description: d.description, meta, body: d.body }), \"precise\").text;\n const findings = lintSkill(join(draftsDir(repo), name, \"SKILL.md\"), text, { projectRoot: repo, others: skills.filter((s) => s.path !== target?.path) }, name);\n const errors = findings.filter((f) => f.severity === \"error\");\n if (errors.length) return { rejected: `lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join(\"; \")}` };\n const id = `${name}-${sha256(text).slice(0, 8)}`;\n const dir = join(draftsDir(repo), id);\n mkdirSync(dir, { recursive: true });\n writeFileSync(join(dir, \"SKILL.md\"), text);\n const record: DraftRecord = {\n id,\n kind: merge ? \"merge\" : d.kind === \"patch\" ? \"patch\" : \"new\",\n name,\n scope,\n ...(target ? { target: target.path, base_sha256: sha256(readFileSync(target.path, \"utf8\")) } : {}),\n session_id: job.session_id,\n created: new Date().toISOString(),\n warnings: findings.filter((f) => f.severity === \"warn\").map((f) => `${f.rule}: ${f.message}`),\n ...(decision.directory ? { directory: decision.directory } : {}),\n scope_why: decision.why,\n ...(decision.globalCandidate ? { global_candidate: true } : {}),\n ...(merge ? { supersedes: merge.supersedes } : {}),\n };\n if (!target && !merge) {\n const peers = promotionPeers(repo, name, env);\n if (peers.length) record.promotion = peers.map((p) => ({ repo: p.repo, path: p.path }));\n indexSkill({ repo, name, path: join(dir, \"SKILL.md\"), status: \"draft\", ...(decision.globalCandidate ? { global_candidate: true } : {}) }, env);\n }\n writeFileSync(join(dir, \"draft.json\"), JSON.stringify(record, null, 1));\n return { id };\n}\n\nexport function listDrafts(repo: string): { dir: string; record: DraftRecord }[] {\n let ids: string[] = [];\n try {\n ids = readdirSync(draftsDir(repo));\n } catch {\n return [];\n }\n const out: { dir: string; record: DraftRecord }[] = [];\n for (const id of ids) {\n const dir = join(draftsDir(repo), id);\n try {\n if (existsSync(join(dir, \"SKILL.md\"))) out.push({ dir, record: JSON.parse(readFileSync(join(dir, \"draft.json\"), \"utf8\")) });\n } catch {}\n }\n return out.sort((a, b) => a.record.created.localeCompare(b.record.created));\n}\n\nexport interface ReviewOptions {\n env?: NodeJS.ProcessEnv;\n resolveScope?: ScopeResolver;\n maxTurns?: number;\n}\n\n/** Review one queued session (the worker's review function). */\nexport async function reviewSession(job: ReviewJob, opts: ReviewOptions = {}): Promise<ReviewOutcome> {\n const skills = findSkills(job.repo);\n const brief = buildBrief(job, skills);\n const run = await runClaude({\n prompt: `${REVIEW_PROMPT}\\n\\n---\\n\\n${brief}`,\n cwd: job.repo,\n tools: [\"Read\", \"Grep\", \"Glob\"],\n maxTurns: opts.maxTurns ?? 8,\n output: \"json\",\n timeoutMs: 10 * 60_000,\n env: opts.env,\n });\n if (!run.ok && !run.result) throw new Error(run.error ?? \"reviewer run failed\");\n const answer = parseReviewerJson(run.result);\n if (!answer) return { drafts: [], rejected: [{ reason: \"reviewer answer was not JSON\" }], costUsd: run.costUsd };\n const drafts: string[] = [];\n const rejected: { reason: string }[] = [];\n for (const d of answer.drafts.slice(0, 2)) {\n const env = opts.env ?? process.env;\n const r = await materializeDraft(job.repo, job, d, skills, opts.resolveScope ?? ((x) => decideScope(job.repo, x, env)), env);\n if (r.id) drafts.push(r.id);\n else rejected.push({ reason: `${d?.name ?? \"?\"}: ${r.rejected}` });\n }\n return { drafts, rejected, costUsd: run.costUsd, note: answer.drafts.length ? undefined : answer.none_reason?.slice(0, 200) };\n}\n","/**\n * The skill loop reviewer's fixed instructions (v1.7). The brief (what happened in one session) follows\n * them on stdin. The reviewer can read the repo (Read / Grep / Glob) and nothing else; it answers with\n * JSON, and the worker — not the model — writes drafts, after redaction and lint.\n */\nexport const REVIEW_PROMPT = `You review one finished Claude Code session in this repository and decide whether it taught a\nreusable procedure worth saving as a skill — a short, step-by-step playbook a future session should follow.\n\nDraft only from the evidence in the brief below: a command that failed and the fix that worked, a correction\nthe user gave, or a learned skill that was used and then failed or was corrected. Never invent steps, commands,\npaths or facts that the brief or the repository does not show. Most sessions teach nothing reusable: then\nanswer with no drafts. A draft is worth it only when the same situation is likely to come up again here.\n\nKinds of draft:\n- \"new\": a procedure no existing skill covers. Check the existing skills in the brief first.\n- \"patch\": an existing learned skill (listed with \"learned: true\") was used in this session and then failed or\n was corrected: give its full revised body. Never patch a skill that is pinned or not learned.\n\nEach draft:\n- name: kebab-case, specific (\"restart-vite-dev-server\", not \"dev-helper\").\n- description: what it does and when to use it, one or two sentences, starting with the action; include\n \"Use when ...\" with the literal error, command or request that should load it. Under 300 characters.\n- scope: \"project\" (about this repository) or \"directory\" (only about one subdirectory; give it in applies_to.paths).\n- applies_to: { paths: [repo-relative paths that exist], commands: [program names the steps run] }.\n- triggers: the literal error strings, commands or user phrases from the brief that this skill answers.\n- provenance: { sessions: [the session id], failures: [failure ids from the brief it came from], corrections: [correction\n numbers from the brief, as strings] } — at least one failure or correction.\n- cites: knowledge-base fact ids from the brief's list that the steps rely on (only ids that are listed).\n- body: markdown with exactly these sections, in order:\n ## When to use — the situation, in two or three lines.\n ## Steps — numbered, concrete commands or actions as the session finally did them.\n ## Pitfalls — what went wrong in the session and why; each line names the failure id or fact id it comes from.\n ## Verify — one command whose exit code proves it worked, in a fenced code block; or, if none exists, one\n sentence a reviewer can check.\n\nHard rules:\n- No secrets, tokens, passwords, keys, personal data, customer data, names of people or money amounts, even if\n the brief shows them. Use placeholders like <token>.\n- At most 2 drafts. No duplicates of existing skills.\n- Do not write or change any file. You may read files in this repository to get paths and commands right.\n\nAnswer with one JSON object and nothing else:\n{\"drafts\": [{\"kind\": \"new\" | \"patch\", \"target\": \"<existing skill name, patch only>\", \"name\": \"...\",\n \"description\": \"...\", \"scope\": \"project\" | \"directory\", \"applies_to\": {\"paths\": [], \"commands\": []},\n \"triggers\": [], \"provenance\": {\"sessions\": [], \"failures\": [], \"corrections\": []}, \"cites\": [], \"body\": \"...\"}],\n \"none_reason\": \"<when drafts is empty: one line on why>\"}\n`;\n"],"mappings":"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","names":["r"]}
@@ -0,0 +1,118 @@
1
+ import {
2
+ materializeDraft,
3
+ parseReviewerJson
4
+ } from "./chunk-2BFG4IBC.js";
5
+ import {
6
+ runClaude
7
+ } from "./chunk-PMOZKUEM.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-2OMDSTSS.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"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  lintVault
3
- } from "./chunk-GYBUBJYM.js";
3
+ } from "./chunk-VBLCUWT4.js";
4
4
 
5
5
  // src/core/healthcheck.ts
6
6
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -338,4 +338,4 @@ export {
338
338
  filterToDrift,
339
339
  formatDriftBanner
340
340
  };
341
- //# sourceMappingURL=chunk-TMMTOFHI.js.map
341
+ //# sourceMappingURL=chunk-34VN47IW.js.map