@theglitchking/semantic-memory 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +19 -2
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/chunk-2BFG4IBC.js +295 -0
- package/dist/chunk-2BFG4IBC.js.map +1 -0
- package/dist/chunk-2OMDSTSS.js +118 -0
- package/dist/chunk-2OMDSTSS.js.map +1 -0
- package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3ML5ESXI.js +224 -0
- package/dist/chunk-3ML5ESXI.js.map +1 -0
- package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
- package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
- package/dist/chunk-ACZKDSMP.js +123 -0
- package/dist/chunk-ACZKDSMP.js.map +1 -0
- package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
- package/dist/chunk-BSXIGPDY.js +137 -0
- package/dist/chunk-BSXIGPDY.js.map +1 -0
- package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/chunk-M2PC64DB.js +195 -0
- package/dist/chunk-M2PC64DB.js.map +1 -0
- package/dist/chunk-MTLYF357.js +130 -0
- package/dist/chunk-MTLYF357.js.map +1 -0
- package/dist/chunk-N77YCMSX.js +113 -0
- package/dist/chunk-N77YCMSX.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.js.map +1 -0
- package/dist/chunk-PMOZKUEM.js +170 -0
- package/dist/chunk-PMOZKUEM.js.map +1 -0
- package/dist/chunk-R6GNP7DS.js +87 -0
- package/dist/chunk-R6GNP7DS.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/chunk-V6PSK4Y4.js +186 -0
- package/dist/chunk-V6PSK4Y4.js.map +1 -0
- package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
- package/dist/chunk-VBLCUWT4.js.map +1 -0
- package/dist/chunk-VQCNWRTL.js +121 -0
- package/dist/chunk-VQCNWRTL.js.map +1 -0
- package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
- package/dist/chunk-Y7H76Q7F.js.map +1 -0
- package/dist/chunk-ZSWI3Y4Y.js +232 -0
- package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
- package/dist/cli/index.js +381 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/core/features.d.ts +38 -0
- package/dist/core/features.js +28 -0
- package/dist/core/index.d.ts +285 -2
- package/dist/core/index.js +170 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/jev.d.ts +35 -0
- package/dist/core/jev.js +20 -0
- package/dist/core/skill-curator.d.ts +1 -0
- package/dist/core/skill-curator.js +41 -0
- package/dist/core/skill-evidence.d.ts +25 -0
- package/dist/core/skill-evidence.js +23 -0
- package/dist/core/skill-queue.d.ts +46 -0
- package/dist/core/skill-queue.js +42 -0
- package/dist/core/skill-review.d.ts +3 -0
- package/dist/core/skill-review.js +29 -0
- package/dist/core/skill-worker.d.ts +21 -0
- package/dist/core/skill-worker.js +70 -0
- package/dist/core/skill-worker.js.map +1 -0
- package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/failure-eval-IUJO47UC.js.map +1 -0
- package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
- package/dist/healthcheck-S7D6YNTA.js.map +1 -0
- package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/mcp/server.js +19 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
- package/dist/skill-land-EHXZW47F.js +31 -0
- package/dist/skill-land-EHXZW47F.js.map +1 -0
- package/dist/skill-ledger-AYXVNFX7.js +18 -0
- package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
- package/dist/skill-merge-HBZBALZX.js +24 -0
- package/dist/skill-merge-HBZBALZX.js.map +1 -0
- package/dist/skill-review-BNvHipn9.d.ts +182 -0
- package/dist/skill-schema-RVB3EHEF.js +30 -0
- package/dist/skill-schema-RVB3EHEF.js.map +1 -0
- package/hooks/hooks.json +26 -1
- package/hooks/jev-bridge.py +109 -0
- package/hooks/session-start.js +5 -0
- package/hooks/skill-loop.js +65 -0
- package/hooks/vault-context.js +104 -6
- package/package.json +4 -2
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-GYBUBJYM.js.map +0 -1
- package/dist/chunk-HUG7E25D.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
- /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
- /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
- /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
- /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-scope.ts"],"sourcesContent":["/**\n * Where a learned skill lives (v1.7): directory, project, or — only by promotion — global.\n *\n * 1. Deterministic first: a draft that names this repo's paths is project-scoped; one whose paths all sit\n * under a single subdirectory is directory-scoped there.\n * 2. Unresolved (names nothing in the repo): in jev mode one batch Jev question, \"is this true in any\n * repository?\"; a yes at >= 0.95 only marks the draft a global *candidate*. Otherwise: project.\n * 3. Global only by promotion: when the cross-repo index shows the same skill name drafted or landed in\n * >= 2 repos, review offers one global skill that replaces the project copies. A person approves it.\n *\n * Private repos (SEMANTIC_MEMORY_PRIVATE_PATHS, default ~/workspace/kentro: a Claude-only boundary) are\n * always project, are never written to the cross-repo index, and never reach Jev.\n */\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, sep } from \"node:path\";\nimport { featureOn } from \"./features.js\";\nimport { askJev, jevVerdict } from \"./jev.js\";\nimport { smHome } from \"./skill-queue.js\";\nimport type { SkillScope } from \"./skill-schema.js\";\n\nexport interface ScopeDecision {\n scope: Exclude<SkillScope, \"global\">;\n /** directory scope: the subdirectory (repo-relative) */\n directory?: string;\n /** Jev said this holds in any repo: offer promotion once another repo has it too */\n globalCandidate: boolean;\n why: string;\n}\n\nexport function privatePaths(env = process.env): string[] {\n const home = env.HOME || homedir();\n return (env.SEMANTIC_MEMORY_PRIVATE_PATHS ?? \"~/workspace/kentro\")\n .split(\",\")\n .map((p) => p.trim().replace(/^~(?=$|\\/)/, home))\n .filter(Boolean);\n}\n\nexport const isPrivateRepo = (repo: string, env = process.env) =>\n privatePaths(env).some((p) => repo === p || repo.startsWith(p.endsWith(sep) ? p : p + sep));\n\n/** Repo-relative paths a draft mentions that exist in the repo (from applies_to.paths and path-like tokens). */\nexport function repoReferences(repo: string, text: string, paths: string[] = []): string[] {\n const tokens = new Set(paths);\n for (const m of text.matchAll(/(?:^|[\\s`'\"(])((?:\\.\\/)?[\\w.@-]+(?:\\/[\\w.@-]+)+\\/?|[\\w-]+\\.(?:sh|js|mjs|ts|py|json|toml|ya?ml|md))(?=[\\s`'\"),:;]|$)/gm)) {\n tokens.add(m[1].replace(/^\\.\\//, \"\").replace(/\\/$/, \"\"));\n }\n return [...tokens].filter((p) => p && !p.startsWith(\"/\") && !p.startsWith(\"~\") && !p.includes(\"..\") && existsSync(join(repo, p)));\n}\n\n/** The one subdirectory every referenced path sits under (two levels at most), or null. */\nfunction commonSubdir(repo: string, refs: string[]): string | null {\n const dirs = refs.map((r) => {\n let isDir = false;\n try {\n isDir = statSync(join(repo, r)).isDirectory();\n } catch {}\n const parts = r.split(\"/\");\n return (isDir ? parts : parts.slice(0, -1)).slice(0, 2).join(\"/\");\n });\n if (!dirs.length || dirs.some((d) => !d)) return null;\n const first = dirs[0];\n return dirs.every((d) => d === first || d.startsWith(first + \"/\")) ? first : null;\n}\n\nexport const SCOPE_QUESTION =\n \"Would these steps work unchanged in any software repository — they name no file, script, package, service or \" +\n \"setting that belongs to one particular repository?\";\n\nexport async function decideScope(\n repo: string,\n draft: { name: string; description: string; body: string; applies_to?: { paths?: string[] } },\n env = process.env,\n): Promise<ScopeDecision> {\n const refs = repoReferences(repo, `${draft.description}\\n${draft.body}`, draft.applies_to?.paths ?? []);\n if (refs.length) {\n const sub = commonSubdir(repo, refs);\n // a subdir that holds its own package/skills is a real unit; a bare src/ or scripts/ is still the whole project\n if (sub && (existsSync(join(repo, sub, \"package.json\")) || existsSync(join(repo, sub, \".claude\")) || existsSync(join(repo, sub, \"pyproject.toml\")))) {\n return { scope: \"directory\", directory: sub, globalCandidate: false, why: `every path it names is under ${sub}/` };\n }\n return { scope: \"project\", globalCandidate: false, why: `names this repo's ${refs.slice(0, 3).join(\", \")}` };\n }\n if (isPrivateRepo(repo, env)) return { scope: \"project\", globalCandidate: false, why: \"private repo: stays here\" };\n if (!featureOn(\"jev\", repo, env)) return { scope: \"project\", globalCandidate: false, why: \"names nothing in the repo; jev mode off → project\" };\n const a = await askJev(\n {\n cwd: repo,\n caller: \"semantic-memory-skill-loop\",\n state: `Skill ${draft.name}: ${draft.description}\\n\\n${draft.body}`.slice(0, 2000),\n questions: [{ key: \"global\", instructions: SCOPE_QUESTION }],\n },\n { env, timeoutMs: 30_000 },\n );\n const yes = jevVerdict(a.scores.global) === true;\n return {\n scope: \"project\",\n globalCandidate: yes,\n why: yes ? `Jev: holds in any repo (p=${a.scores.global?.toFixed(2)}); promotion needs a second repo` : `names nothing in the repo; Jev not sure it is general (${a.error ?? `p=${a.scores.global?.toFixed(2)}`}) → project`,\n };\n}\n\n// --- the cross-repo index: paths and names only ---\n\nexport interface IndexEntry {\n repo: string;\n name: string;\n path: string;\n status: \"draft\" | \"active\" | \"archived\";\n global_candidate?: boolean;\n ts: string;\n}\n\nexport const skillIndexPath = (env = process.env) => join(smHome(env), \"skill-index.jsonl\");\n\nexport function indexSkill(e: Omit<IndexEntry, \"ts\">, env = process.env): void {\n if (isPrivateRepo(e.repo, env)) return;\n mkdirSync(smHome(env), { recursive: true });\n appendFileSync(skillIndexPath(env), JSON.stringify({ ...e, ts: new Date().toISOString() }) + \"\\n\");\n}\n\n/** Latest entry per (repo, name, path). */\nexport function readIndex(env = process.env): IndexEntry[] {\n const latest = new Map<string, IndexEntry>();\n let raw = \"\";\n try {\n raw = readFileSync(skillIndexPath(env), \"utf8\");\n } catch {\n return [];\n }\n for (const line of raw.split(\"\\n\")) {\n try {\n const e = JSON.parse(line) as IndexEntry;\n latest.set(`${e.repo}\\0${e.name}\\0${e.path}`, e);\n } catch {}\n }\n return [...latest.values()];\n}\n\nconst nameTokens = (n: string) => new Set(n.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1));\n\n/** Same lesson: the same name, or names sharing >= 3/4 of their words. */\nexport function sameLesson(a: string, b: string): boolean {\n if (a === b) return true;\n const x = nameTokens(a), y = nameTokens(b);\n const shared = [...x].filter((t) => y.has(t)).length;\n return shared / Math.max(x.size, y.size, 1) >= 0.75;\n}\n\n/** Other repos holding the same lesson (not archived). Promotion is offered when this returns >= 1 repo. */\nexport function promotionPeers(repo: string, name: string, env = process.env): IndexEntry[] {\n if (isPrivateRepo(repo, env)) return [];\n return readIndex(env).filter((e) => e.repo !== repo && e.status !== \"archived\" && sameLesson(e.name, name) && !isPrivateRepo(e.repo, env));\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/core/failure-eval.ts
|
|
2
|
+
var key = (c, p) => `${c}\0${p}`;
|
|
3
|
+
function evaluateFailures(results, judgments, opts = {}) {
|
|
4
|
+
const target = opts.targetPrecision ?? 0.95;
|
|
5
|
+
const minJudged = opts.minJudged ?? 20;
|
|
6
|
+
const thresholds = opts.thresholds ?? Array.from({ length: 17 }, (_, i) => Math.round((0.2 + i * 0.05) * 100) / 100);
|
|
7
|
+
const label = /* @__PURE__ */ new Map();
|
|
8
|
+
const relevantFor = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const j of judgments) {
|
|
10
|
+
label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);
|
|
11
|
+
if (j.relevant) {
|
|
12
|
+
if (!relevantFor.has(j.case)) relevantFor.set(j.case, /* @__PURE__ */ new Set());
|
|
13
|
+
relevantFor.get(j.case).add(j.path);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let answerable = 0, rank1 = 0, recallAtK = 0;
|
|
17
|
+
const unjudged = [];
|
|
18
|
+
for (const r of results) {
|
|
19
|
+
const relevant = relevantFor.get(r.case);
|
|
20
|
+
if (relevant && relevant.size > 0) {
|
|
21
|
+
answerable++;
|
|
22
|
+
if (r.hits[0] && relevant.has(r.hits[0].path)) rank1++;
|
|
23
|
+
if (r.hits.some((h) => relevant.has(h.path))) recallAtK++;
|
|
24
|
+
}
|
|
25
|
+
const top = r.hits[0];
|
|
26
|
+
if (top && !label.has(key(r.case, top.path))) unjudged.push({ case: r.case, path: top.path, score: top.score });
|
|
27
|
+
}
|
|
28
|
+
const rows = thresholds.map((threshold) => {
|
|
29
|
+
let injected = 0, judged = 0, correct = 0;
|
|
30
|
+
for (const r of results) {
|
|
31
|
+
const top = r.hits[0];
|
|
32
|
+
if (!top || top.score < threshold) continue;
|
|
33
|
+
injected++;
|
|
34
|
+
const l = label.get(key(r.case, top.path));
|
|
35
|
+
if (l === void 0) continue;
|
|
36
|
+
judged++;
|
|
37
|
+
if (l) correct++;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
threshold,
|
|
41
|
+
injected,
|
|
42
|
+
judged,
|
|
43
|
+
correct,
|
|
44
|
+
precision: judged > 0 ? correct / judged : null,
|
|
45
|
+
coverage: results.length > 0 ? injected / results.length : 0
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
const meets = (row) => row.precision !== null && row.precision >= target;
|
|
49
|
+
const recommended = rows.find(
|
|
50
|
+
(row, i) => row.judged >= minJudged && meets(row) && rows.slice(i + 1).every((r) => r.judged < minJudged || meets(r))
|
|
51
|
+
)?.threshold ?? null;
|
|
52
|
+
return { cases: results.length, answerable, rank1, recallAtK, rows, recommended, unjudged };
|
|
53
|
+
}
|
|
54
|
+
function formatFailureEval(report, label = "") {
|
|
55
|
+
const pct = (n) => n === null ? " - " : `${(n * 100).toFixed(1)}%`.padStart(6);
|
|
56
|
+
const lines = [
|
|
57
|
+
`Failure-lookup eval${label ? ` (${label})` : ""} \u2014 ${report.cases} cases, ${report.answerable} with a known answer`,
|
|
58
|
+
` answer ranked first: ${report.rank1}/${report.answerable} answer anywhere in hits: ${report.recallAtK}/${report.answerable}`,
|
|
59
|
+
"",
|
|
60
|
+
" threshold injected judged correct precision coverage"
|
|
61
|
+
];
|
|
62
|
+
for (const r of report.rows) {
|
|
63
|
+
lines.push(
|
|
64
|
+
` ${r.threshold.toFixed(2).padStart(9)} ${String(r.injected).padStart(8)} ${String(r.judged).padStart(6)} ${String(r.correct).padStart(7)} ${pct(r.precision).padStart(9)} ${pct(r.coverage).padStart(8)}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(
|
|
69
|
+
report.recommended === null ? " recommended threshold: none reaches the target precision with enough judged injections" : ` recommended threshold: ${report.recommended.toFixed(2)}`
|
|
70
|
+
);
|
|
71
|
+
lines.push(` unjudged top hits: ${report.unjudged.length}`);
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
var meetsBar = (picks, wrong) => picks >= 40 && wrong <= 2 && (picks - wrong) / picks >= 0.95;
|
|
75
|
+
function calibrateJudge(labels, jev, bar = 0.95) {
|
|
76
|
+
const label = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const j of labels) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);
|
|
78
|
+
const relevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };
|
|
79
|
+
const notRelevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };
|
|
80
|
+
const seen = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const r of jev) {
|
|
82
|
+
const k = key(r.case, r.path);
|
|
83
|
+
const l = label.get(k);
|
|
84
|
+
if (l === void 0 || seen.has(k)) continue;
|
|
85
|
+
seen.add(k);
|
|
86
|
+
const side = l ? relevant : notRelevant;
|
|
87
|
+
side.pairs++;
|
|
88
|
+
if (r.p !== null && r.p >= bar) side.yes++;
|
|
89
|
+
else if (r.p !== null && r.p <= 1 - bar + 1e-9) side.no++;
|
|
90
|
+
else side.unsure++;
|
|
91
|
+
}
|
|
92
|
+
const pairs = relevant.pairs + notRelevant.pairs;
|
|
93
|
+
const picks = relevant.yes + relevant.no + notRelevant.yes + notRelevant.no;
|
|
94
|
+
const wrong = relevant.no + notRelevant.yes;
|
|
95
|
+
return {
|
|
96
|
+
pairs,
|
|
97
|
+
relevant,
|
|
98
|
+
notRelevant,
|
|
99
|
+
picks,
|
|
100
|
+
wrong,
|
|
101
|
+
agreement: picks ? (picks - wrong) / picks : null,
|
|
102
|
+
unsureRate: pairs ? (relevant.unsure + notRelevant.unsure) / pairs : 0,
|
|
103
|
+
yesPicks: relevant.yes + notRelevant.yes,
|
|
104
|
+
yesWrong: notRelevant.yes,
|
|
105
|
+
noPicks: relevant.no + notRelevant.no,
|
|
106
|
+
noWrong: relevant.no,
|
|
107
|
+
// both sides separately: a judge that only ever says no would pass a pooled check and still find nothing
|
|
108
|
+
verdict: meetsBar(relevant.yes + notRelevant.yes, notRelevant.yes) && meetsBar(relevant.no + notRelevant.no, relevant.no) ? "jev" : meetsBar(relevant.no + notRelevant.no, relevant.no) ? "hybrid" : "claude"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function evaluateGate(results, judgments, jev, opts = {}) {
|
|
112
|
+
const mode = opts.mode ?? "confirm";
|
|
113
|
+
const thresholds = opts.thresholds ?? [0.3, 0.35, 0.4, 0.5];
|
|
114
|
+
const topK = opts.topK ?? 5;
|
|
115
|
+
const bar = opts.bar ?? 0.95;
|
|
116
|
+
const isCandidate = opts.isCandidate ?? ((p) => /(^|\/)facts\/[^/]+\.md$/.test(p));
|
|
117
|
+
const label = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const j of judgments) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);
|
|
119
|
+
const score = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const r of jev) score.set(key(r.case, r.path), r.p);
|
|
121
|
+
const missing = /* @__PURE__ */ new Map();
|
|
122
|
+
const rows = thresholds.map((threshold) => {
|
|
123
|
+
let candidates = 0, miss = 0, injected = 0, judged = 0, correct = 0;
|
|
124
|
+
for (const r of results) {
|
|
125
|
+
const hit = r.hits.slice(0, topK).find((h) => isCandidate(h.path) && h.score >= threshold);
|
|
126
|
+
if (!hit) continue;
|
|
127
|
+
candidates++;
|
|
128
|
+
const k = key(r.case, hit.path);
|
|
129
|
+
if (!score.has(k)) {
|
|
130
|
+
miss++;
|
|
131
|
+
missing.set(k, { case: r.case, path: hit.path });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const p = score.get(k);
|
|
135
|
+
const inject = mode === "confirm" ? p !== null && p !== void 0 && p >= bar : !(p !== null && p !== void 0 && p <= 1 - bar + 1e-9);
|
|
136
|
+
if (!inject) continue;
|
|
137
|
+
injected++;
|
|
138
|
+
const l = label.get(k);
|
|
139
|
+
if (l === void 0) continue;
|
|
140
|
+
judged++;
|
|
141
|
+
if (l) correct++;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
threshold,
|
|
145
|
+
candidates,
|
|
146
|
+
missing: miss,
|
|
147
|
+
injected,
|
|
148
|
+
judged,
|
|
149
|
+
correct,
|
|
150
|
+
precision: judged ? correct / judged : null,
|
|
151
|
+
coverage: results.length ? injected / results.length : 0,
|
|
152
|
+
meetsBar: meetsBar(judged, judged - correct)
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
return { rows, missing: [...missing.values()] };
|
|
156
|
+
}
|
|
157
|
+
function formatGate(rows, mode = "confirm") {
|
|
158
|
+
const pct = (n) => n === null ? " - " : `${(n * 100).toFixed(1)}%`;
|
|
159
|
+
const what = mode === "confirm" ? "only when Jev says yes at >= 0.95" : "unless Jev says no at <= 0.05";
|
|
160
|
+
const lines = [`Jev gate, ${mode} (inject the first fact >= T ${what})`, "", " T candidates missing injected judged correct precision coverage bar"];
|
|
161
|
+
for (const r of rows) {
|
|
162
|
+
lines.push(
|
|
163
|
+
` ${r.threshold.toFixed(2)} ${String(r.candidates).padStart(10)} ${String(r.missing).padStart(7)} ${String(r.injected).padStart(8)} ${String(r.judged).padStart(6)} ${String(r.correct).padStart(7)} ${pct(r.precision).padStart(9)} ${pct(r.coverage).padStart(8)} ${r.meetsBar ? "yes" : "no"}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
evaluateFailures,
|
|
171
|
+
formatFailureEval,
|
|
172
|
+
meetsBar,
|
|
173
|
+
calibrateJudge,
|
|
174
|
+
evaluateGate,
|
|
175
|
+
formatGate
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=chunk-PJXYCCI2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/failure-eval.ts"],"sourcesContent":["/**\n * Failure-lookup eval (v1.6): given failed tool calls, the hits a search returned for each, and human\n * (or judge) relevance labels, measure how well the top hit answers the failure and pick the score\n * threshold above which injecting it is right often enough. Pure — no I/O, no search.\n */\n\nexport interface FailureCase {\n id: string;\n /** notes folder this failure's repo searches (its docs) */\n notes: string;\n error: string;\n command: string;\n}\n\nexport interface Judgment {\n case: string;\n path: string;\n relevant: boolean;\n}\n\nexport interface CaseResult {\n case: string;\n hits: { path: string; score: number }[];\n}\n\nexport interface ThresholdRow {\n threshold: number;\n /** cases whose top hit scored >= threshold (would be injected) */\n injected: number;\n /** injected cases whose top hit has a label */\n judged: number;\n correct: number;\n /** correct / judged; null when nothing judged */\n precision: number | null;\n /** injected / all cases */\n coverage: number;\n}\n\nexport interface FailureEvalReport {\n cases: number;\n /** cases with at least one relevant path among their labels */\n answerable: number;\n /** answerable cases whose top hit is relevant */\n rank1: number;\n /** answerable cases with a relevant path anywhere in the hits */\n recallAtK: number;\n rows: ThresholdRow[];\n /** lowest threshold reaching the target precision on >= minJudged judged injections; null if none */\n recommended: number | null;\n /** top hits nobody has labelled yet: label these, then re-run */\n unjudged: { case: string; path: string; score: number }[];\n}\n\nconst key = (c: string, p: string) => `${c}\\u0000${p}`;\n\nexport function evaluateFailures(\n results: CaseResult[],\n judgments: Judgment[],\n opts: { targetPrecision?: number; minJudged?: number; thresholds?: number[] } = {}\n): FailureEvalReport {\n const target = opts.targetPrecision ?? 0.95;\n const minJudged = opts.minJudged ?? 20;\n const thresholds = opts.thresholds ?? Array.from({ length: 17 }, (_, i) => Math.round((0.2 + i * 0.05) * 100) / 100);\n\n const label = new Map<string, boolean>();\n const relevantFor = new Map<string, Set<string>>();\n for (const j of judgments) {\n // a positive label wins over a negative one for the same pair\n label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);\n if (j.relevant) {\n if (!relevantFor.has(j.case)) relevantFor.set(j.case, new Set());\n relevantFor.get(j.case)!.add(j.path);\n }\n }\n\n let answerable = 0, rank1 = 0, recallAtK = 0;\n const unjudged: FailureEvalReport[\"unjudged\"] = [];\n for (const r of results) {\n const relevant = relevantFor.get(r.case);\n if (relevant && relevant.size > 0) {\n answerable++;\n if (r.hits[0] && relevant.has(r.hits[0].path)) rank1++;\n if (r.hits.some((h) => relevant.has(h.path))) recallAtK++;\n }\n const top = r.hits[0];\n if (top && !label.has(key(r.case, top.path))) unjudged.push({ case: r.case, path: top.path, score: top.score });\n }\n\n const rows: ThresholdRow[] = thresholds.map((threshold) => {\n let injected = 0, judged = 0, correct = 0;\n for (const r of results) {\n const top = r.hits[0];\n if (!top || top.score < threshold) continue;\n injected++;\n const l = label.get(key(r.case, top.path));\n if (l === undefined) continue;\n judged++;\n if (l) correct++;\n }\n return {\n threshold,\n injected,\n judged,\n correct,\n precision: judged > 0 ? correct / judged : null,\n coverage: results.length > 0 ? injected / results.length : 0,\n };\n });\n\n // lowest threshold where this row and every higher row with enough judged injections reach the target\n const meets = (row: ThresholdRow) => row.precision !== null && row.precision >= target;\n const recommended =\n rows.find(\n (row, i) => row.judged >= minJudged && meets(row) && rows.slice(i + 1).every((r) => r.judged < minJudged || meets(r))\n )?.threshold ?? null;\n\n return { cases: results.length, answerable, rank1, recallAtK, rows, recommended, unjudged };\n}\n\nexport function formatFailureEval(report: FailureEvalReport, label = \"\"): string {\n const pct = (n: number | null) => (n === null ? \" - \" : `${(n * 100).toFixed(1)}%`.padStart(6));\n const lines = [\n `Failure-lookup eval${label ? ` (${label})` : \"\"} — ${report.cases} cases, ${report.answerable} with a known answer`,\n ` answer ranked first: ${report.rank1}/${report.answerable} answer anywhere in hits: ${report.recallAtK}/${report.answerable}`,\n \"\",\n \" threshold injected judged correct precision coverage\",\n ];\n for (const r of report.rows) {\n lines.push(\n ` ${r.threshold.toFixed(2).padStart(9)} ${String(r.injected).padStart(8)} ${String(r.judged).padStart(6)} ${String(r.correct).padStart(7)} ${pct(r.precision).padStart(9)} ${pct(r.coverage).padStart(8)}`\n );\n }\n lines.push(\"\");\n lines.push(\n report.recommended === null\n ? \" recommended threshold: none reaches the target precision with enough judged injections\"\n : ` recommended threshold: ${report.recommended.toFixed(2)}`\n );\n lines.push(` unjudged top hits: ${report.unjudged.length}`);\n return lines.join(\"\\n\");\n}\n\n// --- v1.7 jev mode: the judge's agreement with labels, and the failure hook's Jev gate ---\n\n/** jev-loop's adoption bar: right >= 95% of the time over >= 40 picks, with at most 2 wrong. */\nexport const meetsBar = (picks: number, wrong: number) => picks >= 40 && wrong <= 2 && (picks - wrong) / picks >= 0.95;\n\nexport interface JevJudgment {\n case: string;\n path: string;\n p: number | null;\n}\n\nexport interface JudgeCalibration {\n /** labelled pairs Jev answered */\n pairs: number;\n /** label relevant / not relevant: picks Jev made (>= bar yes, <= 1-bar no), how many disagree, unsure */\n relevant: { pairs: number; yes: number; no: number; unsure: number };\n notRelevant: { pairs: number; yes: number; no: number; unsure: number };\n /** every pick (yes or no): agreement with the labels */\n picks: number;\n wrong: number;\n agreement: number | null;\n unsureRate: number;\n /** yes picks alone (the side that would inject) */\n yesPicks: number;\n yesWrong: number;\n /** no picks alone (the side that would filter) */\n noPicks: number;\n noWrong: number;\n /**\n * \"jev\": Jev alone may judge (its yes picks and its no picks each meet the bar);\n * \"hybrid\": its no picks meet the bar — Jev filters, the rest (yes and unsure) go to Claude;\n * \"claude\": neither side does.\n */\n verdict: \"jev\" | \"hybrid\" | \"claude\";\n}\n\nexport function calibrateJudge(labels: Judgment[], jev: JevJudgment[], bar = 0.95): JudgeCalibration {\n const label = new Map<string, boolean>();\n for (const j of labels) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);\n const relevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };\n const notRelevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };\n const seen = new Set<string>();\n for (const r of jev) {\n const k = key(r.case, r.path);\n const l = label.get(k);\n if (l === undefined || seen.has(k)) continue;\n seen.add(k);\n const side = l ? relevant : notRelevant;\n side.pairs++;\n if (r.p !== null && r.p >= bar) side.yes++;\n else if (r.p !== null && r.p <= 1 - bar + 1e-9) side.no++;\n else side.unsure++;\n }\n const pairs = relevant.pairs + notRelevant.pairs;\n const picks = relevant.yes + relevant.no + notRelevant.yes + notRelevant.no;\n const wrong = relevant.no + notRelevant.yes;\n return {\n pairs,\n relevant,\n notRelevant,\n picks,\n wrong,\n agreement: picks ? (picks - wrong) / picks : null,\n unsureRate: pairs ? (relevant.unsure + notRelevant.unsure) / pairs : 0,\n yesPicks: relevant.yes + notRelevant.yes,\n yesWrong: notRelevant.yes,\n noPicks: relevant.no + notRelevant.no,\n noWrong: relevant.no,\n // both sides separately: a judge that only ever says no would pass a pooled check and still find nothing\n verdict:\n meetsBar(relevant.yes + notRelevant.yes, notRelevant.yes) && meetsBar(relevant.no + notRelevant.no, relevant.no)\n ? \"jev\"\n : meetsBar(relevant.no + notRelevant.no, relevant.no)\n ? \"hybrid\"\n : \"claude\",\n };\n}\n\nexport interface GateRow {\n threshold: number;\n /** cases with a candidate fact (first fact in the top hits scoring >= threshold) */\n candidates: number;\n /** candidates Jev has not answered yet (ask, then re-run) */\n missing: number;\n /** candidates Jev said yes to at >= the bar: would be injected */\n injected: number;\n judged: number;\n correct: number;\n precision: number | null;\n coverage: number;\n meetsBar: boolean;\n}\n\n/**\n * The failure hook's Jev gate, replayed: per threshold T, the candidate is the first fact among the top\n * `topK` hits scoring >= T. `confirm` injects it only when Jev scores it >= the bar; `veto` injects it unless\n * Jev scores it <= 1 - bar (a confident no). Precision is measured on the injected candidates that carry a label.\n */\nexport function evaluateGate(\n results: CaseResult[],\n judgments: Judgment[],\n jev: JevJudgment[],\n opts: { thresholds?: number[]; topK?: number; bar?: number; isCandidate?: (path: string) => boolean; mode?: \"confirm\" | \"veto\" } = {},\n): { rows: GateRow[]; missing: { case: string; path: string }[] } {\n const mode = opts.mode ?? \"confirm\";\n const thresholds = opts.thresholds ?? [0.3, 0.35, 0.4, 0.5];\n const topK = opts.topK ?? 5;\n const bar = opts.bar ?? 0.95;\n const isCandidate = opts.isCandidate ?? ((p: string) => /(^|\\/)facts\\/[^/]+\\.md$/.test(p));\n const label = new Map<string, boolean>();\n for (const j of judgments) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);\n const score = new Map<string, number | null>();\n for (const r of jev) score.set(key(r.case, r.path), r.p);\n const missing = new Map<string, { case: string; path: string }>();\n const rows = thresholds.map((threshold) => {\n let candidates = 0, miss = 0, injected = 0, judged = 0, correct = 0;\n for (const r of results) {\n const hit = r.hits.slice(0, topK).find((h) => isCandidate(h.path) && h.score >= threshold);\n if (!hit) continue;\n candidates++;\n const k = key(r.case, hit.path);\n if (!score.has(k)) {\n miss++;\n missing.set(k, { case: r.case, path: hit.path });\n continue;\n }\n const p = score.get(k);\n const inject = mode === \"confirm\" ? p !== null && p !== undefined && p >= bar : !(p !== null && p !== undefined && p <= 1 - bar + 1e-9);\n if (!inject) continue;\n injected++;\n const l = label.get(k);\n if (l === undefined) continue;\n judged++;\n if (l) correct++;\n }\n return {\n threshold,\n candidates,\n missing: miss,\n injected,\n judged,\n correct,\n precision: judged ? correct / judged : null,\n coverage: results.length ? injected / results.length : 0,\n meetsBar: meetsBar(judged, judged - correct),\n };\n });\n return { rows, missing: [...missing.values()] };\n}\n\nexport function formatGate(rows: GateRow[], mode: \"confirm\" | \"veto\" = \"confirm\"): string {\n const pct = (n: number | null) => (n === null ? \" - \" : `${(n * 100).toFixed(1)}%`);\n const what = mode === \"confirm\" ? \"only when Jev says yes at >= 0.95\" : \"unless Jev says no at <= 0.05\";\n const lines = [`Jev gate, ${mode} (inject the first fact >= T ${what})`, \"\", \" T candidates missing injected judged correct precision coverage bar\"];\n for (const r of rows) {\n lines.push(\n ` ${r.threshold.toFixed(2)} ${String(r.candidates).padStart(10)} ${String(r.missing).padStart(7)} ${String(r.injected).padStart(8)} ${String(r.judged).padStart(6)} ${String(r.correct).padStart(7)} ${pct(r.precision).padStart(9)} ${pct(r.coverage).padStart(8)} ${r.meetsBar ? \"yes\" : \"no\"}`,\n );\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/core/claude-run.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { accessSync, constants, openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { delimiter, join } from "path";
|
|
6
|
+
var MAX_CLAUDE = 5;
|
|
7
|
+
function claudeConcurrency(env = process.env) {
|
|
8
|
+
const n = Number(env.SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY);
|
|
9
|
+
return Number.isFinite(n) && n >= 1 ? Math.min(MAX_CLAUDE, Math.floor(n)) : MAX_CLAUDE;
|
|
10
|
+
}
|
|
11
|
+
var slotsDir = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_SLOTS_DIR || join(env.HOME || homedir(), ".semantic-memory", "claude-slots");
|
|
12
|
+
var alive = (pid) => {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return e.code === "EPERM";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
async function acquireSlot(opts = {}) {
|
|
21
|
+
const env = opts.env ?? process.env;
|
|
22
|
+
const dir = slotsDir(env);
|
|
23
|
+
const max = claudeConcurrency(env);
|
|
24
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30 * 6e4);
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
for (; ; ) {
|
|
27
|
+
for (let i = 0; i < max; i++) {
|
|
28
|
+
const path = join(dir, `slot-${i}`);
|
|
29
|
+
try {
|
|
30
|
+
const fd = openSync(path, "wx");
|
|
31
|
+
writeSync(fd, `${process.pid}
|
|
32
|
+
`);
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
let released = false;
|
|
35
|
+
return () => {
|
|
36
|
+
if (released) return;
|
|
37
|
+
released = true;
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(path);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e.code !== "EEXIST") throw e;
|
|
45
|
+
try {
|
|
46
|
+
const pid = Number(readFileSync(path, "utf8").trim());
|
|
47
|
+
if (!pid || !alive(pid)) unlinkSync(path);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (Date.now() > deadline) throw new Error(`no claude slot free after ${opts.timeoutMs ?? 18e5} ms (${max} in use)`);
|
|
53
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 250));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var executable = (p) => {
|
|
57
|
+
try {
|
|
58
|
+
accessSync(p, constants.X_OK);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function claudeBin(env = process.env) {
|
|
65
|
+
if (env.SEMANTIC_MEMORY_CLAUDE_BIN) return env.SEMANTIC_MEMORY_CLAUDE_BIN;
|
|
66
|
+
if ((env.PATH ?? "").split(delimiter).some((d) => d && executable(join(d, "claude")))) return "claude";
|
|
67
|
+
const home = env.HOME || homedir();
|
|
68
|
+
return [join(home, ".local", "bin", "claude"), join(home, ".claude", "local", "claude")].find(executable) ?? "claude";
|
|
69
|
+
}
|
|
70
|
+
var skillLoopModel = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_LOOP_MODEL || "sonnet";
|
|
71
|
+
function fencedArgs(o, env = process.env) {
|
|
72
|
+
const args = [
|
|
73
|
+
"-p",
|
|
74
|
+
"--tools",
|
|
75
|
+
o.tools.join(","),
|
|
76
|
+
"--disallowedTools",
|
|
77
|
+
"Task,Agent",
|
|
78
|
+
"--strict-mcp-config",
|
|
79
|
+
"--settings",
|
|
80
|
+
JSON.stringify({ disableAllHooks: true }),
|
|
81
|
+
"--setting-sources",
|
|
82
|
+
"project,local",
|
|
83
|
+
"--permission-mode",
|
|
84
|
+
"dontAsk",
|
|
85
|
+
"--no-session-persistence",
|
|
86
|
+
"--max-turns",
|
|
87
|
+
String(o.maxTurns),
|
|
88
|
+
"--model",
|
|
89
|
+
o.model ?? skillLoopModel(env),
|
|
90
|
+
"--output-format",
|
|
91
|
+
o.output ?? "json"
|
|
92
|
+
];
|
|
93
|
+
if (o.allowedTools?.length) args.push("--allowedTools", o.allowedTools.join(","));
|
|
94
|
+
if ((o.output ?? "json") === "stream-json") args.push("--verbose");
|
|
95
|
+
for (const d of o.addDirs ?? []) args.push("--add-dir", d);
|
|
96
|
+
if (o.appendSystemPrompt) args.push("--append-system-prompt", o.appendSystemPrompt);
|
|
97
|
+
return args;
|
|
98
|
+
}
|
|
99
|
+
async function runClaude(o) {
|
|
100
|
+
const env = { ...o.env ?? process.env, SEMANTIC_MEMORY_REVIEWER: "1" };
|
|
101
|
+
const fail = (error) => ({ ok: false, result: "", events: [], costUsd: 0, turns: 0, error });
|
|
102
|
+
let release;
|
|
103
|
+
try {
|
|
104
|
+
release = await acquireSlot({ env });
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return fail(e.message);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return await new Promise((done) => {
|
|
110
|
+
const child = spawn(claudeBin(env), fencedArgs(o, env), { cwd: o.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
111
|
+
let stdout = "";
|
|
112
|
+
let stderr = "";
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
child.kill("SIGKILL");
|
|
115
|
+
done(fail(`timed out after ${o.timeoutMs ?? 6e5} ms`));
|
|
116
|
+
}, o.timeoutMs ?? 6e5);
|
|
117
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
118
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
119
|
+
child.on("error", (e) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
done(fail(`could not start ${claudeBin(env)}: ${e.message}`));
|
|
122
|
+
});
|
|
123
|
+
child.on("close", (code) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
done(parseClaudeOutput(stdout, o.output ?? "json", code, stderr));
|
|
126
|
+
});
|
|
127
|
+
child.stdin.on("error", () => {
|
|
128
|
+
});
|
|
129
|
+
child.stdin.end(o.prompt);
|
|
130
|
+
});
|
|
131
|
+
} finally {
|
|
132
|
+
release();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function parseClaudeOutput(stdout, output, code, stderr = "") {
|
|
136
|
+
const events = [];
|
|
137
|
+
let final = null;
|
|
138
|
+
if (output === "json") {
|
|
139
|
+
try {
|
|
140
|
+
final = JSON.parse(stdout.trim());
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
for (const line of stdout.split("\n")) {
|
|
145
|
+
if (!line.trim()) continue;
|
|
146
|
+
try {
|
|
147
|
+
const e = JSON.parse(line);
|
|
148
|
+
events.push(e);
|
|
149
|
+
if (e.type === "result") final = e;
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!final) return { ok: false, result: "", events, costUsd: 0, turns: 0, error: `no result from claude (exit ${code}): ${stderr.trim().slice(0, 300)}` };
|
|
155
|
+
return {
|
|
156
|
+
ok: !final.is_error && final.subtype !== "error_during_execution",
|
|
157
|
+
result: typeof final.result === "string" ? final.result : "",
|
|
158
|
+
events,
|
|
159
|
+
costUsd: Number(final.total_cost_usd) || 0,
|
|
160
|
+
turns: Number(final.num_turns) || 0,
|
|
161
|
+
error: final.is_error ? String(final.result ?? final.subtype ?? "error") : final.subtype === "error_max_turns" ? "max turns reached" : void 0
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
var skillsCalled = (events) => events.filter((e) => e.type === "assistant").flatMap((e) => Array.isArray(e.message?.content) ? e.message.content : []).filter((c) => c.type === "tool_use" && c.name === "Skill" && typeof c.input?.skill === "string").map((c) => c.input.skill);
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
runClaude,
|
|
168
|
+
skillsCalled
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=chunk-PMOZKUEM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/claude-run.ts"],"sourcesContent":["/**\n * Every `claude` process the skill loop starts goes through here (v1.7): the reviewer, eval runs, graders,\n * description tuning. Two guarantees:\n *\n * - **At most 5 at once, machine-wide.** A slot file under ~/.semantic-memory/claude-slots/ is held for the\n * life of each run (SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY, 1–5, higher clamps to 5). A slot whose\n * holder died is reclaimed.\n * - **Fenced.** No hooks, no MCP servers (claude.ai connectors included), no user settings, only the tools\n * asked for, never Agent/Task (no nested subagents), nothing outside the working directory (dontAsk),\n * no session file, and SEMANTIC_MEMORY_REVIEWER=1 so any semantic-memory hook that still runs exits at once.\n * Verified flag by flag on Claude Code 2.1.310 (.planning/v17-skill-loop-and-jev-mode/notes.md, Phase 2).\n */\nimport { spawn } from \"node:child_process\";\nimport { accessSync, constants, openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { delimiter, join } from \"node:path\";\n\nexport const MAX_CLAUDE = 5;\n\nexport function claudeConcurrency(env = process.env): number {\n const n = Number(env.SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY);\n return Number.isFinite(n) && n >= 1 ? Math.min(MAX_CLAUDE, Math.floor(n)) : MAX_CLAUDE;\n}\n\nexport const slotsDir = (env = process.env) =>\n env.SEMANTIC_MEMORY_CLAUDE_SLOTS_DIR || join(env.HOME || homedir(), \".semantic-memory\", \"claude-slots\");\n\nconst alive = (pid: number) => {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === \"EPERM\";\n }\n};\n\n/** Take one of the machine-wide slots, waiting for one to free up. Resolves to its release function. */\nexport async function acquireSlot(opts: { env?: NodeJS.ProcessEnv; timeoutMs?: number; pollMs?: number } = {}): Promise<() => void> {\n const env = opts.env ?? process.env;\n const dir = slotsDir(env);\n const max = claudeConcurrency(env);\n const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);\n mkdirSync(dir, { recursive: true });\n for (;;) {\n for (let i = 0; i < max; i++) {\n const path = join(dir, `slot-${i}`);\n try {\n const fd = openSync(path, \"wx\");\n writeSync(fd, `${process.pid}\\n`);\n closeSync(fd);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n try {\n unlinkSync(path);\n } catch {}\n };\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"EEXIST\") throw e;\n // held: reclaim it when its holder is gone (crashed worker, killed run)\n try {\n const pid = Number(readFileSync(path, \"utf8\").trim());\n if (!pid || !alive(pid)) unlinkSync(path);\n } catch {}\n }\n }\n if (Date.now() > deadline) throw new Error(`no claude slot free after ${opts.timeoutMs ?? 1_800_000} ms (${max} in use)`);\n await new Promise((r) => setTimeout(r, opts.pollMs ?? 250));\n }\n}\n\nexport interface ClaudeRunOptions {\n prompt: string;\n cwd: string;\n /** built-in tools the run may use; [] = none */\n tools: string[];\n /** with dontAsk, only these permission rules run un-prompted, e.g. \"Edit\", \"Bash(npm test:*)\"; the rest is denied */\n allowedTools?: string[];\n maxTurns: number;\n model?: string;\n /** \"json\": one result object; \"stream-json\": every event (to see which tools were called) */\n output?: \"json\" | \"stream-json\";\n timeoutMs?: number;\n /** extra directories the run may read */\n addDirs?: string[];\n appendSystemPrompt?: string;\n env?: NodeJS.ProcessEnv;\n}\n\nexport interface ClaudeRunResult {\n ok: boolean;\n /** final assistant text */\n result: string;\n /** stream-json events (empty for json output) */\n events: any[];\n costUsd: number;\n turns: number;\n error?: string;\n}\n\nconst executable = (p: string) => {\n try {\n accessSync(p, constants.X_OK);\n return true;\n } catch {\n return false;\n }\n};\n/** `claude` from PATH, else its usual install places: a worker started from a cron session has cron's PATH. */\nexport function claudeBin(env = process.env): string {\n if (env.SEMANTIC_MEMORY_CLAUDE_BIN) return env.SEMANTIC_MEMORY_CLAUDE_BIN;\n if ((env.PATH ?? \"\").split(delimiter).some((d) => d && executable(join(d, \"claude\")))) return \"claude\";\n const home = env.HOME || homedir();\n return [join(home, \".local\", \"bin\", \"claude\"), join(home, \".claude\", \"local\", \"claude\")].find(executable) ?? \"claude\";\n}\nexport const skillLoopModel = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_LOOP_MODEL || \"sonnet\";\n\n/** The fenced argument list (exported for tests: these flags are the security boundary). */\nexport function fencedArgs(o: Pick<ClaudeRunOptions, \"tools\" | \"allowedTools\" | \"maxTurns\" | \"model\" | \"output\" | \"addDirs\" | \"appendSystemPrompt\">, env = process.env): string[] {\n const args = [\n \"-p\",\n \"--tools\", o.tools.join(\",\"),\n \"--disallowedTools\", \"Task,Agent\",\n \"--strict-mcp-config\",\n \"--settings\", JSON.stringify({ disableAllHooks: true }),\n \"--setting-sources\", \"project,local\",\n \"--permission-mode\", \"dontAsk\",\n \"--no-session-persistence\",\n \"--max-turns\", String(o.maxTurns),\n \"--model\", o.model ?? skillLoopModel(env),\n \"--output-format\", o.output ?? \"json\",\n ];\n if (o.allowedTools?.length) args.push(\"--allowedTools\", o.allowedTools.join(\",\"));\n if ((o.output ?? \"json\") === \"stream-json\") args.push(\"--verbose\");\n for (const d of o.addDirs ?? []) args.push(\"--add-dir\", d);\n if (o.appendSystemPrompt) args.push(\"--append-system-prompt\", o.appendSystemPrompt);\n return args;\n}\n\n/** Run one fenced `claude -p` inside a slot. Never throws: failures come back as `ok: false` with the reason. */\nexport async function runClaude(o: ClaudeRunOptions): Promise<ClaudeRunResult> {\n const env = { ...(o.env ?? process.env), SEMANTIC_MEMORY_REVIEWER: \"1\" };\n const fail = (error: string): ClaudeRunResult => ({ ok: false, result: \"\", events: [], costUsd: 0, turns: 0, error });\n let release: () => void;\n try {\n release = await acquireSlot({ env });\n } catch (e) {\n return fail((e as Error).message);\n }\n try {\n return await new Promise<ClaudeRunResult>((done) => {\n const child = spawn(claudeBin(env), fencedArgs(o, env), { cwd: o.cwd, env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n done(fail(`timed out after ${o.timeoutMs ?? 600_000} ms`));\n }, o.timeoutMs ?? 600_000);\n child.stdout.on(\"data\", (d) => (stdout += d));\n child.stderr.on(\"data\", (d) => (stderr += d));\n child.on(\"error\", (e) => {\n clearTimeout(timer);\n done(fail(`could not start ${claudeBin(env)}: ${e.message}`));\n });\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n done(parseClaudeOutput(stdout, o.output ?? \"json\", code, stderr));\n });\n child.stdin.on(\"error\", () => {});\n child.stdin.end(o.prompt);\n });\n } finally {\n release();\n }\n}\n\nexport function parseClaudeOutput(stdout: string, output: \"json\" | \"stream-json\", code: number | null, stderr = \"\"): ClaudeRunResult {\n const events: any[] = [];\n let final: any = null;\n if (output === \"json\") {\n try {\n final = JSON.parse(stdout.trim());\n } catch {}\n } else {\n for (const line of stdout.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line);\n events.push(e);\n if (e.type === \"result\") final = e;\n } catch {}\n }\n }\n if (!final) return { ok: false, result: \"\", events, costUsd: 0, turns: 0, error: `no result from claude (exit ${code}): ${stderr.trim().slice(0, 300)}` };\n return {\n ok: !final.is_error && final.subtype !== \"error_during_execution\",\n result: typeof final.result === \"string\" ? final.result : \"\",\n events,\n costUsd: Number(final.total_cost_usd) || 0,\n turns: Number(final.num_turns) || 0,\n error: final.is_error ? String(final.result ?? final.subtype ?? \"error\") : final.subtype === \"error_max_turns\" ? \"max turns reached\" : undefined,\n };\n}\n\n/** Names of the skills a stream-json run invoked through the Skill tool, in order. */\nexport const skillsCalled = (events: any[]): string[] =>\n events\n .filter((e) => e.type === \"assistant\")\n .flatMap((e) => (Array.isArray(e.message?.content) ? e.message.content : []))\n .filter((c: any) => c.type === \"tool_use\" && c.name === \"Skill\" && typeof c.input?.skill === \"string\")\n .map((c: any) => c.input.skill);\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redact
|
|
3
|
+
} from "./chunk-H2BES6FU.js";
|
|
4
|
+
|
|
5
|
+
// src/core/skill-evidence.ts
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
var FILES = { usage: "skill-usage.jsonl", failures: "failures.jsonl", corrections: "corrections.jsonl" };
|
|
9
|
+
var stateDir = (repoRoot2) => join(repoRoot2, ".claude", ".semantic-memory");
|
|
10
|
+
var evidencePath = (repoRoot2, kind) => join(stateDir(repoRoot2), FILES[kind]);
|
|
11
|
+
function repoRoot(cwd) {
|
|
12
|
+
for (let d = cwd; ; d = dirname(d)) {
|
|
13
|
+
if (existsSync(join(d, ".git"))) return d;
|
|
14
|
+
if (dirname(d) === d) return cwd;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
var TEXT_FIELDS = ["command", "error", "excerpt", "prompt"];
|
|
18
|
+
var maxBytes = (env = process.env) => (Number(env.SEMANTIC_MEMORY_SKILL_LOOP_EVIDENCE_MB) || 5) * 1024 * 1024;
|
|
19
|
+
function appendEvidence(root, kind, record, env = process.env) {
|
|
20
|
+
const path = evidencePath(root, kind);
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
try {
|
|
23
|
+
if (statSync(path).size > maxBytes(env)) renameSync(path, `${path}.1`);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
const out = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...record };
|
|
27
|
+
for (const k of TEXT_FIELDS) if (typeof out[k] === "string") out[k] = redact(out[k], "broad").text;
|
|
28
|
+
appendFileSync(path, JSON.stringify(out) + "\n");
|
|
29
|
+
}
|
|
30
|
+
function readEvidence(root, kind) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const p of [`${evidencePath(root, kind)}.1`, evidencePath(root, kind)]) {
|
|
33
|
+
let raw = "";
|
|
34
|
+
try {
|
|
35
|
+
raw = readFileSync(p, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const line of raw.split("\n")) {
|
|
40
|
+
if (!line.trim()) continue;
|
|
41
|
+
try {
|
|
42
|
+
out.push(JSON.parse(line));
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
var cap = (s, n) => String(s ?? "").slice(0, n);
|
|
50
|
+
var failureRecord = (input) => ({
|
|
51
|
+
session_id: input.session_id ?? null,
|
|
52
|
+
id: input.tool_use_id ?? null,
|
|
53
|
+
tool: input.tool_name ?? null,
|
|
54
|
+
command: cap(typeof input.tool_input?.command === "string" ? input.tool_input.command : JSON.stringify(input.tool_input ?? {}), 500),
|
|
55
|
+
error: cap(input.error, 1e3)
|
|
56
|
+
});
|
|
57
|
+
var CORRECTION_CUES = [
|
|
58
|
+
["no", /^\s*(no|nope|nah)\s*([,.!:;]|-|$)/i],
|
|
59
|
+
["wrong", /^\s*(wrong|incorrect)\b|\bthat'?s (wrong|not right|incorrect|not what)\b/i],
|
|
60
|
+
["not-what-i", /\bnot what i (asked|meant|wanted|said)\b/i],
|
|
61
|
+
["instead", /\b(use|do|run|try|call)\b[^.?!]{0,40}\binstead\b|^\s*instead\b/i],
|
|
62
|
+
["dont", /^\s*don'?t\b|\bdon'?t (do|use|run|touch|change|add|delete|remove|edit|call|push|commit)\b/i],
|
|
63
|
+
["you-broke", /\byou (broke|forgot|missed|skipped|ignored)\b/i],
|
|
64
|
+
["stop", /^\s*stop\b/i]
|
|
65
|
+
];
|
|
66
|
+
var stripQuoted = (p) => p.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^>.*$/gm, " ");
|
|
67
|
+
function detectCorrection(prompt) {
|
|
68
|
+
const text = stripQuoted(prompt);
|
|
69
|
+
for (const [cue, re] of CORRECTION_CUES) if (re.test(text)) return cue;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function slashSkill(prompt) {
|
|
73
|
+
const m = prompt.match(/^\s*\/([a-z0-9][a-z0-9-]*)(\s|$)/);
|
|
74
|
+
return m ? m[1] : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
stateDir,
|
|
79
|
+
evidencePath,
|
|
80
|
+
repoRoot,
|
|
81
|
+
appendEvidence,
|
|
82
|
+
readEvidence,
|
|
83
|
+
failureRecord,
|
|
84
|
+
detectCorrection,
|
|
85
|
+
slashSkill
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=chunk-R6GNP7DS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-evidence.ts"],"sourcesContent":["/**\n * The skill loop's evidence (v1.7): what the between-session review learns from. Hooks append one line\n * per event when the skill loop is on; nothing here decides anything.\n *\n * .claude/.semantic-memory/skill-usage.jsonl {ts, session_id, skill, via: \"tool\" | \"slash\"}\n * .claude/.semantic-memory/failures.jsonl {ts, session_id, id, tool, command, error}\n * .claude/.semantic-memory/corrections.jsonl {ts, session_id, cue, excerpt}\n *\n * The free-text fields (command, error, excerpt) are redacted (broad: these are logs) before anything is\n * written — ids and names are left alone so records still join up. Each record is capped in size, and each file is\n * rotated to `<file>.1` past SEMANTIC_MEMORY_SKILL_LOOP_EVIDENCE_MB (default 5). Standalone for hooks.\n */\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { redact } from \"./redact.js\";\n\nexport type EvidenceKind = \"usage\" | \"failures\" | \"corrections\";\nconst FILES: Record<EvidenceKind, string> = { usage: \"skill-usage.jsonl\", failures: \"failures.jsonl\", corrections: \"corrections.jsonl\" };\n\nexport const stateDir = (repoRoot: string) => join(repoRoot, \".claude\", \".semantic-memory\");\nexport const evidencePath = (repoRoot: string, kind: EvidenceKind) => join(stateDir(repoRoot), FILES[kind]);\n\n/** The repo a hook's cwd belongs to: the nearest ancestor holding .git, else the cwd itself. */\nexport function repoRoot(cwd: string): string {\n for (let d = cwd; ; d = dirname(d)) {\n if (existsSync(join(d, \".git\"))) return d;\n if (dirname(d) === d) return cwd;\n }\n}\n\n// broad redaction would also mask session and tool-use ids (long mixed tokens): only free text is redacted\nconst TEXT_FIELDS = [\"command\", \"error\", \"excerpt\", \"prompt\"];\nconst maxBytes = (env = process.env) => (Number(env.SEMANTIC_MEMORY_SKILL_LOOP_EVIDENCE_MB) || 5) * 1024 * 1024;\n\nexport function appendEvidence(root: string, kind: EvidenceKind, record: Record<string, unknown>, env = process.env): void {\n const path = evidencePath(root, kind);\n mkdirSync(dirname(path), { recursive: true });\n try {\n if (statSync(path).size > maxBytes(env)) renameSync(path, `${path}.1`);\n } catch {}\n const out: Record<string, unknown> = { ts: new Date().toISOString(), ...record };\n for (const k of TEXT_FIELDS) if (typeof out[k] === \"string\") out[k] = redact(out[k] as string, \"broad\").text;\n appendFileSync(path, JSON.stringify(out) + \"\\n\");\n}\n\nexport function readEvidence<T = Record<string, any>>(root: string, kind: EvidenceKind): T[] {\n const out: T[] = [];\n for (const p of [`${evidencePath(root, kind)}.1`, evidencePath(root, kind)]) {\n let raw = \"\";\n try {\n raw = readFileSync(p, \"utf8\");\n } catch {\n continue;\n }\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n out.push(JSON.parse(line));\n } catch {}\n }\n }\n return out;\n}\n\nconst cap = (s: unknown, n: number) => String(s ?? \"\").slice(0, n);\n\nexport const failureRecord = (input: { session_id?: string; tool_use_id?: string; tool_name?: string; tool_input?: any; error?: string }) => ({\n session_id: input.session_id ?? null,\n id: input.tool_use_id ?? null,\n tool: input.tool_name ?? null,\n command: cap(typeof input.tool_input?.command === \"string\" ? input.tool_input.command : JSON.stringify(input.tool_input ?? {}), 500),\n error: cap(input.error, 1000),\n});\n\n// A correction: the user telling the agent it got something wrong. Only these cues, so ordinary\n// prompts (\"I don't know why…\", \"no rush\") do not count.\nconst CORRECTION_CUES: [string, RegExp][] = [\n [\"no\", /^\\s*(no|nope|nah)\\s*([,.!:;]|-|$)/i],\n [\"wrong\", /^\\s*(wrong|incorrect)\\b|\\bthat'?s (wrong|not right|incorrect|not what)\\b/i],\n [\"not-what-i\", /\\bnot what i (asked|meant|wanted|said)\\b/i],\n [\"instead\", /\\b(use|do|run|try|call)\\b[^.?!]{0,40}\\binstead\\b|^\\s*instead\\b/i],\n [\"dont\", /^\\s*don'?t\\b|\\bdon'?t (do|use|run|touch|change|add|delete|remove|edit|call|push|commit)\\b/i],\n [\"you-broke\", /\\byou (broke|forgot|missed|skipped|ignored)\\b/i],\n [\"stop\", /^\\s*stop\\b/i],\n];\n\n/** Fenced code, inline code and quoted lines are machinery, not the user talking. */\nconst stripQuoted = (p: string) => p.replace(/```[\\s\\S]*?```/g, \" \").replace(/`[^`]*`/g, \" \").replace(/^>.*$/gm, \" \");\n\nexport function detectCorrection(prompt: string): string | null {\n const text = stripQuoted(prompt);\n for (const [cue, re] of CORRECTION_CUES) if (re.test(text)) return cue;\n return null;\n}\n\n/** `/name …` typed by the user: Claude Code runs the skill without a Skill tool call, so PostToolUse never sees it. */\nexport function slashSkill(prompt: string): string | null {\n const m = prompt.match(/^\\s*\\/([a-z0-9][a-z0-9-]*)(\\s|$)/);\n return m ? m[1] : null;\n}\n"],"mappings":"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","names":["repoRoot"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stateDir
|
|
3
|
+
} from "./chunk-R6GNP7DS.js";
|
|
4
|
+
|
|
5
|
+
// src/core/skill-ledger.ts
|
|
6
|
+
import { createHash, randomBytes } from "crypto";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
var ledgerPath = (repo) => join(stateDir(repo), "skill-ledger.jsonl");
|
|
10
|
+
var blobDir = (repo) => join(stateDir(repo), "skill-ledger-blobs");
|
|
11
|
+
var archiveDir = (repo) => join(stateDir(repo), "skills-archive");
|
|
12
|
+
var sha256 = (b) => createHash("sha256").update(b).digest("hex");
|
|
13
|
+
function putBlob(repo, bytes) {
|
|
14
|
+
const h = sha256(bytes);
|
|
15
|
+
const p = join(blobDir(repo), h);
|
|
16
|
+
if (!existsSync(p)) {
|
|
17
|
+
mkdirSync(blobDir(repo), { recursive: true });
|
|
18
|
+
writeFileSync(p, bytes);
|
|
19
|
+
}
|
|
20
|
+
return h;
|
|
21
|
+
}
|
|
22
|
+
var snapshot = (repo, path) => existsSync(path) ? putBlob(repo, readFileSync(path)) : null;
|
|
23
|
+
function applyChange(repo, meta, ops) {
|
|
24
|
+
const files = [];
|
|
25
|
+
for (const op of ops) {
|
|
26
|
+
const before = snapshot(repo, op.path);
|
|
27
|
+
if (op.content === null) {
|
|
28
|
+
if (before) {
|
|
29
|
+
unlinkSync(op.path);
|
|
30
|
+
try {
|
|
31
|
+
rmdirSync(dirname(op.path));
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
mkdirSync(dirname(op.path), { recursive: true });
|
|
37
|
+
writeFileSync(op.path, op.content);
|
|
38
|
+
}
|
|
39
|
+
files.push({ path: op.path, before, after: snapshot(repo, op.path) });
|
|
40
|
+
}
|
|
41
|
+
const entry = { id: `${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`, ts: (/* @__PURE__ */ new Date()).toISOString(), ...meta, files };
|
|
42
|
+
mkdirSync(stateDir(repo), { recursive: true });
|
|
43
|
+
appendFileSync(ledgerPath(repo), JSON.stringify(entry) + "\n");
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
function readLedger(repo) {
|
|
47
|
+
try {
|
|
48
|
+
return readFileSync(ledgerPath(repo), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function rollback(repo, id) {
|
|
54
|
+
const entry = readLedger(repo).find((e) => e.id === id);
|
|
55
|
+
if (!entry) throw new Error(`no ledger entry ${id}`);
|
|
56
|
+
for (const f of entry.files) {
|
|
57
|
+
const now = existsSync(f.path) ? sha256(readFileSync(f.path)) : null;
|
|
58
|
+
if (now !== f.after) throw new Error(`${f.path} changed since ${id}: roll back the later change first`);
|
|
59
|
+
}
|
|
60
|
+
const ops = entry.files.map((f) => ({ path: f.path, content: f.before ? readFileSync(join(blobDir(repo), f.before)) : null }));
|
|
61
|
+
return applyChange(repo, { action: "rollback", actor: "user", skill: entry.skill, why: `rollback of ${entry.action} ${id}`, reverts: id }, ops);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
ledgerPath,
|
|
66
|
+
archiveDir,
|
|
67
|
+
applyChange,
|
|
68
|
+
readLedger,
|
|
69
|
+
rollback
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=chunk-TKHWVISV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-ledger.ts"],"sourcesContent":["/**\n * The skill loop's ledger (v1.7): every change the loop or a person makes to a learned skill — land, patch,\n * stale, archive, restore, promote, merge, reject — as one line with who, why, and each file's bytes before\n * and after (stored once by sha256 in skill-ledger-blobs/). That is what makes a single change reversible:\n * `rollback <id>` puts every file it touched back exactly as it was. Nothing is lost: a file the loop removes\n * (an archive moves a skill out of .claude/skills/) keeps its bytes in the blob store first.\n */\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { stateDir } from \"./skill-evidence.js\";\n\nexport type LedgerAction = \"land\" | \"patch\" | \"stale\" | \"archive\" | \"restore\" | \"promote\" | \"merge\" | \"reject\" | \"rollback\";\n\nexport interface FileChange {\n path: string;\n /** sha256 of the bytes before (null: the file did not exist) */\n before: string | null;\n after: string | null;\n}\n\nexport interface LedgerEntry {\n id: string;\n ts: string;\n action: LedgerAction;\n actor: \"user\" | \"curator\" | \"reviewer\";\n skill: string;\n why: string;\n files: FileChange[];\n /** rollback: the entry it reversed */\n reverts?: string;\n}\n\nexport const ledgerPath = (repo: string) => join(stateDir(repo), \"skill-ledger.jsonl\");\nconst blobDir = (repo: string) => join(stateDir(repo), \"skill-ledger-blobs\");\nexport const archiveDir = (repo: string) => join(stateDir(repo), \"skills-archive\");\nconst sha256 = (b: Buffer | string) => createHash(\"sha256\").update(b).digest(\"hex\");\n\nfunction putBlob(repo: string, bytes: Buffer): string {\n const h = sha256(bytes);\n const p = join(blobDir(repo), h);\n if (!existsSync(p)) {\n mkdirSync(blobDir(repo), { recursive: true });\n writeFileSync(p, bytes);\n }\n return h;\n}\n\nconst snapshot = (repo: string, path: string): string | null => (existsSync(path) ? putBlob(repo, readFileSync(path)) : null);\n\n/**\n * Apply file writes and removals as one ledgered change. `ops`: write `content` to `path`; null content removes\n * the file (its bytes are in the blob store already, so rollback restores them) and its folder if now empty.\n */\nexport function applyChange(\n repo: string,\n meta: { action: LedgerAction; actor: LedgerEntry[\"actor\"]; skill: string; why: string; reverts?: string },\n ops: { path: string; content: string | Buffer | null }[],\n): LedgerEntry {\n const files: FileChange[] = [];\n for (const op of ops) {\n const before = snapshot(repo, op.path);\n if (op.content === null) {\n if (before) {\n unlinkSync(op.path);\n try {\n rmdirSync(dirname(op.path)); // only when empty: a skill folder with nothing left would still be listed\n } catch {}\n }\n } else {\n mkdirSync(dirname(op.path), { recursive: true });\n writeFileSync(op.path, op.content);\n }\n files.push({ path: op.path, before, after: snapshot(repo, op.path) });\n }\n const entry: LedgerEntry = { id: `${Date.now().toString(36)}-${randomBytes(3).toString(\"hex\")}`, ts: new Date().toISOString(), ...meta, files };\n mkdirSync(stateDir(repo), { recursive: true });\n appendFileSync(ledgerPath(repo), JSON.stringify(entry) + \"\\n\");\n return entry;\n}\n\nexport function readLedger(repo: string): LedgerEntry[] {\n try {\n return readFileSync(ledgerPath(repo), \"utf8\").split(\"\\n\").filter(Boolean).map((l) => JSON.parse(l));\n } catch {\n return [];\n }\n}\n\n/** Undo one entry: every file it touched goes back to its exact bytes before (or aside, if it did not exist). */\nexport function rollback(repo: string, id: string): LedgerEntry {\n const entry = readLedger(repo).find((e) => e.id === id);\n if (!entry) throw new Error(`no ledger entry ${id}`);\n for (const f of entry.files) {\n const now = existsSync(f.path) ? sha256(readFileSync(f.path)) : null;\n if (now !== f.after) throw new Error(`${f.path} changed since ${id}: roll back the later change first`);\n }\n const ops = entry.files.map((f) => ({ path: f.path, content: f.before ? readFileSync(join(blobDir(repo), f.before)) : null }));\n return applyChange(repo, { action: \"rollback\", actor: \"user\", skill: entry.skill, why: `rollback of ${entry.action} ${id}`, reverts: id }, ops);\n}\n"],"mappings":";;;;;AAOA,SAAS,YAAY,mBAAmB;AACxC,SAAS,gBAAgB,YAAY,WAAW,cAAc,WAAW,YAAY,qBAAqB;AAC1G,SAAS,SAAS,YAAY;AAwBvB,IAAM,aAAa,CAAC,SAAiB,KAAK,SAAS,IAAI,GAAG,oBAAoB;AACrF,IAAM,UAAU,CAAC,SAAiB,KAAK,SAAS,IAAI,GAAG,oBAAoB;AACpE,IAAM,aAAa,CAAC,SAAiB,KAAK,SAAS,IAAI,GAAG,gBAAgB;AACjF,IAAM,SAAS,CAAC,MAAuB,WAAW,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK;AAElF,SAAS,QAAQ,MAAc,OAAuB;AACpD,QAAM,IAAI,OAAO,KAAK;AACtB,QAAM,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC;AAC/B,MAAI,CAAC,WAAW,CAAC,GAAG;AAClB,cAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,kBAAc,GAAG,KAAK;AAAA,EACxB;AACA,SAAO;AACT;AAEA,IAAM,WAAW,CAAC,MAAc,SAAiC,WAAW,IAAI,IAAI,QAAQ,MAAM,aAAa,IAAI,CAAC,IAAI;AAMjH,SAAS,YACd,MACA,MACA,KACa;AACb,QAAM,QAAsB,CAAC;AAC7B,aAAW,MAAM,KAAK;AACpB,UAAM,SAAS,SAAS,MAAM,GAAG,IAAI;AACrC,QAAI,GAAG,YAAY,MAAM;AACvB,UAAI,QAAQ;AACV,mBAAW,GAAG,IAAI;AAClB,YAAI;AACF,oBAAU,QAAQ,GAAG,IAAI,CAAC;AAAA,QAC5B,QAAQ;AAAA,QAAC;AAAA,MACX;AAAA,IACF,OAAO;AACL,gBAAU,QAAQ,GAAG,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC/C,oBAAc,GAAG,MAAM,GAAG,OAAO;AAAA,IACnC;AACA,UAAM,KAAK,EAAE,MAAM,GAAG,MAAM,QAAQ,OAAO,SAAS,MAAM,GAAG,IAAI,EAAE,CAAC;AAAA,EACtE;AACA,QAAM,QAAqB,EAAE,IAAI,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC,IAAI,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,GAAG,MAAM,MAAM;AAC9I,YAAU,SAAS,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7C,iBAAe,WAAW,IAAI,GAAG,KAAK,UAAU,KAAK,IAAI,IAAI;AAC7D,SAAO;AACT;AAEO,SAAS,WAAW,MAA6B;AACtD,MAAI;AACF,WAAO,aAAa,WAAW,IAAI,GAAG,MAAM,EAAE,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;AAAA,EACpG,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAGO,SAAS,SAAS,MAAc,IAAyB;AAC9D,QAAM,QAAQ,WAAW,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AACtD,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mBAAmB,EAAE,EAAE;AACnD,aAAW,KAAK,MAAM,OAAO;AAC3B,UAAM,MAAM,WAAW,EAAE,IAAI,IAAI,OAAO,aAAa,EAAE,IAAI,CAAC,IAAI;AAChE,QAAI,QAAQ,EAAE,MAAO,OAAM,IAAI,MAAM,GAAG,EAAE,IAAI,kBAAkB,EAAE,oCAAoC;AAAA,EACxG;AACA,QAAM,MAAM,MAAM,MAAM,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,SAAS,aAAa,KAAK,QAAQ,IAAI,GAAG,EAAE,MAAM,CAAC,IAAI,KAAK,EAAE;AAC7H,SAAO,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,QAAQ,OAAO,MAAM,OAAO,KAAK,eAAe,MAAM,MAAM,IAAI,EAAE,IAAI,SAAS,GAAG,GAAG,GAAG;AAChJ;","names":[]}
|