@theglitchking/semantic-memory 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +19 -2
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/chunk-2BFG4IBC.js +295 -0
- package/dist/chunk-2BFG4IBC.js.map +1 -0
- package/dist/chunk-2OMDSTSS.js +118 -0
- package/dist/chunk-2OMDSTSS.js.map +1 -0
- package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3ML5ESXI.js +224 -0
- package/dist/chunk-3ML5ESXI.js.map +1 -0
- package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
- package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
- package/dist/chunk-ACZKDSMP.js +123 -0
- package/dist/chunk-ACZKDSMP.js.map +1 -0
- package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
- package/dist/chunk-BSXIGPDY.js +137 -0
- package/dist/chunk-BSXIGPDY.js.map +1 -0
- package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/chunk-M2PC64DB.js +195 -0
- package/dist/chunk-M2PC64DB.js.map +1 -0
- package/dist/chunk-MTLYF357.js +130 -0
- package/dist/chunk-MTLYF357.js.map +1 -0
- package/dist/chunk-N77YCMSX.js +113 -0
- package/dist/chunk-N77YCMSX.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.js.map +1 -0
- package/dist/chunk-PMOZKUEM.js +170 -0
- package/dist/chunk-PMOZKUEM.js.map +1 -0
- package/dist/chunk-R6GNP7DS.js +87 -0
- package/dist/chunk-R6GNP7DS.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/chunk-V6PSK4Y4.js +186 -0
- package/dist/chunk-V6PSK4Y4.js.map +1 -0
- package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
- package/dist/chunk-VBLCUWT4.js.map +1 -0
- package/dist/chunk-VQCNWRTL.js +121 -0
- package/dist/chunk-VQCNWRTL.js.map +1 -0
- package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
- package/dist/chunk-Y7H76Q7F.js.map +1 -0
- package/dist/chunk-ZSWI3Y4Y.js +232 -0
- package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
- package/dist/cli/index.js +381 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/core/features.d.ts +38 -0
- package/dist/core/features.js +28 -0
- package/dist/core/index.d.ts +285 -2
- package/dist/core/index.js +170 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/jev.d.ts +35 -0
- package/dist/core/jev.js +20 -0
- package/dist/core/skill-curator.d.ts +1 -0
- package/dist/core/skill-curator.js +41 -0
- package/dist/core/skill-evidence.d.ts +25 -0
- package/dist/core/skill-evidence.js +23 -0
- package/dist/core/skill-queue.d.ts +46 -0
- package/dist/core/skill-queue.js +42 -0
- package/dist/core/skill-review.d.ts +3 -0
- package/dist/core/skill-review.js +29 -0
- package/dist/core/skill-worker.d.ts +21 -0
- package/dist/core/skill-worker.js +70 -0
- package/dist/core/skill-worker.js.map +1 -0
- package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/failure-eval-IUJO47UC.js.map +1 -0
- package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
- package/dist/healthcheck-S7D6YNTA.js.map +1 -0
- package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/mcp/server.js +19 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
- package/dist/skill-land-EHXZW47F.js +31 -0
- package/dist/skill-land-EHXZW47F.js.map +1 -0
- package/dist/skill-ledger-AYXVNFX7.js +18 -0
- package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
- package/dist/skill-merge-HBZBALZX.js +24 -0
- package/dist/skill-merge-HBZBALZX.js.map +1 -0
- package/dist/skill-review-BNvHipn9.d.ts +182 -0
- package/dist/skill-schema-RVB3EHEF.js +30 -0
- package/dist/skill-schema-RVB3EHEF.js.map +1 -0
- package/hooks/hooks.json +26 -1
- package/hooks/jev-bridge.py +109 -0
- package/hooks/session-start.js +5 -0
- package/hooks/skill-loop.js +65 -0
- package/hooks/vault-context.js +104 -6
- package/package.json +4 -2
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-GYBUBJYM.js.map +0 -1
- package/dist/chunk-HUG7E25D.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
- /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
- /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
- /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
- /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listDrafts,
|
|
3
|
+
transcriptTimeline
|
|
4
|
+
} from "./chunk-2BFG4IBC.js";
|
|
5
|
+
import {
|
|
6
|
+
applyChange
|
|
7
|
+
} from "./chunk-TKHWVISV.js";
|
|
8
|
+
import {
|
|
9
|
+
indexSkill
|
|
10
|
+
} from "./chunk-N77YCMSX.js";
|
|
11
|
+
import {
|
|
12
|
+
buildEvalSet,
|
|
13
|
+
runTrigger
|
|
14
|
+
} from "./chunk-BSXIGPDY.js";
|
|
15
|
+
import {
|
|
16
|
+
parseVerify,
|
|
17
|
+
runTaskEval
|
|
18
|
+
} from "./chunk-MTLYF357.js";
|
|
19
|
+
import {
|
|
20
|
+
findSkills,
|
|
21
|
+
lintSkill,
|
|
22
|
+
parseSkill,
|
|
23
|
+
renderSkill
|
|
24
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
25
|
+
import {
|
|
26
|
+
queueDir
|
|
27
|
+
} from "./chunk-M2PC64DB.js";
|
|
28
|
+
|
|
29
|
+
// src/core/skill-land.ts
|
|
30
|
+
import { spawnSync } from "child_process";
|
|
31
|
+
import { createHash } from "crypto";
|
|
32
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
33
|
+
import { homedir } from "os";
|
|
34
|
+
import { basename, dirname, join } from "path";
|
|
35
|
+
|
|
36
|
+
// src/core/skill-gate.ts
|
|
37
|
+
var GATE_MIN_POSITIVE_RATE = 0.5;
|
|
38
|
+
var GATE_MAX_FALSE_TRIGGER_RATE = 0.25;
|
|
39
|
+
var pct = (n) => `${Math.round(n * 100)}%`;
|
|
40
|
+
function gateSkill(candidate, current) {
|
|
41
|
+
return current ? gatePatch(candidate, current) : gateNew(candidate);
|
|
42
|
+
}
|
|
43
|
+
function gateNew(s) {
|
|
44
|
+
const fails = [];
|
|
45
|
+
if (s.positiveRate === null) fails.push("held-out positive trigger rate not measured");
|
|
46
|
+
else if (s.positiveRate < GATE_MIN_POSITIVE_RATE) fails.push(`positive trigger rate ${pct(s.positiveRate)} < ${pct(GATE_MIN_POSITIVE_RATE)}`);
|
|
47
|
+
if (s.falseTriggerRate === null) fails.push("near-miss false-trigger rate not measured");
|
|
48
|
+
else {
|
|
49
|
+
if (s.falseTriggerRate > GATE_MAX_FALSE_TRIGGER_RATE) fails.push(`false-trigger rate ${pct(s.falseTriggerRate)} > ${pct(GATE_MAX_FALSE_TRIGGER_RATE)}`);
|
|
50
|
+
if (s.positiveRate !== null && s.positiveRate <= s.falseTriggerRate) fails.push(`positive rate ${pct(s.positiveRate)} does not beat false-trigger rate ${pct(s.falseTriggerRate)}`);
|
|
51
|
+
}
|
|
52
|
+
if (s.taskWith === null || s.taskWithout === null) fails.push("task check not measured");
|
|
53
|
+
else if (s.taskWith <= s.taskWithout) fails.push(`task pass ${pct(s.taskWith)} with the skill does not beat ${pct(s.taskWithout)} without`);
|
|
54
|
+
return fails.length ? { verdict: "keep-draft", reasons: fails } : { verdict: "adopt", reasons: ["all gate checks passed"] };
|
|
55
|
+
}
|
|
56
|
+
function gatePatch(patched, current) {
|
|
57
|
+
const regress = [];
|
|
58
|
+
const unmeasured = [];
|
|
59
|
+
const check = (label, now, before, higherIsBetter) => {
|
|
60
|
+
if (before === null) return;
|
|
61
|
+
if (now === null) return void unmeasured.push(`${label} not measured for the patch`);
|
|
62
|
+
if (higherIsBetter ? now < before : now > before) regress.push(`${label} ${pct(before)} -> ${pct(now)}`);
|
|
63
|
+
};
|
|
64
|
+
check("positive trigger rate", patched.positiveRate, current.positiveRate, true);
|
|
65
|
+
check("false-trigger rate", patched.falseTriggerRate, current.falseTriggerRate, false);
|
|
66
|
+
check("task pass rate", patched.taskWith, current.taskWith, true);
|
|
67
|
+
if (regress.length) return { verdict: "reject", reasons: regress.map((r) => `regresses: ${r}`) };
|
|
68
|
+
if (unmeasured.length) return { verdict: "keep-draft", reasons: unmeasured };
|
|
69
|
+
return { verdict: "adopt", reasons: ["no score dropped versus the current version"] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/core/skill-land.ts
|
|
73
|
+
var sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
74
|
+
var home = (env = process.env) => env.HOME || homedir();
|
|
75
|
+
function destinationFor(repo, r, opts = {}) {
|
|
76
|
+
if (opts.global) return join(home(opts.env), ".claude", "skills", r.name, "SKILL.md");
|
|
77
|
+
if (r.kind === "patch" && r.target) return r.target;
|
|
78
|
+
return r.scope === "directory" && r.directory ? join(repo, r.directory, ".claude", "skills", r.name, "SKILL.md") : join(repo, ".claude", "skills", r.name, "SKILL.md");
|
|
79
|
+
}
|
|
80
|
+
function getDraft(repo, id) {
|
|
81
|
+
const d = listDrafts(repo).find((x) => x.record.id === id);
|
|
82
|
+
if (!d) throw new Error(`no draft ${id} (skill-loop review lists them)`);
|
|
83
|
+
return { ...d, text: readFileSync(join(d.dir, "SKILL.md"), "utf8") };
|
|
84
|
+
}
|
|
85
|
+
function diff(a, b) {
|
|
86
|
+
const r = spawnSync("git", ["diff", "--no-index", "--no-color", "--", a, b], { encoding: "utf8" });
|
|
87
|
+
return r.stdout || "";
|
|
88
|
+
}
|
|
89
|
+
function describeDrafts(repo, env = process.env) {
|
|
90
|
+
return listDrafts(repo).map(({ dir, record }) => {
|
|
91
|
+
const file = join(dir, "SKILL.md");
|
|
92
|
+
const text = readFileSync(file, "utf8");
|
|
93
|
+
const s = parseSkill(text);
|
|
94
|
+
return {
|
|
95
|
+
id: record.id,
|
|
96
|
+
kind: record.kind,
|
|
97
|
+
name: record.name,
|
|
98
|
+
scope: record.scope,
|
|
99
|
+
destination: destinationFor(repo, record, { env }),
|
|
100
|
+
scope_why: record.scope_why,
|
|
101
|
+
provenance: s.meta?.provenance ?? null,
|
|
102
|
+
warnings: record.warnings,
|
|
103
|
+
evals: record.evals,
|
|
104
|
+
promotion: record.promotion,
|
|
105
|
+
global_candidate: record.global_candidate,
|
|
106
|
+
verify: parseVerify(s.body),
|
|
107
|
+
text,
|
|
108
|
+
...record.kind === "patch" && record.target && existsSync(record.target) ? { diff: diff(record.target, file) } : {}
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function landedText(text, global) {
|
|
113
|
+
const s = parseSkill(text);
|
|
114
|
+
const meta = {
|
|
115
|
+
...s.meta,
|
|
116
|
+
status: "active",
|
|
117
|
+
...global ? { scope: "global", applies_to: { paths: [], commands: s.meta.applies_to.commands }, cites: [] } : {}
|
|
118
|
+
};
|
|
119
|
+
return renderSkill({ name: s.name, description: s.description, meta, body: s.body });
|
|
120
|
+
}
|
|
121
|
+
function approveDraft(repo, id, opts = {}) {
|
|
122
|
+
const env = opts.env ?? process.env;
|
|
123
|
+
const d = getDraft(repo, id);
|
|
124
|
+
const r = d.record;
|
|
125
|
+
const gate = r.evals?.gate;
|
|
126
|
+
if (gate && gate.verdict !== "adopt" && !opts.override) {
|
|
127
|
+
throw new Error(`the eval gate said ${gate.verdict}: ${gate.reasons.join("; ")} \u2014 re-run --eval, or approve with --override "<why>"`);
|
|
128
|
+
}
|
|
129
|
+
if (opts.promote && !r.promotion?.length) throw new Error(`${id} has no promotion: a skill goes global only when another repo has the same lesson`);
|
|
130
|
+
const dest = destinationFor(repo, r, { global: opts.promote, env });
|
|
131
|
+
const text = landedText(d.text, !!opts.promote);
|
|
132
|
+
const others = findSkills(repo, { home: home(env) }).filter((s) => s.path !== dest);
|
|
133
|
+
const errors = lintSkill(dest, text, { projectRoot: repo, others }, r.name).filter((f) => f.severity === "error");
|
|
134
|
+
if (errors.length) throw new Error(`does not pass lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join("; ")}`);
|
|
135
|
+
const replaced = r.kind === "merge" ? r.supersedes ?? [] : [];
|
|
136
|
+
if (r.kind === "patch") {
|
|
137
|
+
if (!existsSync(dest) || sha256(readFileSync(dest)) !== r.base_sha256) throw new Error(`${dest} changed since this patch was drafted: reject it and let the next review redo it`);
|
|
138
|
+
} else if (existsSync(dest) && !replaced.includes(dest)) {
|
|
139
|
+
throw new Error(`${dest} already exists`);
|
|
140
|
+
}
|
|
141
|
+
for (const p of replaced) if (!existsSync(p)) throw new Error(`${p}, which this merge replaces, is gone: reject the merge`);
|
|
142
|
+
const why = `${r.kind}${opts.promote ? " promoted to global" : ""} from session ${r.session_id}${gate && gate.verdict !== "adopt" ? `; eval gate ${gate.verdict}, overridden: ${opts.override}` : ""}`;
|
|
143
|
+
const archived = replaced.filter((p) => p !== dest).flatMap((p) => [
|
|
144
|
+
{ path: join(repo, ".claude", ".semantic-memory", "skills-archive", basename(dirname(p)), "SKILL.md"), content: readFileSync(p) },
|
|
145
|
+
{ path: p, content: null }
|
|
146
|
+
]);
|
|
147
|
+
const entry = applyChange(repo, { action: opts.promote ? "promote" : r.kind === "patch" ? "patch" : r.kind === "merge" ? "merge" : "land", actor: "user", skill: r.name, why }, [{ path: dest, content: text }, ...archived]);
|
|
148
|
+
for (const p of replaced) if (p !== dest) indexSkill({ repo, name: basename(dirname(p)), path: p, status: "archived" }, env);
|
|
149
|
+
if (opts.promote) {
|
|
150
|
+
for (const p of r.promotion) {
|
|
151
|
+
if (!existsSync(p.path) || !p.path.includes(`${join(".claude", "skills")}`) || !/^skill_loop:/m.test(readFileSync(p.path, "utf8"))) continue;
|
|
152
|
+
applyChange(p.repo, { action: "archive", actor: "user", skill: r.name, why: `replaced by the global skill ${dest}` }, [
|
|
153
|
+
{ path: join(p.repo, ".claude", ".semantic-memory", "skills-archive", r.name, "SKILL.md"), content: readFileSync(p.path) },
|
|
154
|
+
{ path: p.path, content: null }
|
|
155
|
+
]);
|
|
156
|
+
indexSkill({ repo: p.repo, name: r.name, path: p.path, status: "archived" }, env);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
indexSkill({ repo: opts.promote ? "global" : repo, name: r.name, path: dest, status: "active" }, env);
|
|
160
|
+
rmSync(d.dir, { recursive: true, force: true });
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
163
|
+
function rejectDraft(repo, id, why = "rejected in review") {
|
|
164
|
+
const d = getDraft(repo, id);
|
|
165
|
+
const entry = applyChange(repo, { action: "reject", actor: "user", skill: d.record.name, why: `${why} (draft ${id}, session ${d.record.session_id})` }, []);
|
|
166
|
+
rmSync(d.dir, { recursive: true, force: true });
|
|
167
|
+
return entry;
|
|
168
|
+
}
|
|
169
|
+
function reviewedPrompts(repo, env = process.env) {
|
|
170
|
+
const dir = join(queueDir(env), "done");
|
|
171
|
+
let files = [];
|
|
172
|
+
try {
|
|
173
|
+
files = readdirSync(dir);
|
|
174
|
+
} catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const f of files) {
|
|
179
|
+
try {
|
|
180
|
+
const job = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
181
|
+
if (job.repo !== repo) continue;
|
|
182
|
+
for (const line of transcriptTimeline(job.transcript_path, 1e4)) {
|
|
183
|
+
if (line.startsWith("USER: ")) out.push({ session: job.session_id, text: line.slice(6) });
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
async function evalDraft(repo, id, opts = {}) {
|
|
191
|
+
const env = opts.env ?? process.env;
|
|
192
|
+
const d = getDraft(repo, id);
|
|
193
|
+
const skill = parseSkill(d.text);
|
|
194
|
+
const prompts = reviewedPrompts(repo, env);
|
|
195
|
+
const set = buildEvalSet(skill, prompts);
|
|
196
|
+
const trigger = await runTrigger(set, { repoRoot: repo, skillRaw: d.text, env });
|
|
197
|
+
let taskWith = null, taskWithout = null, task;
|
|
198
|
+
if (opts.task) {
|
|
199
|
+
const own = prompts.find((p) => skill.meta?.provenance.sessions.includes(p.session));
|
|
200
|
+
if (own) {
|
|
201
|
+
const t = await runTaskEval({ repoRoot: repo, skillRaw: d.text, prompt: own.text, env });
|
|
202
|
+
taskWith = t.with.passed ? 1 : 0;
|
|
203
|
+
taskWithout = t.without.passed ? 1 : 0;
|
|
204
|
+
task = { with: t.with.evidence, without: t.without.evidence };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const gate = gateSkill({ positiveRate: trigger.heldout.positiveRate, falseTriggerRate: trigger.heldout.falseTriggerRate, taskWith, taskWithout });
|
|
208
|
+
const evals = { at: (/* @__PURE__ */ new Date()).toISOString(), prompts: set.length, runs: trigger.runs, heldout: trigger.heldout, train: trigger.train, task, gate };
|
|
209
|
+
writeFileSync(join(d.dir, "draft.json"), JSON.stringify({ ...d.record, evals }, null, 1));
|
|
210
|
+
return evals;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export {
|
|
214
|
+
GATE_MIN_POSITIVE_RATE,
|
|
215
|
+
GATE_MAX_FALSE_TRIGGER_RATE,
|
|
216
|
+
gateSkill,
|
|
217
|
+
destinationFor,
|
|
218
|
+
describeDrafts,
|
|
219
|
+
approveDraft,
|
|
220
|
+
rejectDraft,
|
|
221
|
+
reviewedPrompts,
|
|
222
|
+
evalDraft
|
|
223
|
+
};
|
|
224
|
+
//# sourceMappingURL=chunk-3ML5ESXI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-land.ts","../src/core/skill-gate.ts"],"sourcesContent":["/**\n * Landing drafts (v1.7): `skill-loop review`. Nothing a reviewer drafts reaches .claude/skills/ until a\n * person approves it here; every landing, patch, promotion and rejection is a ledger entry (skill-ledger.ts).\n *\n * - new draft → <repo>/.claude/skills/<name>/ (project) or <repo>/<dir>/.claude/skills/<name>/ (directory)\n * - patch draft → replaces its target, only if the target is byte-for-byte what the draft was made from\n * - promotion → ~/.claude/skills/<name>/ (global), replacing the landed project copies in the other repos\n *\n * Evals run here, on request: trigger evals freely (no tools that touch anything), task evals only with\n * --task, since they run the draft's own Verify command.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { applyChange, type LedgerEntry } from \"./skill-ledger.js\";\nimport { listDrafts, transcriptTimeline, type DraftRecord } from \"./skill-review.js\";\nimport { findSkills, lintSkill, parseSkill, renderSkill, type SkillLoopMeta } from \"./skill-schema.js\";\nimport { indexSkill } from \"./skill-scope.js\";\nimport { queueDir } from \"./skill-queue.js\";\nimport { buildEvalSet, type CandidatePrompt } from \"./skill-eval/sets.js\";\nimport { runTrigger } from \"./skill-eval/trigger.js\";\nimport { runTaskEval, parseVerify } from \"./skill-eval/task.js\";\nimport { gateSkill } from \"./skill-gate.js\";\n\nconst sha256 = (s: string | Buffer) => createHash(\"sha256\").update(s).digest(\"hex\");\nconst home = (env = process.env) => env.HOME || homedir();\n\nexport function destinationFor(repo: string, r: DraftRecord, opts: { global?: boolean; env?: NodeJS.ProcessEnv } = {}): string {\n if (opts.global) return join(home(opts.env), \".claude\", \"skills\", r.name, \"SKILL.md\");\n if (r.kind === \"patch\" && r.target) return r.target;\n return r.scope === \"directory\" && r.directory ? join(repo, r.directory, \".claude\", \"skills\", r.name, \"SKILL.md\") : join(repo, \".claude\", \"skills\", r.name, \"SKILL.md\");\n}\n\nfunction getDraft(repo: string, id: string) {\n const d = listDrafts(repo).find((x) => x.record.id === id);\n if (!d) throw new Error(`no draft ${id} (skill-loop review lists them)`);\n return { ...d, text: readFileSync(join(d.dir, \"SKILL.md\"), \"utf8\") };\n}\n\nfunction diff(a: string, b: string): string {\n const r = spawnSync(\"git\", [\"diff\", \"--no-index\", \"--no-color\", \"--\", a, b], { encoding: \"utf8\" });\n return r.stdout || \"\";\n}\n\nexport interface DraftView {\n id: string;\n kind: DraftRecord[\"kind\"];\n name: string;\n scope: string;\n destination: string;\n scope_why?: string;\n provenance: SkillLoopMeta[\"provenance\"] | null;\n warnings: string[];\n evals?: Record<string, unknown>;\n promotion?: DraftRecord[\"promotion\"];\n global_candidate?: boolean;\n /** the Verify command a task eval would run, shown before anyone runs it */\n verify: ReturnType<typeof parseVerify>;\n text: string;\n diff?: string;\n}\n\nexport function describeDrafts(repo: string, env = process.env): DraftView[] {\n return listDrafts(repo).map(({ dir, record }) => {\n const file = join(dir, \"SKILL.md\");\n const text = readFileSync(file, \"utf8\");\n const s = parseSkill(text);\n return {\n id: record.id,\n kind: record.kind,\n name: record.name,\n scope: record.scope,\n destination: destinationFor(repo, record, { env }),\n scope_why: record.scope_why,\n provenance: s.meta?.provenance ?? null,\n warnings: record.warnings,\n evals: record.evals,\n promotion: record.promotion,\n global_candidate: record.global_candidate,\n verify: parseVerify(s.body),\n text,\n ...(record.kind === \"patch\" && record.target && existsSync(record.target) ? { diff: diff(record.target, file) } : {}),\n };\n });\n}\n\n/** The draft's text as it will land: status active, and for a promotion global with nothing repo-specific. */\nfunction landedText(text: string, global: boolean): string {\n const s = parseSkill(text);\n const meta: SkillLoopMeta = {\n ...s.meta!,\n status: \"active\",\n ...(global ? { scope: \"global\" as const, applies_to: { paths: [], commands: s.meta!.applies_to.commands }, cites: [] } : {}),\n };\n return renderSkill({ name: s.name, description: s.description, meta, body: s.body });\n}\n\nexport function approveDraft(repo: string, id: string, opts: { promote?: boolean; override?: string; env?: NodeJS.ProcessEnv } = {}): LedgerEntry {\n const env = opts.env ?? process.env;\n const d = getDraft(repo, id);\n const r = d.record;\n // evals are optional, but once they ran a failing draft stays a draft unless a person overrides (ledgered)\n const gate = (r.evals as { gate?: { verdict: string; reasons: string[] } } | undefined)?.gate;\n if (gate && gate.verdict !== \"adopt\" && !opts.override) {\n throw new Error(`the eval gate said ${gate.verdict}: ${gate.reasons.join(\"; \")} — re-run --eval, or approve with --override \"<why>\"`);\n }\n if (opts.promote && !r.promotion?.length) throw new Error(`${id} has no promotion: a skill goes global only when another repo has the same lesson`);\n const dest = destinationFor(repo, r, { global: opts.promote, env });\n const text = landedText(d.text, !!opts.promote);\n const others = findSkills(repo, { home: home(env) }).filter((s) => s.path !== dest);\n const errors = lintSkill(dest, text, { projectRoot: repo, others }, r.name).filter((f) => f.severity === \"error\");\n if (errors.length) throw new Error(`does not pass lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join(\"; \")}`);\n const replaced = r.kind === \"merge\" ? (r.supersedes ?? []) : [];\n if (r.kind === \"patch\") {\n if (!existsSync(dest) || sha256(readFileSync(dest)) !== r.base_sha256) throw new Error(`${dest} changed since this patch was drafted: reject it and let the next review redo it`);\n } else if (existsSync(dest) && !replaced.includes(dest)) {\n throw new Error(`${dest} already exists`);\n }\n for (const p of replaced) if (!existsSync(p)) throw new Error(`${p}, which this merge replaces, is gone: reject the merge`);\n const why = `${r.kind}${opts.promote ? \" promoted to global\" : \"\"} from session ${r.session_id}${gate && gate.verdict !== \"adopt\" ? `; eval gate ${gate.verdict}, overridden: ${opts.override}` : \"\"}`;\n // a merge lands the new skill and archives the ones it supersedes, in one ledger entry (one rollback)\n const archived = replaced.filter((p) => p !== dest).flatMap((p) => [\n { path: join(repo, \".claude\", \".semantic-memory\", \"skills-archive\", basename(dirname(p)), \"SKILL.md\"), content: readFileSync(p) as Buffer | string | null },\n { path: p, content: null },\n ]);\n const entry = applyChange(repo, { action: opts.promote ? \"promote\" : r.kind === \"patch\" ? \"patch\" : r.kind === \"merge\" ? \"merge\" : \"land\", actor: \"user\", skill: r.name, why }, [{ path: dest, content: text }, ...archived]);\n for (const p of replaced) if (p !== dest) indexSkill({ repo, name: basename(dirname(p)), path: p, status: \"archived\" }, env);\n if (opts.promote) {\n // the project copies the global skill replaces: only landed, learned ones, each ledgered in its own repo\n for (const p of r.promotion!) {\n if (!existsSync(p.path) || !p.path.includes(`${join(\".claude\", \"skills\")}`) || !/^skill_loop:/m.test(readFileSync(p.path, \"utf8\"))) continue;\n applyChange(p.repo, { action: \"archive\", actor: \"user\", skill: r.name, why: `replaced by the global skill ${dest}` }, [\n { path: join(p.repo, \".claude\", \".semantic-memory\", \"skills-archive\", r.name, \"SKILL.md\"), content: readFileSync(p.path) },\n { path: p.path, content: null },\n ]);\n indexSkill({ repo: p.repo, name: r.name, path: p.path, status: \"archived\" }, env);\n }\n }\n indexSkill({ repo: opts.promote ? \"global\" : repo, name: r.name, path: dest, status: \"active\" }, env);\n rmSync(d.dir, { recursive: true, force: true });\n return entry;\n}\n\nexport function rejectDraft(repo: string, id: string, why = \"rejected in review\"): LedgerEntry {\n const d = getDraft(repo, id);\n const entry = applyChange(repo, { action: \"reject\", actor: \"user\", skill: d.record.name, why: `${why} (draft ${id}, session ${d.record.session_id})` }, []);\n rmSync(d.dir, { recursive: true, force: true });\n return entry;\n}\n\n/** User prompts from this repo's reviewed sessions: the material trigger evals are built from. */\nexport function reviewedPrompts(repo: string, env = process.env): CandidatePrompt[] {\n const dir = join(queueDir(env), \"done\");\n let files: string[] = [];\n try {\n files = readdirSync(dir);\n } catch {\n return [];\n }\n const out: CandidatePrompt[] = [];\n for (const f of files) {\n try {\n const job = JSON.parse(readFileSync(join(dir, f), \"utf8\"));\n if (job.repo !== repo) continue;\n for (const line of transcriptTimeline(job.transcript_path, 10_000)) {\n if (line.startsWith(\"USER: \")) out.push({ session: job.session_id, text: line.slice(6) });\n }\n } catch {}\n }\n return out;\n}\n\n/** Run the evals on a draft and store the scores and the gate's verdict in its record. */\nexport async function evalDraft(repo: string, id: string, opts: { task?: boolean; env?: NodeJS.ProcessEnv } = {}) {\n const env = opts.env ?? process.env;\n const d = getDraft(repo, id);\n const skill = parseSkill(d.text);\n const prompts = reviewedPrompts(repo, env);\n const set = buildEvalSet(skill, prompts);\n const trigger = await runTrigger(set, { repoRoot: repo, skillRaw: d.text, env });\n let taskWith: number | null = null, taskWithout: number | null = null, task: unknown;\n if (opts.task) {\n const own = prompts.find((p) => skill.meta?.provenance.sessions.includes(p.session));\n if (own) {\n const t = await runTaskEval({ repoRoot: repo, skillRaw: d.text, prompt: own.text, env });\n taskWith = t.with.passed ? 1 : 0;\n taskWithout = t.without.passed ? 1 : 0;\n task = { with: t.with.evidence, without: t.without.evidence };\n }\n }\n const gate = gateSkill({ positiveRate: trigger.heldout.positiveRate, falseTriggerRate: trigger.heldout.falseTriggerRate, taskWith, taskWithout });\n const evals = { at: new Date().toISOString(), prompts: set.length, runs: trigger.runs, heldout: trigger.heldout, train: trigger.train, task, gate };\n writeFileSync(join(d.dir, \"draft.json\"), JSON.stringify({ ...d.record, evals }, null, 1));\n return evals;\n}\n","/**\n * Adoption gate for learned skills (v1.7): turns eval scores into adopt / keep-draft / reject. Pure — no I/O.\n * A new skill must trigger on its own evidence, stay quiet on near-misses and beat doing the task without it;\n * a patch must not make any score worse. A failing draft stays a draft with its scores.\n */\n\n/** A skill that fires on fewer than half its own held-out prompts is not findable. */\nexport const GATE_MIN_POSITIVE_RATE = 0.5;\n/** More than a quarter false triggers on near-misses means it would hijack unrelated work. */\nexport const GATE_MAX_FALSE_TRIGGER_RATE = 0.25;\n\n/** Held-out trigger rates and task pass rates, each 0..1 or null when not measured. */\nexport interface SkillScores {\n positiveRate: number | null;\n falseTriggerRate: number | null;\n taskWith: number | null;\n taskWithout: number | null;\n}\n\nexport interface GateVerdict {\n verdict: \"adopt\" | \"keep-draft\" | \"reject\";\n reasons: string[];\n}\n\nconst pct = (n: number) => `${Math.round(n * 100)}%`;\n\nexport function gateSkill(candidate: SkillScores, current?: SkillScores): GateVerdict {\n return current ? gatePatch(candidate, current) : gateNew(candidate);\n}\n\nfunction gateNew(s: SkillScores): GateVerdict {\n const fails: string[] = [];\n if (s.positiveRate === null) fails.push(\"held-out positive trigger rate not measured\");\n else if (s.positiveRate < GATE_MIN_POSITIVE_RATE) fails.push(`positive trigger rate ${pct(s.positiveRate)} < ${pct(GATE_MIN_POSITIVE_RATE)}`);\n if (s.falseTriggerRate === null) fails.push(\"near-miss false-trigger rate not measured\");\n else {\n if (s.falseTriggerRate > GATE_MAX_FALSE_TRIGGER_RATE) fails.push(`false-trigger rate ${pct(s.falseTriggerRate)} > ${pct(GATE_MAX_FALSE_TRIGGER_RATE)}`);\n if (s.positiveRate !== null && s.positiveRate <= s.falseTriggerRate) fails.push(`positive rate ${pct(s.positiveRate)} does not beat false-trigger rate ${pct(s.falseTriggerRate)}`);\n }\n if (s.taskWith === null || s.taskWithout === null) fails.push(\"task check not measured\");\n else if (s.taskWith <= s.taskWithout) fails.push(`task pass ${pct(s.taskWith)} with the skill does not beat ${pct(s.taskWithout)} without`);\n return fails.length ? { verdict: \"keep-draft\", reasons: fails } : { verdict: \"adopt\", reasons: [\"all gate checks passed\"] };\n}\n\nfunction gatePatch(patched: SkillScores, current: SkillScores): GateVerdict {\n const regress: string[] = [];\n const unmeasured: string[] = [];\n const check = (label: string, now: number | null, before: number | null, higherIsBetter: boolean) => {\n if (before === null) return;\n if (now === null) return void unmeasured.push(`${label} not measured for the patch`);\n if (higherIsBetter ? now < before : now > before) regress.push(`${label} ${pct(before)} -> ${pct(now)}`);\n };\n check(\"positive trigger rate\", patched.positiveRate, current.positiveRate, true);\n check(\"false-trigger rate\", patched.falseTriggerRate, current.falseTriggerRate, false);\n check(\"task pass rate\", patched.taskWith, current.taskWith, true);\n if (regress.length) return { verdict: \"reject\", reasons: regress.map((r) => `regresses: ${r}`) };\n if (unmeasured.length) return { verdict: \"keep-draft\", reasons: unmeasured };\n return { verdict: \"adopt\", reasons: [\"no score dropped versus the current version\"] };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendToSection,
|
|
3
3
|
replaceSectionBody
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
redact
|
|
7
|
-
} from "./chunk-H2BES6FU.js";
|
|
4
|
+
} from "./chunk-EXAUVIGW.js";
|
|
8
5
|
import {
|
|
9
6
|
matter_default
|
|
10
7
|
} from "./chunk-J7LYDRML.js";
|
|
8
|
+
import {
|
|
9
|
+
redact
|
|
10
|
+
} from "./chunk-H2BES6FU.js";
|
|
11
11
|
|
|
12
12
|
// src/core/profile.ts
|
|
13
13
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
@@ -108,4 +108,4 @@ export {
|
|
|
108
108
|
initSpeakerProfile,
|
|
109
109
|
updateProfileSection
|
|
110
110
|
};
|
|
111
|
-
//# sourceMappingURL=chunk-
|
|
111
|
+
//# sourceMappingURL=chunk-6TIKWOY4.js.map
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listDossiers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EXAUVIGW.js";
|
|
4
4
|
import {
|
|
5
5
|
deriveSessionDir
|
|
6
6
|
} from "./chunk-EYVFC745.js";
|
|
7
|
-
import {
|
|
8
|
-
redactDeep
|
|
9
|
-
} from "./chunk-H2BES6FU.js";
|
|
10
7
|
import {
|
|
11
8
|
matter_default
|
|
12
9
|
} from "./chunk-J7LYDRML.js";
|
|
10
|
+
import {
|
|
11
|
+
redactDeep
|
|
12
|
+
} from "./chunk-H2BES6FU.js";
|
|
13
13
|
|
|
14
14
|
// src/core/lexicon.ts
|
|
15
15
|
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
@@ -180,4 +180,4 @@ export {
|
|
|
180
180
|
findAliasConflicts,
|
|
181
181
|
removeAlias
|
|
182
182
|
};
|
|
183
|
-
//# sourceMappingURL=chunk-
|
|
183
|
+
//# sourceMappingURL=chunk-7UAVDOI6.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runTrigger
|
|
3
|
+
} from "./chunk-BSXIGPDY.js";
|
|
4
|
+
import {
|
|
5
|
+
runClaude
|
|
6
|
+
} from "./chunk-PMOZKUEM.js";
|
|
7
|
+
import {
|
|
8
|
+
parseSkill
|
|
9
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
10
|
+
import {
|
|
11
|
+
matter_default
|
|
12
|
+
} from "./chunk-J7LYDRML.js";
|
|
13
|
+
|
|
14
|
+
// src/core/skill-eval/prompts/improve-description.ts
|
|
15
|
+
var DESCRIPTION_LIMIT = 1024;
|
|
16
|
+
var list = (rs) => rs.map((r) => ` - "${r.text}" (triggered ${r.triggers}/${r.runs} times)
|
|
17
|
+
`).join("");
|
|
18
|
+
function improveDescriptionPrompt(a) {
|
|
19
|
+
let p = `You are optimizing a skill description for a Claude Code skill called "${a.name}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details.
|
|
20
|
+
|
|
21
|
+
The description appears in Claude's "available_skills" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.
|
|
22
|
+
|
|
23
|
+
Here's the current description:
|
|
24
|
+
<current_description>
|
|
25
|
+
"${a.description}"
|
|
26
|
+
</current_description>
|
|
27
|
+
|
|
28
|
+
Current scores (train ${a.score}):
|
|
29
|
+
<scores_summary>
|
|
30
|
+
`;
|
|
31
|
+
if (a.missed.length) p += `FAILED TO TRIGGER (should have triggered but didn't):
|
|
32
|
+
${list(a.missed)}
|
|
33
|
+
`;
|
|
34
|
+
if (a.falseTriggers.length) p += `FALSE TRIGGERS (triggered but shouldn't have):
|
|
35
|
+
${list(a.falseTriggers)}
|
|
36
|
+
`;
|
|
37
|
+
if (a.history.length) {
|
|
38
|
+
p += "PREVIOUS ATTEMPTS (do NOT repeat these -- try something structurally different):\n\n";
|
|
39
|
+
for (const h of a.history) p += `<attempt train=${h.score}>
|
|
40
|
+
Description: "${h.description}"
|
|
41
|
+
</attempt>
|
|
42
|
+
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
return p + `</scores_summary>
|
|
46
|
+
|
|
47
|
+
Skill content (for context on what the skill does):
|
|
48
|
+
<skill_content>
|
|
49
|
+
${a.skillContent}
|
|
50
|
+
</skill_content>
|
|
51
|
+
|
|
52
|
+
Based on the failures, write a new and improved description that is more likely to trigger correctly. Do not overfit: don't produce an ever-expanding list of specific queries. Generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The description is injected into ALL queries, so keep it to about 100-200 words. There is a hard limit of ${DESCRIPTION_LIMIT} characters -- stay comfortably under it.
|
|
53
|
+
|
|
54
|
+
Tips:
|
|
55
|
+
- Phrase it in the imperative -- "Use this skill for" rather than "this skill does".
|
|
56
|
+
- Focus on the user's intent, what they are trying to achieve, vs. the implementation details.
|
|
57
|
+
- The description competes with other skills for Claude's attention -- make it distinctive and immediately recognizable.
|
|
58
|
+
- If you're getting lots of failures after repeated attempts, change things up: different sentence structures or wordings.
|
|
59
|
+
|
|
60
|
+
Please respond with only the new description text in <new_description> tags, nothing else.`;
|
|
61
|
+
}
|
|
62
|
+
var shortenPrompt = (base, tooLong) => `${base}
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
A previous attempt produced this description, which at ${tooLong.length} characters is over the ${DESCRIPTION_LIMIT}-character hard limit:
|
|
67
|
+
|
|
68
|
+
"${tooLong}"
|
|
69
|
+
|
|
70
|
+
Rewrite it to be under ${DESCRIPTION_LIMIT} characters while keeping the most important trigger words and intent coverage. Respond with only the new description in <new_description> tags.`;
|
|
71
|
+
var parseNewDescription = (text) => (text.match(/<new_description>([\s\S]*?)<\/new_description>/)?.[1] ?? text).trim().replace(/^"|"$/g, "").trim();
|
|
72
|
+
|
|
73
|
+
// src/core/skill-eval/tune.ts
|
|
74
|
+
var MAX_TUNE_ITERATIONS = 3;
|
|
75
|
+
function withDescription(raw, description) {
|
|
76
|
+
const p = matter_default(raw);
|
|
77
|
+
return matter_default.stringify(p.content, { ...p.data, description });
|
|
78
|
+
}
|
|
79
|
+
var score = (r) => r.train.accuracy;
|
|
80
|
+
var frac = (r) => `${r.results.filter((x) => x.split === "train" && x.triggered === x.should_trigger).length}/${r.train.n}`;
|
|
81
|
+
async function tuneDescription(o) {
|
|
82
|
+
const skill = parseSkill(o.skillRaw);
|
|
83
|
+
const tr = (raw) => runTrigger(o.prompts, { repoRoot: o.repoRoot, skillRaw: raw, env: o.env });
|
|
84
|
+
const cand = (description, r) => ({ description, train: score(r), heldout: r.heldout.accuracy });
|
|
85
|
+
let last = await tr(o.skillRaw);
|
|
86
|
+
const original = cand(skill.description, last);
|
|
87
|
+
const candidates = [original];
|
|
88
|
+
const history = [];
|
|
89
|
+
for (let i = 0; i < Math.min(o.maxIterations ?? MAX_TUNE_ITERATIONS, MAX_TUNE_ITERATIONS); i++) {
|
|
90
|
+
const prev = candidates.at(-1);
|
|
91
|
+
history.push({ description: prev.description, score: frac(last) });
|
|
92
|
+
const train = last.results.filter((r2) => r2.split === "train");
|
|
93
|
+
const prompt = improveDescriptionPrompt({
|
|
94
|
+
name: skill.name,
|
|
95
|
+
skillContent: skill.body,
|
|
96
|
+
description: prev.description,
|
|
97
|
+
score: frac(last),
|
|
98
|
+
missed: train.filter((r2) => r2.should_trigger && !r2.triggered),
|
|
99
|
+
falseTriggers: train.filter((r2) => !r2.should_trigger && r2.triggered),
|
|
100
|
+
history: history.slice(0, -1)
|
|
101
|
+
});
|
|
102
|
+
const env = o.env;
|
|
103
|
+
let r = await runClaude({ prompt, cwd: o.repoRoot, tools: [], maxTurns: 1, env });
|
|
104
|
+
if (!r.ok) break;
|
|
105
|
+
let description = parseNewDescription(r.result);
|
|
106
|
+
if (description.length > DESCRIPTION_LIMIT) {
|
|
107
|
+
r = await runClaude({ prompt: shortenPrompt(prompt, description), cwd: o.repoRoot, tools: [], maxTurns: 1, env });
|
|
108
|
+
if (r.ok) description = parseNewDescription(r.result);
|
|
109
|
+
}
|
|
110
|
+
if (!description || description.length > DESCRIPTION_LIMIT || candidates.some((c) => c.description === description)) continue;
|
|
111
|
+
last = await tr(withDescription(o.skillRaw, description));
|
|
112
|
+
candidates.push(cand(description, last));
|
|
113
|
+
}
|
|
114
|
+
const best = candidates.reduce((b, c) => (c.heldout ?? -1) > (b.heldout ?? -1) || (c.heldout ?? -1) === (b.heldout ?? -1) && (c.train ?? -1) > (b.train ?? -1) ? c : b);
|
|
115
|
+
return { original, best, improved: best !== original, candidates };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
MAX_TUNE_ITERATIONS,
|
|
120
|
+
withDescription,
|
|
121
|
+
tuneDescription
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=chunk-ACZKDSMP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-eval/prompts/improve-description.ts","../src/core/skill-eval/tune.ts"],"sourcesContent":["/**\n * Adapted from Anthropic's skill-creator (https://github.com/anthropics/claude-plugins-official, Apache License 2.0); modified for semantic-memory.\n */\n\nexport interface DescriptionAttempt {\n description: string;\n /** \"passed/total\" on the train prompts */\n score: string;\n}\n\nexport interface PromptResult {\n text: string;\n triggers: number;\n runs: number;\n}\n\nexport const DESCRIPTION_LIMIT = 1024;\n\nconst list = (rs: PromptResult[]) => rs.map((r) => ` - \"${r.text}\" (triggered ${r.triggers}/${r.runs} times)\\n`).join(\"\");\n\n/** Ask for a better description from the train failures only (held-out results never reach the proposer). */\nexport function improveDescriptionPrompt(a: {\n name: string;\n skillContent: string;\n description: string;\n score: string;\n missed: PromptResult[];\n falseTriggers: PromptResult[];\n history: DescriptionAttempt[];\n}): string {\n let p = `You are optimizing a skill description for a Claude Code skill called \"${a.name}\". A \"skill\" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details.\n\nThe description appears in Claude's \"available_skills\" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.\n\nHere's the current description:\n<current_description>\n\"${a.description}\"\n</current_description>\n\nCurrent scores (train ${a.score}):\n<scores_summary>\n`;\n if (a.missed.length) p += `FAILED TO TRIGGER (should have triggered but didn't):\\n${list(a.missed)}\\n`;\n if (a.falseTriggers.length) p += `FALSE TRIGGERS (triggered but shouldn't have):\\n${list(a.falseTriggers)}\\n`;\n if (a.history.length) {\n p += \"PREVIOUS ATTEMPTS (do NOT repeat these -- try something structurally different):\\n\\n\";\n for (const h of a.history) p += `<attempt train=${h.score}>\\nDescription: \"${h.description}\"\\n</attempt>\\n\\n`;\n }\n return (\n p +\n `</scores_summary>\n\nSkill content (for context on what the skill does):\n<skill_content>\n${a.skillContent}\n</skill_content>\n\nBased on the failures, write a new and improved description that is more likely to trigger correctly. Do not overfit: don't produce an ever-expanding list of specific queries. Generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The description is injected into ALL queries, so keep it to about 100-200 words. There is a hard limit of ${DESCRIPTION_LIMIT} characters -- stay comfortably under it.\n\nTips:\n- Phrase it in the imperative -- \"Use this skill for\" rather than \"this skill does\".\n- Focus on the user's intent, what they are trying to achieve, vs. the implementation details.\n- The description competes with other skills for Claude's attention -- make it distinctive and immediately recognizable.\n- If you're getting lots of failures after repeated attempts, change things up: different sentence structures or wordings.\n\nPlease respond with only the new description text in <new_description> tags, nothing else.`\n );\n}\n\n/** The follow-up for a description over the limit. */\nexport const shortenPrompt = (base: string, tooLong: string) =>\n `${base}\\n\\n---\\n\\nA previous attempt produced this description, which at ${tooLong.length} characters is over the ${DESCRIPTION_LIMIT}-character hard limit:\\n\\n\"${tooLong}\"\\n\\n` +\n `Rewrite it to be under ${DESCRIPTION_LIMIT} characters while keeping the most important trigger words and intent coverage. Respond with only the new description in <new_description> tags.`;\n\n/** The description inside <new_description> tags, or the whole reply when untagged. */\nexport const parseNewDescription = (text: string): string =>\n (text.match(/<new_description>([\\s\\S]*?)<\\/new_description>/)?.[1] ?? text).trim().replace(/^\"|\"$/g, \"\").trim();\n","/**\n * Description tuning (v1.7): propose better descriptions from the train failures, score every candidate\n * with the trigger eval, keep the best by held-out score. At most 3 iterations. Returns the proposal and the\n * scores — it never writes the skill file.\n */\nimport { runClaude } from \"../claude-run.js\";\nimport matter from \"../matter.js\";\nimport { parseSkill } from \"../skill-schema.js\";\nimport { DESCRIPTION_LIMIT, improveDescriptionPrompt, parseNewDescription, shortenPrompt, type DescriptionAttempt } from \"./prompts/improve-description.js\";\nimport type { EvalPrompt } from \"./sets.js\";\nimport { runTrigger, type TriggerReport } from \"./trigger.js\";\n\nexport const MAX_TUNE_ITERATIONS = 3;\n\nexport interface TuneCandidate {\n description: string;\n train: number | null;\n heldout: number | null;\n}\n\nexport interface TuneReport {\n original: TuneCandidate;\n best: TuneCandidate;\n /** the best candidate is not the original */\n improved: boolean;\n candidates: TuneCandidate[];\n}\n\n/** The skill file with its frontmatter description replaced and nothing else changed. */\nexport function withDescription(raw: string, description: string): string {\n const p = matter(raw);\n return matter.stringify(p.content, { ...p.data, description });\n}\n\nconst score = (r: TriggerReport): TuneCandidate[\"train\"] => r.train.accuracy;\nconst frac = (r: TriggerReport) => `${r.results.filter((x) => x.split === \"train\" && x.triggered === x.should_trigger).length}/${r.train.n}`;\n\nexport async function tuneDescription(o: { repoRoot: string; skillRaw: string; prompts: EvalPrompt[]; env?: NodeJS.ProcessEnv; maxIterations?: number }): Promise<TuneReport> {\n const skill = parseSkill(o.skillRaw);\n const tr = (raw: string) => runTrigger(o.prompts, { repoRoot: o.repoRoot, skillRaw: raw, env: o.env });\n const cand = (description: string, r: TriggerReport): TuneCandidate => ({ description, train: score(r), heldout: r.heldout.accuracy });\n let last = await tr(o.skillRaw);\n const original = cand(skill.description, last);\n const candidates = [original];\n const history: DescriptionAttempt[] = [];\n for (let i = 0; i < Math.min(o.maxIterations ?? MAX_TUNE_ITERATIONS, MAX_TUNE_ITERATIONS); i++) {\n const prev = candidates.at(-1)!;\n history.push({ description: prev.description, score: frac(last) });\n // only train results go to the proposer; held-out stays unseen so it can judge\n const train = last.results.filter((r) => r.split === \"train\");\n const prompt = improveDescriptionPrompt({\n name: skill.name,\n skillContent: skill.body,\n description: prev.description,\n score: frac(last),\n missed: train.filter((r) => r.should_trigger && !r.triggered),\n falseTriggers: train.filter((r) => !r.should_trigger && r.triggered),\n history: history.slice(0, -1),\n });\n const env = o.env;\n let r = await runClaude({ prompt, cwd: o.repoRoot, tools: [], maxTurns: 1, env });\n if (!r.ok) break;\n let description = parseNewDescription(r.result);\n if (description.length > DESCRIPTION_LIMIT) {\n r = await runClaude({ prompt: shortenPrompt(prompt, description), cwd: o.repoRoot, tools: [], maxTurns: 1, env });\n if (r.ok) description = parseNewDescription(r.result);\n }\n if (!description || description.length > DESCRIPTION_LIMIT || candidates.some((c) => c.description === description)) continue;\n last = await tr(withDescription(o.skillRaw, description));\n candidates.push(cand(description, last));\n }\n // best by held-out, ties by train, then the earlier candidate (the original wins a full tie)\n const best = candidates.reduce((b, c) => ((c.heldout ?? -1) > (b.heldout ?? -1) || ((c.heldout ?? -1) === (b.heldout ?? -1) && (c.train ?? -1) > (b.train ?? -1)) ? c : b));\n return { original, best, improved: best !== original, candidates };\n}\n\n"],"mappings":"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","names":["r"]}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadSchema,
|
|
3
3
|
validateNote
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-Y7H76Q7F.js";
|
|
5
|
+
import {
|
|
6
|
+
matter_default
|
|
7
|
+
} from "./chunk-J7LYDRML.js";
|
|
5
8
|
import {
|
|
6
9
|
redact,
|
|
7
10
|
redactDeep
|
|
8
11
|
} from "./chunk-H2BES6FU.js";
|
|
9
|
-
import {
|
|
10
|
-
matter_default
|
|
11
|
-
} from "./chunk-J7LYDRML.js";
|
|
12
12
|
|
|
13
13
|
// src/core/crud.ts
|
|
14
14
|
import { readFile, writeFile, unlink, rename, mkdir } from "fs/promises";
|
|
@@ -943,4 +943,4 @@ export {
|
|
|
943
943
|
regenerateAgentsContract,
|
|
944
944
|
inspectAgentsContract
|
|
945
945
|
};
|
|
946
|
-
//# sourceMappingURL=chunk-
|
|
946
|
+
//# sourceMappingURL=chunk-AIXQFTSB.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClaude,
|
|
3
|
+
skillsCalled
|
|
4
|
+
} from "./chunk-PMOZKUEM.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-BSXIGPDY.js.map
|