@theglitchking/semantic-memory 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +61 -0
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +19 -2
  6. package/THIRD_PARTY_NOTICES.md +17 -0
  7. package/commands/jev-mode.md +18 -0
  8. package/commands/skill-loop.md +23 -0
  9. package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
  10. package/dist/chunk-3FPUBAEW.js +113 -0
  11. package/dist/chunk-3FPUBAEW.js.map +1 -0
  12. package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
  13. package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
  14. package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
  15. package/dist/chunk-BEACL25M.js +224 -0
  16. package/dist/chunk-BEACL25M.js.map +1 -0
  17. package/dist/chunk-CATPGINC.js +186 -0
  18. package/dist/chunk-CATPGINC.js.map +1 -0
  19. package/dist/chunk-DBFD3MTT.js +130 -0
  20. package/dist/chunk-DBFD3MTT.js.map +1 -0
  21. package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
  22. package/dist/chunk-GYR7U6KA.js +80 -0
  23. package/dist/chunk-GYR7U6KA.js.map +1 -0
  24. package/dist/chunk-IEZVFKUT.js +157 -0
  25. package/dist/chunk-IEZVFKUT.js.map +1 -0
  26. package/dist/chunk-MQS7JEBD.js +137 -0
  27. package/dist/chunk-MQS7JEBD.js.map +1 -0
  28. package/dist/chunk-NCGM36NX.js +118 -0
  29. package/dist/chunk-NCGM36NX.js.map +1 -0
  30. package/dist/chunk-NIZLBNH4.js +165 -0
  31. package/dist/chunk-NIZLBNH4.js.map +1 -0
  32. package/dist/chunk-OT3MV7XF.js +123 -0
  33. package/dist/chunk-OT3MV7XF.js.map +1 -0
  34. package/dist/chunk-P3GLY42T.js +291 -0
  35. package/dist/chunk-P3GLY42T.js.map +1 -0
  36. package/dist/chunk-PJXYCCI2.js +177 -0
  37. package/dist/chunk-PJXYCCI2.js.map +1 -0
  38. package/dist/chunk-R6GNP7DS.js +87 -0
  39. package/dist/chunk-R6GNP7DS.js.map +1 -0
  40. package/dist/chunk-TKHWVISV.js +71 -0
  41. package/dist/chunk-TKHWVISV.js.map +1 -0
  42. package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
  43. package/dist/chunk-VBLCUWT4.js.map +1 -0
  44. package/dist/chunk-VQCNWRTL.js +121 -0
  45. package/dist/chunk-VQCNWRTL.js.map +1 -0
  46. package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
  47. package/dist/chunk-Y7H76Q7F.js.map +1 -0
  48. package/dist/chunk-ZSWI3Y4Y.js +232 -0
  49. package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
  50. package/dist/cli/index.js +381 -52
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/core/features.d.ts +38 -0
  53. package/dist/core/features.js +28 -0
  54. package/dist/core/index.d.ts +285 -2
  55. package/dist/core/index.js +170 -21
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/jev.d.ts +35 -0
  58. package/dist/core/jev.js +20 -0
  59. package/dist/core/skill-curator.d.ts +1 -0
  60. package/dist/core/skill-curator.js +41 -0
  61. package/dist/core/skill-evidence.d.ts +25 -0
  62. package/dist/core/skill-evidence.js +23 -0
  63. package/dist/core/skill-queue.d.ts +40 -0
  64. package/dist/core/skill-queue.js +38 -0
  65. package/dist/core/skill-review.d.ts +3 -0
  66. package/dist/core/skill-review.js +29 -0
  67. package/dist/core/skill-worker.d.ts +21 -0
  68. package/dist/core/skill-worker.js +70 -0
  69. package/dist/core/skill-worker.js.map +1 -0
  70. package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
  71. package/dist/failure-eval-IUJO47UC.js +18 -0
  72. package/dist/failure-eval-IUJO47UC.js.map +1 -0
  73. package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
  74. package/dist/healthcheck-S7D6YNTA.js.map +1 -0
  75. package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
  76. package/dist/lexicon-SEJSLBHO.js.map +1 -0
  77. package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
  78. package/dist/lint-3JNLRM5U.js.map +1 -0
  79. package/dist/mcp/server.js +19 -17
  80. package/dist/mcp/server.js.map +1 -1
  81. package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
  82. package/dist/profile-SNULTJ27.js.map +1 -0
  83. package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
  84. package/dist/schema-RDYUK6CC.js.map +1 -0
  85. package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
  86. package/dist/skill-land-XU2APPBJ.js +31 -0
  87. package/dist/skill-land-XU2APPBJ.js.map +1 -0
  88. package/dist/skill-ledger-AYXVNFX7.js +18 -0
  89. package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
  90. package/dist/skill-merge-GXBQLBZA.js +24 -0
  91. package/dist/skill-merge-GXBQLBZA.js.map +1 -0
  92. package/dist/skill-review-BaW5n0Zg.d.ts +179 -0
  93. package/dist/skill-schema-RVB3EHEF.js +30 -0
  94. package/dist/skill-schema-RVB3EHEF.js.map +1 -0
  95. package/hooks/hooks.json +26 -1
  96. package/hooks/jev-bridge.py +109 -0
  97. package/hooks/session-start.js +5 -0
  98. package/hooks/skill-loop.js +65 -0
  99. package/hooks/vault-context.js +104 -6
  100. package/package.json +4 -2
  101. package/dist/chunk-5F5ZIAW2.js +0 -79
  102. package/dist/chunk-5F5ZIAW2.js.map +0 -1
  103. package/dist/chunk-GYBUBJYM.js.map +0 -1
  104. package/dist/chunk-HUG7E25D.js.map +0 -1
  105. package/dist/failure-eval-37JRNTIW.js +0 -10
  106. /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
  107. /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
  108. /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
  109. /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
  110. /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
  111. /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
  112. /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
  113. /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
  114. /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
  115. /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
  116. /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
  117. /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
