@theglitchking/semantic-memory 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +61 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +19 -2
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3FPUBAEW.js +113 -0
- package/dist/chunk-3FPUBAEW.js.map +1 -0
- package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
- package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
- package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
- package/dist/chunk-BEACL25M.js +224 -0
- package/dist/chunk-BEACL25M.js.map +1 -0
- package/dist/chunk-CATPGINC.js +186 -0
- package/dist/chunk-CATPGINC.js.map +1 -0
- package/dist/chunk-DBFD3MTT.js +130 -0
- package/dist/chunk-DBFD3MTT.js.map +1 -0
- package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/chunk-IEZVFKUT.js +157 -0
- package/dist/chunk-IEZVFKUT.js.map +1 -0
- package/dist/chunk-MQS7JEBD.js +137 -0
- package/dist/chunk-MQS7JEBD.js.map +1 -0
- package/dist/chunk-NCGM36NX.js +118 -0
- package/dist/chunk-NCGM36NX.js.map +1 -0
- package/dist/chunk-NIZLBNH4.js +165 -0
- package/dist/chunk-NIZLBNH4.js.map +1 -0
- package/dist/chunk-OT3MV7XF.js +123 -0
- package/dist/chunk-OT3MV7XF.js.map +1 -0
- package/dist/chunk-P3GLY42T.js +291 -0
- package/dist/chunk-P3GLY42T.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.js.map +1 -0
- package/dist/chunk-R6GNP7DS.js +87 -0
- package/dist/chunk-R6GNP7DS.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
- package/dist/chunk-VBLCUWT4.js.map +1 -0
- package/dist/chunk-VQCNWRTL.js +121 -0
- package/dist/chunk-VQCNWRTL.js.map +1 -0
- package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
- package/dist/chunk-Y7H76Q7F.js.map +1 -0
- package/dist/chunk-ZSWI3Y4Y.js +232 -0
- package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
- package/dist/cli/index.js +381 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/core/features.d.ts +38 -0
- package/dist/core/features.js +28 -0
- package/dist/core/index.d.ts +285 -2
- package/dist/core/index.js +170 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/jev.d.ts +35 -0
- package/dist/core/jev.js +20 -0
- package/dist/core/skill-curator.d.ts +1 -0
- package/dist/core/skill-curator.js +41 -0
- package/dist/core/skill-evidence.d.ts +25 -0
- package/dist/core/skill-evidence.js +23 -0
- package/dist/core/skill-queue.d.ts +40 -0
- package/dist/core/skill-queue.js +38 -0
- package/dist/core/skill-review.d.ts +3 -0
- package/dist/core/skill-review.js +29 -0
- package/dist/core/skill-worker.d.ts +21 -0
- package/dist/core/skill-worker.js +70 -0
- package/dist/core/skill-worker.js.map +1 -0
- package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/failure-eval-IUJO47UC.js.map +1 -0
- package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
- package/dist/healthcheck-S7D6YNTA.js.map +1 -0
- package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/mcp/server.js +19 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
- package/dist/skill-land-XU2APPBJ.js +31 -0
- package/dist/skill-land-XU2APPBJ.js.map +1 -0
- package/dist/skill-ledger-AYXVNFX7.js +18 -0
- package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
- package/dist/skill-merge-GXBQLBZA.js +24 -0
- package/dist/skill-merge-GXBQLBZA.js.map +1 -0
- package/dist/skill-review-BaW5n0Zg.d.ts +179 -0
- package/dist/skill-schema-RVB3EHEF.js +30 -0
- package/dist/skill-schema-RVB3EHEF.js.map +1 -0
- package/hooks/hooks.json +26 -1
- package/hooks/jev-bridge.py +109 -0
- package/hooks/session-start.js +5 -0
- package/hooks/skill-loop.js +65 -0
- package/hooks/vault-context.js +104 -6
- package/package.json +4 -2
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-GYBUBJYM.js.map +0 -1
- package/dist/chunk-HUG7E25D.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
- /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
- /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
- /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
- /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/core/features.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join, dirname, delimiter } from "path";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
var ENV = { skill_loop: "SEMANTIC_MEMORY_SKILL_LOOP", jev: "SEMANTIC_MEMORY_JEV" };
|
|
7
|
+
var truthy = (v) => v === true || v === 1 || typeof v === "string" && /^(1|on|true|yes)$/i.test(v.trim());
|
|
8
|
+
var falsy = (v) => v === false || v === 0 || typeof v === "string" && /^(0|off|false|no)$/i.test(v.trim());
|
|
9
|
+
var repoConfigPath = (projectRoot) => join(projectRoot, ".claude", ".semantic-memory", "config");
|
|
10
|
+
function readRepoConfig(projectRoot) {
|
|
11
|
+
try {
|
|
12
|
+
const data = YAML.parse(readFileSync(repoConfigPath(projectRoot), "utf8"));
|
|
13
|
+
return data && typeof data === "object" ? data : {};
|
|
14
|
+
} catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var jevLoopDir = (env = process.env) => env.JEV_LOOP_DIR || join(env.HOME || homedir(), ".claude", "jev-loop");
|
|
19
|
+
var claudeSettingsPath = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_SETTINGS || join(env.HOME || homedir(), ".claude", "settings.json");
|
|
20
|
+
function onPath(bin, env = process.env) {
|
|
21
|
+
return (env.PATH || "").split(delimiter).some((d) => d && existsSync(join(d, bin)));
|
|
22
|
+
}
|
|
23
|
+
function jevAvailable(env = process.env) {
|
|
24
|
+
const dir = jevLoopDir(env);
|
|
25
|
+
if (!existsSync(join(dir, "router.py"))) return { available: false, reason: `jev-loop not found at ${dir} (router.py missing; set JEV_LOOP_DIR)` };
|
|
26
|
+
if (!env.TYPESAFE_API_KEY) {
|
|
27
|
+
let inSettings = false;
|
|
28
|
+
try {
|
|
29
|
+
inSettings = Boolean(JSON.parse(readFileSync(claudeSettingsPath(env), "utf8"))?.env?.TYPESAFE_API_KEY);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
if (!inSettings) return { available: false, reason: "TYPESAFE_API_KEY is not set (env or ~/.claude/settings.json env)" };
|
|
33
|
+
}
|
|
34
|
+
if (!onPath("python3", env)) return { available: false, reason: "python3 not found on PATH" };
|
|
35
|
+
return { available: true };
|
|
36
|
+
}
|
|
37
|
+
function featureState(feature, projectRoot, env = process.env) {
|
|
38
|
+
let on = false;
|
|
39
|
+
let why = "default";
|
|
40
|
+
const raw = env[ENV[feature]];
|
|
41
|
+
if (raw !== void 0 && raw !== "") {
|
|
42
|
+
on = truthy(raw);
|
|
43
|
+
why = "env";
|
|
44
|
+
}
|
|
45
|
+
if (feature === "skill_loop" && projectRoot) {
|
|
46
|
+
const repo = readRepoConfig(projectRoot).skill_loop;
|
|
47
|
+
if (truthy(repo) || falsy(repo)) {
|
|
48
|
+
on = truthy(repo);
|
|
49
|
+
why = "repo";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (feature === "jev" && on) {
|
|
53
|
+
const a = jevAvailable(env);
|
|
54
|
+
return { feature, on, why, ...a };
|
|
55
|
+
}
|
|
56
|
+
return { feature, on, why, available: true };
|
|
57
|
+
}
|
|
58
|
+
var featureOn = (feature, projectRoot, env = process.env) => {
|
|
59
|
+
const s = featureState(feature, projectRoot, env);
|
|
60
|
+
return s.on && s.available;
|
|
61
|
+
};
|
|
62
|
+
function setSettingsEnv(key, value, env = process.env) {
|
|
63
|
+
const path = claudeSettingsPath(env);
|
|
64
|
+
let data = {};
|
|
65
|
+
let raw;
|
|
66
|
+
if (existsSync(path)) {
|
|
67
|
+
raw = readFileSync(path, "utf8");
|
|
68
|
+
try {
|
|
69
|
+
data = raw.trim() ? JSON.parse(raw) : {};
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new Error(`${path} is not valid JSON (${e.message}); not changed`);
|
|
72
|
+
}
|
|
73
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error(`${path} is not a JSON object; not changed`);
|
|
74
|
+
}
|
|
75
|
+
if (data.env !== void 0 && (typeof data.env !== "object" || data.env === null || Array.isArray(data.env))) {
|
|
76
|
+
throw new Error(`${path}: "env" is not an object; not changed`);
|
|
77
|
+
}
|
|
78
|
+
const nextEnv = { ...data.env ?? {} };
|
|
79
|
+
if (value === void 0) delete nextEnv[key];
|
|
80
|
+
else nextEnv[key] = value;
|
|
81
|
+
data.env = nextEnv;
|
|
82
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
83
|
+
let backup;
|
|
84
|
+
if (raw !== void 0) {
|
|
85
|
+
backup = `${path}.bak`;
|
|
86
|
+
copyFileSync(path, backup);
|
|
87
|
+
}
|
|
88
|
+
const indent = raw?.match(/^[{\[]\s*\n([ \t]+)/)?.[1] ?? " ";
|
|
89
|
+
writeFileSync(path, JSON.stringify(data, null, indent) + "\n");
|
|
90
|
+
return { path, backup };
|
|
91
|
+
}
|
|
92
|
+
function setRepoConfig(projectRoot, key, value) {
|
|
93
|
+
const path = repoConfigPath(projectRoot);
|
|
94
|
+
const data = readRepoConfig(projectRoot);
|
|
95
|
+
if (value === void 0) delete data[key];
|
|
96
|
+
else data[key] = value;
|
|
97
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
98
|
+
writeFileSync(path, Object.keys(data).length ? YAML.stringify(data) : "");
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
function describeFeature(s) {
|
|
102
|
+
const name = s.feature === "skill_loop" ? "skill loop" : "jev mode";
|
|
103
|
+
const src = s.why === "default" ? "default" : s.why === "env" ? `env ${ENV[s.feature]}` : "repo override in .claude/.semantic-memory/config";
|
|
104
|
+
if (s.on && !s.available) return `${name}: on but unavailable (${s.reason}) [${src}]`;
|
|
105
|
+
return `${name}: ${s.on ? "on" : "off"} [${src}]`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
ENV,
|
|
110
|
+
repoConfigPath,
|
|
111
|
+
readRepoConfig,
|
|
112
|
+
jevLoopDir,
|
|
113
|
+
claudeSettingsPath,
|
|
114
|
+
jevAvailable,
|
|
115
|
+
featureState,
|
|
116
|
+
featureOn,
|
|
117
|
+
setSettingsEnv,
|
|
118
|
+
setRepoConfig,
|
|
119
|
+
describeFeature
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=chunk-VQCNWRTL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/features.ts"],"sourcesContent":["/**\n * The v1.7 feature switches, both off by default (v1.7):\n *\n * - skill loop (`SEMANTIC_MEMORY_SKILL_LOOP=1`): learn procedures as skills between sessions. A repo can\n * override the global switch with `skill_loop: on | off` in `<repo>/.claude/.semantic-memory/config`.\n * - jev mode (`SEMANTIC_MEMORY_JEV=1`): answer closed-set decisions with Jev through jev-loop. Env only.\n *\n * Standalone (no embedder, no vault): hooks import it from dist/core/features.js on every event, so it\n * must stay cheap — a few stat/read calls.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname, delimiter } from \"node:path\";\nimport YAML from \"yaml\";\n\nexport type Feature = \"skill_loop\" | \"jev\";\n\nexport interface FeatureState {\n feature: Feature;\n on: boolean;\n /** Where the on/off came from. */\n why: \"default\" | \"env\" | \"repo\";\n /** jev only: whether jev-loop, the key and python are present. Always true for the skill loop. */\n available: boolean;\n /** Why it is unavailable, when it is on but cannot run. */\n reason?: string;\n}\n\nexport const ENV: Record<Feature, string> = { skill_loop: \"SEMANTIC_MEMORY_SKILL_LOOP\", jev: \"SEMANTIC_MEMORY_JEV\" };\n\nconst truthy = (v: unknown) => v === true || v === 1 || (typeof v === \"string\" && /^(1|on|true|yes)$/i.test(v.trim()));\nconst falsy = (v: unknown) => v === false || v === 0 || (typeof v === \"string\" && /^(0|off|false|no)$/i.test(v.trim()));\n\nexport const repoConfigPath = (projectRoot: string) => join(projectRoot, \".claude\", \".semantic-memory\", \"config\");\n\n/** `<repo>/.claude/.semantic-memory/config` as an object ({} when missing or unparseable). */\nexport function readRepoConfig(projectRoot: string): Record<string, unknown> {\n try {\n const data = YAML.parse(readFileSync(repoConfigPath(projectRoot), \"utf8\"));\n return data && typeof data === \"object\" ? data : {};\n } catch {\n return {};\n }\n}\n\nexport const jevLoopDir = (env = process.env) => env.JEV_LOOP_DIR || join(env.HOME || homedir(), \".claude\", \"jev-loop\");\nexport const claudeSettingsPath = (env = process.env) =>\n env.SEMANTIC_MEMORY_CLAUDE_SETTINGS || join(env.HOME || homedir(), \".claude\", \"settings.json\");\n\nfunction onPath(bin: string, env = process.env): boolean {\n return (env.PATH || \"\").split(delimiter).some((d) => d && existsSync(join(d, bin)));\n}\n\n/** Whether jev mode could run: jev-loop's router, the TypeSafe key (env or settings, never read out), python3. */\nexport function jevAvailable(env = process.env): { available: boolean; reason?: string } {\n const dir = jevLoopDir(env);\n if (!existsSync(join(dir, \"router.py\"))) return { available: false, reason: `jev-loop not found at ${dir} (router.py missing; set JEV_LOOP_DIR)` };\n if (!env.TYPESAFE_API_KEY) {\n let inSettings = false;\n try {\n inSettings = Boolean(JSON.parse(readFileSync(claudeSettingsPath(env), \"utf8\"))?.env?.TYPESAFE_API_KEY);\n } catch {}\n if (!inSettings) return { available: false, reason: \"TYPESAFE_API_KEY is not set (env or ~/.claude/settings.json env)\" };\n }\n if (!onPath(\"python3\", env)) return { available: false, reason: \"python3 not found on PATH\" };\n return { available: true };\n}\n\nexport function featureState(feature: Feature, projectRoot?: string, env = process.env): FeatureState {\n let on = false;\n let why: FeatureState[\"why\"] = \"default\";\n const raw = env[ENV[feature]];\n if (raw !== undefined && raw !== \"\") {\n on = truthy(raw);\n why = \"env\";\n }\n if (feature === \"skill_loop\" && projectRoot) {\n const repo = readRepoConfig(projectRoot).skill_loop;\n if (truthy(repo) || falsy(repo)) {\n on = truthy(repo);\n why = \"repo\";\n }\n }\n if (feature === \"jev\" && on) {\n const a = jevAvailable(env);\n return { feature, on, why, ...a };\n }\n return { feature, on, why, available: true };\n}\n\n/** On and able to run. The check every hook path makes first. */\nexport const featureOn = (feature: Feature, projectRoot?: string, env = process.env) => {\n const s = featureState(feature, projectRoot, env);\n return s.on && s.available;\n};\n\n/**\n * Set (true / false) or clear (undefined) one env key in ~/.claude/settings.json, touching nothing else.\n * Keeps a `.bak` of the previous file. Refuses (throws, writes nothing) when the file is not valid JSON.\n */\nexport function setSettingsEnv(key: string, value: string | undefined, env = process.env): { path: string; backup?: string } {\n const path = claudeSettingsPath(env);\n let data: Record<string, any> = {};\n let raw: string | undefined;\n if (existsSync(path)) {\n raw = readFileSync(path, \"utf8\");\n try {\n data = raw.trim() ? JSON.parse(raw) : {};\n } catch (e) {\n throw new Error(`${path} is not valid JSON (${(e as Error).message}); not changed`);\n }\n if (!data || typeof data !== \"object\" || Array.isArray(data)) throw new Error(`${path} is not a JSON object; not changed`);\n }\n if (data.env !== undefined && (typeof data.env !== \"object\" || data.env === null || Array.isArray(data.env))) {\n throw new Error(`${path}: \"env\" is not an object; not changed`);\n }\n const nextEnv = { ...(data.env ?? {}) };\n if (value === undefined) delete nextEnv[key];\n else nextEnv[key] = value;\n data.env = nextEnv;\n mkdirSync(dirname(path), { recursive: true });\n let backup: string | undefined;\n if (raw !== undefined) {\n backup = `${path}.bak`;\n copyFileSync(path, backup);\n }\n const indent = raw?.match(/^[{\\[]\\s*\\n([ \\t]+)/)?.[1] ?? \" \";\n writeFileSync(path, JSON.stringify(data, null, indent) + \"\\n\");\n return { path, backup };\n}\n\n/** Set or clear `<key>: <value>` in the repo config, keeping the other keys. */\nexport function setRepoConfig(projectRoot: string, key: string, value: string | boolean | undefined): string {\n const path = repoConfigPath(projectRoot);\n const data = readRepoConfig(projectRoot);\n if (value === undefined) delete data[key];\n else data[key] = value;\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, Object.keys(data).length ? YAML.stringify(data) : \"\");\n return path;\n}\n\nexport function describeFeature(s: FeatureState): string {\n const name = s.feature === \"skill_loop\" ? \"skill loop\" : \"jev mode\";\n const src = s.why === \"default\" ? \"default\" : s.why === \"env\" ? `env ${ENV[s.feature]}` : \"repo override in .claude/.semantic-memory/config\";\n if (s.on && !s.available) return `${name}: on but unavailable (${s.reason}) [${src}]`;\n return `${name}: ${s.on ? \"on\" : \"off\"} [${src}]`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/schema.ts","../src/core/schema-default.ts"],"sourcesContent":["import { readFile, writeFile, access } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport matter from \"./matter.js\";\nimport { DEFAULT_SCHEMA_YAML } from \"./schema-default.js\";\n\nexport interface VaultSchema {\n version: number;\n provenance_fields: string[];\n status_enum: string[];\n confidence_enum: string[];\n severity_enum: string[];\n types: Record<string, { description?: string; required: string[] }>;\n lint: {\n missing_provenance: { applies_to: string[]; severity: \"warn\" | \"error\" };\n stale: { max_age_days: number; severity: \"warn\" | \"error\" };\n schema_violations: { severity: \"warn\" | \"error\" };\n };\n}\n\nexport type Severity = \"warn\" | \"error\";\n\nexport interface LintFinding {\n path: string;\n rule: \"missing_provenance\" | \"stale\" | \"schema_violations\" | \"broken_links\" | \"code_symbols\" | \"decay_candidates\" | \"alias_conflicts\" | \"decoys\" | \"secrets\" | \"skills\";\n severity: Severity;\n message: string;\n}\n\nexport async function loadSchema(vaultPath: string): Promise<VaultSchema> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n let raw: string;\n try {\n await access(schemaPath, constants.R_OK);\n raw = await readFile(schemaPath, \"utf-8\");\n } catch {\n raw = DEFAULT_SCHEMA_YAML;\n }\n const parsed = parseYaml(raw) as VaultSchema;\n return parsed;\n}\n\n/**\n * Bootstrap the schema file in a vault by writing the default if none exists.\n * Used by the `--install-schema` CLI flag. Returns the path that was (or would be) written.\n */\nexport async function installDefaultSchema(vaultPath: string, force = false): Promise<{ written: boolean; path: string }> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!force) {\n try {\n await access(schemaPath, constants.F_OK);\n return { written: false, path: schemaPath };\n } catch {\n // not present → write it\n }\n }\n await writeFile(schemaPath, DEFAULT_SCHEMA_YAML, \"utf-8\");\n return { written: true, path: schemaPath };\n}\n\n/**\n * Validate a single note's frontmatter + content against the schema.\n * Returns findings (possibly empty). Does NOT throw — lint never throws.\n */\nexport function validateNote(\n relativePath: string,\n rawFileContent: string,\n schema: VaultSchema,\n opts: { todayIso?: string } = {}\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const { data: fm } = matter(rawFileContent);\n\n const typeName = typeof fm.type === \"string\" ? fm.type : \"note\";\n const typeDef = schema.types[typeName];\n\n if (!typeDef) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `unknown type \"${typeName}\" — allowed: ${Object.keys(schema.types).join(\", \")}`,\n });\n return findings; // can't validate required fields if type is unknown\n }\n\n // Required field check\n for (const field of typeDef.required) {\n if (fm[field] === undefined || fm[field] === null || fm[field] === \"\") {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `missing required field \"${field}\" for type \"${typeName}\"`,\n });\n }\n }\n\n // Enum checks\n if (fm.status !== undefined && !schema.status_enum.includes(String(fm.status))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `status \"${fm.status}\" not in enum [${schema.status_enum.join(\", \")}]`,\n });\n }\n if (fm.confidence !== undefined && !schema.confidence_enum.includes(String(fm.confidence))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `confidence \"${fm.confidence}\" not in enum [${schema.confidence_enum.join(\", \")}]`,\n });\n }\n if (typeName === \"gotcha\" && fm.severity !== undefined && !schema.severity_enum.includes(String(fm.severity))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `severity \"${fm.severity}\" not in enum [${schema.severity_enum.join(\", \")}]`,\n });\n }\n\n // Missing provenance\n if (schema.lint.missing_provenance.applies_to.includes(typeName)) {\n const hasSources = Array.isArray(fm.sources) ? fm.sources.length > 0 : typeof fm.sources === \"string\" && fm.sources.length > 0;\n const hasDerived = Array.isArray(fm.derived_from) ? fm.derived_from.length > 0 : typeof fm.derived_from === \"string\" && fm.derived_from.length > 0;\n if (!hasSources && !hasDerived) {\n findings.push({\n path: relativePath,\n rule: \"missing_provenance\",\n severity: schema.lint.missing_provenance.severity,\n message: `no sources or derived_from on ${typeName}`,\n });\n }\n }\n\n // Stale\n if (schema.lint.stale.max_age_days > 0 && fm.last_verified) {\n const today = opts.todayIso ?? new Date().toISOString().slice(0, 10);\n const verified = String(fm.last_verified);\n const ageDays = dayDiff(today, verified);\n if (ageDays !== null && ageDays > schema.lint.stale.max_age_days) {\n findings.push({\n path: relativePath,\n rule: \"stale\",\n severity: schema.lint.stale.severity,\n message: `last_verified ${verified} is ${ageDays} days old (limit ${schema.lint.stale.max_age_days})`,\n });\n }\n }\n\n return findings;\n}\n\nfunction dayDiff(todayIso: string, thenIso: string): number | null {\n const a = Date.parse(todayIso);\n const b = Date.parse(thenIso);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.round((a - b) / 86400000);\n}\n","// Default vault schema shipped with semantic-sidekick.\n// Intentionally minimal — the plan is to grow from pain, not preempt it.\n// Users can override with a vault.schema.yml at the vault root.\n\nexport const DEFAULT_SCHEMA_YAML = `# semantic-sidekick vault schema (minimal starter).\n# Override by placing a vault.schema.yml at the vault root.\n\nversion: 1\n\n# Provenance fields — apply to all types, enforced by lint.find_missing_provenance.\n# sources: URLs or filesystem paths the note was derived from\n# derived_from: wikilinks to other notes this one synthesizes\n# last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).\n# Set at creation and by verify_note; NOT bumped on ordinary edits.\n# Drives confidence decay (see the decay: block below).\n# evergreen: true → the note is pinned against decay while last_verified is\n# within 365 days; after that it decays normally (re-affirm to reset).\n# status: one of: draft | active | stale | deprecated | archived\n# confidence: one of: high | medium | low\nprovenance_fields:\n - sources\n - derived_from\n - last_verified\n - status\n - confidence\n\n# Allowed status values (enforced by schema_violations lint).\nstatus_enum: [draft, active, stale, deprecated, archived]\nconfidence_enum: [high, medium, low]\n\n# Note types. Each type declares its required frontmatter.\n# Any field not listed as required is allowed but not enforced.\ntypes:\n note:\n description: Generic note — default type when \\`type\\` frontmatter is absent\n required: [title, status]\n\n decision:\n description: Architectural or process decision with dated rationale\n required: [title, status, decided_on, decision_maker]\n\n gotcha:\n description: Compressed lesson from a past incident/bug/surprise\n required: [title, status, last_verified, severity]\n\n source:\n description: External reference (paper, blog post, doc) used as provenance\n required: [title, status, source]\n\n alias:\n description: Learned human→artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.\n required: [canonical]\n\n dossier:\n description: Entity-centric living note (v1.5). One per critical component — purpose, failure modes, knobs, incident log, current state. Accretes in place.\n required: [title, status, entity]\n\n profile:\n description: Speaker profile (v1.5). Models how THIS human communicates — severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.\n required: [title, status]\n\n# Severity values for gotcha type.\nseverity_enum: [low, medium, high, critical]\n\n# Lint policy. Each rule has a default severity that pre-commit/CI consumes.\nlint:\n missing_provenance:\n # Triggers when a note type in this list has no sources AND no derived_from.\n applies_to: [note, decision, gotcha]\n severity: warn\n stale:\n # Triggers when last_verified is older than this many days (0 = disabled).\n max_age_days: 180\n severity: warn\n schema_violations:\n # Triggers on missing required fields, unknown type, or enum mismatch.\n severity: error\n\n# Confidence decay (v1.3). Down-weights search results by time since last_verified.\n# Composes multiplicatively with load_priority — it never removes notes, only ranks\n# them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.\ndecay:\n enabled: true\n default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)\n per_type:\n decision: 365\n note: 365\n gotcha: 180\n source: null # null = never decays\n proposal: 14\n floor: 0.1 # notes never drop below this multiplier\n hotness_boost:\n enabled: false # extend half-life for heavily-backlinked notes (opt-in)\n cap: 2.0\n\n# Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob\n# (composes multiplicatively with decay + load_priority). First matching rule wins.\n# The default down-weights archived notes so retired docs stop dominating results.\npath_class:\n enabled: true\n rules:\n - glob: \"archive/**\"\n multiplier: 0.3\n\n# Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per\n# the local selection log) so load-bearing knowledge floats up. Bounded by cap to\n# stop a feedback runaway; composes multiplicatively with decay + path_class +\n# load_priority. A no-op until the selection log has citations. The complementary\n# signal (retrieved-but-never-cited \"decoys\") is surfaced by lint_vault, NEVER\n# auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.\nusage_boost:\n enabled: true\n cap: 1.5 # max multiplier a note can earn from citations\n per_citation: 0.1 # multiplier += per_citation × citation_count, then capped\n\n# Token-frugal tool surface (v1.4). When enabled, the server registers a reduced\n# tool set in modes that don't need the full surface (outage-silence → search/read\n# only). DEFAULT OFF — mid-session mode switches don't re-register, so opt in and\n# dogfood before relying on it.\ntools:\n conditional: false\n\n# Selection-logging telemetry (v1.3.1). Records what search returned and which\n# note the answer used, to a LOCAL append-only JSONL at\n# .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.\n# Feeds the decay_candidates lint and (later) usage-feedback ranking. Set\n# enabled: false to disable all logging.\ntelemetry:\n enabled: true\n`;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
matter_default
|
|
3
|
+
} from "./chunk-J7LYDRML.js";
|
|
4
|
+
import {
|
|
5
|
+
findSecrets
|
|
6
|
+
} from "./chunk-H2BES6FU.js";
|
|
7
|
+
|
|
8
|
+
// src/core/skill-schema.ts
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join, basename } from "path";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
var SKILL_SCOPES = ["directory", "project", "global"];
|
|
14
|
+
var SKILL_STATUSES = ["draft", "active", "stale", "archived"];
|
|
15
|
+
var SKILL_SECTIONS = ["When to use", "Steps", "Pitfalls", "Verify"];
|
|
16
|
+
var GENERATED_BY = "semantic-memory";
|
|
17
|
+
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
18
|
+
function parseSkill(raw) {
|
|
19
|
+
let data = {};
|
|
20
|
+
let body = raw;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = matter_default(raw);
|
|
23
|
+
data = parsed.data ?? {};
|
|
24
|
+
body = parsed.content;
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
const m = data.skill_loop;
|
|
28
|
+
const meta = m && typeof m === "object" ? {
|
|
29
|
+
schema: m.schema,
|
|
30
|
+
scope: m.scope,
|
|
31
|
+
status: m.status,
|
|
32
|
+
pinned: m.pinned === true,
|
|
33
|
+
triggers: strings(m.triggers),
|
|
34
|
+
applies_to: { paths: strings(m.applies_to?.paths), commands: strings(m.applies_to?.commands) },
|
|
35
|
+
provenance: {
|
|
36
|
+
sessions: strings(m.provenance?.sessions),
|
|
37
|
+
failures: strings(m.provenance?.failures),
|
|
38
|
+
corrections: strings(m.provenance?.corrections)
|
|
39
|
+
},
|
|
40
|
+
cites: strings(m.cites),
|
|
41
|
+
supersedes: strings(m.supersedes)
|
|
42
|
+
} : null;
|
|
43
|
+
return {
|
|
44
|
+
name: typeof data.name === "string" ? data.name : "",
|
|
45
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
46
|
+
meta,
|
|
47
|
+
rawMeta: m,
|
|
48
|
+
body,
|
|
49
|
+
sections: [...body.matchAll(/^##\s+(.+?)\s*$/gm)].map((x) => x[1]),
|
|
50
|
+
frontmatter: data
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function renderSkill(skill) {
|
|
54
|
+
const fm = {
|
|
55
|
+
name: skill.name,
|
|
56
|
+
description: skill.description,
|
|
57
|
+
...skill.extra ?? {},
|
|
58
|
+
metadata: { generated_by: GENERATED_BY },
|
|
59
|
+
skill_loop: {
|
|
60
|
+
schema: 1,
|
|
61
|
+
scope: skill.meta.scope,
|
|
62
|
+
status: skill.meta.status,
|
|
63
|
+
pinned: skill.meta.pinned,
|
|
64
|
+
triggers: skill.meta.triggers,
|
|
65
|
+
applies_to: skill.meta.applies_to,
|
|
66
|
+
provenance: skill.meta.provenance,
|
|
67
|
+
cites: skill.meta.cites,
|
|
68
|
+
supersedes: skill.meta.supersedes
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return `---
|
|
72
|
+
${YAML.stringify(fm, { lineWidth: 0 })}---
|
|
73
|
+
|
|
74
|
+
${skill.body.trim()}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".semantic-sidekick-index", ".planning"]);
|
|
78
|
+
function skillsIn(skillsDir, scope) {
|
|
79
|
+
let entries = [];
|
|
80
|
+
try {
|
|
81
|
+
entries = readdirSync(skillsDir);
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const e of entries) {
|
|
87
|
+
const path = join(skillsDir, e, "SKILL.md");
|
|
88
|
+
try {
|
|
89
|
+
const skill = parseSkill(readFileSync(path, "utf8"));
|
|
90
|
+
out.push({ path, dir: join(skillsDir, e), scope, learned: isLearned(skill), skill });
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
var isLearned = (s) => s.meta !== null || s.frontmatter.metadata?.generated_by === GENERATED_BY;
|
|
97
|
+
function directorySkillDirs(root, depth = 6) {
|
|
98
|
+
const out = [];
|
|
99
|
+
const walk = (dir, d) => {
|
|
100
|
+
if (d > depth) return;
|
|
101
|
+
let entries = [];
|
|
102
|
+
try {
|
|
103
|
+
entries = readdirSync(dir);
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const e of entries) {
|
|
108
|
+
if (SKIP_DIRS.has(e) || e.startsWith(".") && e !== ".claude") continue;
|
|
109
|
+
const p = join(dir, e);
|
|
110
|
+
let st;
|
|
111
|
+
try {
|
|
112
|
+
st = statSync(p);
|
|
113
|
+
} catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!st.isDirectory()) continue;
|
|
117
|
+
if (e === ".claude") {
|
|
118
|
+
if (dir !== root && existsSync(join(p, "skills"))) out.push(join(p, "skills"));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
walk(p, d + 1);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
walk(root, 0);
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function findSkills(projectRoot, opts = {}) {
|
|
128
|
+
const home = opts.home ?? process.env.HOME ?? homedir();
|
|
129
|
+
return [
|
|
130
|
+
...skillsIn(join(projectRoot, ".claude", "skills"), "project"),
|
|
131
|
+
...directorySkillDirs(projectRoot).flatMap((d) => skillsIn(d, "directory")),
|
|
132
|
+
...opts.global === false ? [] : skillsIn(join(home, ".claude", "skills"), "global")
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
function factIds(projectRoot) {
|
|
136
|
+
let kb = join(projectRoot, ".documentation", "knowledge-base");
|
|
137
|
+
try {
|
|
138
|
+
const cfg = JSON.parse(readFileSync(join(projectRoot, ".claude", "hit-em-with-the-docs.json"), "utf8"));
|
|
139
|
+
if (typeof cfg?.vault?.root === "string") kb = join(projectRoot, cfg.vault.root);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return new Set(readdirSync(join(kb, "facts")).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")));
|
|
144
|
+
} catch {
|
|
145
|
+
return /* @__PURE__ */ new Set();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
149
|
+
var VAGUE = /\b(helps? with|handles?|deals? with|manages?|various|stuff|things|misc(ellaneous)?|general purpose)\b/i;
|
|
150
|
+
var DESCRIPTION_MAX = 1024;
|
|
151
|
+
var DESCRIPTION_MIN = 40;
|
|
152
|
+
function lintSkill(path, raw, ctx, dirName) {
|
|
153
|
+
const out = [];
|
|
154
|
+
const add = (rule, severity, message) => out.push({ path, rule, severity, message });
|
|
155
|
+
const s = parseSkill(raw);
|
|
156
|
+
if (!raw.startsWith("---")) add("frontmatter", "error", "no YAML frontmatter");
|
|
157
|
+
if (!s.name) add("name", "error", "missing `name`");
|
|
158
|
+
else if (!NAME_RE.test(s.name) || s.name.length > 64) add("name", "error", `name "${s.name}" is not kebab-case (a-z, 0-9, -; at most 64)`);
|
|
159
|
+
else if (dirName && dirName !== s.name) add("name", "error", `name "${s.name}" differs from its folder "${dirName}"`);
|
|
160
|
+
const d = s.description.trim();
|
|
161
|
+
if (!d) add("description", "error", "missing `description` (the only text Claude sees before loading the skill)");
|
|
162
|
+
else {
|
|
163
|
+
if (d.length < DESCRIPTION_MIN) add("description", "error", `description is ${d.length} chars; say what it does and when to use it (>= ${DESCRIPTION_MIN})`);
|
|
164
|
+
if (d.length > DESCRIPTION_MAX) add("description", "error", `description is ${d.length} chars (max ${DESCRIPTION_MAX})`);
|
|
165
|
+
if (!/\bwhen\b|\bbefore\b|\bafter\b|\bif\b/i.test(d)) add("description", "error", 'description does not say when to use it (e.g. "Use when \u2026")');
|
|
166
|
+
if (VAGUE.test(d)) add("description", "warn", `description uses a vague verb ("${d.match(VAGUE)[0]}"): name the concrete action`);
|
|
167
|
+
}
|
|
168
|
+
const m = s.meta;
|
|
169
|
+
if (!m) {
|
|
170
|
+
add("skill_loop", "error", "missing `skill_loop:` block");
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
if (m.schema !== 1) add("skill_loop", "error", `skill_loop.schema must be 1 (got ${JSON.stringify(m.schema)})`);
|
|
174
|
+
if (!SKILL_SCOPES.includes(m.scope)) add("skill_loop", "error", `skill_loop.scope must be one of ${SKILL_SCOPES.join(", ")}`);
|
|
175
|
+
if (!SKILL_STATUSES.includes(m.status)) add("skill_loop", "error", `skill_loop.status must be one of ${SKILL_STATUSES.join(", ")}`);
|
|
176
|
+
if (m.triggers.length === 0) add("skill_loop", "warn", "no `triggers`: list the literal errors, commands or phrases this skill answers");
|
|
177
|
+
const evidence = m.provenance.sessions.length + m.provenance.failures.length + m.provenance.corrections.length;
|
|
178
|
+
if (evidence === 0) add("provenance", "error", "no provenance: a learned skill needs the session, failure or correction it came from");
|
|
179
|
+
for (const want of SKILL_SECTIONS) {
|
|
180
|
+
if (!s.sections.some((h) => h.toLowerCase() === want.toLowerCase())) add("sections", "error", `missing section "## ${want}"`);
|
|
181
|
+
}
|
|
182
|
+
if (m.cites.length && m.scope !== "global") {
|
|
183
|
+
const facts = ctx.facts ?? factIds(ctx.projectRoot);
|
|
184
|
+
for (const c of m.cites) if (!facts.has(c.replace(/\.md$/, "").split("/").pop())) add("cites", "error", `cites "${c}", which is not a knowledge-base fact here`);
|
|
185
|
+
}
|
|
186
|
+
if (m.scope !== "global") {
|
|
187
|
+
for (const p of m.applies_to.paths) {
|
|
188
|
+
if (!existsSync(join(ctx.projectRoot, p))) add("applies_to", "error", `applies_to path "${p}" does not exist in this repo`);
|
|
189
|
+
}
|
|
190
|
+
if (m.scope === "directory" && m.applies_to.paths.length === 0) add("applies_to", "error", "a directory-scoped skill needs applies_to.paths");
|
|
191
|
+
}
|
|
192
|
+
for (const hit of findSecrets(raw)) add("secrets", "error", `contains a ${hit.kind} at line ${hit.line}`);
|
|
193
|
+
if (s.name) {
|
|
194
|
+
for (const o of ctx.others ?? []) {
|
|
195
|
+
if (o.path === path || o.learned) continue;
|
|
196
|
+
if (basename(o.dir) === s.name || o.skill.name === s.name) {
|
|
197
|
+
add("name_clash", "error", `a ${o.scope} skill not written by the skill loop already uses the name "${s.name}" (${o.path})`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
function lintSkills(projectRoot, opts = {}) {
|
|
204
|
+
const all = findSkills(projectRoot, { home: opts.home });
|
|
205
|
+
const facts = factIds(projectRoot);
|
|
206
|
+
const findings = [];
|
|
207
|
+
let files = 0;
|
|
208
|
+
for (const f of all.filter((x) => x.learned)) {
|
|
209
|
+
files++;
|
|
210
|
+
findings.push(...lintSkill(f.path, readFileSync(f.path, "utf8"), { projectRoot, facts, others: all }, basename(f.dir)));
|
|
211
|
+
}
|
|
212
|
+
for (const p of opts.extra ?? []) {
|
|
213
|
+
files++;
|
|
214
|
+
findings.push(...lintSkill(p, readFileSync(p, "utf8"), { projectRoot, facts, others: all }));
|
|
215
|
+
}
|
|
216
|
+
return { files, findings };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export {
|
|
220
|
+
SKILL_SCOPES,
|
|
221
|
+
SKILL_STATUSES,
|
|
222
|
+
SKILL_SECTIONS,
|
|
223
|
+
GENERATED_BY,
|
|
224
|
+
parseSkill,
|
|
225
|
+
renderSkill,
|
|
226
|
+
isLearned,
|
|
227
|
+
findSkills,
|
|
228
|
+
factIds,
|
|
229
|
+
lintSkill,
|
|
230
|
+
lintSkills
|
|
231
|
+
};
|
|
232
|
+
//# sourceMappingURL=chunk-ZSWI3Y4Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-schema.ts"],"sourcesContent":["/**\n * The skill loop's skill format (v1.7): an ordinary Claude Code SKILL.md plus a `skill_loop:` frontmatter\n * block that marks it as learned and carries its scope, status and evidence. Claude Code ignores keys it\n * does not know (verified on 2.1.310), so a learned skill loads like any other. `metadata.generated_by`\n * is the string-only marker the agentskills.io spec reserves for clients.\n *\n * ---\n * name: <kebab-name>\n * description: <what it does + when to use it>\n * metadata: { generated_by: semantic-memory }\n * skill_loop:\n * schema: 1\n * scope: project # directory | project | global\n * status: active # draft | active | stale | archived\n * pinned: false\n * triggers: [...] # literal errors / commands / phrases (not `symptoms`: hewtd #46)\n * applies_to: { paths: [...], commands: [...] }\n * provenance: { sessions: [...], failures: [...], corrections: [...] }\n * cites: [<kb-fact-id>]\n * supersedes: []\n * ---\n * ## When to use · ## Steps · ## Pitfalls · ## Verify\n *\n * Counters (uses, last used) never live in the file: see skill-usage.jsonl.\n */\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, basename } from \"node:path\";\nimport YAML from \"yaml\";\nimport matter from \"./matter.js\";\nimport { findSecrets } from \"./redact.js\";\n\nexport const SKILL_SCOPES = [\"directory\", \"project\", \"global\"] as const;\nexport const SKILL_STATUSES = [\"draft\", \"active\", \"stale\", \"archived\"] as const;\nexport const SKILL_SECTIONS = [\"When to use\", \"Steps\", \"Pitfalls\", \"Verify\"] as const;\nexport const GENERATED_BY = \"semantic-memory\";\n\nexport type SkillScope = (typeof SKILL_SCOPES)[number];\nexport type SkillStatus = (typeof SKILL_STATUSES)[number];\n\nexport interface SkillLoopMeta {\n schema: 1;\n scope: SkillScope;\n status: SkillStatus;\n pinned: boolean;\n triggers: string[];\n applies_to: { paths: string[]; commands: string[] };\n provenance: { sessions: string[]; failures: string[]; corrections: string[] };\n cites: string[];\n supersedes: string[];\n}\n\nexport interface ParsedSkill {\n name: string;\n description: string;\n /** null when the file has no `skill_loop:` block: not a learned skill */\n meta: SkillLoopMeta | null;\n /** the raw `skill_loop` value, for lint messages on a malformed block */\n rawMeta: unknown;\n body: string;\n /** `## ` headings in the body, in order */\n sections: string[];\n frontmatter: Record<string, unknown>;\n}\n\nconst strings = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x) => typeof x === \"string\") : []);\n\nexport function parseSkill(raw: string): ParsedSkill {\n let data: Record<string, any> = {};\n let body = raw;\n try {\n const parsed = matter(raw);\n data = parsed.data ?? {};\n body = parsed.content;\n } catch {\n // malformed frontmatter: Claude Code still loads the skill with no fields; lint reports it\n }\n const m = data.skill_loop;\n const meta: SkillLoopMeta | null =\n m && typeof m === \"object\"\n ? {\n schema: m.schema,\n scope: m.scope,\n status: m.status,\n pinned: m.pinned === true,\n triggers: strings(m.triggers),\n applies_to: { paths: strings(m.applies_to?.paths), commands: strings(m.applies_to?.commands) },\n provenance: {\n sessions: strings(m.provenance?.sessions),\n failures: strings(m.provenance?.failures),\n corrections: strings(m.provenance?.corrections),\n },\n cites: strings(m.cites),\n supersedes: strings(m.supersedes),\n }\n : null;\n return {\n name: typeof data.name === \"string\" ? data.name : \"\",\n description: typeof data.description === \"string\" ? data.description : \"\",\n meta,\n rawMeta: m,\n body,\n sections: [...body.matchAll(/^##\\s+(.+?)\\s*$/gm)].map((x) => x[1]),\n frontmatter: data,\n };\n}\n\n/** A learned skill's SKILL.md text. Field order is fixed so a re-render of an unchanged skill is byte-identical. */\nexport function renderSkill(skill: { name: string; description: string; meta: SkillLoopMeta; body: string; extra?: Record<string, unknown> }): string {\n const fm = {\n name: skill.name,\n description: skill.description,\n ...(skill.extra ?? {}),\n metadata: { generated_by: GENERATED_BY },\n skill_loop: {\n schema: 1,\n scope: skill.meta.scope,\n status: skill.meta.status,\n pinned: skill.meta.pinned,\n triggers: skill.meta.triggers,\n applies_to: skill.meta.applies_to,\n provenance: skill.meta.provenance,\n cites: skill.meta.cites,\n supersedes: skill.meta.supersedes,\n },\n };\n return `---\\n${YAML.stringify(fm, { lineWidth: 0 })}---\\n\\n${skill.body.trim()}\\n`;\n}\n\n// --- discovery ---\n\nexport interface SkillFile {\n path: string;\n /** directory holding SKILL.md; its basename is the skill's name in Claude Code */\n dir: string;\n scope: SkillScope;\n learned: boolean;\n skill: ParsedSkill;\n}\n\nconst SKIP_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \".semantic-sidekick-index\", \".planning\"]);\n\nfunction skillsIn(skillsDir: string, scope: SkillScope): SkillFile[] {\n let entries: string[] = [];\n try {\n entries = readdirSync(skillsDir);\n } catch {\n return [];\n }\n const out: SkillFile[] = [];\n for (const e of entries) {\n const path = join(skillsDir, e, \"SKILL.md\");\n try {\n const skill = parseSkill(readFileSync(path, \"utf8\"));\n out.push({ path, dir: join(skillsDir, e), scope, learned: isLearned(skill), skill });\n } catch {}\n }\n return out;\n}\n\nexport const isLearned = (s: ParsedSkill) =>\n s.meta !== null || (s.frontmatter.metadata as Record<string, unknown> | undefined)?.generated_by === GENERATED_BY;\n\n/** `<subdir>/.claude/skills` folders below the repo root (not the root's own), skipping vendored trees. */\nfunction directorySkillDirs(root: string, depth = 6): string[] {\n const out: string[] = [];\n const walk = (dir: string, d: number) => {\n if (d > depth) return;\n let entries: string[] = [];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const e of entries) {\n if (SKIP_DIRS.has(e) || (e.startsWith(\".\") && e !== \".claude\")) continue;\n const p = join(dir, e);\n let st;\n try {\n st = statSync(p);\n } catch {\n continue;\n }\n if (!st.isDirectory()) continue;\n if (e === \".claude\") {\n if (dir !== root && existsSync(join(p, \"skills\"))) out.push(join(p, \"skills\"));\n continue;\n }\n walk(p, d + 1);\n }\n };\n walk(root, 0);\n return out;\n}\n\n/** Every skill Claude Code would load for this repo: directory, project and global scope. */\nexport function findSkills(projectRoot: string, opts: { home?: string; global?: boolean } = {}): SkillFile[] {\n const home = opts.home ?? process.env.HOME ?? homedir();\n return [\n ...skillsIn(join(projectRoot, \".claude\", \"skills\"), \"project\"),\n ...directorySkillDirs(projectRoot).flatMap((d) => skillsIn(d, \"directory\")),\n ...(opts.global === false ? [] : skillsIn(join(home, \".claude\", \"skills\"), \"global\")),\n ];\n}\n\n/** Knowledge-base fact ids (file basenames) in this repo, for `cites`. */\nexport function factIds(projectRoot: string): Set<string> {\n let kb = join(projectRoot, \".documentation\", \"knowledge-base\");\n try {\n const cfg = JSON.parse(readFileSync(join(projectRoot, \".claude\", \"hit-em-with-the-docs.json\"), \"utf8\"));\n if (typeof cfg?.vault?.root === \"string\") kb = join(projectRoot, cfg.vault.root);\n } catch {}\n try {\n return new Set(readdirSync(join(kb, \"facts\")).filter((f) => f.endsWith(\".md\")).map((f) => f.replace(/\\.md$/, \"\")));\n } catch {\n return new Set();\n }\n}\n\n// --- lint ---\n\nexport interface SkillFinding {\n path: string;\n rule:\n | \"frontmatter\"\n | \"name\"\n | \"description\"\n | \"skill_loop\"\n | \"provenance\"\n | \"sections\"\n | \"cites\"\n | \"applies_to\"\n | \"secrets\"\n | \"name_clash\";\n severity: \"error\" | \"warn\";\n message: string;\n}\n\nconst NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n// verbs that say nothing about what the skill does: Claude cannot tell when to load it\nconst VAGUE = /\\b(helps? with|handles?|deals? with|manages?|various|stuff|things|misc(ellaneous)?|general purpose)\\b/i;\nconst DESCRIPTION_MAX = 1024;\nconst DESCRIPTION_MIN = 40;\n\nexport interface SkillLintContext {\n /** repo root the skill belongs to (applies_to paths resolve against it) */\n projectRoot: string;\n facts?: Set<string>;\n /** every other skill Claude Code would load alongside (for name clashes) */\n others?: SkillFile[];\n}\n\n/** Lint one learned skill. `dirName` is the folder name Claude Code will use (defaults to the skill's name). */\nexport function lintSkill(path: string, raw: string, ctx: SkillLintContext, dirName?: string): SkillFinding[] {\n const out: SkillFinding[] = [];\n const add = (rule: SkillFinding[\"rule\"], severity: SkillFinding[\"severity\"], message: string) => out.push({ path, rule, severity, message });\n const s = parseSkill(raw);\n if (!raw.startsWith(\"---\")) add(\"frontmatter\", \"error\", \"no YAML frontmatter\");\n if (!s.name) add(\"name\", \"error\", \"missing `name`\");\n else if (!NAME_RE.test(s.name) || s.name.length > 64) add(\"name\", \"error\", `name \"${s.name}\" is not kebab-case (a-z, 0-9, -; at most 64)`);\n else if (dirName && dirName !== s.name) add(\"name\", \"error\", `name \"${s.name}\" differs from its folder \"${dirName}\"`);\n\n const d = s.description.trim();\n if (!d) add(\"description\", \"error\", \"missing `description` (the only text Claude sees before loading the skill)\");\n else {\n if (d.length < DESCRIPTION_MIN) add(\"description\", \"error\", `description is ${d.length} chars; say what it does and when to use it (>= ${DESCRIPTION_MIN})`);\n if (d.length > DESCRIPTION_MAX) add(\"description\", \"error\", `description is ${d.length} chars (max ${DESCRIPTION_MAX})`);\n if (!/\\bwhen\\b|\\bbefore\\b|\\bafter\\b|\\bif\\b/i.test(d)) add(\"description\", \"error\", \"description does not say when to use it (e.g. \\\"Use when …\\\")\");\n if (VAGUE.test(d)) add(\"description\", \"warn\", `description uses a vague verb (\"${d.match(VAGUE)![0]}\"): name the concrete action`);\n }\n\n const m = s.meta;\n if (!m) {\n add(\"skill_loop\", \"error\", \"missing `skill_loop:` block\");\n return out;\n }\n if (m.schema !== 1) add(\"skill_loop\", \"error\", `skill_loop.schema must be 1 (got ${JSON.stringify(m.schema)})`);\n if (!SKILL_SCOPES.includes(m.scope)) add(\"skill_loop\", \"error\", `skill_loop.scope must be one of ${SKILL_SCOPES.join(\", \")}`);\n if (!SKILL_STATUSES.includes(m.status)) add(\"skill_loop\", \"error\", `skill_loop.status must be one of ${SKILL_STATUSES.join(\", \")}`);\n if (m.triggers.length === 0) add(\"skill_loop\", \"warn\", \"no `triggers`: list the literal errors, commands or phrases this skill answers\");\n const evidence = m.provenance.sessions.length + m.provenance.failures.length + m.provenance.corrections.length;\n if (evidence === 0) add(\"provenance\", \"error\", \"no provenance: a learned skill needs the session, failure or correction it came from\");\n\n for (const want of SKILL_SECTIONS) {\n if (!s.sections.some((h) => h.toLowerCase() === want.toLowerCase())) add(\"sections\", \"error\", `missing section \"## ${want}\"`);\n }\n\n // a global skill serves every repo: its facts are not this repo's, so cites resolve only for directory / project\n if (m.cites.length && m.scope !== \"global\") {\n const facts = ctx.facts ?? factIds(ctx.projectRoot);\n for (const c of m.cites) if (!facts.has(c.replace(/\\.md$/, \"\").split(\"/\").pop()!)) add(\"cites\", \"error\", `cites \"${c}\", which is not a knowledge-base fact here`);\n }\n\n if (m.scope !== \"global\") {\n for (const p of m.applies_to.paths) {\n if (!existsSync(join(ctx.projectRoot, p))) add(\"applies_to\", \"error\", `applies_to path \"${p}\" does not exist in this repo`);\n }\n if (m.scope === \"directory\" && m.applies_to.paths.length === 0) add(\"applies_to\", \"error\", \"a directory-scoped skill needs applies_to.paths\");\n }\n\n for (const hit of findSecrets(raw)) add(\"secrets\", \"error\", `contains a ${hit.kind} at line ${hit.line}`);\n\n if (s.name) {\n for (const o of ctx.others ?? []) {\n if (o.path === path || o.learned) continue;\n if (basename(o.dir) === s.name || o.skill.name === s.name) {\n add(\"name_clash\", \"error\", `a ${o.scope} skill not written by the skill loop already uses the name \"${s.name}\" (${o.path})`);\n }\n }\n }\n return out;\n}\n\n/** Lint every learned skill this repo would load (and its drafts, when given). */\nexport function lintSkills(projectRoot: string, opts: { home?: string; extra?: string[] } = {}): { files: number; findings: SkillFinding[] } {\n const all = findSkills(projectRoot, { home: opts.home });\n const facts = factIds(projectRoot);\n const findings: SkillFinding[] = [];\n let files = 0;\n for (const f of all.filter((x) => x.learned)) {\n files++;\n findings.push(...lintSkill(f.path, readFileSync(f.path, \"utf8\"), { projectRoot, facts, others: all }, basename(f.dir)));\n }\n for (const p of opts.extra ?? []) {\n files++;\n findings.push(...lintSkill(p, readFileSync(p, \"utf8\"), { projectRoot, facts, others: all }));\n }\n return { files, findings };\n}\n"],"mappings":"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","names":[]}
|