@theglitchking/semantic-memory 1.6.0 → 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 +90 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +49 -29
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/{chunk-7PNF3AP2.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3FPUBAEW.js +113 -0
- package/dist/chunk-3FPUBAEW.js.map +1 -0
- package/dist/{chunk-WZEKIUYG.js → chunk-6TIKWOY4.js} +10 -8
- package/dist/{chunk-WZEKIUYG.js.map → chunk-6TIKWOY4.js.map} +1 -1
- package/dist/{chunk-NBY4TRP7.js → chunk-7M34JDTH.js} +3 -3
- package/dist/{chunk-FTRXH2WC.js → chunk-7UAVDOI6.js} +15 -12
- package/dist/chunk-7UAVDOI6.js.map +1 -0
- package/dist/{chunk-BBP3DG2U.js → chunk-AIXQFTSB.js} +25 -26
- package/dist/{chunk-BBP3DG2U.js.map → chunk-AIXQFTSB.js.map} +1 -1
- 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-IGHCVAAL.js → chunk-CJUY55GG.js} +6 -3
- package/dist/{chunk-IGHCVAAL.js.map → chunk-CJUY55GG.js.map} +1 -1
- package/dist/chunk-DBFD3MTT.js +130 -0
- package/dist/chunk-DBFD3MTT.js.map +1 -0
- package/dist/{chunk-XAXK7NM4.js → chunk-EXAUVIGW.js} +9 -7
- package/dist/{chunk-XAXK7NM4.js.map → chunk-EXAUVIGW.js.map} +1 -1
- package/dist/{chunk-LGQPP5FQ.js → chunk-EYVFC745.js} +2 -2
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/{chunk-OEF5U5GV.js → chunk-H2BES6FU.js} +10 -2
- package/dist/{chunk-OEF5U5GV.js.map → chunk-H2BES6FU.js.map} +1 -1
- package/dist/chunk-IEZVFKUT.js +157 -0
- package/dist/chunk-IEZVFKUT.js.map +1 -0
- package/dist/chunk-J7LYDRML.js +9 -0
- package/dist/chunk-J7LYDRML.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-44BQDRXD.js → chunk-SHSOCFJH.js} +4 -1
- package/dist/chunk-SHSOCFJH.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/{chunk-CAX46XLY.js → chunk-VBLCUWT4.js} +23 -12
- 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-SX427M2R.js → chunk-XKGK643R.js} +2 -2
- package/dist/{chunk-FURCIJBO.js → chunk-Y7H76Q7F.js} +6 -3
- 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 +389 -60
- 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 -20
- 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/redact.js +1 -1
- 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-RWTIMGZN.js → dossier-RXBLQOXC.js} +5 -4
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/healthcheck-S7D6YNTA.js +21 -0
- package/dist/indexer-RZPH2PVL.js +9 -0
- package/dist/{lexicon-IVNJTMWE.js → lexicon-SEJSLBHO.js} +6 -5
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/lint-3JNLRM5U.js +18 -0
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/log-RM2NDNPH.js +11 -0
- package/dist/log-RM2NDNPH.js.map +1 -0
- package/dist/matter-SNJ2WDL6.js +8 -0
- package/dist/matter-SNJ2WDL6.js.map +1 -0
- package/dist/mcp/server.js +43 -38
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-UE4YVTDC.js → profile-SNULTJ27.js} +6 -5
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-LY6533NJ.js → schema-RDYUK6CC.js} +3 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/{search-MZC4DC7H.js → search-325ULY57.js} +4 -3
- package/dist/{search-MZC4DC7H.js.map → search-325ULY57.js.map} +1 -1
- 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/dist/{telemetry-B4KP3UVE.js → telemetry-YAG3VA4Y.js} +4 -4
- package/dist/telemetry-YAG3VA4Y.js.map +1 -0
- package/docs/regression-snapshot.md +5 -5
- 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-44BQDRXD.js.map +0 -1
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-CAX46XLY.js.map +0 -1
- package/dist/chunk-FTRXH2WC.js.map +0 -1
- package/dist/chunk-FURCIJBO.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- package/dist/healthcheck-3FSNL2U7.js +0 -20
- package/dist/indexer-RAKJ7JR3.js +0 -8
- package/dist/lint-36WTW762.js +0 -17
- package/dist/log-YIXF6CYM.js +0 -11
- /package/dist/{chunk-7PNF3AP2.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-NBY4TRP7.js.map → chunk-7M34JDTH.js.map} +0 -0
- /package/dist/{chunk-LGQPP5FQ.js.map → chunk-EYVFC745.js.map} +0 -0
- /package/dist/{chunk-SX427M2R.js.map → chunk-XKGK643R.js.map} +0 -0
- /package/dist/{dossier-RWTIMGZN.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-3FSNL2U7.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{indexer-RAKJ7JR3.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lexicon-IVNJTMWE.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{lint-36WTW762.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{log-YIXF6CYM.js.map → dossier-RXBLQOXC.js.map} +0 -0
- /package/dist/{profile-UE4YVTDC.js.map → failure-eval-IUJO47UC.js.map} +0 -0
- /package/dist/{schema-LY6533NJ.js.map → healthcheck-S7D6YNTA.js.map} +0 -0
- /package/dist/{telemetry-B4KP3UVE.js.map → indexer-RZPH2PVL.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-land.ts","../src/core/skill-gate.ts"],"sourcesContent":["/**\n * Landing drafts (v1.7): `skill-loop review`. Nothing a reviewer drafts reaches .claude/skills/ until a\n * person approves it here; every landing, patch, promotion and rejection is a ledger entry (skill-ledger.ts).\n *\n * - new draft → <repo>/.claude/skills/<name>/ (project) or <repo>/<dir>/.claude/skills/<name>/ (directory)\n * - patch draft → replaces its target, only if the target is byte-for-byte what the draft was made from\n * - promotion → ~/.claude/skills/<name>/ (global), replacing the landed project copies in the other repos\n *\n * Evals run here, on request: trigger evals freely (no tools that touch anything), task evals only with\n * --task, since they run the draft's own Verify command.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { applyChange, type LedgerEntry } from \"./skill-ledger.js\";\nimport { listDrafts, transcriptTimeline, type DraftRecord } from \"./skill-review.js\";\nimport { findSkills, lintSkill, parseSkill, renderSkill, type SkillLoopMeta } from \"./skill-schema.js\";\nimport { indexSkill } from \"./skill-scope.js\";\nimport { queueDir } from \"./skill-queue.js\";\nimport { buildEvalSet, type CandidatePrompt } from \"./skill-eval/sets.js\";\nimport { runTrigger } from \"./skill-eval/trigger.js\";\nimport { runTaskEval, parseVerify } from \"./skill-eval/task.js\";\nimport { gateSkill } from \"./skill-gate.js\";\n\nconst sha256 = (s: string | Buffer) => createHash(\"sha256\").update(s).digest(\"hex\");\nconst home = (env = process.env) => env.HOME || homedir();\n\nexport function destinationFor(repo: string, r: DraftRecord, opts: { global?: boolean; env?: NodeJS.ProcessEnv } = {}): string {\n if (opts.global) return join(home(opts.env), \".claude\", \"skills\", r.name, \"SKILL.md\");\n if (r.kind === \"patch\" && r.target) return r.target;\n return r.scope === \"directory\" && r.directory ? join(repo, r.directory, \".claude\", \"skills\", r.name, \"SKILL.md\") : join(repo, \".claude\", \"skills\", r.name, \"SKILL.md\");\n}\n\nfunction getDraft(repo: string, id: string) {\n const d = listDrafts(repo).find((x) => x.record.id === id);\n if (!d) throw new Error(`no draft ${id} (skill-loop review lists them)`);\n return { ...d, text: readFileSync(join(d.dir, \"SKILL.md\"), \"utf8\") };\n}\n\nfunction diff(a: string, b: string): string {\n const r = spawnSync(\"git\", [\"diff\", \"--no-index\", \"--no-color\", \"--\", a, b], { encoding: \"utf8\" });\n return r.stdout || \"\";\n}\n\nexport interface DraftView {\n id: string;\n kind: DraftRecord[\"kind\"];\n name: string;\n scope: string;\n destination: string;\n scope_why?: string;\n provenance: SkillLoopMeta[\"provenance\"] | null;\n warnings: string[];\n evals?: Record<string, unknown>;\n promotion?: DraftRecord[\"promotion\"];\n global_candidate?: boolean;\n /** the Verify command a task eval would run, shown before anyone runs it */\n verify: ReturnType<typeof parseVerify>;\n text: string;\n diff?: string;\n}\n\nexport function describeDrafts(repo: string, env = process.env): DraftView[] {\n return listDrafts(repo).map(({ dir, record }) => {\n const file = join(dir, \"SKILL.md\");\n const text = readFileSync(file, \"utf8\");\n const s = parseSkill(text);\n return {\n id: record.id,\n kind: record.kind,\n name: record.name,\n scope: record.scope,\n destination: destinationFor(repo, record, { env }),\n scope_why: record.scope_why,\n provenance: s.meta?.provenance ?? null,\n warnings: record.warnings,\n evals: record.evals,\n promotion: record.promotion,\n global_candidate: record.global_candidate,\n verify: parseVerify(s.body),\n text,\n ...(record.kind === \"patch\" && record.target && existsSync(record.target) ? { diff: diff(record.target, file) } : {}),\n };\n });\n}\n\n/** The draft's text as it will land: status active, and for a promotion global with nothing repo-specific. */\nfunction landedText(text: string, global: boolean): string {\n const s = parseSkill(text);\n const meta: SkillLoopMeta = {\n ...s.meta!,\n status: \"active\",\n ...(global ? { scope: \"global\" as const, applies_to: { paths: [], commands: s.meta!.applies_to.commands }, cites: [] } : {}),\n };\n return renderSkill({ name: s.name, description: s.description, meta, body: s.body });\n}\n\nexport function approveDraft(repo: string, id: string, opts: { promote?: boolean; override?: string; env?: NodeJS.ProcessEnv } = {}): LedgerEntry {\n const env = opts.env ?? process.env;\n const d = getDraft(repo, id);\n const r = d.record;\n // evals are optional, but once they ran a failing draft stays a draft unless a person overrides (ledgered)\n const gate = (r.evals as { gate?: { verdict: string; reasons: string[] } } | undefined)?.gate;\n if (gate && gate.verdict !== \"adopt\" && !opts.override) {\n throw new Error(`the eval gate said ${gate.verdict}: ${gate.reasons.join(\"; \")} — re-run --eval, or approve with --override \"<why>\"`);\n }\n if (opts.promote && !r.promotion?.length) throw new Error(`${id} has no promotion: a skill goes global only when another repo has the same lesson`);\n const dest = destinationFor(repo, r, { global: opts.promote, env });\n const text = landedText(d.text, !!opts.promote);\n const others = findSkills(repo, { home: home(env) }).filter((s) => s.path !== dest);\n const errors = lintSkill(dest, text, { projectRoot: repo, others }, r.name).filter((f) => f.severity === \"error\");\n if (errors.length) throw new Error(`does not pass lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join(\"; \")}`);\n const replaced = r.kind === \"merge\" ? (r.supersedes ?? []) : [];\n if (r.kind === \"patch\") {\n if (!existsSync(dest) || sha256(readFileSync(dest)) !== r.base_sha256) throw new Error(`${dest} changed since this patch was drafted: reject it and let the next review redo it`);\n } else if (existsSync(dest) && !replaced.includes(dest)) {\n throw new Error(`${dest} already exists`);\n }\n for (const p of replaced) if (!existsSync(p)) throw new Error(`${p}, which this merge replaces, is gone: reject the merge`);\n const why = `${r.kind}${opts.promote ? \" promoted to global\" : \"\"} from session ${r.session_id}${gate && gate.verdict !== \"adopt\" ? `; eval gate ${gate.verdict}, overridden: ${opts.override}` : \"\"}`;\n // a merge lands the new skill and archives the ones it supersedes, in one ledger entry (one rollback)\n const archived = replaced.filter((p) => p !== dest).flatMap((p) => [\n { path: join(repo, \".claude\", \".semantic-memory\", \"skills-archive\", basename(dirname(p)), \"SKILL.md\"), content: readFileSync(p) as Buffer | string | null },\n { path: p, content: null },\n ]);\n const entry = applyChange(repo, { action: opts.promote ? \"promote\" : r.kind === \"patch\" ? \"patch\" : r.kind === \"merge\" ? \"merge\" : \"land\", actor: \"user\", skill: r.name, why }, [{ path: dest, content: text }, ...archived]);\n for (const p of replaced) if (p !== dest) indexSkill({ repo, name: basename(dirname(p)), path: p, status: \"archived\" }, env);\n if (opts.promote) {\n // the project copies the global skill replaces: only landed, learned ones, each ledgered in its own repo\n for (const p of r.promotion!) {\n if (!existsSync(p.path) || !p.path.includes(`${join(\".claude\", \"skills\")}`) || !/^skill_loop:/m.test(readFileSync(p.path, \"utf8\"))) continue;\n applyChange(p.repo, { action: \"archive\", actor: \"user\", skill: r.name, why: `replaced by the global skill ${dest}` }, [\n { path: join(p.repo, \".claude\", \".semantic-memory\", \"skills-archive\", r.name, \"SKILL.md\"), content: readFileSync(p.path) },\n { path: p.path, content: null },\n ]);\n indexSkill({ repo: p.repo, name: r.name, path: p.path, status: \"archived\" }, env);\n }\n }\n indexSkill({ repo: opts.promote ? \"global\" : repo, name: r.name, path: dest, status: \"active\" }, env);\n rmSync(d.dir, { recursive: true, force: true });\n return entry;\n}\n\nexport function rejectDraft(repo: string, id: string, why = \"rejected in review\"): LedgerEntry {\n const d = getDraft(repo, id);\n const entry = applyChange(repo, { action: \"reject\", actor: \"user\", skill: d.record.name, why: `${why} (draft ${id}, session ${d.record.session_id})` }, []);\n rmSync(d.dir, { recursive: true, force: true });\n return entry;\n}\n\n/** User prompts from this repo's reviewed sessions: the material trigger evals are built from. */\nexport function reviewedPrompts(repo: string, env = process.env): CandidatePrompt[] {\n const dir = join(queueDir(env), \"done\");\n let files: string[] = [];\n try {\n files = readdirSync(dir);\n } catch {\n return [];\n }\n const out: CandidatePrompt[] = [];\n for (const f of files) {\n try {\n const job = JSON.parse(readFileSync(join(dir, f), \"utf8\"));\n if (job.repo !== repo) continue;\n for (const line of transcriptTimeline(job.transcript_path, 10_000)) {\n if (line.startsWith(\"USER: \")) out.push({ session: job.session_id, text: line.slice(6) });\n }\n } catch {}\n }\n return out;\n}\n\n/** Run the evals on a draft and store the scores and the gate's verdict in its record. */\nexport async function evalDraft(repo: string, id: string, opts: { task?: boolean; env?: NodeJS.ProcessEnv } = {}) {\n const env = opts.env ?? process.env;\n const d = getDraft(repo, id);\n const skill = parseSkill(d.text);\n const prompts = reviewedPrompts(repo, env);\n const set = buildEvalSet(skill, prompts);\n const trigger = await runTrigger(set, { repoRoot: repo, skillRaw: d.text, env });\n let taskWith: number | null = null, taskWithout: number | null = null, task: unknown;\n if (opts.task) {\n const own = prompts.find((p) => skill.meta?.provenance.sessions.includes(p.session));\n if (own) {\n const t = await runTaskEval({ repoRoot: repo, skillRaw: d.text, prompt: own.text, env });\n taskWith = t.with.passed ? 1 : 0;\n taskWithout = t.without.passed ? 1 : 0;\n task = { with: t.with.evidence, without: t.without.evidence };\n }\n }\n const gate = gateSkill({ positiveRate: trigger.heldout.positiveRate, falseTriggerRate: trigger.heldout.falseTriggerRate, taskWith, taskWithout });\n const evals = { at: new Date().toISOString(), prompts: set.length, runs: trigger.runs, heldout: trigger.heldout, train: trigger.train, task, gate };\n writeFileSync(join(d.dir, \"draft.json\"), JSON.stringify({ ...d.record, evals }, null, 1));\n return evals;\n}\n","/**\n * Adoption gate for learned skills (v1.7): turns eval scores into adopt / keep-draft / reject. Pure — no I/O.\n * A new skill must trigger on its own evidence, stay quiet on near-misses and beat doing the task without it;\n * a patch must not make any score worse. A failing draft stays a draft with its scores.\n */\n\n/** A skill that fires on fewer than half its own held-out prompts is not findable. */\nexport const GATE_MIN_POSITIVE_RATE = 0.5;\n/** More than a quarter false triggers on near-misses means it would hijack unrelated work. */\nexport const GATE_MAX_FALSE_TRIGGER_RATE = 0.25;\n\n/** Held-out trigger rates and task pass rates, each 0..1 or null when not measured. */\nexport interface SkillScores {\n positiveRate: number | null;\n falseTriggerRate: number | null;\n taskWith: number | null;\n taskWithout: number | null;\n}\n\nexport interface GateVerdict {\n verdict: \"adopt\" | \"keep-draft\" | \"reject\";\n reasons: string[];\n}\n\nconst pct = (n: number) => `${Math.round(n * 100)}%`;\n\nexport function gateSkill(candidate: SkillScores, current?: SkillScores): GateVerdict {\n return current ? gatePatch(candidate, current) : gateNew(candidate);\n}\n\nfunction gateNew(s: SkillScores): GateVerdict {\n const fails: string[] = [];\n if (s.positiveRate === null) fails.push(\"held-out positive trigger rate not measured\");\n else if (s.positiveRate < GATE_MIN_POSITIVE_RATE) fails.push(`positive trigger rate ${pct(s.positiveRate)} < ${pct(GATE_MIN_POSITIVE_RATE)}`);\n if (s.falseTriggerRate === null) fails.push(\"near-miss false-trigger rate not measured\");\n else {\n if (s.falseTriggerRate > GATE_MAX_FALSE_TRIGGER_RATE) fails.push(`false-trigger rate ${pct(s.falseTriggerRate)} > ${pct(GATE_MAX_FALSE_TRIGGER_RATE)}`);\n if (s.positiveRate !== null && s.positiveRate <= s.falseTriggerRate) fails.push(`positive rate ${pct(s.positiveRate)} does not beat false-trigger rate ${pct(s.falseTriggerRate)}`);\n }\n if (s.taskWith === null || s.taskWithout === null) fails.push(\"task check not measured\");\n else if (s.taskWith <= s.taskWithout) fails.push(`task pass ${pct(s.taskWith)} with the skill does not beat ${pct(s.taskWithout)} without`);\n return fails.length ? { verdict: \"keep-draft\", reasons: fails } : { verdict: \"adopt\", reasons: [\"all gate checks passed\"] };\n}\n\nfunction gatePatch(patched: SkillScores, current: SkillScores): GateVerdict {\n const regress: string[] = [];\n const unmeasured: string[] = [];\n const check = (label: string, now: number | null, before: number | null, higherIsBetter: boolean) => {\n if (before === null) return;\n if (now === null) return void unmeasured.push(`${label} not measured for the patch`);\n if (higherIsBetter ? now < before : now > before) regress.push(`${label} ${pct(before)} -> ${pct(now)}`);\n };\n check(\"positive trigger rate\", patched.positiveRate, current.positiveRate, true);\n check(\"false-trigger rate\", patched.falseTriggerRate, current.falseTriggerRate, false);\n check(\"task pass rate\", patched.taskWith, current.taskWith, true);\n if (regress.length) return { verdict: \"reject\", reasons: regress.map((r) => `regresses: ${r}`) };\n if (unmeasured.length) return { verdict: \"keep-draft\", reasons: unmeasured };\n return { verdict: \"adopt\", reasons: [\"no score dropped versus the current version\"] };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyChange,
|
|
3
|
+
archiveDir,
|
|
4
|
+
readLedger
|
|
5
|
+
} from "./chunk-TKHWVISV.js";
|
|
6
|
+
import {
|
|
7
|
+
indexSkill
|
|
8
|
+
} from "./chunk-3FPUBAEW.js";
|
|
9
|
+
import {
|
|
10
|
+
parseVerify
|
|
11
|
+
} from "./chunk-DBFD3MTT.js";
|
|
12
|
+
import {
|
|
13
|
+
findSkills,
|
|
14
|
+
parseSkill,
|
|
15
|
+
renderSkill
|
|
16
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
17
|
+
import {
|
|
18
|
+
readEvidence,
|
|
19
|
+
stateDir
|
|
20
|
+
} from "./chunk-R6GNP7DS.js";
|
|
21
|
+
|
|
22
|
+
// src/core/skill-curator.ts
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import { spawnSync } from "child_process";
|
|
26
|
+
var STALE_DAYS = 14;
|
|
27
|
+
var ARCHIVE_DAYS = 30;
|
|
28
|
+
var CURATOR_INTERVAL_DAYS = 7;
|
|
29
|
+
var IDLE_HOURS = 2;
|
|
30
|
+
var DAY = 864e5;
|
|
31
|
+
var curatorStatePath = (repo) => join(stateDir(repo), "curator.json");
|
|
32
|
+
function readState(repo) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(curatorStatePath(repo), "utf8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeState(repo, s) {
|
|
40
|
+
mkdirSync(stateDir(repo), { recursive: true });
|
|
41
|
+
writeFileSync(curatorStatePath(repo), JSON.stringify(s, null, 1));
|
|
42
|
+
}
|
|
43
|
+
function lastUsed(repo) {
|
|
44
|
+
const m = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const u of readEvidence(repo, "usage")) {
|
|
46
|
+
const t = Date.parse(String(u.ts));
|
|
47
|
+
const name = String(u.skill).split(":").pop();
|
|
48
|
+
if (Number.isFinite(t) && t > (m.get(name) ?? 0)) m.set(name, t);
|
|
49
|
+
}
|
|
50
|
+
return m;
|
|
51
|
+
}
|
|
52
|
+
function landedAt(s, ledger) {
|
|
53
|
+
const e = ledger.filter((x) => x.skill === s.skill.name && ["land", "patch", "restore", "merge", "promote"].includes(x.action)).pop();
|
|
54
|
+
return e ? Date.parse(e.ts) : statSync(s.path).mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
function plan(repo, now = Date.now(), env = process.env) {
|
|
57
|
+
const all = findSkills(repo, { home: env.HOME, global: false });
|
|
58
|
+
const learned = all.filter((s) => s.learned && s.skill.meta);
|
|
59
|
+
const used = lastUsed(repo);
|
|
60
|
+
const ledger = readLedger(repo);
|
|
61
|
+
const referenced = new Set(learned.flatMap((s) => [...s.skill.meta.supersedes ?? [], ...s.skill.meta.cites].map((x) => x.split("/").pop().replace(/\.md$/, ""))));
|
|
62
|
+
for (const s of all) for (const o of learned) if (s !== o && s.skill.body.includes(`/${o.skill.name}`)) referenced.add(o.skill.name);
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const s of learned) {
|
|
65
|
+
const name = s.skill.name;
|
|
66
|
+
const meta = s.skill.meta;
|
|
67
|
+
const base = { skill: name, path: s.path };
|
|
68
|
+
if (meta.pinned) {
|
|
69
|
+
out.push({ ...base, action: "skip", why: "pinned" });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (referenced.has(name)) {
|
|
73
|
+
out.push({ ...base, action: "skip", why: "cited by another skill" });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const last = used.get(name);
|
|
77
|
+
const since = last ?? landedAt(s, ledger);
|
|
78
|
+
const idle = (now - since) / DAY;
|
|
79
|
+
if (last === void 0 && idle < STALE_DAYS) {
|
|
80
|
+
out.push({ ...base, action: "skip", why: `never used, landed ${idle.toFixed(0)} d ago` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (idle >= ARCHIVE_DAYS) out.push({ ...base, action: "archive", why: `unused ${Math.floor(idle)} days` });
|
|
84
|
+
else if (idle >= STALE_DAYS && meta.status === "active") out.push({ ...base, action: "stale", why: `unused ${Math.floor(idle)} days` });
|
|
85
|
+
else if (idle < STALE_DAYS && meta.status === "stale") out.push({ ...base, action: "reactivate", why: `used ${Math.floor(idle)} days ago` });
|
|
86
|
+
else out.push({ ...base, action: "keep", why: `${meta.status}, ${last === void 0 ? "landed" : "used"} ${Math.floor(idle)} days ago` });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
var withStatus = (raw, status) => {
|
|
91
|
+
const s = parseSkill(raw);
|
|
92
|
+
return renderSkill({ name: s.name, description: s.description, meta: { ...s.meta, status }, body: s.body });
|
|
93
|
+
};
|
|
94
|
+
function curate(repo, opts = {}) {
|
|
95
|
+
const env = opts.env ?? process.env;
|
|
96
|
+
const now = opts.now ?? Date.now();
|
|
97
|
+
const actions = plan(repo, now, env);
|
|
98
|
+
const ids = [];
|
|
99
|
+
if (!opts.dryRun) {
|
|
100
|
+
for (const a of actions) {
|
|
101
|
+
if (a.action === "skip" || a.action === "keep") continue;
|
|
102
|
+
const raw = readFileSync(a.path, "utf8");
|
|
103
|
+
if (a.action === "archive") {
|
|
104
|
+
const dest = join(archiveDir(repo), a.skill, "SKILL.md");
|
|
105
|
+
ids.push(applyChange(repo, { action: "archive", actor: "curator", skill: a.skill, why: a.why }, [
|
|
106
|
+
{ path: dest, content: withStatus(raw, "archived") },
|
|
107
|
+
{ path: a.path, content: null }
|
|
108
|
+
]).id);
|
|
109
|
+
indexSkill({ repo, name: a.skill, path: a.path, status: "archived" }, env);
|
|
110
|
+
} else {
|
|
111
|
+
const status = a.action === "stale" ? "stale" : "active";
|
|
112
|
+
ids.push(applyChange(repo, { action: a.action === "stale" ? "stale" : "restore", actor: "curator", skill: a.skill, why: a.why }, [{ path: a.path, content: withStatus(raw, status) }]).id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const st = readState(repo);
|
|
116
|
+
writeState(repo, { ...st, last_run: new Date(now).toISOString(), last_report: actions.filter((a) => a.action !== "skip" && a.action !== "keep").map((a) => `${a.action} ${a.skill}`).join(", ") || "nothing to do" });
|
|
117
|
+
}
|
|
118
|
+
return { actions, ledger: ids };
|
|
119
|
+
}
|
|
120
|
+
function curatorDue(repo, now = Date.now()) {
|
|
121
|
+
const st = readState(repo);
|
|
122
|
+
if (!st.first_seen) {
|
|
123
|
+
writeState(repo, { ...st, first_seen: new Date(now).toISOString() });
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (now - Date.parse(st.first_seen) < CURATOR_INTERVAL_DAYS * DAY) return false;
|
|
127
|
+
if (st.last_run && now - Date.parse(st.last_run) < CURATOR_INTERVAL_DAYS * DAY) return false;
|
|
128
|
+
let last = 0;
|
|
129
|
+
for (const f of ["skill-usage.jsonl", "failures.jsonl", "corrections.jsonl"]) {
|
|
130
|
+
try {
|
|
131
|
+
last = Math.max(last, statSync(join(stateDir(repo), f)).mtimeMs);
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return now - last >= IDLE_HOURS * 36e5;
|
|
136
|
+
}
|
|
137
|
+
function restore(repo, name) {
|
|
138
|
+
const archived = join(archiveDir(repo), name, "SKILL.md");
|
|
139
|
+
if (!existsSync(archived)) throw new Error(`no archived skill "${name}" (archived: ${listArchived(repo).join(", ") || "none"})`);
|
|
140
|
+
const from = readLedger(repo).filter((e) => e.action === "archive" && e.skill === name).pop();
|
|
141
|
+
const original = from?.files.find((f) => f.after === null)?.path ?? join(repo, ".claude", "skills", name, "SKILL.md");
|
|
142
|
+
if (existsSync(original)) throw new Error(`${original} exists already`);
|
|
143
|
+
return applyChange(repo, { action: "restore", actor: "user", skill: name, why: "restored from skills-archive" }, [
|
|
144
|
+
{ path: original, content: withStatus(readFileSync(archived, "utf8"), "active") },
|
|
145
|
+
{ path: archived, content: null }
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
function listArchived(repo) {
|
|
149
|
+
try {
|
|
150
|
+
return readdirSync(archiveDir(repo)).filter((d) => existsSync(join(archiveDir(repo), d, "SKILL.md")));
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function verifySkills(repo, opts = {}) {
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const s of findSkills(repo, { home: opts.env?.HOME, global: false }).filter((x) => x.learned)) {
|
|
158
|
+
const v = parseVerify(s.skill.body);
|
|
159
|
+
if (v.kind !== "command") {
|
|
160
|
+
out.push({ skill: s.skill.name, result: v.kind === "prose" ? "prose" : "none", detail: v.kind === "prose" ? v.text.slice(0, 120) : "no ## Verify" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const r = spawnSync("bash", ["-c", v.command], { cwd: repo, timeout: opts.timeoutMs ?? 12e4, encoding: "utf8", env: opts.env ?? process.env });
|
|
164
|
+
out.push({ skill: s.skill.name, result: r.status === 0 ? "pass" : "fail", detail: r.status === 0 ? v.command : `${v.command} \u2192 exit ${r.status}: ${(r.stderr || r.stdout || "").trim().slice(0, 200)}` });
|
|
165
|
+
}
|
|
166
|
+
const st = readState(repo);
|
|
167
|
+
writeState(repo, { ...st, flagged: out.filter((x) => x.result === "fail").map((x) => ({ name: x.skill, reason: x.detail, at: (/* @__PURE__ */ new Date()).toISOString() })) });
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
var curatorState = readState;
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
STALE_DAYS,
|
|
174
|
+
ARCHIVE_DAYS,
|
|
175
|
+
CURATOR_INTERVAL_DAYS,
|
|
176
|
+
IDLE_HOURS,
|
|
177
|
+
curatorStatePath,
|
|
178
|
+
plan,
|
|
179
|
+
curate,
|
|
180
|
+
curatorDue,
|
|
181
|
+
restore,
|
|
182
|
+
listArchived,
|
|
183
|
+
verifySkills,
|
|
184
|
+
curatorState
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=chunk-CATPGINC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-curator.ts"],"sourcesContent":["/**\n * The skill loop's curator (v1.7): keeps learned skills from piling up. Deterministic — no model call:\n *\n * - a learned skill unused for 14 days becomes `stale`; for 30 days it is archived to\n * .claude/.semantic-memory/skills-archive/<name>/ (outside .claude/skills/, so Claude Code stops loading it)\n * - a stale skill that is used again goes back to `active`\n * - skipped: pinned skills, skills another skill cites or supersedes, never-used skills younger than 14 days,\n * and every skill the loop did not write\n *\n * Runs idle-triggered from SessionStart (every 7 days, when the repo has been quiet >= 2 h, never in the first\n * 7 days after the loop first saw the repo) or by hand. Every change is a ledger entry; `restore` brings an\n * archived skill back. Merging is separate and opt-in (skill-merge.ts).\n */\nimport { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\nimport { applyChange, archiveDir, readLedger, type LedgerEntry } from \"./skill-ledger.js\";\nimport { findSkills, parseSkill, renderSkill, type SkillFile } from \"./skill-schema.js\";\nimport { indexSkill } from \"./skill-scope.js\";\nimport { parseVerify } from \"./skill-eval/task.js\";\n\nexport const STALE_DAYS = 14;\nexport const ARCHIVE_DAYS = 30;\nexport const CURATOR_INTERVAL_DAYS = 7;\nexport const IDLE_HOURS = 2;\nconst DAY = 86_400_000;\n\nexport const curatorStatePath = (repo: string) => join(stateDir(repo), \"curator.json\");\n\ninterface CuratorState {\n first_seen?: string;\n last_run?: string;\n last_report?: string;\n flagged?: { name: string; reason: string; at: string }[];\n}\n\nfunction readState(repo: string): CuratorState {\n try {\n return JSON.parse(readFileSync(curatorStatePath(repo), \"utf8\"));\n } catch {\n return {};\n }\n}\nfunction writeState(repo: string, s: CuratorState) {\n mkdirSync(stateDir(repo), { recursive: true });\n writeFileSync(curatorStatePath(repo), JSON.stringify(s, null, 1));\n}\n\nexport interface CuratorAction {\n skill: string;\n path: string;\n action: \"stale\" | \"archive\" | \"reactivate\" | \"keep\" | \"skip\";\n why: string;\n}\n\n/** When the skill was last used here (ms), from skill-usage.jsonl; null if never. */\nfunction lastUsed(repo: string): Map<string, number> {\n const m = new Map<string, number>();\n for (const u of readEvidence(repo, \"usage\")) {\n const t = Date.parse(String(u.ts));\n const name = String(u.skill).split(\":\").pop()!;\n if (Number.isFinite(t) && t > (m.get(name) ?? 0)) m.set(name, t);\n }\n return m;\n}\n\n/** When the skill landed here (ms): its ledger entry, else the file's mtime. */\nfunction landedAt(s: SkillFile, ledger: LedgerEntry[]): number {\n const e = ledger.filter((x) => x.skill === s.skill.name && [\"land\", \"patch\", \"restore\", \"merge\", \"promote\"].includes(x.action)).pop();\n return e ? Date.parse(e.ts) : statSync(s.path).mtimeMs;\n}\n\nexport function plan(repo: string, now = Date.now(), env = process.env): CuratorAction[] {\n const all = findSkills(repo, { home: env.HOME, global: false });\n const learned = all.filter((s) => s.learned && s.skill.meta);\n const used = lastUsed(repo);\n const ledger = readLedger(repo);\n // a skill another one cites or supersedes is still load-bearing\n const referenced = new Set(learned.flatMap((s) => [...(s.skill.meta!.supersedes ?? []), ...s.skill.meta!.cites].map((x) => x.split(\"/\").pop()!.replace(/\\.md$/, \"\"))));\n for (const s of all) for (const o of learned) if (s !== o && s.skill.body.includes(`/${o.skill.name}`)) referenced.add(o.skill.name);\n const out: CuratorAction[] = [];\n for (const s of learned) {\n const name = s.skill.name;\n const meta = s.skill.meta!;\n const base = { skill: name, path: s.path };\n if (meta.pinned) { out.push({ ...base, action: \"skip\", why: \"pinned\" }); continue; }\n if (referenced.has(name)) { out.push({ ...base, action: \"skip\", why: \"cited by another skill\" }); continue; }\n const last = used.get(name);\n const since = last ?? landedAt(s, ledger);\n const idle = (now - since) / DAY;\n if (last === undefined && idle < STALE_DAYS) { out.push({ ...base, action: \"skip\", why: `never used, landed ${idle.toFixed(0)} d ago` }); continue; }\n if (idle >= ARCHIVE_DAYS) out.push({ ...base, action: \"archive\", why: `unused ${Math.floor(idle)} days` });\n else if (idle >= STALE_DAYS && meta.status === \"active\") out.push({ ...base, action: \"stale\", why: `unused ${Math.floor(idle)} days` });\n else if (idle < STALE_DAYS && meta.status === \"stale\") out.push({ ...base, action: \"reactivate\", why: `used ${Math.floor(idle)} days ago` });\n else out.push({ ...base, action: \"keep\", why: `${meta.status}, ${last === undefined ? \"landed\" : \"used\"} ${Math.floor(idle)} days ago` });\n }\n return out;\n}\n\nconst withStatus = (raw: string, status: \"active\" | \"stale\" | \"archived\") => {\n const s = parseSkill(raw);\n return renderSkill({ name: s.name, description: s.description, meta: { ...s.meta!, status }, body: s.body });\n};\n\n/** Run the curator. Dry run: report only. Returns the actions (and the ledger ids of the changes made). */\nexport function curate(repo: string, opts: { dryRun?: boolean; now?: number; env?: NodeJS.ProcessEnv } = {}): { actions: CuratorAction[]; ledger: string[] } {\n const env = opts.env ?? process.env;\n const now = opts.now ?? Date.now();\n const actions = plan(repo, now, env);\n const ids: string[] = [];\n if (!opts.dryRun) {\n for (const a of actions) {\n if (a.action === \"skip\" || a.action === \"keep\") continue;\n const raw = readFileSync(a.path, \"utf8\");\n if (a.action === \"archive\") {\n const dest = join(archiveDir(repo), a.skill, \"SKILL.md\");\n ids.push(applyChange(repo, { action: \"archive\", actor: \"curator\", skill: a.skill, why: a.why }, [\n { path: dest, content: withStatus(raw, \"archived\") },\n { path: a.path, content: null },\n ]).id);\n indexSkill({ repo, name: a.skill, path: a.path, status: \"archived\" }, env);\n } else {\n const status = a.action === \"stale\" ? \"stale\" : \"active\";\n ids.push(applyChange(repo, { action: a.action === \"stale\" ? \"stale\" : \"restore\", actor: \"curator\", skill: a.skill, why: a.why }, [{ path: a.path, content: withStatus(raw, status) }]).id);\n }\n }\n const st = readState(repo);\n writeState(repo, { ...st, last_run: new Date(now).toISOString(), last_report: actions.filter((a) => a.action !== \"skip\" && a.action !== \"keep\").map((a) => `${a.action} ${a.skill}`).join(\", \") || \"nothing to do\" });\n }\n return { actions, ledger: ids };\n}\n\n/** Whether SessionStart should start the curator now (and records first sight of the repo). */\nexport function curatorDue(repo: string, now = Date.now()): boolean {\n const st = readState(repo);\n if (!st.first_seen) {\n writeState(repo, { ...st, first_seen: new Date(now).toISOString() });\n return false;\n }\n if (now - Date.parse(st.first_seen) < CURATOR_INTERVAL_DAYS * DAY) return false;\n if (st.last_run && now - Date.parse(st.last_run) < CURATOR_INTERVAL_DAYS * DAY) return false;\n // quiet: the last thing the loop recorded here is >= 2 h old (the session starting now is not activity yet)\n let last = 0;\n for (const f of [\"skill-usage.jsonl\", \"failures.jsonl\", \"corrections.jsonl\"]) {\n try {\n last = Math.max(last, statSync(join(stateDir(repo), f)).mtimeMs);\n } catch {}\n }\n return now - last >= IDLE_HOURS * 3_600_000;\n}\n\n/** Bring an archived skill back where it was, active again. */\nexport function restore(repo: string, name: string): LedgerEntry {\n const archived = join(archiveDir(repo), name, \"SKILL.md\");\n if (!existsSync(archived)) throw new Error(`no archived skill \"${name}\" (archived: ${listArchived(repo).join(\", \") || \"none\"})`);\n const from = readLedger(repo).filter((e) => e.action === \"archive\" && e.skill === name).pop();\n const original = from?.files.find((f) => f.after === null)?.path ?? join(repo, \".claude\", \"skills\", name, \"SKILL.md\");\n if (existsSync(original)) throw new Error(`${original} exists already`);\n return applyChange(repo, { action: \"restore\", actor: \"user\", skill: name, why: \"restored from skills-archive\" }, [\n { path: original, content: withStatus(readFileSync(archived, \"utf8\"), \"active\") },\n { path: archived, content: null },\n ]);\n}\n\nexport function listArchived(repo: string): string[] {\n try {\n return readdirSync(archiveDir(repo)).filter((d) => existsSync(join(archiveDir(repo), d, \"SKILL.md\")));\n } catch {\n return [];\n }\n}\n\n/** Run each landed learned skill's Verify command (they were approved by a person); flag failures, archive nothing. */\nexport function verifySkills(repo: string, opts: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): { skill: string; result: \"pass\" | \"fail\" | \"prose\" | \"none\"; detail: string }[] {\n const out: { skill: string; result: \"pass\" | \"fail\" | \"prose\" | \"none\"; detail: string }[] = [];\n for (const s of findSkills(repo, { home: opts.env?.HOME, global: false }).filter((x) => x.learned)) {\n const v = parseVerify(s.skill.body);\n if (v.kind !== \"command\") {\n out.push({ skill: s.skill.name, result: v.kind === \"prose\" ? \"prose\" : \"none\", detail: v.kind === \"prose\" ? v.text.slice(0, 120) : \"no ## Verify\" });\n continue;\n }\n const r = spawnSync(\"bash\", [\"-c\", v.command], { cwd: repo, timeout: opts.timeoutMs ?? 120_000, encoding: \"utf8\", env: opts.env ?? process.env });\n out.push({ skill: s.skill.name, result: r.status === 0 ? \"pass\" : \"fail\", detail: r.status === 0 ? v.command : `${v.command} → exit ${r.status}: ${(r.stderr || r.stdout || \"\").trim().slice(0, 200)}` });\n }\n const st = readState(repo);\n writeState(repo, { ...st, flagged: out.filter((x) => x.result === \"fail\").map((x) => ({ name: x.skill, reason: x.detail, at: new Date().toISOString() })) });\n return out;\n}\n\nexport const curatorState = readState;\n"],"mappings":"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","names":[]}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
matter_default
|
|
3
|
+
} from "./chunk-J7LYDRML.js";
|
|
4
|
+
|
|
1
5
|
// src/core/indexer.ts
|
|
2
6
|
import { unified } from "unified";
|
|
3
7
|
import remarkParse from "remark-parse";
|
|
4
8
|
import remarkWikiLink from "remark-wiki-link";
|
|
5
|
-
import matter from "gray-matter";
|
|
6
9
|
import { glob } from "glob";
|
|
7
10
|
import { readFile, writeFile, stat } from "fs/promises";
|
|
8
11
|
import { existsSync } from "fs";
|
|
@@ -11,7 +14,7 @@ var DOCS_CACHE_FILE = "docs.cache.json";
|
|
|
11
14
|
var CHUNK_TARGET_CHARS = 2e3;
|
|
12
15
|
function parseFrontmatter(raw, file) {
|
|
13
16
|
try {
|
|
14
|
-
return
|
|
17
|
+
return matter_default(raw);
|
|
15
18
|
} catch (err) {
|
|
16
19
|
process.stderr.write(`Invalid frontmatter in ${file}, indexing body only: ${err?.message?.split("\n")[0] ?? err}
|
|
17
20
|
`);
|
|
@@ -183,4 +186,4 @@ var Indexer = class {
|
|
|
183
186
|
export {
|
|
184
187
|
Indexer
|
|
185
188
|
};
|
|
186
|
-
//# sourceMappingURL=chunk-
|
|
189
|
+
//# sourceMappingURL=chunk-CJUY55GG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/indexer.ts"],"sourcesContent":["import { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkWikiLink from \"remark-wiki-link\";\nimport matter from \"gray-matter\";\nimport { glob } from \"glob\";\nimport { readFile, writeFile, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative } from \"node:path\";\nimport type { IndexedDocument } from \"./types.js\";\n\nconst DOCS_CACHE_FILE = \"docs.cache.json\";\n\nconst CHUNK_TARGET_CHARS = 2000; // ~512 tokens\n\n// Invalid YAML (e.g. an unquoted colon in a value) keeps the body searchable with empty frontmatter\nfunction parseFrontmatter(raw: string, file: string): { data: Record<string, unknown>; content: string } {\n try {\n return matter(raw);\n } catch (err: any) {\n process.stderr.write(`Invalid frontmatter in ${file}, indexing body only: ${err?.message?.split(\"\\n\")[0] ?? err}\\n`);\n return { data: {}, content: raw.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---[^\\S\\r\\n]*(\\r?\\n|$)/, \"\") };\n }\n}\n\nexport class Indexer {\n private notesPath: string;\n private processor: any;\n\n constructor(notesPath: string) {\n this.notesPath = notesPath;\n this.processor = unified().use(remarkParse).use(remarkWikiLink);\n }\n\n async indexAll(): Promise<IndexedDocument[]> {\n // follow: true descends into symlinked directories. Without this, users\n // who symlink an external folder into their vault (a common pattern for\n // sharing notes across projects) get silently empty indexing for the\n // symlinked content. glob does cycle detection via visited inodes so\n // circular symlinks are handled safely.\n const files = await glob(\"**/*.md\", { cwd: this.notesPath, follow: true });\n // One unreadable file must not take the whole corpus down with it\n const docs = await Promise.all(\n files.map((file) =>\n this.indexFile(join(this.notesPath, file), file).catch((err) => {\n process.stderr.write(`Skipping ${file}: ${err?.message ?? err}\\n`);\n return null;\n })\n )\n );\n return docs.filter((d): d is IndexedDocument => d !== null);\n }\n\n /**\n * Load parsed-doc cache from disk. Returns null if missing or unreadable.\n * The cache is a verbatim JSON dump of a prior indexAll() result, written by\n * saveDocsCache. Consumers (search CLI / hook path) prefer this over re-parsing\n * 500+ markdown files; the tradeoff is that cache may be stale if the vault\n * changed since the last reindex — same staleness window as the vector cache.\n */\n async loadDocsCache(indexPath: string): Promise<IndexedDocument[] | null> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n if (!existsSync(p)) return null;\n try {\n const raw = await readFile(p, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return null;\n return parsed as IndexedDocument[];\n } catch {\n return null;\n }\n }\n\n async saveDocsCache(indexPath: string, docs: IndexedDocument[]): Promise<void> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n await writeFile(p, JSON.stringify(docs), \"utf-8\");\n }\n\n async indexFile(\n absolutePath: string,\n relativePath: string\n ): Promise<IndexedDocument> {\n const [raw, fileStat] = await Promise.all([\n readFile(absolutePath, \"utf-8\"),\n stat(absolutePath),\n ]);\n const { data: frontmatter, content } = parseFrontmatter(raw, relativePath);\n const tree = this.processor.parse(content);\n\n const wikilinks = this.extractWikilinks(tree);\n const tags = this.extractTags(content, frontmatter);\n const headers = this.extractHeaders(tree);\n const plainText = this.stripMarkdown(content);\n const chunks = this.chunkText(plainText);\n\n // Symptom-keyed indexing (v1.4 Phase 5). Each verbatim `symptoms:` phrase is\n // added as its own chunk, so a terse symptom query (\"it hangs after reindex\")\n // matches the note directly — the fix for asymmetric retrieval (query-shaped\n // keys stored alongside document-shaped content). Synthetic child chunks point\n // at the same note path; no index-format change.\n const symptoms = Array.isArray(frontmatter.symptoms)\n ? (frontmatter.symptoms as unknown[]).filter((s): s is string => typeof s === \"string\" && s.trim().length > 0).map((s) => s.trim())\n : [];\n const allChunks = symptoms.length > 0 ? [...chunks, ...symptoms] : chunks;\n\n const title =\n (frontmatter.title as string) ||\n headers[0] ||\n basename(relativePath, \".md\");\n\n // Resolve modification time: prefer frontmatter date fields over fs.stat\n // Supports hit-em-with-the-docs (last_updated) and common alternatives\n const mtime = this.resolveMtime(frontmatter, fileStat.mtime);\n\n // Optional hit-em-with-the-docs fields (only populated when present)\n const loadPriority =\n typeof frontmatter.load_priority === \"number\"\n ? Math.min(10, Math.max(1, frontmatter.load_priority))\n : undefined;\n const status =\n typeof frontmatter.status === \"string\" ? frontmatter.status : undefined;\n const tier =\n typeof frontmatter.tier === \"string\" ? frontmatter.tier : undefined;\n const domains = Array.isArray(frontmatter.domains)\n ? (frontmatter.domains as string[])\n : undefined;\n const purpose =\n typeof frontmatter.purpose === \"string\" ? frontmatter.purpose : undefined;\n const relatedDocs = Array.isArray(frontmatter.related_docs)\n ? (frontmatter.related_docs as string[]).filter((v) => typeof v === \"string\")\n : undefined;\n\n return {\n path: relativePath,\n title,\n content: plainText,\n frontmatter,\n wikilinks,\n tags,\n headers,\n chunks: allChunks,\n mtime,\n ...(loadPriority !== undefined && { loadPriority }),\n ...(status !== undefined && { status }),\n ...(tier !== undefined && { tier }),\n ...(domains !== undefined && { domains }),\n ...(purpose !== undefined && { purpose }),\n ...(relatedDocs !== undefined && { relatedDocs }),\n };\n }\n\n /**\n * Resolve the best available modification date for a document.\n * Priority: last_updated → updated → date → lastmod → fs.stat mtime\n * Accepts YYYY-MM-DD strings or full ISO timestamps.\n */\n private resolveMtime(\n frontmatter: Record<string, unknown>,\n statMtime: Date\n ): string {\n const candidates = [\n frontmatter.last_updated,\n frontmatter.updated,\n frontmatter.date,\n frontmatter.lastmod,\n ];\n for (const val of candidates) {\n if (!val) continue;\n const str = val instanceof Date ? val.toISOString() : String(val);\n const parsed = new Date(str);\n if (!isNaN(parsed.getTime())) return parsed.toISOString();\n }\n return statMtime.toISOString();\n }\n\n private extractWikilinks(tree: any): string[] {\n const links: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"wikiLink\") {\n links.push(node.value || node.data?.alias || \"\");\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return [...new Set(links.filter(Boolean))];\n }\n\n private extractTags(content: string, frontmatter: Record<string, unknown>): string[] {\n const inlineTags = [...content.matchAll(/(?:^|\\s)#([a-zA-Z][\\w-/]*)/g)].map(\n (m) => m[1]\n );\n\n const fmTags = Array.isArray(frontmatter.tags)\n ? (frontmatter.tags as string[])\n : [];\n\n return [...new Set([...fmTags, ...inlineTags])];\n }\n\n private extractHeaders(tree: any): string[] {\n const headers: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"heading\") {\n const text = this.nodeToText(node);\n if (text) headers.push(text);\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return headers;\n }\n\n private nodeToText(node: any): string {\n if (node.type === \"text\") return node.value;\n if (node.children) return node.children.map((c: any) => this.nodeToText(c)).join(\"\");\n return \"\";\n }\n\n private stripMarkdown(content: string): string {\n return content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\")\n .replace(/!\\[.*?\\]\\(.*?\\)/g, \"\")\n .replace(/\\[([^\\]]+)\\]\\(.*?\\)/g, \"$1\")\n .replace(/#{1,6}\\s+/g, \"\")\n .replace(/[*_~]{1,3}/g, \"\")\n .replace(/>\\s+/g, \"\")\n .replace(/\\|.*\\|/g, \"\")\n .replace(/-{3,}/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n }\n\n chunkText(text: string): string[] {\n if (text.length <= CHUNK_TARGET_CHARS) return [text];\n\n const sentences = text.match(/[^.!?\\n]+[.!?\\n]+|[^.!?\\n]+$/g) || [text];\n const chunks: string[] = [];\n let current = \"\";\n\n for (const sentence of sentences) {\n if (current.length + sentence.length > CHUNK_TARGET_CHARS && current) {\n chunks.push(current.trim());\n current = \"\";\n }\n current += sentence;\n }\n if (current.trim()) chunks.push(current.trim());\n\n return chunks;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/indexer.ts"],"sourcesContent":["import { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkWikiLink from \"remark-wiki-link\";\nimport matter from \"./matter.js\";\nimport { glob } from \"glob\";\nimport { readFile, writeFile, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative } from \"node:path\";\nimport type { IndexedDocument } from \"./types.js\";\n\nconst DOCS_CACHE_FILE = \"docs.cache.json\";\n\nconst CHUNK_TARGET_CHARS = 2000; // ~512 tokens\n\n// Invalid YAML (e.g. an unquoted colon in a value) keeps the body searchable with empty frontmatter\nfunction parseFrontmatter(raw: string, file: string): { data: Record<string, unknown>; content: string } {\n try {\n return matter(raw);\n } catch (err: any) {\n process.stderr.write(`Invalid frontmatter in ${file}, indexing body only: ${err?.message?.split(\"\\n\")[0] ?? err}\\n`);\n return { data: {}, content: raw.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---[^\\S\\r\\n]*(\\r?\\n|$)/, \"\") };\n }\n}\n\nexport class Indexer {\n private notesPath: string;\n private processor: any;\n\n constructor(notesPath: string) {\n this.notesPath = notesPath;\n this.processor = unified().use(remarkParse).use(remarkWikiLink);\n }\n\n async indexAll(): Promise<IndexedDocument[]> {\n // follow: true descends into symlinked directories. Without this, users\n // who symlink an external folder into their vault (a common pattern for\n // sharing notes across projects) get silently empty indexing for the\n // symlinked content. glob does cycle detection via visited inodes so\n // circular symlinks are handled safely.\n const files = await glob(\"**/*.md\", { cwd: this.notesPath, follow: true });\n // One unreadable file must not take the whole corpus down with it\n const docs = await Promise.all(\n files.map((file) =>\n this.indexFile(join(this.notesPath, file), file).catch((err) => {\n process.stderr.write(`Skipping ${file}: ${err?.message ?? err}\\n`);\n return null;\n })\n )\n );\n return docs.filter((d): d is IndexedDocument => d !== null);\n }\n\n /**\n * Load parsed-doc cache from disk. Returns null if missing or unreadable.\n * The cache is a verbatim JSON dump of a prior indexAll() result, written by\n * saveDocsCache. Consumers (search CLI / hook path) prefer this over re-parsing\n * 500+ markdown files; the tradeoff is that cache may be stale if the vault\n * changed since the last reindex — same staleness window as the vector cache.\n */\n async loadDocsCache(indexPath: string): Promise<IndexedDocument[] | null> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n if (!existsSync(p)) return null;\n try {\n const raw = await readFile(p, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return null;\n return parsed as IndexedDocument[];\n } catch {\n return null;\n }\n }\n\n async saveDocsCache(indexPath: string, docs: IndexedDocument[]): Promise<void> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n await writeFile(p, JSON.stringify(docs), \"utf-8\");\n }\n\n async indexFile(\n absolutePath: string,\n relativePath: string\n ): Promise<IndexedDocument> {\n const [raw, fileStat] = await Promise.all([\n readFile(absolutePath, \"utf-8\"),\n stat(absolutePath),\n ]);\n const { data: frontmatter, content } = parseFrontmatter(raw, relativePath);\n const tree = this.processor.parse(content);\n\n const wikilinks = this.extractWikilinks(tree);\n const tags = this.extractTags(content, frontmatter);\n const headers = this.extractHeaders(tree);\n const plainText = this.stripMarkdown(content);\n const chunks = this.chunkText(plainText);\n\n // Symptom-keyed indexing (v1.4 Phase 5). Each verbatim `symptoms:` phrase is\n // added as its own chunk, so a terse symptom query (\"it hangs after reindex\")\n // matches the note directly — the fix for asymmetric retrieval (query-shaped\n // keys stored alongside document-shaped content). Synthetic child chunks point\n // at the same note path; no index-format change.\n const symptoms = Array.isArray(frontmatter.symptoms)\n ? (frontmatter.symptoms as unknown[]).filter((s): s is string => typeof s === \"string\" && s.trim().length > 0).map((s) => s.trim())\n : [];\n const allChunks = symptoms.length > 0 ? [...chunks, ...symptoms] : chunks;\n\n const title =\n (frontmatter.title as string) ||\n headers[0] ||\n basename(relativePath, \".md\");\n\n // Resolve modification time: prefer frontmatter date fields over fs.stat\n // Supports hit-em-with-the-docs (last_updated) and common alternatives\n const mtime = this.resolveMtime(frontmatter, fileStat.mtime);\n\n // Optional hit-em-with-the-docs fields (only populated when present)\n const loadPriority =\n typeof frontmatter.load_priority === \"number\"\n ? Math.min(10, Math.max(1, frontmatter.load_priority))\n : undefined;\n const status =\n typeof frontmatter.status === \"string\" ? frontmatter.status : undefined;\n const tier =\n typeof frontmatter.tier === \"string\" ? frontmatter.tier : undefined;\n const domains = Array.isArray(frontmatter.domains)\n ? (frontmatter.domains as string[])\n : undefined;\n const purpose =\n typeof frontmatter.purpose === \"string\" ? frontmatter.purpose : undefined;\n const relatedDocs = Array.isArray(frontmatter.related_docs)\n ? (frontmatter.related_docs as string[]).filter((v) => typeof v === \"string\")\n : undefined;\n\n return {\n path: relativePath,\n title,\n content: plainText,\n frontmatter,\n wikilinks,\n tags,\n headers,\n chunks: allChunks,\n mtime,\n ...(loadPriority !== undefined && { loadPriority }),\n ...(status !== undefined && { status }),\n ...(tier !== undefined && { tier }),\n ...(domains !== undefined && { domains }),\n ...(purpose !== undefined && { purpose }),\n ...(relatedDocs !== undefined && { relatedDocs }),\n };\n }\n\n /**\n * Resolve the best available modification date for a document.\n * Priority: last_updated → updated → date → lastmod → fs.stat mtime\n * Accepts YYYY-MM-DD strings or full ISO timestamps.\n */\n private resolveMtime(\n frontmatter: Record<string, unknown>,\n statMtime: Date\n ): string {\n const candidates = [\n frontmatter.last_updated,\n frontmatter.updated,\n frontmatter.date,\n frontmatter.lastmod,\n ];\n for (const val of candidates) {\n if (!val) continue;\n const str = val instanceof Date ? val.toISOString() : String(val);\n const parsed = new Date(str);\n if (!isNaN(parsed.getTime())) return parsed.toISOString();\n }\n return statMtime.toISOString();\n }\n\n private extractWikilinks(tree: any): string[] {\n const links: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"wikiLink\") {\n links.push(node.value || node.data?.alias || \"\");\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return [...new Set(links.filter(Boolean))];\n }\n\n private extractTags(content: string, frontmatter: Record<string, unknown>): string[] {\n const inlineTags = [...content.matchAll(/(?:^|\\s)#([a-zA-Z][\\w-/]*)/g)].map(\n (m) => m[1]\n );\n\n const fmTags = Array.isArray(frontmatter.tags)\n ? (frontmatter.tags as string[])\n : [];\n\n return [...new Set([...fmTags, ...inlineTags])];\n }\n\n private extractHeaders(tree: any): string[] {\n const headers: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"heading\") {\n const text = this.nodeToText(node);\n if (text) headers.push(text);\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return headers;\n }\n\n private nodeToText(node: any): string {\n if (node.type === \"text\") return node.value;\n if (node.children) return node.children.map((c: any) => this.nodeToText(c)).join(\"\");\n return \"\";\n }\n\n private stripMarkdown(content: string): string {\n return content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\")\n .replace(/!\\[.*?\\]\\(.*?\\)/g, \"\")\n .replace(/\\[([^\\]]+)\\]\\(.*?\\)/g, \"$1\")\n .replace(/#{1,6}\\s+/g, \"\")\n .replace(/[*_~]{1,3}/g, \"\")\n .replace(/>\\s+/g, \"\")\n .replace(/\\|.*\\|/g, \"\")\n .replace(/-{3,}/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n }\n\n chunkText(text: string): string[] {\n if (text.length <= CHUNK_TARGET_CHARS) return [text];\n\n const sentences = text.match(/[^.!?\\n]+[.!?\\n]+|[^.!?\\n]+$/g) || [text];\n const chunks: string[] = [];\n let current = \"\";\n\n for (const sentence of sentences) {\n if (current.length + sentence.length > CHUNK_TARGET_CHARS && current) {\n chunks.push(current.trim());\n current = \"\";\n }\n current += sentence;\n }\n if (current.trim()) chunks.push(current.trim());\n\n return chunks;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runClaude
|
|
3
|
+
} from "./chunk-IEZVFKUT.js";
|
|
4
|
+
import {
|
|
5
|
+
parseSkill
|
|
6
|
+
} from "./chunk-ZSWI3Y4Y.js";
|
|
7
|
+
import {
|
|
8
|
+
featureOn
|
|
9
|
+
} from "./chunk-VQCNWRTL.js";
|
|
10
|
+
import {
|
|
11
|
+
askJev,
|
|
12
|
+
jevVerdict
|
|
13
|
+
} from "./chunk-GYR7U6KA.js";
|
|
14
|
+
|
|
15
|
+
// src/core/skill-eval/task.ts
|
|
16
|
+
import { execFile, execFileSync } from "child_process";
|
|
17
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { promisify } from "util";
|
|
21
|
+
|
|
22
|
+
// src/core/skill-eval/prompts/grader.ts
|
|
23
|
+
var graderPrompt = (verify, outcome) => `You are a grader. Decide whether the outcome below satisfies the expectation.
|
|
24
|
+
|
|
25
|
+
PASS: there is clear evidence the expectation is true AND the evidence reflects genuine task completion, not surface-level compliance.
|
|
26
|
+
FAIL: there is no evidence, or the evidence contradicts the expectation, or it is superficial (a claim of success with nothing to back it).
|
|
27
|
+
Judge only from the outcome text; do not assume anything it does not say.
|
|
28
|
+
|
|
29
|
+
<expectation>
|
|
30
|
+
${verify}
|
|
31
|
+
</expectation>
|
|
32
|
+
|
|
33
|
+
<outcome>
|
|
34
|
+
${outcome}
|
|
35
|
+
</outcome>
|
|
36
|
+
|
|
37
|
+
Answer with only JSON: {"passed": true|false, "evidence": "<one sentence quoting or describing what you found>"}`;
|
|
38
|
+
function parseGrade(text) {
|
|
39
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
40
|
+
try {
|
|
41
|
+
const j = JSON.parse(m ? m[0] : "");
|
|
42
|
+
return { passed: j.passed === true, evidence: String(j.evidence ?? "") };
|
|
43
|
+
} catch {
|
|
44
|
+
return { passed: false, evidence: "grader gave no JSON" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/skill-eval/task.ts
|
|
49
|
+
var execFileP = promisify(execFile);
|
|
50
|
+
function parseVerify(body) {
|
|
51
|
+
const m = body.match(/^##\s+Verify\s*$([\s\S]*?)(?=^##\s|(?![\s\S]))/m);
|
|
52
|
+
const text = m?.[1].trim() ?? "";
|
|
53
|
+
if (!text) return { kind: "none" };
|
|
54
|
+
const fenced = text.match(/```[^\n]*\n([\s\S]*?)```/);
|
|
55
|
+
if (fenced?.[1].trim()) return { kind: "command", command: fenced[1].trim() };
|
|
56
|
+
const inline = text.replace(/^(?:[-*]|\d+\.)\s+/, "").match(/^`([^`\n]+)`/);
|
|
57
|
+
return inline ? { kind: "command", command: inline[1].trim() } : { kind: "prose", text };
|
|
58
|
+
}
|
|
59
|
+
var OUTCOME_CAP = 2e3;
|
|
60
|
+
async function runArm(o, withSkill) {
|
|
61
|
+
const env = o.env ?? process.env;
|
|
62
|
+
const skill = parseSkill(o.skillRaw);
|
|
63
|
+
const verify = parseVerify(skill.body);
|
|
64
|
+
const holder = mkdtempSync(join(tmpdir(), "sm-skill-task-"));
|
|
65
|
+
const dir = join(holder, "repo");
|
|
66
|
+
const git = (...a) => execFileSync("git", ["-C", o.repoRoot, ...a], { stdio: "pipe" });
|
|
67
|
+
try {
|
|
68
|
+
git("worktree", "add", "--detach", dir, "HEAD");
|
|
69
|
+
const skillDir = join(dir, ".claude", "skills", skill.name);
|
|
70
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
71
|
+
if (withSkill) {
|
|
72
|
+
mkdirSync(skillDir, { recursive: true });
|
|
73
|
+
writeFileSync(join(skillDir, "SKILL.md"), o.skillRaw);
|
|
74
|
+
}
|
|
75
|
+
const allowedTools = ["Edit", "Write", ...(skill.meta?.applies_to.commands ?? []).map((c) => `Bash(${c}:*)`)];
|
|
76
|
+
const run = await runClaude({
|
|
77
|
+
prompt: o.prompt,
|
|
78
|
+
cwd: dir,
|
|
79
|
+
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
|
|
80
|
+
allowedTools,
|
|
81
|
+
maxTurns: o.maxTurns ?? 12,
|
|
82
|
+
env,
|
|
83
|
+
timeoutMs: o.timeoutMs
|
|
84
|
+
});
|
|
85
|
+
const finalText = run.result.slice(0, OUTCOME_CAP);
|
|
86
|
+
if (!run.ok) return { passed: false, evidence: "the run failed", finalText, error: run.error };
|
|
87
|
+
if (verify.kind === "none") return { passed: false, evidence: "the skill has no ## Verify to check", finalText };
|
|
88
|
+
if (verify.kind === "command") {
|
|
89
|
+
try {
|
|
90
|
+
await execFileP("bash", ["-c", verify.command], { cwd: dir, timeout: o.verifyTimeoutMs ?? 12e4, env });
|
|
91
|
+
return { passed: true, evidence: `\`${verify.command}\` exited 0`, finalText };
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return { passed: false, evidence: `\`${verify.command}\` failed: ${String(e.message).slice(0, 200)}`, finalText };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (featureOn("jev", o.repoRoot, env)) {
|
|
97
|
+
const a = await askJev(
|
|
98
|
+
{ cwd: o.repoRoot, caller: "semantic-memory-skill-loop", state: finalText, questions: [{ key: "verify", instructions: `Does this outcome satisfy: ${verify.text}?` }] },
|
|
99
|
+
{ env }
|
|
100
|
+
);
|
|
101
|
+
const p = a.scores.verify;
|
|
102
|
+
return { passed: jevVerdict(p) === true, evidence: a.error ? `jev: ${a.error}` : `jev p=${p}`, finalText };
|
|
103
|
+
}
|
|
104
|
+
const g = await runClaude({ prompt: graderPrompt(verify.text, finalText), cwd: dir, tools: [], maxTurns: 1, env, timeoutMs: o.timeoutMs });
|
|
105
|
+
return { ...g.ok ? parseGrade(g.result) : { passed: false, evidence: `grader failed: ${g.error}` }, finalText };
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { passed: false, evidence: "could not set up the scratch copy", finalText: "", error: e.message };
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
git("worktree", "remove", "--force", dir);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
rmSync(holder, { recursive: true, force: true });
|
|
114
|
+
try {
|
|
115
|
+
git("worktree", "prune");
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function runTaskEval(o) {
|
|
121
|
+
const skill = parseSkill(o.skillRaw);
|
|
122
|
+
const [w, wo] = await Promise.all([runArm(o, true), runArm(o, false)]);
|
|
123
|
+
return { skill: skill.name, verify: parseVerify(skill.body), with: w, without: wo };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
parseVerify,
|
|
128
|
+
runTaskEval
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=chunk-DBFD3MTT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-eval/task.ts","../src/core/skill-eval/prompts/grader.ts"],"sourcesContent":["/**\n * Task eval (v1.7): replay a task prompt with and without the skill, each in its own throwaway\n * `git worktree` of HEAD (the real working tree is never touched), then check the outcome with the skill's\n * `## Verify`: a command is run here, in the scratch copy; prose is graded — by Jev in jev mode, else by\n * the grader prompt through `claude -p`.\n */\nimport { execFile, execFileSync } from \"node:child_process\";\nimport { mkdirSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { runClaude } from \"../claude-run.js\";\nimport { featureOn } from \"../features.js\";\nimport { askJev, jevVerdict } from \"../jev.js\";\nimport { parseSkill } from \"../skill-schema.js\";\nimport { graderPrompt, parseGrade } from \"./prompts/grader.js\";\n\nconst execFileP = promisify(execFile);\n\nexport type Verify = { kind: \"command\"; command: string } | { kind: \"prose\"; text: string } | { kind: \"none\" };\n\n/** The `## Verify` section as a command (fenced block, or a leading inline-backtick span) or as prose. */\nexport function parseVerify(body: string): Verify {\n const m = body.match(/^##\\s+Verify\\s*$([\\s\\S]*?)(?=^##\\s|(?![\\s\\S]))/m);\n const text = m?.[1].trim() ?? \"\";\n if (!text) return { kind: \"none\" };\n const fenced = text.match(/```[^\\n]*\\n([\\s\\S]*?)```/);\n if (fenced?.[1].trim()) return { kind: \"command\", command: fenced[1].trim() };\n const inline = text.replace(/^(?:[-*]|\\d+\\.)\\s+/, \"\").match(/^`([^`\\n]+)`/);\n return inline ? { kind: \"command\", command: inline[1].trim() } : { kind: \"prose\", text };\n}\n\nexport interface TaskArm {\n passed: boolean;\n evidence: string;\n /** the run's final text */\n finalText: string;\n error?: string;\n}\n\nexport interface TaskReport {\n skill: string;\n verify: Verify;\n with: TaskArm;\n without: TaskArm;\n}\n\nexport interface TaskOptions {\n repoRoot: string;\n skillRaw: string;\n prompt: string;\n env?: NodeJS.ProcessEnv;\n maxTurns?: number;\n timeoutMs?: number;\n verifyTimeoutMs?: number;\n}\n\nconst OUTCOME_CAP = 2000;\n\nasync function runArm(o: TaskOptions, withSkill: boolean): Promise<TaskArm> {\n const env = o.env ?? process.env;\n const skill = parseSkill(o.skillRaw);\n const verify = parseVerify(skill.body);\n const holder = mkdtempSync(join(tmpdir(), \"sm-skill-task-\"));\n const dir = join(holder, \"repo\");\n const git = (...a: string[]) => execFileSync(\"git\", [\"-C\", o.repoRoot, ...a], { stdio: \"pipe\" });\n try {\n git(\"worktree\", \"add\", \"--detach\", dir, \"HEAD\");\n const skillDir = join(dir, \".claude\", \"skills\", skill.name);\n rmSync(skillDir, { recursive: true, force: true }); // the baseline must not have it, the other arm gets this exact text\n if (withSkill) {\n mkdirSync(skillDir, { recursive: true });\n writeFileSync(join(skillDir, \"SKILL.md\"), o.skillRaw);\n }\n // dontAsk denies whatever is not allowed: edits, and only the commands the skill says it applies to\n const allowedTools = [\"Edit\", \"Write\", ...(skill.meta?.applies_to.commands ?? []).map((c) => `Bash(${c}:*)`)];\n const run = await runClaude({\n prompt: o.prompt,\n cwd: dir,\n tools: [\"Read\", \"Grep\", \"Glob\", \"Edit\", \"Write\", \"Bash\"],\n allowedTools,\n maxTurns: o.maxTurns ?? 12,\n env,\n timeoutMs: o.timeoutMs,\n });\n const finalText = run.result.slice(0, OUTCOME_CAP);\n if (!run.ok) return { passed: false, evidence: \"the run failed\", finalText, error: run.error };\n if (verify.kind === \"none\") return { passed: false, evidence: \"the skill has no ## Verify to check\", finalText };\n if (verify.kind === \"command\") {\n try {\n await execFileP(\"bash\", [\"-c\", verify.command], { cwd: dir, timeout: o.verifyTimeoutMs ?? 120_000, env });\n return { passed: true, evidence: `\\`${verify.command}\\` exited 0`, finalText };\n } catch (e) {\n return { passed: false, evidence: `\\`${verify.command}\\` failed: ${String((e as Error).message).slice(0, 200)}`, finalText };\n }\n }\n if (featureOn(\"jev\", o.repoRoot, env)) {\n const a = await askJev(\n { cwd: o.repoRoot, caller: \"semantic-memory-skill-loop\", state: finalText, questions: [{ key: \"verify\", instructions: `Does this outcome satisfy: ${verify.text}?` }] },\n { env },\n );\n const p = a.scores.verify;\n return { passed: jevVerdict(p) === true, evidence: a.error ? `jev: ${a.error}` : `jev p=${p}`, finalText };\n }\n const g = await runClaude({ prompt: graderPrompt(verify.text, finalText), cwd: dir, tools: [], maxTurns: 1, env, timeoutMs: o.timeoutMs });\n return { ...(g.ok ? parseGrade(g.result) : { passed: false, evidence: `grader failed: ${g.error}` }), finalText };\n } catch (e) {\n return { passed: false, evidence: \"could not set up the scratch copy\", finalText: \"\", error: (e as Error).message };\n } finally {\n try {\n git(\"worktree\", \"remove\", \"--force\", dir);\n } catch {}\n rmSync(holder, { recursive: true, force: true });\n try {\n git(\"worktree\", \"prune\");\n } catch {}\n }\n}\n\nexport async function runTaskEval(o: TaskOptions): Promise<TaskReport> {\n const skill = parseSkill(o.skillRaw);\n const [w, wo] = await Promise.all([runArm(o, true), runArm(o, false)]);\n return { skill: skill.name, verify: parseVerify(skill.body), with: w, without: wo };\n}\n","/**\n * Adapted from Anthropic's skill-creator (https://github.com/anthropics/claude-plugins-official, Apache License 2.0); modified for semantic-memory.\n */\n\n/**\n * Grade one outcome against a skill's `## Verify` text (jev mode off). Skill-creator's grader rules: a pass needs\n * clear evidence of real completion, not surface compliance; no evidence, or superficial evidence, is a fail.\n */\nexport const graderPrompt = (verify: string, outcome: string) =>\n `You are a grader. Decide whether the outcome below satisfies the expectation.\n\nPASS: there is clear evidence the expectation is true AND the evidence reflects genuine task completion, not surface-level compliance.\nFAIL: there is no evidence, or the evidence contradicts the expectation, or it is superficial (a claim of success with nothing to back it).\nJudge only from the outcome text; do not assume anything it does not say.\n\n<expectation>\n${verify}\n</expectation>\n\n<outcome>\n${outcome}\n</outcome>\n\nAnswer with only JSON: {\"passed\": true|false, \"evidence\": \"<one sentence quoting or describing what you found>\"}`;\n\n/** Parse the grader's reply; anything unparseable is a fail (the safe default). */\nexport function parseGrade(text: string): { passed: boolean; evidence: string } {\n const m = text.match(/\\{[\\s\\S]*\\}/);\n try {\n const j = JSON.parse(m ? m[0] : \"\");\n return { passed: j.passed === true, evidence: String(j.evidence ?? \"\") };\n } catch {\n return { passed: false, evidence: \"grader gave no JSON\" };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAMA,SAAS,UAAU,oBAAoB;AACvC,SAAS,WAAW,aAAa,QAAQ,qBAAqB;AAC9D,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,iBAAiB;;;ACFnB,IAAM,eAAe,CAAC,QAAgB,YAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM;AAAA;AAAA;AAAA;AAAA,EAIN,OAAO;AAAA;AAAA;AAAA;AAMF,SAAS,WAAW,MAAqD;AAC9E,QAAM,IAAI,KAAK,MAAM,aAAa;AAClC,MAAI;AACF,UAAM,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE;AAClC,WAAO,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,OAAO,EAAE,YAAY,EAAE,EAAE;AAAA,EACzE,QAAQ;AACN,WAAO,EAAE,QAAQ,OAAO,UAAU,sBAAsB;AAAA,EAC1D;AACF;;;ADjBA,IAAM,YAAY,UAAU,QAAQ;AAK7B,SAAS,YAAY,MAAsB;AAChD,QAAM,IAAI,KAAK,MAAM,iDAAiD;AACtE,QAAM,OAAO,IAAI,CAAC,EAAE,KAAK,KAAK;AAC9B,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,OAAO;AACjC,QAAM,SAAS,KAAK,MAAM,0BAA0B;AACpD,MAAI,SAAS,CAAC,EAAE,KAAK,EAAG,QAAO,EAAE,MAAM,WAAW,SAAS,OAAO,CAAC,EAAE,KAAK,EAAE;AAC5E,QAAM,SAAS,KAAK,QAAQ,sBAAsB,EAAE,EAAE,MAAM,cAAc;AAC1E,SAAO,SAAS,EAAE,MAAM,WAAW,SAAS,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,KAAK;AACzF;AA2BA,IAAM,cAAc;AAEpB,eAAe,OAAO,GAAgB,WAAsC;AAC1E,QAAM,MAAM,EAAE,OAAO,QAAQ;AAC7B,QAAM,QAAQ,WAAW,EAAE,QAAQ;AACnC,QAAM,SAAS,YAAY,MAAM,IAAI;AACrC,QAAM,SAAS,YAAY,KAAK,OAAO,GAAG,gBAAgB,CAAC;AAC3D,QAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,QAAM,MAAM,IAAI,MAAgB,aAAa,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,OAAO,CAAC;AAC/F,MAAI;AACF,QAAI,YAAY,OAAO,YAAY,KAAK,MAAM;AAC9C,UAAM,WAAW,KAAK,KAAK,WAAW,UAAU,MAAM,IAAI;AAC1D,WAAO,UAAU,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACjD,QAAI,WAAW;AACb,gBAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AACvC,oBAAc,KAAK,UAAU,UAAU,GAAG,EAAE,QAAQ;AAAA,IACtD;AAEA,UAAM,eAAe,CAAC,QAAQ,SAAS,IAAI,MAAM,MAAM,WAAW,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC;AAC5G,UAAM,MAAM,MAAM,UAAU;AAAA,MAC1B,QAAQ,EAAE;AAAA,MACV,KAAK;AAAA,MACL,OAAO,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,SAAS,MAAM;AAAA,MACvD;AAAA,MACA,UAAU,EAAE,YAAY;AAAA,MACxB;AAAA,MACA,WAAW,EAAE;AAAA,IACf,CAAC;AACD,UAAM,YAAY,IAAI,OAAO,MAAM,GAAG,WAAW;AACjD,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,QAAQ,OAAO,UAAU,kBAAkB,WAAW,OAAO,IAAI,MAAM;AAC7F,QAAI,OAAO,SAAS,OAAQ,QAAO,EAAE,QAAQ,OAAO,UAAU,uCAAuC,UAAU;AAC/G,QAAI,OAAO,SAAS,WAAW;AAC7B,UAAI;AACF,cAAM,UAAU,QAAQ,CAAC,MAAM,OAAO,OAAO,GAAG,EAAE,KAAK,KAAK,SAAS,EAAE,mBAAmB,MAAS,IAAI,CAAC;AACxG,eAAO,EAAE,QAAQ,MAAM,UAAU,KAAK,OAAO,OAAO,eAAe,UAAU;AAAA,MAC/E,SAAS,GAAG;AACV,eAAO,EAAE,QAAQ,OAAO,UAAU,KAAK,OAAO,OAAO,cAAc,OAAQ,EAAY,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,IAAI,UAAU;AAAA,MAC7H;AAAA,IACF;AACA,QAAI,UAAU,OAAO,EAAE,UAAU,GAAG,GAAG;AACrC,YAAM,IAAI,MAAM;AAAA,QACd,EAAE,KAAK,EAAE,UAAU,QAAQ,8BAA8B,OAAO,WAAW,WAAW,CAAC,EAAE,KAAK,UAAU,cAAc,8BAA8B,OAAO,IAAI,IAAI,CAAC,EAAE;AAAA,QACtK,EAAE,IAAI;AAAA,MACR;AACA,YAAM,IAAI,EAAE,OAAO;AACnB,aAAO,EAAE,QAAQ,WAAW,CAAC,MAAM,MAAM,UAAU,EAAE,QAAQ,QAAQ,EAAE,KAAK,KAAK,SAAS,CAAC,IAAI,UAAU;AAAA,IAC3G;AACA,UAAM,IAAI,MAAM,UAAU,EAAE,QAAQ,aAAa,OAAO,MAAM,SAAS,GAAG,KAAK,KAAK,OAAO,CAAC,GAAG,UAAU,GAAG,KAAK,WAAW,EAAE,UAAU,CAAC;AACzI,WAAO,EAAE,GAAI,EAAE,KAAK,WAAW,EAAE,MAAM,IAAI,EAAE,QAAQ,OAAO,UAAU,kBAAkB,EAAE,KAAK,GAAG,GAAI,UAAU;AAAA,EAClH,SAAS,GAAG;AACV,WAAO,EAAE,QAAQ,OAAO,UAAU,qCAAqC,WAAW,IAAI,OAAQ,EAAY,QAAQ;AAAA,EACpH,UAAE;AACA,QAAI;AACF,UAAI,YAAY,UAAU,WAAW,GAAG;AAAA,IAC1C,QAAQ;AAAA,IAAC;AACT,WAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC/C,QAAI;AACF,UAAI,YAAY,OAAO;AAAA,IACzB,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;AAEA,eAAsB,YAAY,GAAqC;AACrE,QAAM,QAAQ,WAAW,EAAE,QAAQ;AACnC,QAAM,CAAC,GAAG,EAAE,IAAI,MAAM,QAAQ,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC;AACrE,SAAO,EAAE,OAAO,MAAM,MAAM,QAAQ,YAAY,MAAM,IAAI,GAAG,MAAM,GAAG,SAAS,GAAG;AACpF;","names":[]}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveSessionDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EYVFC745.js";
|
|
4
|
+
import {
|
|
5
|
+
matter_default
|
|
6
|
+
} from "./chunk-J7LYDRML.js";
|
|
4
7
|
import {
|
|
5
8
|
redact
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-H2BES6FU.js";
|
|
7
10
|
|
|
8
11
|
// src/core/dossier.ts
|
|
9
12
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
10
13
|
import { existsSync } from "fs";
|
|
11
14
|
import { join } from "path";
|
|
12
15
|
import { glob } from "glob";
|
|
13
|
-
import matter from "gray-matter";
|
|
14
16
|
|
|
15
17
|
// src/core/section.ts
|
|
16
18
|
function extractSection(markdown, heading) {
|
|
@@ -101,7 +103,7 @@ async function listDossiers(vaultPath) {
|
|
|
101
103
|
for (const rel of files) {
|
|
102
104
|
try {
|
|
103
105
|
const raw = await readFile(join(vaultPath, rel), "utf-8");
|
|
104
|
-
const { data, content } =
|
|
106
|
+
const { data, content } = matter_default(raw);
|
|
105
107
|
if (data.type !== "dossier") continue;
|
|
106
108
|
const entity = typeof data.entity === "string" && data.entity.trim() ? data.entity.trim() : rel.replace(/^dossiers\//, "").replace(/\.md$/, "");
|
|
107
109
|
const aliases = Array.isArray(data.aliases) ? data.aliases.map(String).map((a) => a.trim()).filter(Boolean) : [];
|
|
@@ -166,7 +168,7 @@ async function readDossierRaw(vaultPath, rel) {
|
|
|
166
168
|
if (!existsSync(abs)) return null;
|
|
167
169
|
try {
|
|
168
170
|
const raw = await readFile(abs, "utf-8");
|
|
169
|
-
const { data } =
|
|
171
|
+
const { data } = matter_default(raw);
|
|
170
172
|
if (data.type !== "dossier") return null;
|
|
171
173
|
const entity = typeof data.entity === "string" ? data.entity : rel;
|
|
172
174
|
return { raw, entity };
|
|
@@ -202,7 +204,7 @@ async function initDossier(vaultPath, entity, opts = {}) {
|
|
|
202
204
|
};
|
|
203
205
|
const body = dossierTemplate(entity, { purpose: opts.purpose === void 0 ? void 0 : purpose.text, seeded_from: opts.seeded_from });
|
|
204
206
|
await mkdir(join(vaultPath, DOSSIER_DIR), { recursive: true });
|
|
205
|
-
await writeFile(abs,
|
|
207
|
+
await writeFile(abs, matter_default.stringify(body, fm), "utf-8");
|
|
206
208
|
await compileDossiers(vaultPath);
|
|
207
209
|
return { path: rel, created: true, entity, ...purpose.count ? { redacted: purpose.count } : {} };
|
|
208
210
|
}
|
|
@@ -314,4 +316,4 @@ export {
|
|
|
314
316
|
appendToSection,
|
|
315
317
|
replaceSectionBody
|
|
316
318
|
};
|
|
317
|
-
//# sourceMappingURL=chunk-
|
|
319
|
+
//# sourceMappingURL=chunk-EXAUVIGW.js.map
|