@theglitchking/semantic-memory 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +61 -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-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3FPUBAEW.js +113 -0
- package/dist/chunk-3FPUBAEW.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-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
- package/dist/chunk-BEACL25M.js +224 -0
- package/dist/chunk-BEACL25M.js.map +1 -0
- package/dist/chunk-CATPGINC.js +186 -0
- package/dist/chunk-CATPGINC.js.map +1 -0
- package/dist/chunk-DBFD3MTT.js +130 -0
- package/dist/chunk-DBFD3MTT.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-IEZVFKUT.js +157 -0
- package/dist/chunk-IEZVFKUT.js.map +1 -0
- package/dist/chunk-MQS7JEBD.js +137 -0
- package/dist/chunk-MQS7JEBD.js.map +1 -0
- package/dist/chunk-NCGM36NX.js +118 -0
- package/dist/chunk-NCGM36NX.js.map +1 -0
- package/dist/chunk-NIZLBNH4.js +165 -0
- package/dist/chunk-NIZLBNH4.js.map +1 -0
- package/dist/chunk-OT3MV7XF.js +123 -0
- package/dist/chunk-OT3MV7XF.js.map +1 -0
- package/dist/chunk-P3GLY42T.js +291 -0
- package/dist/chunk-P3GLY42T.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.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-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 +40 -0
- package/dist/core/skill-queue.js +38 -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-XU2APPBJ.js +31 -0
- package/dist/skill-land-XU2APPBJ.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-GXBQLBZA.js +24 -0
- package/dist/skill-merge-GXBQLBZA.js.map +1 -0
- package/dist/skill-review-BaW5n0Zg.d.ts +179 -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-P3GLY42T.js";
|
|
5
|
+
import {
|
|
6
|
+
applyChange
|
|
7
|
+
} from "./chunk-TKHWVISV.js";
|
|
8
|
+
import {
|
|
9
|
+
indexSkill
|
|
10
|
+
} from "./chunk-3FPUBAEW.js";
|
|
11
|
+
import {
|
|
12
|
+
buildEvalSet,
|
|
13
|
+
runTrigger
|
|
14
|
+
} from "./chunk-MQS7JEBD.js";
|
|
15
|
+
import {
|
|
16
|
+
parseVerify,
|
|
17
|
+
runTaskEval
|
|
18
|
+
} from "./chunk-DBFD3MTT.js";
|
|
19
|
+
import {
|
|
20
|
+
findSkills,
|
|
21
|
+
lintSkill,
|
|
22
|
+
parseSkill,
|
|
23
|
+
renderSkill
|
|
24
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
25
|
+
import {
|
|
26
|
+
queueDir
|
|
27
|
+
} from "./chunk-NIZLBNH4.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-BEACL25M.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":[]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyChange,
|
|
3
|
+
archiveDir,
|
|
4
|
+
readLedger
|
|
5
|
+
} from "./chunk-TKHWVISV.js";
|
|
6
|
+
import {
|
|
7
|
+
indexSkill
|
|
8
|
+
} from "./chunk-3FPUBAEW.js";
|
|
9
|
+
import {
|
|
10
|
+
parseVerify
|
|
11
|
+
} from "./chunk-DBFD3MTT.js";
|
|
12
|
+
import {
|
|
13
|
+
findSkills,
|
|
14
|
+
parseSkill,
|
|
15
|
+
renderSkill
|
|
16
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
17
|
+
import {
|
|
18
|
+
readEvidence,
|
|
19
|
+
stateDir
|
|
20
|
+
} from "./chunk-R6GNP7DS.js";
|
|
21
|
+
|
|
22
|
+
// src/core/skill-curator.ts
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import { spawnSync } from "child_process";
|
|
26
|
+
var STALE_DAYS = 14;
|
|
27
|
+
var ARCHIVE_DAYS = 30;
|
|
28
|
+
var CURATOR_INTERVAL_DAYS = 7;
|
|
29
|
+
var IDLE_HOURS = 2;
|
|
30
|
+
var DAY = 864e5;
|
|
31
|
+
var curatorStatePath = (repo) => join(stateDir(repo), "curator.json");
|
|
32
|
+
function readState(repo) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(curatorStatePath(repo), "utf8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeState(repo, s) {
|
|
40
|
+
mkdirSync(stateDir(repo), { recursive: true });
|
|
41
|
+
writeFileSync(curatorStatePath(repo), JSON.stringify(s, null, 1));
|
|
42
|
+
}
|
|
43
|
+
function lastUsed(repo) {
|
|
44
|
+
const m = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const u of readEvidence(repo, "usage")) {
|
|
46
|
+
const t = Date.parse(String(u.ts));
|
|
47
|
+
const name = String(u.skill).split(":").pop();
|
|
48
|
+
if (Number.isFinite(t) && t > (m.get(name) ?? 0)) m.set(name, t);
|
|
49
|
+
}
|
|
50
|
+
return m;
|
|
51
|
+
}
|
|
52
|
+
function landedAt(s, ledger) {
|
|
53
|
+
const e = ledger.filter((x) => x.skill === s.skill.name && ["land", "patch", "restore", "merge", "promote"].includes(x.action)).pop();
|
|
54
|
+
return e ? Date.parse(e.ts) : statSync(s.path).mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
function plan(repo, now = Date.now(), env = process.env) {
|
|
57
|
+
const all = findSkills(repo, { home: env.HOME, global: false });
|
|
58
|
+
const learned = all.filter((s) => s.learned && s.skill.meta);
|
|
59
|
+
const used = lastUsed(repo);
|
|
60
|
+
const ledger = readLedger(repo);
|
|
61
|
+
const referenced = new Set(learned.flatMap((s) => [...s.skill.meta.supersedes ?? [], ...s.skill.meta.cites].map((x) => x.split("/").pop().replace(/\.md$/, ""))));
|
|
62
|
+
for (const s of all) for (const o of learned) if (s !== o && s.skill.body.includes(`/${o.skill.name}`)) referenced.add(o.skill.name);
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const s of learned) {
|
|
65
|
+
const name = s.skill.name;
|
|
66
|
+
const meta = s.skill.meta;
|
|
67
|
+
const base = { skill: name, path: s.path };
|
|
68
|
+
if (meta.pinned) {
|
|
69
|
+
out.push({ ...base, action: "skip", why: "pinned" });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (referenced.has(name)) {
|
|
73
|
+
out.push({ ...base, action: "skip", why: "cited by another skill" });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const last = used.get(name);
|
|
77
|
+
const since = last ?? landedAt(s, ledger);
|
|
78
|
+
const idle = (now - since) / DAY;
|
|
79
|
+
if (last === void 0 && idle < STALE_DAYS) {
|
|
80
|
+
out.push({ ...base, action: "skip", why: `never used, landed ${idle.toFixed(0)} d ago` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (idle >= ARCHIVE_DAYS) out.push({ ...base, action: "archive", why: `unused ${Math.floor(idle)} days` });
|
|
84
|
+
else if (idle >= STALE_DAYS && meta.status === "active") out.push({ ...base, action: "stale", why: `unused ${Math.floor(idle)} days` });
|
|
85
|
+
else if (idle < STALE_DAYS && meta.status === "stale") out.push({ ...base, action: "reactivate", why: `used ${Math.floor(idle)} days ago` });
|
|
86
|
+
else out.push({ ...base, action: "keep", why: `${meta.status}, ${last === void 0 ? "landed" : "used"} ${Math.floor(idle)} days ago` });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
var withStatus = (raw, status) => {
|
|
91
|
+
const s = parseSkill(raw);
|
|
92
|
+
return renderSkill({ name: s.name, description: s.description, meta: { ...s.meta, status }, body: s.body });
|
|
93
|
+
};
|
|
94
|
+
function curate(repo, opts = {}) {
|
|
95
|
+
const env = opts.env ?? process.env;
|
|
96
|
+
const now = opts.now ?? Date.now();
|
|
97
|
+
const actions = plan(repo, now, env);
|
|
98
|
+
const ids = [];
|
|
99
|
+
if (!opts.dryRun) {
|
|
100
|
+
for (const a of actions) {
|
|
101
|
+
if (a.action === "skip" || a.action === "keep") continue;
|
|
102
|
+
const raw = readFileSync(a.path, "utf8");
|
|
103
|
+
if (a.action === "archive") {
|
|
104
|
+
const dest = join(archiveDir(repo), a.skill, "SKILL.md");
|
|
105
|
+
ids.push(applyChange(repo, { action: "archive", actor: "curator", skill: a.skill, why: a.why }, [
|
|
106
|
+
{ path: dest, content: withStatus(raw, "archived") },
|
|
107
|
+
{ path: a.path, content: null }
|
|
108
|
+
]).id);
|
|
109
|
+
indexSkill({ repo, name: a.skill, path: a.path, status: "archived" }, env);
|
|
110
|
+
} else {
|
|
111
|
+
const status = a.action === "stale" ? "stale" : "active";
|
|
112
|
+
ids.push(applyChange(repo, { action: a.action === "stale" ? "stale" : "restore", actor: "curator", skill: a.skill, why: a.why }, [{ path: a.path, content: withStatus(raw, status) }]).id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const st = readState(repo);
|
|
116
|
+
writeState(repo, { ...st, last_run: new Date(now).toISOString(), last_report: actions.filter((a) => a.action !== "skip" && a.action !== "keep").map((a) => `${a.action} ${a.skill}`).join(", ") || "nothing to do" });
|
|
117
|
+
}
|
|
118
|
+
return { actions, ledger: ids };
|
|
119
|
+
}
|
|
120
|
+
function curatorDue(repo, now = Date.now()) {
|
|
121
|
+
const st = readState(repo);
|
|
122
|
+
if (!st.first_seen) {
|
|
123
|
+
writeState(repo, { ...st, first_seen: new Date(now).toISOString() });
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (now - Date.parse(st.first_seen) < CURATOR_INTERVAL_DAYS * DAY) return false;
|
|
127
|
+
if (st.last_run && now - Date.parse(st.last_run) < CURATOR_INTERVAL_DAYS * DAY) return false;
|
|
128
|
+
let last = 0;
|
|
129
|
+
for (const f of ["skill-usage.jsonl", "failures.jsonl", "corrections.jsonl"]) {
|
|
130
|
+
try {
|
|
131
|
+
last = Math.max(last, statSync(join(stateDir(repo), f)).mtimeMs);
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return now - last >= IDLE_HOURS * 36e5;
|
|
136
|
+
}
|
|
137
|
+
function restore(repo, name) {
|
|
138
|
+
const archived = join(archiveDir(repo), name, "SKILL.md");
|
|
139
|
+
if (!existsSync(archived)) throw new Error(`no archived skill "${name}" (archived: ${listArchived(repo).join(", ") || "none"})`);
|
|
140
|
+
const from = readLedger(repo).filter((e) => e.action === "archive" && e.skill === name).pop();
|
|
141
|
+
const original = from?.files.find((f) => f.after === null)?.path ?? join(repo, ".claude", "skills", name, "SKILL.md");
|
|
142
|
+
if (existsSync(original)) throw new Error(`${original} exists already`);
|
|
143
|
+
return applyChange(repo, { action: "restore", actor: "user", skill: name, why: "restored from skills-archive" }, [
|
|
144
|
+
{ path: original, content: withStatus(readFileSync(archived, "utf8"), "active") },
|
|
145
|
+
{ path: archived, content: null }
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
function listArchived(repo) {
|
|
149
|
+
try {
|
|
150
|
+
return readdirSync(archiveDir(repo)).filter((d) => existsSync(join(archiveDir(repo), d, "SKILL.md")));
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function verifySkills(repo, opts = {}) {
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const s of findSkills(repo, { home: opts.env?.HOME, global: false }).filter((x) => x.learned)) {
|
|
158
|
+
const v = parseVerify(s.skill.body);
|
|
159
|
+
if (v.kind !== "command") {
|
|
160
|
+
out.push({ skill: s.skill.name, result: v.kind === "prose" ? "prose" : "none", detail: v.kind === "prose" ? v.text.slice(0, 120) : "no ## Verify" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const r = spawnSync("bash", ["-c", v.command], { cwd: repo, timeout: opts.timeoutMs ?? 12e4, encoding: "utf8", env: opts.env ?? process.env });
|
|
164
|
+
out.push({ skill: s.skill.name, result: r.status === 0 ? "pass" : "fail", detail: r.status === 0 ? v.command : `${v.command} \u2192 exit ${r.status}: ${(r.stderr || r.stdout || "").trim().slice(0, 200)}` });
|
|
165
|
+
}
|
|
166
|
+
const st = readState(repo);
|
|
167
|
+
writeState(repo, { ...st, flagged: out.filter((x) => x.result === "fail").map((x) => ({ name: x.skill, reason: x.detail, at: (/* @__PURE__ */ new Date()).toISOString() })) });
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
var curatorState = readState;
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
STALE_DAYS,
|
|
174
|
+
ARCHIVE_DAYS,
|
|
175
|
+
CURATOR_INTERVAL_DAYS,
|
|
176
|
+
IDLE_HOURS,
|
|
177
|
+
curatorStatePath,
|
|
178
|
+
plan,
|
|
179
|
+
curate,
|
|
180
|
+
curatorDue,
|
|
181
|
+
restore,
|
|
182
|
+
listArchived,
|
|
183
|
+
verifySkills,
|
|
184
|
+
curatorState
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=chunk-CATPGINC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-curator.ts"],"sourcesContent":["/**\n * The skill loop's curator (v1.7): keeps learned skills from piling up. Deterministic — no model call:\n *\n * - a learned skill unused for 14 days becomes `stale`; for 30 days it is archived to\n * .claude/.semantic-memory/skills-archive/<name>/ (outside .claude/skills/, so Claude Code stops loading it)\n * - a stale skill that is used again goes back to `active`\n * - skipped: pinned skills, skills another skill cites or supersedes, never-used skills younger than 14 days,\n * and every skill the loop did not write\n *\n * Runs idle-triggered from SessionStart (every 7 days, when the repo has been quiet >= 2 h, never in the first\n * 7 days after the loop first saw the repo) or by hand. Every change is a ledger entry; `restore` brings an\n * archived skill back. Merging is separate and opt-in (skill-merge.ts).\n */\nimport { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\nimport { applyChange, archiveDir, readLedger, type LedgerEntry } from \"./skill-ledger.js\";\nimport { findSkills, parseSkill, renderSkill, type SkillFile } from \"./skill-schema.js\";\nimport { indexSkill } from \"./skill-scope.js\";\nimport { parseVerify } from \"./skill-eval/task.js\";\n\nexport const STALE_DAYS = 14;\nexport const ARCHIVE_DAYS = 30;\nexport const CURATOR_INTERVAL_DAYS = 7;\nexport const IDLE_HOURS = 2;\nconst DAY = 86_400_000;\n\nexport const curatorStatePath = (repo: string) => join(stateDir(repo), \"curator.json\");\n\ninterface CuratorState {\n first_seen?: string;\n last_run?: string;\n last_report?: string;\n flagged?: { name: string; reason: string; at: string }[];\n}\n\nfunction readState(repo: string): CuratorState {\n try {\n return JSON.parse(readFileSync(curatorStatePath(repo), \"utf8\"));\n } catch {\n return {};\n }\n}\nfunction writeState(repo: string, s: CuratorState) {\n mkdirSync(stateDir(repo), { recursive: true });\n writeFileSync(curatorStatePath(repo), JSON.stringify(s, null, 1));\n}\n\nexport interface CuratorAction {\n skill: string;\n path: string;\n action: \"stale\" | \"archive\" | \"reactivate\" | \"keep\" | \"skip\";\n why: string;\n}\n\n/** When the skill was last used here (ms), from skill-usage.jsonl; null if never. */\nfunction lastUsed(repo: string): Map<string, number> {\n const m = new Map<string, number>();\n for (const u of readEvidence(repo, \"usage\")) {\n const t = Date.parse(String(u.ts));\n const name = String(u.skill).split(\":\").pop()!;\n if (Number.isFinite(t) && t > (m.get(name) ?? 0)) m.set(name, t);\n }\n return m;\n}\n\n/** When the skill landed here (ms): its ledger entry, else the file's mtime. */\nfunction landedAt(s: SkillFile, ledger: LedgerEntry[]): number {\n const e = ledger.filter((x) => x.skill === s.skill.name && [\"land\", \"patch\", \"restore\", \"merge\", \"promote\"].includes(x.action)).pop();\n return e ? Date.parse(e.ts) : statSync(s.path).mtimeMs;\n}\n\nexport function plan(repo: string, now = Date.now(), env = process.env): CuratorAction[] {\n const all = findSkills(repo, { home: env.HOME, global: false });\n const learned = all.filter((s) => s.learned && s.skill.meta);\n const used = lastUsed(repo);\n const ledger = readLedger(repo);\n // a skill another one cites or supersedes is still load-bearing\n const referenced = new Set(learned.flatMap((s) => [...(s.skill.meta!.supersedes ?? []), ...s.skill.meta!.cites].map((x) => x.split(\"/\").pop()!.replace(/\\.md$/, \"\"))));\n for (const s of all) for (const o of learned) if (s !== o && s.skill.body.includes(`/${o.skill.name}`)) referenced.add(o.skill.name);\n const out: CuratorAction[] = [];\n for (const s of learned) {\n const name = s.skill.name;\n const meta = s.skill.meta!;\n const base = { skill: name, path: s.path };\n if (meta.pinned) { out.push({ ...base, action: \"skip\", why: \"pinned\" }); continue; }\n if (referenced.has(name)) { out.push({ ...base, action: \"skip\", why: \"cited by another skill\" }); continue; }\n const last = used.get(name);\n const since = last ?? landedAt(s, ledger);\n const idle = (now - since) / DAY;\n if (last === undefined && idle < STALE_DAYS) { out.push({ ...base, action: \"skip\", why: `never used, landed ${idle.toFixed(0)} d ago` }); continue; }\n if (idle >= ARCHIVE_DAYS) out.push({ ...base, action: \"archive\", why: `unused ${Math.floor(idle)} days` });\n else if (idle >= STALE_DAYS && meta.status === \"active\") out.push({ ...base, action: \"stale\", why: `unused ${Math.floor(idle)} days` });\n else if (idle < STALE_DAYS && meta.status === \"stale\") out.push({ ...base, action: \"reactivate\", why: `used ${Math.floor(idle)} days ago` });\n else out.push({ ...base, action: \"keep\", why: `${meta.status}, ${last === undefined ? \"landed\" : \"used\"} ${Math.floor(idle)} days ago` });\n }\n return out;\n}\n\nconst withStatus = (raw: string, status: \"active\" | \"stale\" | \"archived\") => {\n const s = parseSkill(raw);\n return renderSkill({ name: s.name, description: s.description, meta: { ...s.meta!, status }, body: s.body });\n};\n\n/** Run the curator. Dry run: report only. Returns the actions (and the ledger ids of the changes made). */\nexport function curate(repo: string, opts: { dryRun?: boolean; now?: number; env?: NodeJS.ProcessEnv } = {}): { actions: CuratorAction[]; ledger: string[] } {\n const env = opts.env ?? process.env;\n const now = opts.now ?? Date.now();\n const actions = plan(repo, now, env);\n const ids: string[] = [];\n if (!opts.dryRun) {\n for (const a of actions) {\n if (a.action === \"skip\" || a.action === \"keep\") continue;\n const raw = readFileSync(a.path, \"utf8\");\n if (a.action === \"archive\") {\n const dest = join(archiveDir(repo), a.skill, \"SKILL.md\");\n ids.push(applyChange(repo, { action: \"archive\", actor: \"curator\", skill: a.skill, why: a.why }, [\n { path: dest, content: withStatus(raw, \"archived\") },\n { path: a.path, content: null },\n ]).id);\n indexSkill({ repo, name: a.skill, path: a.path, status: \"archived\" }, env);\n } else {\n const status = a.action === \"stale\" ? \"stale\" : \"active\";\n ids.push(applyChange(repo, { action: a.action === \"stale\" ? \"stale\" : \"restore\", actor: \"curator\", skill: a.skill, why: a.why }, [{ path: a.path, content: withStatus(raw, status) }]).id);\n }\n }\n const st = readState(repo);\n writeState(repo, { ...st, last_run: new Date(now).toISOString(), last_report: actions.filter((a) => a.action !== \"skip\" && a.action !== \"keep\").map((a) => `${a.action} ${a.skill}`).join(\", \") || \"nothing to do\" });\n }\n return { actions, ledger: ids };\n}\n\n/** Whether SessionStart should start the curator now (and records first sight of the repo). */\nexport function curatorDue(repo: string, now = Date.now()): boolean {\n const st = readState(repo);\n if (!st.first_seen) {\n writeState(repo, { ...st, first_seen: new Date(now).toISOString() });\n return false;\n }\n if (now - Date.parse(st.first_seen) < CURATOR_INTERVAL_DAYS * DAY) return false;\n if (st.last_run && now - Date.parse(st.last_run) < CURATOR_INTERVAL_DAYS * DAY) return false;\n // quiet: the last thing the loop recorded here is >= 2 h old (the session starting now is not activity yet)\n let last = 0;\n for (const f of [\"skill-usage.jsonl\", \"failures.jsonl\", \"corrections.jsonl\"]) {\n try {\n last = Math.max(last, statSync(join(stateDir(repo), f)).mtimeMs);\n } catch {}\n }\n return now - last >= IDLE_HOURS * 3_600_000;\n}\n\n/** Bring an archived skill back where it was, active again. */\nexport function restore(repo: string, name: string): LedgerEntry {\n const archived = join(archiveDir(repo), name, \"SKILL.md\");\n if (!existsSync(archived)) throw new Error(`no archived skill \"${name}\" (archived: ${listArchived(repo).join(\", \") || \"none\"})`);\n const from = readLedger(repo).filter((e) => e.action === \"archive\" && e.skill === name).pop();\n const original = from?.files.find((f) => f.after === null)?.path ?? join(repo, \".claude\", \"skills\", name, \"SKILL.md\");\n if (existsSync(original)) throw new Error(`${original} exists already`);\n return applyChange(repo, { action: \"restore\", actor: \"user\", skill: name, why: \"restored from skills-archive\" }, [\n { path: original, content: withStatus(readFileSync(archived, \"utf8\"), \"active\") },\n { path: archived, content: null },\n ]);\n}\n\nexport function listArchived(repo: string): string[] {\n try {\n return readdirSync(archiveDir(repo)).filter((d) => existsSync(join(archiveDir(repo), d, \"SKILL.md\")));\n } catch {\n return [];\n }\n}\n\n/** Run each landed learned skill's Verify command (they were approved by a person); flag failures, archive nothing. */\nexport function verifySkills(repo: string, opts: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): { skill: string; result: \"pass\" | \"fail\" | \"prose\" | \"none\"; detail: string }[] {\n const out: { skill: string; result: \"pass\" | \"fail\" | \"prose\" | \"none\"; detail: string }[] = [];\n for (const s of findSkills(repo, { home: opts.env?.HOME, global: false }).filter((x) => x.learned)) {\n const v = parseVerify(s.skill.body);\n if (v.kind !== \"command\") {\n out.push({ skill: s.skill.name, result: v.kind === \"prose\" ? \"prose\" : \"none\", detail: v.kind === \"prose\" ? v.text.slice(0, 120) : \"no ## Verify\" });\n continue;\n }\n const r = spawnSync(\"bash\", [\"-c\", v.command], { cwd: repo, timeout: opts.timeoutMs ?? 120_000, encoding: \"utf8\", env: opts.env ?? process.env });\n out.push({ skill: s.skill.name, result: r.status === 0 ? \"pass\" : \"fail\", detail: r.status === 0 ? v.command : `${v.command} → exit ${r.status}: ${(r.stderr || r.stdout || \"\").trim().slice(0, 200)}` });\n }\n const st = readState(repo);\n writeState(repo, { ...st, flagged: out.filter((x) => x.result === \"fail\").map((x) => ({ name: x.skill, reason: x.detail, at: new Date().toISOString() })) });\n return out;\n}\n\nexport const curatorState = readState;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClaude
|
|
3
|
+
} from "./chunk-IEZVFKUT.js";
|
|
4
|
+
import {
|
|
5
|
+
parseSkill
|
|
6
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
7
|
+
import {
|
|
8
|
+
featureOn
|
|
9
|
+
} from "./chunk-VQCNWRTL.js";
|
|
10
|
+
import {
|
|
11
|
+
askJev,
|
|
12
|
+
jevVerdict
|
|
13
|
+
} from "./chunk-GYR7U6KA.js";
|
|
14
|
+
|
|
15
|
+
// src/core/skill-eval/task.ts
|
|
16
|
+
import { execFile, execFileSync } from "child_process";
|
|
17
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { promisify } from "util";
|
|
21
|
+
|
|
22
|
+
// src/core/skill-eval/prompts/grader.ts
|
|
23
|
+
var graderPrompt = (verify, outcome) => `You are a grader. Decide whether the outcome below satisfies the expectation.
|
|
24
|
+
|
|
25
|
+
PASS: there is clear evidence the expectation is true AND the evidence reflects genuine task completion, not surface-level compliance.
|
|
26
|
+
FAIL: there is no evidence, or the evidence contradicts the expectation, or it is superficial (a claim of success with nothing to back it).
|
|
27
|
+
Judge only from the outcome text; do not assume anything it does not say.
|
|
28
|
+
|
|
29
|
+
<expectation>
|
|
30
|
+
${verify}
|
|
31
|
+
</expectation>
|
|
32
|
+
|
|
33
|
+
<outcome>
|
|
34
|
+
${outcome}
|
|
35
|
+
</outcome>
|
|
36
|
+
|
|
37
|
+
Answer with only JSON: {"passed": true|false, "evidence": "<one sentence quoting or describing what you found>"}`;
|
|
38
|
+
function parseGrade(text) {
|
|
39
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
40
|
+
try {
|
|
41
|
+
const j = JSON.parse(m ? m[0] : "");
|
|
42
|
+
return { passed: j.passed === true, evidence: String(j.evidence ?? "") };
|
|
43
|
+
} catch {
|
|
44
|
+
return { passed: false, evidence: "grader gave no JSON" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/skill-eval/task.ts
|
|
49
|
+
var execFileP = promisify(execFile);
|
|
50
|
+
function parseVerify(body) {
|
|
51
|
+
const m = body.match(/^##\s+Verify\s*$([\s\S]*?)(?=^##\s|(?![\s\S]))/m);
|
|
52
|
+
const text = m?.[1].trim() ?? "";
|
|
53
|
+
if (!text) return { kind: "none" };
|
|
54
|
+
const fenced = text.match(/```[^\n]*\n([\s\S]*?)```/);
|
|
55
|
+
if (fenced?.[1].trim()) return { kind: "command", command: fenced[1].trim() };
|
|
56
|
+
const inline = text.replace(/^(?:[-*]|\d+\.)\s+/, "").match(/^`([^`\n]+)`/);
|
|
57
|
+
return inline ? { kind: "command", command: inline[1].trim() } : { kind: "prose", text };
|
|
58
|
+
}
|
|
59
|
+
var OUTCOME_CAP = 2e3;
|
|
60
|
+
async function runArm(o, withSkill) {
|
|
61
|
+
const env = o.env ?? process.env;
|
|
62
|
+
const skill = parseSkill(o.skillRaw);
|
|
63
|
+
const verify = parseVerify(skill.body);
|
|
64
|
+
const holder = mkdtempSync(join(tmpdir(), "sm-skill-task-"));
|
|
65
|
+
const dir = join(holder, "repo");
|
|
66
|
+
const git = (...a) => execFileSync("git", ["-C", o.repoRoot, ...a], { stdio: "pipe" });
|
|
67
|
+
try {
|
|
68
|
+
git("worktree", "add", "--detach", dir, "HEAD");
|
|
69
|
+
const skillDir = join(dir, ".claude", "skills", skill.name);
|
|
70
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
71
|
+
if (withSkill) {
|
|
72
|
+
mkdirSync(skillDir, { recursive: true });
|
|
73
|
+
writeFileSync(join(skillDir, "SKILL.md"), o.skillRaw);
|
|
74
|
+
}
|
|
75
|
+
const allowedTools = ["Edit", "Write", ...(skill.meta?.applies_to.commands ?? []).map((c) => `Bash(${c}:*)`)];
|
|
76
|
+
const run = await runClaude({
|
|
77
|
+
prompt: o.prompt,
|
|
78
|
+
cwd: dir,
|
|
79
|
+
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
|
|
80
|
+
allowedTools,
|
|
81
|
+
maxTurns: o.maxTurns ?? 12,
|
|
82
|
+
env,
|
|
83
|
+
timeoutMs: o.timeoutMs
|
|
84
|
+
});
|
|
85
|
+
const finalText = run.result.slice(0, OUTCOME_CAP);
|
|
86
|
+
if (!run.ok) return { passed: false, evidence: "the run failed", finalText, error: run.error };
|
|
87
|
+
if (verify.kind === "none") return { passed: false, evidence: "the skill has no ## Verify to check", finalText };
|
|
88
|
+
if (verify.kind === "command") {
|
|
89
|
+
try {
|
|
90
|
+
await execFileP("bash", ["-c", verify.command], { cwd: dir, timeout: o.verifyTimeoutMs ?? 12e4, env });
|
|
91
|
+
return { passed: true, evidence: `\`${verify.command}\` exited 0`, finalText };
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return { passed: false, evidence: `\`${verify.command}\` failed: ${String(e.message).slice(0, 200)}`, finalText };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (featureOn("jev", o.repoRoot, env)) {
|
|
97
|
+
const a = await askJev(
|
|
98
|
+
{ cwd: o.repoRoot, caller: "semantic-memory-skill-loop", state: finalText, questions: [{ key: "verify", instructions: `Does this outcome satisfy: ${verify.text}?` }] },
|
|
99
|
+
{ env }
|
|
100
|
+
);
|
|
101
|
+
const p = a.scores.verify;
|
|
102
|
+
return { passed: jevVerdict(p) === true, evidence: a.error ? `jev: ${a.error}` : `jev p=${p}`, finalText };
|
|
103
|
+
}
|
|
104
|
+
const g = await runClaude({ prompt: graderPrompt(verify.text, finalText), cwd: dir, tools: [], maxTurns: 1, env, timeoutMs: o.timeoutMs });
|
|
105
|
+
return { ...g.ok ? parseGrade(g.result) : { passed: false, evidence: `grader failed: ${g.error}` }, finalText };
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { passed: false, evidence: "could not set up the scratch copy", finalText: "", error: e.message };
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
git("worktree", "remove", "--force", dir);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
rmSync(holder, { recursive: true, force: true });
|
|
114
|
+
try {
|
|
115
|
+
git("worktree", "prune");
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function runTaskEval(o) {
|
|
121
|
+
const skill = parseSkill(o.skillRaw);
|
|
122
|
+
const [w, wo] = await Promise.all([runArm(o, true), runArm(o, false)]);
|
|
123
|
+
return { skill: skill.name, verify: parseVerify(skill.body), with: w, without: wo };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
parseVerify,
|
|
128
|
+
runTaskEval
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=chunk-DBFD3MTT.js.map
|