@@ -0,0 +1,165 @@
1
+ import {
2
+ readEvidence,
3
+ stateDir
4
+ } from "./chunk-R6GNP7DS.js";
5
+
6
+ // src/core/skill-queue.ts
7
+ import { appendFileSync, existsSync, mkdirSync, openSync, writeSync, closeSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, statSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ var smHome = (env = process.env) => env.SEMANTIC_MEMORY_STATE_HOME || join(env.HOME || homedir(), ".semantic-memory");
11
+ var queueDir = (env = process.env) => join(smHome(env), "review-queue");
12
+ var runsPath = (env = process.env) => join(smHome(env), "skill-loop-runs.jsonl");
13
+ var lockPath = (repo) => join(stateDir(repo), "skill-loop.lock");
14
+ var logPath = (repo) => join(stateDir(repo), "skill-loop.log");
15
+ var dailyRunCap = (env = process.env) => Math.max(0, Number(env.SEMANTIC_MEMORY_SKILL_LOOP_DAILY_RUNS ?? 10) || 0);
16
+ var minToolCalls = (env = process.env) => Number(env.SEMANTIC_MEMORY_SKILL_LOOP_MIN_TOOL_CALLS) || 20;
17
+ var TRANSCRIPT_SCAN_MAX = 50 * 1024 * 1024;
18
+ var safeName = (s) => s.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
19
+ function countToolCalls(transcript) {
20
+ if (!transcript) return 0;
21
+ try {
22
+ if (statSync(transcript).size > TRANSCRIPT_SCAN_MAX) return 0;
23
+ return readFileSync(transcript, "utf8").split('"type":"tool_use"').length - 1;
24
+ } catch {
25
+ return 0;
26
+ }
27
+ }
28
+ function sessionJob(repo, input, env = process.env) {
29
+ const session_id = typeof input.session_id === "string" ? input.session_id : "";
30
+ if (!session_id) return null;
31
+ const failures = readEvidence(repo, "failures").filter((f) => f.session_id === session_id);
32
+ const corrections = readEvidence(repo, "corrections").filter((c) => c.session_id === session_id).length;
33
+ const skills_used = [...new Set(readEvidence(repo, "usage").filter((u) => u.session_id === session_id).map((u) => String(u.skill)))];
34
+ const transcript_path = typeof input.transcript_path === "string" ? input.transcript_path : null;
35
+ const tool_calls = failures.length || corrections ? 0 : countToolCalls(transcript_path);
36
+ const why = failures.length ? `${failures.length} failure(s)` : corrections ? `${corrections} correction(s)` : tool_calls >= minToolCalls(env) ? `${tool_calls} tool calls` : "";
37
+ if (!why) return null;
38
+ return {
39
+ session_id,
40
+ repo,
41
+ transcript_path,
42
+ reason: `${why}; ended: ${input.reason ?? "unknown"}`,
43
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
44
+ evidence: { failures: failures.map((f) => String(f.id ?? "")).filter(Boolean), corrections, skills_used, tool_calls }
45
+ };
46
+ }
47
+ function enqueue(job, env = process.env) {
48
+ mkdirSync(queueDir(env), { recursive: true });
49
+ const path = join(queueDir(env), `${safeName(job.session_id)}.json`);
50
+ if (["done", "failed"].some((d) => existsSync(join(queueDir(env), d, `${safeName(job.session_id)}.json`)))) return path;
51
+ writeFileSync(path, JSON.stringify(job, null, 1));
52
+ return path;
53
+ }
54
+ function pendingJobs(repo, env = process.env) {
55
+ let files = [];
56
+ try {
57
+ files = readdirSync(queueDir(env)).filter((f) => f.endsWith(".json"));
58
+ } catch {
59
+ return [];
60
+ }
61
+ const jobs = [];
62
+ for (const f of files) {
63
+ try {
64
+ const path = join(queueDir(env), f);
65
+ const job = JSON.parse(readFileSync(path, "utf8"));
66
+ if (job.repo === repo) jobs.push({ path, job });
67
+ } catch {
68
+ }
69
+ }
70
+ return jobs.sort((a, b) => a.job.ts.localeCompare(b.job.ts));
71
+ }
72
+ function finishJob(path, outcome, result, env = process.env) {
73
+ const dir = join(queueDir(env), outcome);
74
+ mkdirSync(dir, { recursive: true });
75
+ const job = JSON.parse(readFileSync(path, "utf8"));
76
+ writeFileSync(path, JSON.stringify({ ...job, finished: (/* @__PURE__ */ new Date()).toISOString(), result }, null, 1));
77
+ renameSync(path, join(dir, path.split("/").pop()));
78
+ }
79
+ function bumpAttempts(path) {
80
+ const job = JSON.parse(readFileSync(path, "utf8"));
81
+ job.attempts = (job.attempts ?? 0) + 1;
82
+ writeFileSync(path, JSON.stringify(job, null, 1));
83
+ return job.attempts;
84
+ }
85
+ var localDay = (d = /* @__PURE__ */ new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
86
+ function runsToday(env = process.env, now = /* @__PURE__ */ new Date()) {
87
+ try {
88
+ const day = localDay(now);
89
+ return readFileSync(runsPath(env), "utf8").split("\n").filter((l) => l.includes(`"day":"${day}"`)).length;
90
+ } catch {
91
+ return 0;
92
+ }
93
+ }
94
+ function recordRun(repo, session_id, env = process.env, now = /* @__PURE__ */ new Date()) {
95
+ mkdirSync(smHome(env), { recursive: true });
96
+ appendFileSync(runsPath(env), JSON.stringify({ day: localDay(now), ts: now.toISOString(), repo, session_id }) + "\n");
97
+ }
98
+ var alive = (pid) => {
99
+ try {
100
+ process.kill(pid, 0);
101
+ return true;
102
+ } catch (e) {
103
+ return e.code === "EPERM";
104
+ }
105
+ };
106
+ function lockHolder(repo) {
107
+ try {
108
+ const pid = Number(readFileSync(lockPath(repo), "utf8").trim());
109
+ return pid && alive(pid) ? pid : null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ function takeLock(repo) {
115
+ mkdirSync(stateDir(repo), { recursive: true });
116
+ for (let attempt = 0; attempt < 2; attempt++) {
117
+ try {
118
+ const fd = openSync(lockPath(repo), "wx");
119
+ writeSync(fd, `${process.pid}
120
+ `);
121
+ closeSync(fd);
122
+ return () => {
123
+ try {
124
+ if (Number(readFileSync(lockPath(repo), "utf8").trim()) === process.pid) unlinkSync(lockPath(repo));
125
+ } catch {
126
+ }
127
+ };
128
+ } catch (e) {
129
+ if (e.code !== "EEXIST") throw e;
130
+ if (lockHolder(repo) !== null) return null;
131
+ try {
132
+ unlinkSync(lockPath(repo));
133
+ } catch {
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ function log(repo, line) {
140
+ try {
141
+ mkdirSync(stateDir(repo), { recursive: true });
142
+ appendFileSync(logPath(repo), `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
143
+ `);
144
+ } catch {
145
+ }
146
+ }
147
+
148
+ export {
149
+ smHome,
150
+ queueDir,
151
+ lockPath,
152
+ logPath,
153
+ dailyRunCap,
154
+ sessionJob,
155
+ enqueue,
156
+ pendingJobs,
157
+ finishJob,
158
+ bumpAttempts,
159
+ runsToday,
160
+ recordRun,
161
+ lockHolder,
162
+ takeLock,
163
+ log
164
+ };
165
+ //# sourceMappingURL=chunk-NIZLBNH4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/skill-queue.ts"],"sourcesContent":["/**\n * The skill loop's review queue (v1.7). SessionEnd writes one job per session that has signal and starts\n * the worker detached; the worker drains this repo's jobs oldest first, inside a global daily cap. The\n * session never waits: SessionEnd hooks get 1.5 s by default, so the hook only writes and spawns.\n *\n * ~/.semantic-memory/review-queue/<session>.json pending jobs (all repos)\n * ~/.semantic-memory/review-queue/{done,failed}/ finished jobs, never re-reviewed\n * ~/.semantic-memory/skill-loop-runs.jsonl one line per review run (the daily cap)\n * <repo>/.claude/.semantic-memory/skill-loop.lock the repo's worker (pid)\n * <repo>/.claude/.semantic-memory/skill-loop.log what the worker did\n *\n * Standalone for hooks (no embedder).\n */\nimport { appendFileSync, existsSync, mkdirSync, openSync, writeSync, closeSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\n\nexport interface ReviewJob {\n session_id: string;\n repo: string;\n transcript_path: string | null;\n reason: string;\n ts: string;\n evidence: { failures: string[]; corrections: number; skills_used: string[]; tool_calls: number };\n attempts?: number;\n}\n\nexport const smHome = (env = process.env) => env.SEMANTIC_MEMORY_STATE_HOME || join(env.HOME || homedir(), \".semantic-memory\");\nexport const queueDir = (env = process.env) => join(smHome(env), \"review-queue\");\nconst runsPath = (env = process.env) => join(smHome(env), \"skill-loop-runs.jsonl\");\nexport const lockPath = (repo: string) => join(stateDir(repo), \"skill-loop.lock\");\nexport const logPath = (repo: string) => join(stateDir(repo), \"skill-loop.log\");\n\nexport const dailyRunCap = (env = process.env) => Math.max(0, Number(env.SEMANTIC_MEMORY_SKILL_LOOP_DAILY_RUNS ?? 10) || 0);\nconst minToolCalls = (env = process.env) => Number(env.SEMANTIC_MEMORY_SKILL_LOOP_MIN_TOOL_CALLS) || 20;\n// shortcut: counts tool calls by scanning the transcript text; above this size the count is skipped (signal then\n// needs a failure or a correction). Upgrade if long tool-only sessions should qualify.\nconst TRANSCRIPT_SCAN_MAX = 50 * 1024 * 1024;\n\nconst safeName = (s: string) => s.replace(/[^A-Za-z0-9_.-]/g, \"_\").slice(0, 120);\n\nfunction countToolCalls(transcript: string | null): number {\n if (!transcript) return 0;\n try {\n if (statSync(transcript).size > TRANSCRIPT_SCAN_MAX) return 0;\n return readFileSync(transcript, \"utf8\").split('\"type\":\"tool_use\"').length - 1;\n } catch {\n return 0;\n }\n}\n\n/** The job SessionEnd would write for this session, or null when the session has nothing to learn from. */\nexport function sessionJob(repo: string, input: { session_id?: string; transcript_path?: string; reason?: string }, env = process.env): ReviewJob | null {\n const session_id = typeof input.session_id === \"string\" ? input.session_id : \"\";\n if (!session_id) return null;\n const failures = readEvidence(repo, \"failures\").filter((f) => f.session_id === session_id);\n const corrections = readEvidence(repo, \"corrections\").filter((c) => c.session_id === session_id).length;\n const skills_used = [...new Set(readEvidence(repo, \"usage\").filter((u) => u.session_id === session_id).map((u) => String(u.skill)))];\n const transcript_path = typeof input.transcript_path === \"string\" ? input.transcript_path : null;\n const tool_calls = failures.length || corrections ? 0 : countToolCalls(transcript_path);\n const why = failures.length ? `${failures.length} failure(s)` : corrections ? `${corrections} correction(s)` : tool_calls >= minToolCalls(env) ? `${tool_calls} tool calls` : \"\";\n if (!why) return null;\n return {\n session_id,\n repo,\n transcript_path,\n reason: `${why}; ended: ${input.reason ?? \"unknown\"}`,\n ts: new Date().toISOString(),\n evidence: { failures: failures.map((f) => String(f.id ?? \"\")).filter(Boolean), corrections, skills_used, tool_calls },\n };\n}\n\nexport function enqueue(job: ReviewJob, env = process.env): string {\n mkdirSync(queueDir(env), { recursive: true });\n const path = join(queueDir(env), `${safeName(job.session_id)}.json`);\n // a session that ends twice (e.g. /clear then exit) is one job, and one already reviewed stays done\n if ([\"done\", \"failed\"].some((d) => existsSync(join(queueDir(env), d, `${safeName(job.session_id)}.json`)))) return path;\n writeFileSync(path, JSON.stringify(job, null, 1));\n return path;\n}\n\nexport function pendingJobs(repo: string, env = process.env): { path: string; job: ReviewJob }[] {\n let files: string[] = [];\n try {\n files = readdirSync(queueDir(env)).filter((f) => f.endsWith(\".json\"));\n } catch {\n return [];\n }\n const jobs: { path: string; job: ReviewJob }[] = [];\n for (const f of files) {\n try {\n const path = join(queueDir(env), f);\n const job = JSON.parse(readFileSync(path, \"utf8\")) as ReviewJob;\n if (job.repo === repo) jobs.push({ path, job });\n } catch {}\n }\n return jobs.sort((a, b) => a.job.ts.localeCompare(b.job.ts));\n}\n\nexport function finishJob(path: string, outcome: \"done\" | \"failed\", result: Record<string, unknown>, env = process.env): void {\n const dir = join(queueDir(env), outcome);\n mkdirSync(dir, { recursive: true });\n const job = JSON.parse(readFileSync(path, \"utf8\"));\n writeFileSync(path, JSON.stringify({ ...job, finished: new Date().toISOString(), result }, null, 1));\n renameSync(path, join(dir, path.split(\"/\").pop()!));\n}\n\nexport function bumpAttempts(path: string): number {\n const job = JSON.parse(readFileSync(path, \"utf8\")) as ReviewJob;\n job.attempts = (job.attempts ?? 0) + 1;\n writeFileSync(path, JSON.stringify(job, null, 1));\n return job.attempts;\n}\n\nconst localDay = (d = new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n\nexport function runsToday(env = process.env, now = new Date()): number {\n try {\n const day = localDay(now);\n return readFileSync(runsPath(env), \"utf8\").split(\"\\n\").filter((l) => l.includes(`\"day\":\"${day}\"`)).length;\n } catch {\n return 0;\n }\n}\n\nexport function recordRun(repo: string, session_id: string, env = process.env, now = new Date()): void {\n mkdirSync(smHome(env), { recursive: true });\n appendFileSync(runsPath(env), JSON.stringify({ day: localDay(now), ts: now.toISOString(), repo, session_id }) + \"\\n\");\n}\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\nexport function lockHolder(repo: string): number | null {\n try {\n const pid = Number(readFileSync(lockPath(repo), \"utf8\").trim());\n return pid && alive(pid) ? pid : null;\n } catch {\n return null;\n }\n}\n\n/** Take the repo's worker lock (reclaiming a dead holder's). Returns a release function, or null if held. */\nexport function takeLock(repo: string): (() => void) | null {\n mkdirSync(stateDir(repo), { recursive: true });\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const fd = openSync(lockPath(repo), \"wx\");\n writeSync(fd, `${process.pid}\\n`);\n closeSync(fd);\n return () => {\n try {\n if (Number(readFileSync(lockPath(repo), \"utf8\").trim()) === process.pid) unlinkSync(lockPath(repo));\n } catch {}\n };\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"EEXIST\") throw e;\n if (lockHolder(repo) !== null) return null;\n try {\n unlinkSync(lockPath(repo));\n } catch {}\n }\n }\n return null;\n}\n\nexport function log(repo: string, line: string): void {\n try {\n mkdirSync(stateDir(repo), { recursive: true });\n appendFileSync(logPath(repo), `${new Date().toISOString()} ${line}\\n`);\n } catch {}\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,123 @@
1
+ import {
2
+ runTrigger
3
+ } from "./chunk-MQS7JEBD.js";
4
+ import {
5
+ runClaude
6
+ } from "./chunk-IEZVFKUT.js";
7
+ import {
8
+ parseSkill
9
+ } from "./chunk-ZSWI3Y4Y.js";
10
+ import {
11
+ matter_default
12
+ } from "./chunk-J7LYDRML.js";
13
+
14
+ // src/core/skill-eval/prompts/improve-description.ts
15
+ var DESCRIPTION_LIMIT = 1024;
16
+ var list = (rs) => rs.map((r) => ` - "${r.text}" (triggered ${r.triggers}/${r.runs} times)
17
+ `).join("");
18
+ function improveDescriptionPrompt(a) {
19
+ let p = `You are optimizing a skill description for a Claude Code skill called "${a.name}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details.
20
+
21
+ The description appears in Claude's "available_skills" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.
22
+
23
+ Here's the current description:
24
+ <current_description>
25
+ "${a.description}"
26
+ </current_description>
27
+
28
+ Current scores (train ${a.score}):
29
+ <scores_summary>
30
+ `;
31
+ if (a.missed.length) p += `FAILED TO TRIGGER (should have triggered but didn't):
32
+ ${list(a.missed)}
33
+ `;
34
+ if (a.falseTriggers.length) p += `FALSE TRIGGERS (triggered but shouldn't have):
35
+ ${list(a.falseTriggers)}
36
+ `;
37
+ if (a.history.length) {
38
+ p += "PREVIOUS ATTEMPTS (do NOT repeat these -- try something structurally different):\n\n";
39
+ for (const h of a.history) p += `<attempt train=${h.score}>
40
+ Description: "${h.description}"
41
+ </attempt>
42
+
43
+ `;
44
+ }
45
+ return p + `</scores_summary>
46
+
47
+ Skill content (for context on what the skill does):
48
+ <skill_content>
49
+ ${a.skillContent}
50
+ </skill_content>
51
+
52
+ Based on the failures, write a new and improved description that is more likely to trigger correctly. Do not overfit: don't produce an ever-expanding list of specific queries. Generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The description is injected into ALL queries, so keep it to about 100-200 words. There is a hard limit of ${DESCRIPTION_LIMIT} characters -- stay comfortably under it.
53
+
54
+ Tips:
55
+ - Phrase it in the imperative -- "Use this skill for" rather than "this skill does".
56
+ - Focus on the user's intent, what they are trying to achieve, vs. the implementation details.
57
+ - The description competes with other skills for Claude's attention -- make it distinctive and immediately recognizable.
58
+ - If you're getting lots of failures after repeated attempts, change things up: different sentence structures or wordings.
59
+
60
+ Please respond with only the new description text in <new_description> tags, nothing else.`;
61
+ }
62
+ var shortenPrompt = (base, tooLong) => `${base}
63
+
64
+ ---
65
+
66
+ A previous attempt produced this description, which at ${tooLong.length} characters is over the ${DESCRIPTION_LIMIT}-character hard limit:
67
+
68
+ "${tooLong}"
69
+
70
+ Rewrite it to be under ${DESCRIPTION_LIMIT} characters while keeping the most important trigger words and intent coverage. Respond with only the new description in <new_description> tags.`;
71
+ var parseNewDescription = (text) => (text.match(/<new_description>([\s\S]*?)<\/new_description>/)?.[1] ?? text).trim().replace(/^"|"$/g, "").trim();
72
+
73
+ // src/core/skill-eval/tune.ts
74
+ var MAX_TUNE_ITERATIONS = 3;
75
+ function withDescription(raw, description) {
76
+ const p = matter_default(raw);
77
+ return matter_default.stringify(p.content, { ...p.data, description });
78
+ }
79
+ var score = (r) => r.train.accuracy;
80
+ var frac = (r) => `${r.results.filter((x) => x.split === "train" && x.triggered === x.should_trigger).length}/${r.train.n}`;
81
+ async function tuneDescription(o) {
82
+ const skill = parseSkill(o.skillRaw);
83
+ const tr = (raw) => runTrigger(o.prompts, { repoRoot: o.repoRoot, skillRaw: raw, env: o.env });
84
+ const cand = (description, r) => ({ description, train: score(r), heldout: r.heldout.accuracy });
85
+ let last = await tr(o.skillRaw);
86
+ const original = cand(skill.description, last);
87
+ const candidates = [original];
88
+ const history = [];
89
+ for (let i = 0; i < Math.min(o.maxIterations ?? MAX_TUNE_ITERATIONS, MAX_TUNE_ITERATIONS); i++) {
90
+ const prev = candidates.at(-1);
91
+ history.push({ description: prev.description, score: frac(last) });
92
+ const train = last.results.filter((r2) => r2.split === "train");
93
+ const prompt = improveDescriptionPrompt({
94
+ name: skill.name,
95
+ skillContent: skill.body,
96
+ description: prev.description,
97
+ score: frac(last),
98
+ missed: train.filter((r2) => r2.should_trigger && !r2.triggered),
99
+ falseTriggers: train.filter((r2) => !r2.should_trigger && r2.triggered),
100
+ history: history.slice(0, -1)
101
+ });
102
+ const env = o.env;
103
+ let r = await runClaude({ prompt, cwd: o.repoRoot, tools: [], maxTurns: 1, env });
104
+ if (!r.ok) break;
105
+ let description = parseNewDescription(r.result);
106
+ if (description.length > DESCRIPTION_LIMIT) {
107
+ r = await runClaude({ prompt: shortenPrompt(prompt, description), cwd: o.repoRoot, tools: [], maxTurns: 1, env });
108
+ if (r.ok) description = parseNewDescription(r.result);
109
+ }
110
+ if (!description || description.length > DESCRIPTION_LIMIT || candidates.some((c) => c.description === description)) continue;
111
+ last = await tr(withDescription(o.skillRaw, description));
112
+ candidates.push(cand(description, last));
113
+ }
114
+ const best = candidates.reduce((b, c) => (c.heldout ?? -1) > (b.heldout ?? -1) || (c.heldout ?? -1) === (b.heldout ?? -1) && (c.train ?? -1) > (b.train ?? -1) ? c : b);
115
+ return { original, best, improved: best !== original, candidates };
116
+ }
117
+
118
+ export {
119
+ MAX_TUNE_ITERATIONS,
120
+ withDescription,
121
+ tuneDescription
122
+ };
123
+ //# sourceMappingURL=chunk-OT3MV7XF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/skill-eval/prompts/improve-description.ts","../src/core/skill-eval/tune.ts"],"sourcesContent":["/**\n * Adapted from Anthropic's skill-creator (https://github.com/anthropics/claude-plugins-official, Apache License 2.0); modified for semantic-memory.\n */\n\nexport interface DescriptionAttempt {\n description: string;\n /** \"passed/total\" on the train prompts */\n score: string;\n}\n\nexport interface PromptResult {\n text: string;\n triggers: number;\n runs: number;\n}\n\nexport const DESCRIPTION_LIMIT = 1024;\n\nconst list = (rs: PromptResult[]) => rs.map((r) => ` - \"${r.text}\" (triggered ${r.triggers}/${r.runs} times)\\n`).join(\"\");\n\n/** Ask for a better description from the train failures only (held-out results never reach the proposer). */\nexport function improveDescriptionPrompt(a: {\n name: string;\n skillContent: string;\n description: string;\n score: string;\n missed: PromptResult[];\n falseTriggers: PromptResult[];\n history: DescriptionAttempt[];\n}): string {\n let p = `You are optimizing a skill description for a Claude Code skill called \"${a.name}\". A \"skill\" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details.\n\nThe description appears in Claude's \"available_skills\" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.\n\nHere's the current description:\n<current_description>\n\"${a.description}\"\n</current_description>\n\nCurrent scores (train ${a.score}):\n<scores_summary>\n`;\n if (a.missed.length) p += `FAILED TO TRIGGER (should have triggered but didn't):\\n${list(a.missed)}\\n`;\n if (a.falseTriggers.length) p += `FALSE TRIGGERS (triggered but shouldn't have):\\n${list(a.falseTriggers)}\\n`;\n if (a.history.length) {\n p += \"PREVIOUS ATTEMPTS (do NOT repeat these -- try something structurally different):\\n\\n\";\n for (const h of a.history) p += `<attempt train=${h.score}>\\nDescription: \"${h.description}\"\\n</attempt>\\n\\n`;\n }\n return (\n p +\n `</scores_summary>\n\nSkill content (for context on what the skill does):\n<skill_content>\n${a.skillContent}\n</skill_content>\n\nBased on the failures, write a new and improved description that is more likely to trigger correctly. Do not overfit: don't produce an ever-expanding list of specific queries. Generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The description is injected into ALL queries, so keep it to about 100-200 words. There is a hard limit of ${DESCRIPTION_LIMIT} characters -- stay comfortably under it.\n\nTips:\n- Phrase it in the imperative -- \"Use this skill for\" rather than \"this skill does\".\n- Focus on the user's intent, what they are trying to achieve, vs. the implementation details.\n- The description competes with other skills for Claude's attention -- make it distinctive and immediately recognizable.\n- If you're getting lots of failures after repeated attempts, change things up: different sentence structures or wordings.\n\nPlease respond with only the new description text in <new_description> tags, nothing else.`\n );\n}\n\n/** The follow-up for a description over the limit. */\nexport const shortenPrompt = (base: string, tooLong: string) =>\n `${base}\\n\\n---\\n\\nA previous attempt produced this description, which at ${tooLong.length} characters is over the ${DESCRIPTION_LIMIT}-character hard limit:\\n\\n\"${tooLong}\"\\n\\n` +\n `Rewrite it to be under ${DESCRIPTION_LIMIT} characters while keeping the most important trigger words and intent coverage. Respond with only the new description in <new_description> tags.`;\n\n/** The description inside <new_description> tags, or the whole reply when untagged. */\nexport const parseNewDescription = (text: string): string =>\n (text.match(/<new_description>([\\s\\S]*?)<\\/new_description>/)?.[1] ?? text).trim().replace(/^\"|\"$/g, \"\").trim();\n","/**\n * Description tuning (v1.7): propose better descriptions from the train failures, score every candidate\n * with the trigger eval, keep the best by held-out score. At most 3 iterations. Returns the proposal and the\n * scores — it never writes the skill file.\n */\nimport { runClaude } from \"../claude-run.js\";\nimport matter from \"../matter.js\";\nimport { parseSkill } from \"../skill-schema.js\";\nimport { DESCRIPTION_LIMIT, improveDescriptionPrompt, parseNewDescription, shortenPrompt, type DescriptionAttempt } from \"./prompts/improve-description.js\";\nimport type { EvalPrompt } from \"./sets.js\";\nimport { runTrigger, type TriggerReport } from \"./trigger.js\";\n\nexport const MAX_TUNE_ITERATIONS = 3;\n\nexport interface TuneCandidate {\n description: string;\n train: number | null;\n heldout: number | null;\n}\n\nexport interface TuneReport {\n original: TuneCandidate;\n best: TuneCandidate;\n /** the best candidate is not the original */\n improved: boolean;\n candidates: TuneCandidate[];\n}\n\n/** The skill file with its frontmatter description replaced and nothing else changed. */\nexport function withDescription(raw: string, description: string): string {\n const p = matter(raw);\n return matter.stringify(p.content, { ...p.data, description });\n}\n\nconst score = (r: TriggerReport): TuneCandidate[\"train\"] => r.train.accuracy;\nconst frac = (r: TriggerReport) => `${r.results.filter((x) => x.split === \"train\" && x.triggered === x.should_trigger).length}/${r.train.n}`;\n\nexport async function tuneDescription(o: { repoRoot: string; skillRaw: string; prompts: EvalPrompt[]; env?: NodeJS.ProcessEnv; maxIterations?: number }): Promise<TuneReport> {\n const skill = parseSkill(o.skillRaw);\n const tr = (raw: string) => runTrigger(o.prompts, { repoRoot: o.repoRoot, skillRaw: raw, env: o.env });\n const cand = (description: string, r: TriggerReport): TuneCandidate => ({ description, train: score(r), heldout: r.heldout.accuracy });\n let last = await tr(o.skillRaw);\n const original = cand(skill.description, last);\n const candidates = [original];\n const history: DescriptionAttempt[] = [];\n for (let i = 0; i < Math.min(o.maxIterations ?? MAX_TUNE_ITERATIONS, MAX_TUNE_ITERATIONS); i++) {\n const prev = candidates.at(-1)!;\n history.push({ description: prev.description, score: frac(last) });\n // only train results go to the proposer; held-out stays unseen so it can judge\n const train = last.results.filter((r) => r.split === \"train\");\n const prompt = improveDescriptionPrompt({\n name: skill.name,\n skillContent: skill.body,\n description: prev.description,\n score: frac(last),\n missed: train.filter((r) => r.should_trigger && !r.triggered),\n falseTriggers: train.filter((r) => !r.should_trigger && r.triggered),\n history: history.slice(0, -1),\n });\n const env = o.env;\n let r = await runClaude({ prompt, cwd: o.repoRoot, tools: [], maxTurns: 1, env });\n if (!r.ok) break;\n let description = parseNewDescription(r.result);\n if (description.length > DESCRIPTION_LIMIT) {\n r = await runClaude({ prompt: shortenPrompt(prompt, description), cwd: o.repoRoot, tools: [], maxTurns: 1, env });\n if (r.ok) description = parseNewDescription(r.result);\n }\n if (!description || description.length > DESCRIPTION_LIMIT || candidates.some((c) => c.description === description)) continue;\n last = await tr(withDescription(o.skillRaw, description));\n candidates.push(cand(description, last));\n }\n // best by held-out, ties by train, then the earlier candidate (the original wins a full tie)\n const best = candidates.reduce((b, c) => ((c.heldout ?? -1) > (b.heldout ?? -1) || ((c.heldout ?? -1) === (b.heldout ?? -1) && (c.train ?? -1) > (b.train ?? -1)) ? c : b));\n return { original, best, improved: best !== original, candidates };\n}\n\n"],"mappings":"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","names":["r"]}
@@ -0,0 +1,291 @@
1
+ import {
2
+ decideScope,
3
+ indexSkill,
4
+ promotionPeers
5
+ } from "./chunk-3FPUBAEW.js";
6
+ import {
7
+ runClaude
8
+ } from "./chunk-IEZVFKUT.js";
9
+ import {
10
+ factIds,
11
+ findSkills,
12
+ lintSkill,
13
+ renderSkill
14
+ } from "./chunk-ZSWI3Y4Y.js";
15
+ import {
16
+ readEvidence,
17
+ stateDir
18
+ } from "./chunk-R6GNP7DS.js";
19
+ import {
20
+ redact
21
+ } from "./chunk-H2BES6FU.js";
22
+
23
+ // src/core/skill-review.ts
24
+ import { createHash } from "crypto";
25
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
26
+ import { basename, join } from "path";
27
+
28
+ // src/core/skill-review-prompt.ts
29
+ var REVIEW_PROMPT = `You review one finished Claude Code session in this repository and decide whether it taught a
30
+ reusable procedure worth saving as a skill \u2014 a short, step-by-step playbook a future session should follow.
31
+
32
+ Draft only from the evidence in the brief below: a command that failed and the fix that worked, a correction
33
+ the user gave, or a learned skill that was used and then failed or was corrected. Never invent steps, commands,
34
+ paths or facts that the brief or the repository does not show. Most sessions teach nothing reusable: then
35
+ answer with no drafts. A draft is worth it only when the same situation is likely to come up again here.
36
+
37
+ Kinds of draft:
38
+ - "new": a procedure no existing skill covers. Check the existing skills in the brief first.
39
+ - "patch": an existing learned skill (listed with "learned: true") was used in this session and then failed or
40
+ was corrected: give its full revised body. Never patch a skill that is pinned or not learned.
41
+
42
+ Each draft:
43
+ - name: kebab-case, specific ("restart-vite-dev-server", not "dev-helper").
44
+ - description: what it does and when to use it, one or two sentences, starting with the action; include
45
+ "Use when ..." with the literal error, command or request that should load it. Under 300 characters.
46
+ - scope: "project" (about this repository) or "directory" (only about one subdirectory; give it in applies_to.paths).
47
+ - applies_to: { paths: [repo-relative paths that exist], commands: [program names the steps run] }.
48
+ - triggers: the literal error strings, commands or user phrases from the brief that this skill answers.
49
+ - provenance: { sessions: [the session id], failures: [failure ids from the brief it came from], corrections: [correction
50
+ numbers from the brief, as strings] } \u2014 at least one failure or correction.
51
+ - cites: knowledge-base fact ids from the brief's list that the steps rely on (only ids that are listed).
52
+ - body: markdown with exactly these sections, in order:
53
+ ## When to use \u2014 the situation, in two or three lines.
54
+ ## Steps \u2014 numbered, concrete commands or actions as the session finally did them.
55
+ ## Pitfalls \u2014 what went wrong in the session and why; each line names the failure id or fact id it comes from.
56
+ ## Verify \u2014 one command whose exit code proves it worked, in a fenced code block; or, if none exists, one
57
+ sentence a reviewer can check.
58
+
59
+ Hard rules:
60
+ - No secrets, tokens, passwords, keys, personal data, customer data, names of people or money amounts, even if
61
+ the brief shows them. Use placeholders like <token>.
62
+ - At most 2 drafts. No duplicates of existing skills.
63
+ - Do not write or change any file. You may read files in this repository to get paths and commands right.
64
+
65
+ Answer with one JSON object and nothing else:
66
+ {"drafts": [{"kind": "new" | "patch", "target": "<existing skill name, patch only>", "name": "...",
67
+ "description": "...", "scope": "project" | "directory", "applies_to": {"paths": [], "commands": []},
68
+ "triggers": [], "provenance": {"sessions": [], "failures": [], "corrections": []}, "cites": [], "body": "..."}],
69
+ "none_reason": "<when drafts is empty: one line on why>"}
70
+ `;
71
+
72
+ // src/core/skill-review.ts
73
+ var draftsDir = (repo) => join(stateDir(repo), "skill-drafts");
74
+ var BRIEF_MAX = 4e4;
75
+ var TIMELINE_MAX = 120;
76
+ function transcriptTimeline(path, max = TIMELINE_MAX) {
77
+ if (!path) return [];
78
+ let raw = "";
79
+ try {
80
+ if (statSync(path).size > 200 * 1024 * 1024) return ["(transcript too large to read)"];
81
+ raw = readFileSync(path, "utf8");
82
+ } catch {
83
+ return [];
84
+ }
85
+ const out = [];
86
+ const calls = /* @__PURE__ */ new Map();
87
+ for (const line of raw.split("\n")) {
88
+ let e;
89
+ try {
90
+ e = JSON.parse(line);
91
+ } catch {
92
+ continue;
93
+ }
94
+ const content = e?.message?.content;
95
+ if (e?.type === "user" && typeof content === "string" && !e.isMeta) {
96
+ if (!content.trimStart().startsWith("<")) out.push(`USER: ${content.slice(0, 600)}`);
97
+ } else if (e?.type === "assistant" && Array.isArray(content)) {
98
+ for (const c of content) {
99
+ if (c?.type !== "tool_use") continue;
100
+ const input = c.input ?? {};
101
+ const what = typeof input.command === "string" ? input.command : typeof input.file_path === "string" ? input.file_path : typeof input.skill === "string" ? input.skill : JSON.stringify(input);
102
+ calls.set(c.id, `${c.name}: ${String(what).slice(0, 300)}`);
103
+ }
104
+ } else if (e?.type === "user" && Array.isArray(content)) {
105
+ for (const c of content) {
106
+ if (c?.type !== "tool_result") continue;
107
+ const call = calls.get(c.tool_use_id) ?? "tool";
108
+ const text = typeof c.content === "string" ? c.content : Array.isArray(c.content) ? c.content.map((x) => x?.text ?? "").join(" ") : "";
109
+ out.push(c.is_error ? `TOOL ${call}
110
+ \u2192 ERROR (${c.tool_use_id}): ${text.slice(0, 500)}` : `TOOL ${call} \u2192 ok`);
111
+ }
112
+ }
113
+ }
114
+ if (out.length <= max) return out;
115
+ const errors = out.filter((l) => l.includes("\u2192 ERROR")).slice(-Math.floor(max / 2));
116
+ const tail = out.slice(-(max - errors.length)).filter((l) => !errors.includes(l));
117
+ return [`(${out.length - errors.length - tail.length} earlier entries omitted)`, ...errors, ...tail];
118
+ }
119
+ function topLevel(repo) {
120
+ try {
121
+ return readdirSync(repo).filter((f) => !f.startsWith(".") || f === ".claude" || f === ".documentation").slice(0, 60);
122
+ } catch {
123
+ return [];
124
+ }
125
+ }
126
+ function buildBrief(job, skills = findSkills(job.repo)) {
127
+ const repo = job.repo;
128
+ const failures = readEvidence(repo, "failures").filter((f) => f.session_id === job.session_id);
129
+ const corrections = readEvidence(repo, "corrections").filter((c) => c.session_id === job.session_id);
130
+ const used = new Set(job.evidence.skills_used);
131
+ const r = (t) => redact(t, "broad").text;
132
+ const lines = [
133
+ `# Session ${job.session_id}`,
134
+ `Repository: ${basename(repo)} \u2014 top level: ${topLevel(repo).join(", ")}`,
135
+ `Why reviewed: ${job.reason}`,
136
+ "",
137
+ "## Failures (from the evidence log)",
138
+ ...failures.length ? failures.map((f) => `- id ${f.id}: \`${r(String(f.command))}\`
139
+ error: ${r(String(f.error).slice(0, 500))}`) : ["(none)"],
140
+ "",
141
+ "## Corrections the user gave",
142
+ ...corrections.length ? corrections.map((c, i) => `- correction ${i + 1} (${c.cue}): ${r(String(c.excerpt))}`) : ["(none)"],
143
+ "",
144
+ "## Skills this repository loads",
145
+ ...skills.length ? skills.map((s) => `- ${s.skill.name || basename(s.dir)} (${s.scope}; learned: ${s.learned}${s.skill.meta?.pinned ? "; pinned" : ""}${used.has(s.skill.name) ? "; USED this session" : ""}): ${r(s.skill.description.slice(0, 200))}`) : ["(none)"],
146
+ ...skills.filter((s) => s.learned && used.has(s.skill.name)).flatMap((s) => ["", `### Current text of the learned skill ${s.skill.name} (used this session)`, r(s.skill.body.slice(0, 4e3))]),
147
+ "",
148
+ "## Knowledge-base fact ids (for cites)",
149
+ [...factIds(repo)].slice(0, 200).join(", ") || "(none)",
150
+ "",
151
+ "## Timeline (prompts, tool calls, errors)",
152
+ ...transcriptTimeline(job.transcript_path).map(r)
153
+ ];
154
+ let brief = lines.join("\n");
155
+ if (brief.length > BRIEF_MAX) brief = brief.slice(0, BRIEF_MAX) + "\n(brief cut at its size cap)";
156
+ return brief;
157
+ }
158
+ function parseReviewerJson(text) {
159
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
160
+ const candidates = [fenced?.[1], text, text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1)].filter(Boolean);
161
+ for (const c of candidates) {
162
+ try {
163
+ const v = JSON.parse(c.trim());
164
+ if (v && Array.isArray(v.drafts)) return v;
165
+ } catch {
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && !x.includes("[REDACTED]")) : [];
171
+ var sha256 = (s) => createHash("sha256").update(s).digest("hex");
172
+ async function materializeDraft(repo, job, d, skills, resolveScope = (x) => decideScope(repo, x), env = process.env, merge) {
173
+ if (!d || typeof d.name !== "string" || typeof d.body !== "string" || typeof d.description !== "string") return { rejected: "malformed draft" };
174
+ let target;
175
+ if (d.kind === "patch") {
176
+ target = skills.find((s) => s.skill.name === d.target);
177
+ if (!target) return { rejected: `patch target "${d.target}" not found` };
178
+ if (!target.learned) return { rejected: `"${d.target}" was not written by the skill loop: never patched` };
179
+ if (target.skill.meta?.pinned) return { rejected: `"${d.target}" is pinned` };
180
+ }
181
+ const prov = {
182
+ sessions: [.../* @__PURE__ */ new Set([job.session_id, ...strs(d.provenance?.sessions)])],
183
+ failures: strs(d.provenance?.failures),
184
+ corrections: strs(d.provenance?.corrections)
185
+ };
186
+ if (prov.failures.length + prov.corrections.length === 0 && job.evidence.failures.length + job.evidence.corrections === 0) {
187
+ return { rejected: "no failure or correction behind it" };
188
+ }
189
+ const decision = target ? { scope: target.scope === "directory" ? "directory" : "project", globalCandidate: false, why: "patch: the skill's own scope" } : await resolveScope(d);
190
+ const scope = target?.scope ?? decision.scope;
191
+ const paths = strs(d.applies_to?.paths);
192
+ if (decision.directory && !paths.includes(decision.directory)) paths.unshift(decision.directory);
193
+ const name = d.kind === "patch" ? target.skill.name : d.name;
194
+ const meta = {
195
+ schema: 1,
196
+ scope,
197
+ status: "draft",
198
+ pinned: false,
199
+ triggers: strs(d.triggers),
200
+ applies_to: { paths, commands: strs(d.applies_to?.commands) },
201
+ provenance: prov,
202
+ cites: strs(d.cites),
203
+ supersedes: merge ? merge.supersedes.map((p) => basename(join(p, ".."))) : target?.skill.meta?.supersedes ?? []
204
+ };
205
+ const text = redact(renderSkill({ name, description: d.description, meta, body: d.body }), "precise").text;
206
+ const findings = lintSkill(join(draftsDir(repo), name, "SKILL.md"), text, { projectRoot: repo, others: skills.filter((s) => s.path !== target?.path) }, name);
207
+ const errors = findings.filter((f) => f.severity === "error");
208
+ if (errors.length) return { rejected: `lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join("; ")}` };
209
+ const id = `${name}-${sha256(text).slice(0, 8)}`;
210
+ const dir = join(draftsDir(repo), id);
211
+ mkdirSync(dir, { recursive: true });
212
+ writeFileSync(join(dir, "SKILL.md"), text);
213
+ const record = {
214
+ id,
215
+ kind: merge ? "merge" : d.kind === "patch" ? "patch" : "new",
216
+ name,
217
+ scope,
218
+ ...target ? { target: target.path, base_sha256: sha256(readFileSync(target.path, "utf8")) } : {},
219
+ session_id: job.session_id,
220
+ created: (/* @__PURE__ */ new Date()).toISOString(),
221
+ warnings: findings.filter((f) => f.severity === "warn").map((f) => `${f.rule}: ${f.message}`),
222
+ ...decision.directory ? { directory: decision.directory } : {},
223
+ scope_why: decision.why,
224
+ ...decision.globalCandidate ? { global_candidate: true } : {},
225
+ ...merge ? { supersedes: merge.supersedes } : {}
226
+ };
227
+ if (!target && !merge) {
228
+ const peers = promotionPeers(repo, name, env);
229
+ if (peers.length) record.promotion = peers.map((p) => ({ repo: p.repo, path: p.path }));
230
+ indexSkill({ repo, name, path: join(dir, "SKILL.md"), status: "draft", ...decision.globalCandidate ? { global_candidate: true } : {} }, env);
231
+ }
232
+ writeFileSync(join(dir, "draft.json"), JSON.stringify(record, null, 1));
233
+ return { id };
234
+ }
235
+ function listDrafts(repo) {
236
+ let ids = [];
237
+ try {
238
+ ids = readdirSync(draftsDir(repo));
239
+ } catch {
240
+ return [];
241
+ }
242
+ const out = [];
243
+ for (const id of ids) {
244
+ const dir = join(draftsDir(repo), id);
245
+ try {
246
+ if (existsSync(join(dir, "SKILL.md"))) out.push({ dir, record: JSON.parse(readFileSync(join(dir, "draft.json"), "utf8")) });
247
+ } catch {
248
+ }
249
+ }
250
+ return out.sort((a, b) => a.record.created.localeCompare(b.record.created));
251
+ }
252
+ async function reviewSession(job, opts = {}) {
253
+ const skills = findSkills(job.repo);
254
+ const brief = buildBrief(job, skills);
255
+ const run = await runClaude({
256
+ prompt: `${REVIEW_PROMPT}
257
+
258
+ ---
259
+
260
+ ${brief}`,
261
+ cwd: job.repo,
262
+ tools: ["Read", "Grep", "Glob"],
263
+ maxTurns: opts.maxTurns ?? 8,
264
+ output: "json",
265
+ timeoutMs: 10 * 6e4,
266
+ env: opts.env
267
+ });
268
+ if (!run.ok && !run.result) throw new Error(run.error ?? "reviewer run failed");
269
+ const answer = parseReviewerJson(run.result);
270
+ if (!answer) return { drafts: [], rejected: [{ reason: "reviewer answer was not JSON" }], costUsd: run.costUsd };
271
+ const drafts = [];
272
+ const rejected = [];
273
+ for (const d of answer.drafts.slice(0, 2)) {
274
+ const env = opts.env ?? process.env;
275
+ const r = await materializeDraft(job.repo, job, d, skills, opts.resolveScope ?? ((x) => decideScope(job.repo, x, env)), env);
276
+ if (r.id) drafts.push(r.id);
277
+ else rejected.push({ reason: `${d?.name ?? "?"}: ${r.rejected}` });
278
+ }
279
+ return { drafts, rejected, costUsd: run.costUsd, note: answer.drafts.length ? void 0 : answer.none_reason?.slice(0, 200) };
280
+ }
281
+
282
+ export {
283
+ draftsDir,
284
+ transcriptTimeline,
285
+ buildBrief,
286
+ parseReviewerJson,
287
+ materializeDraft,
288
+ listDrafts,
289
+ reviewSession
290
+ };
291
+ //# sourceMappingURL=chunk-P3GLY42T.js.map