diff --git a/openclaw.plugin.json b/openclaw.plugin.json index e6c1b0533..dfc4c1544 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -39,8 +39,8 @@ "skills/briefing", "skills/citation-fixer", "skills/concept-synthesis", - "skills/cross-modal-review", "skills/cron-scheduler", + "skills/cross-modal-review", "skills/daily-task-manager", "skills/daily-task-prep", "skills/data-research", @@ -54,10 +54,11 @@ "skills/media-ingest", "skills/meeting-ingestion", "skills/minion-orchestrator", + "skills/obsidian-gbrain-safe-index", "skills/perplexity-research", "skills/query", - "skills/reports", "skills/repo-architecture", + "skills/reports", "skills/signal-detector", "skills/skill-creator", "skills/skillify", diff --git a/skills/RESOLVER.md b/skills/RESOLVER.md index 44cb82d3d..f458fd434 100644 --- a/skills/RESOLVER.md +++ b/skills/RESOLVER.md @@ -60,7 +60,8 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef | "get more out of gbrain", "is my brain set up right", "weekly brain checkup", "advise me on my brain", "gbrain advisor" | `skills/gbrain-advisor/SKILL.md` | | Save or load reports | `skills/reports/SKILL.md` | | "Create a skill", "improve this skill" | `skills/skill-creator/SKILL.md` | -| "Skillify this", "is this a skill?", "make this proper" | `skills/skillify/SKILL.md` | +| "save this learning to the vault", "capture this skill in Obsidian", "record this workflow in my notes", "put this setup change in the knowledge base" | `skills/skill-vault-capture-policy/SKILL.md` | +| "Skillify this", "is this a skill?", "make this proper", "add tests and evals for this" | `skills/skillify/SKILL.md` | | "Compress my resolver", "AGENTS.md too large", "RESOLVER.md too big", "functional area dispatcher", "shrink routing table" | `skills/functional-area-resolver/SKILL.md` | | "Is gbrain healthy?", morning health check, skillpack-check | `skills/skillpack-check/SKILL.md` | | "harvest this skill into gbrain", "publish this skill to gbrain", "lift this skill upstream", "share this skill with other gbrain clients", "promote my skill to gbrain" | `skills/skillpack-harvest/SKILL.md` | @@ -78,6 +79,7 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef | "Set up GBrain", first boot | `skills/setup/SKILL.md` | | "Now what?", "fill my brain", "cold start", "bootstrap", "import my data", "what should I import first" | `skills/cold-start/SKILL.md` | | "Migrate from Obsidian/Notion/Logseq" | `skills/migrate/SKILL.md` | +| "Connect Obsidian to gbrain", "sync vault and gbrain", "safe/free gbrain index", "embed gbrain after vault update", "is gbrain synced with my vault" | `skills/obsidian-gbrain-safe-index/SKILL.md` | | Brain health check, maintenance run | `skills/maintain/SKILL.md` | | "Extract links", "build link graph", "populate timeline" | `skills/maintain/SKILL.md` (extraction sections) | | "Run dream", "process today's session", "synthesize my conversations", "consolidate yesterday's conversations", "what patterns did you see", "did the dream cycle run" | `skills/maintain/SKILL.md` (dream cycle section) | diff --git a/skills/manifest.json b/skills/manifest.json index 0e1b84934..4a946b7de 100644 --- a/skills/manifest.json +++ b/skills/manifest.json @@ -2,7 +2,7 @@ "name": "gbrain", "version": "0.32.3.0", "conformance_version": "1.0.0", - "description": "Personal knowledge brain with hybrid RAG search \u2014 GStack mod for agent platforms", + "description": "Personal knowledge brain with hybrid RAG search — GStack mod for agent platforms", "skills": [ { "name": "ingest", @@ -34,6 +34,11 @@ "path": "migrate/SKILL.md", "description": "Universal migration from Obsidian, Notion, Logseq, markdown, CSV, JSON, Roam" }, + { + "name": "obsidian-gbrain-safe-index", + "path": "obsidian-gbrain-safe-index/SKILL.md", + "description": "Connect and sync an Obsidian-style Markdown vault with gbrain using a cost-controlled import-first workflow, explicit embedding gates, and durable skill/workflow capture." + }, { "name": "setup", "path": "setup/SKILL.md", @@ -263,6 +268,11 @@ "name": "skill-optimizer", "path": "skill-optimizer/SKILL.md", "description": "Self-evolving skill optimization via gbrain skillopt — SkillOpt-paper-grounded text-space optimizer with validation gating (median-of-3 + epsilon=0.05), bundled-skill safety, bootstrap review sentinel, per-skill DB lock, and atomic versioned writes." + }, + { + "name": "skill-vault-capture-policy", + "path": "skill-vault-capture-policy/SKILL.md", + "description": "Capture durable operational learnings, new agent skills, and environment/setup changes into the Obsidian vault instead of transient chat memory." } ], "dependencies": { diff --git a/skills/obsidian-gbrain-safe-index/SKILL.md b/skills/obsidian-gbrain-safe-index/SKILL.md new file mode 100644 index 000000000..946350080 --- /dev/null +++ b/skills/obsidian-gbrain-safe-index/SKILL.md @@ -0,0 +1,137 @@ +--- +name: obsidian-gbrain-safe-index +version: 1.0.0 +description: | + Connect, maintain, and sync an Obsidian-style Markdown vault with gbrain while preserving a cost-controlled workflow: the vault remains the source of truth, gbrain is the searchable/embedded index, durable skills/workflows are captured into the vault, and paid embedding runs only after explicit approval. +triggers: + - "connect Obsidian to gbrain" + - "import my vault to gbrain" + - "sync vault and gbrain" + - "capture this skill in my vault" + - "embed gbrain after vault update" + - "is gbrain synced with my vault" +tools: + - terminal + - read_file + - search_files + - write_file + - patch +mutating: true +--- + +# Obsidian → gbrain Safe Index and Capture + +## Contract + +This skill guarantees: + +- Treats the user's Obsidian-style Markdown vault as the source of truth before gbrain indexing. +- Keeps gbrain in conservative mode unless the user explicitly approves a more expensive mode. +- Imports vault changes with `--no-embed` first, then embeds only after explicit approval for the specific paid action. +- Captures durable new skills, workflows, and environment learnings into the vault instead of leaving them only in chat memory. +- Verifies every sync with concrete `gbrain stats`, search mode, and, when embeddings run, exact embedded chunk counts. + +## Phases + +1. **Resolve the vault path.** + - Prefer an existing environment variable such as `OBSIDIAN_VAULT_PATH` or `WIKI_PATH`. + - If no path is configured, search likely note directories and ask the user before writing. + - Verify the directory exists and contains markdown files or an `.obsidian` directory. + +2. **Read vault operating rules before writing.** + - If the vault has `SCHEMA.md`, `index.md`, `log.md`, `AGENTS.md`, or similar operating files, read them before ingest/query/major edit. + - Respect immutable source folders such as `raw/` when the vault declares them. + - Use the vault's native link convention, usually Obsidian `[[wikilinks]]`, for durable relationships. + +3. **MECE/capture decision.** + - If new knowledge belongs on an existing page, update that page. + - If it is a distinct recurring workflow or operational policy, create a small meta or concept page following the vault schema. + - Update the vault index/catalog for every new page when the vault maintains one. + - Append a log entry for meaningful vault updates when the vault maintains a log. + +4. **Safe gbrain import path.** + - Pre-check source directory; do not import a nonexistent path. + - Run `gbrain config set search.mode conservative` before/after risky reinit steps. + - Run `gbrain import "$OBSIDIAN_VAULT_PATH" --no-embed`. + - Run `gbrain extract links --source fs --dir "$OBSIDIAN_VAULT_PATH"` when wikilinks changed materially. + +5. **Paid embedding gate.** + - Do not run `gbrain embed --stale` unless the user explicitly asks or a prior instruction clearly approved this exact paid action. + - Before embedding, verify provider readiness with `gbrain providers test --model `. + - Confirm the configured embedding dimensions match the local schema. + - After embedding, verify `gbrain stats` and record exact `Pages`, `Chunks`, `Embedded`, and `Links` counts. + +6. **Final verification and vault echo.** + - Run `gbrain stats` and `gbrain search modes`. + - If vault files changed, re-import with `--no-embed`; if embedding was approved, embed stale chunks afterward. + - Report what changed, what was free/local, what used API billing, and what remains pending. + +## Output Format + +Use a compact status table: + +| Item | Status | +|---|---| +| Vault path | `/path` | +| Vault updated | yes/no + files | +| gbrain mode | conservative/balanced/tokenmax | +| Import | `--no-embed` completed / skipped / failed | +| Pages/chunks | exact counts from `gbrain stats` | +| Embeddings | exact count; note whether this run used API billing | +| Links | exact count | +| Background jobs | none / list exact jobs | + +Then include: + +- **Safe next step:** free/local action. +- **Paid next step:** embedding/LLM action, if any, with explicit approval requirement. + +## Anti-Patterns + +- Creating a duplicate skill/page when an existing Obsidian, gbrain, or vault-ingest skill already covers the workflow. +- Running `gbrain embed --stale`, `gbrain dream`, `gbrain autopilot --install`, `gbrain onboard --auto`, or `tokenmax` without explicit cost approval. +- Importing a nonexistent or wrong directory and treating a zero-page import as success. +- Forgetting to update the vault index/catalog and log after creating or materially updating vault pages. +- Recording API keys, tokens, or raw secrets in the vault or final response. + +## Tools Used + +- `read_file` — read vault schema/index/log and target notes. +- `search_files` — find existing vault pages and avoid duplicates. +- `write_file` / `patch` — create or update vault pages. +- `terminal` — run `gbrain`, `git`, and environment checks with secret values redacted. + +## Safe Commands + +```bash +export PATH="$HOME/.bun/bin:$PATH" +gbrain config set search.mode conservative +gbrain import "$OBSIDIAN_VAULT_PATH" --no-embed +gbrain extract links --source fs --dir "$OBSIDIAN_VAULT_PATH" +gbrain stats +gbrain search modes +``` + +## Paid / Approval-Gated Commands + +```bash +gbrain providers test --model +gbrain embed --stale +gbrain dream +gbrain autopilot --install +gbrain onboard --auto --max-usd 5 +gbrain config set search.mode tokenmax +``` + +## Verification Checklist + +- [ ] Vault path exists and is the intended source. +- [ ] Vault operating files were read before edits when present. +- [ ] Existing pages/skills were searched to avoid duplicates. +- [ ] New/updated vault pages follow the vault schema and link convention. +- [ ] Vault index/catalog updated for new pages when present. +- [ ] Vault log appended for meaningful actions when present. +- [ ] `gbrain import ... --no-embed` completed. +- [ ] `gbrain stats` recorded pages/chunks/embeddings/links. +- [ ] `gbrain search modes` confirms conservative mode unless a different mode was explicitly approved. +- [ ] No paid/background commands ran without approval. diff --git a/skills/obsidian-gbrain-safe-index/routing-eval.jsonl b/skills/obsidian-gbrain-safe-index/routing-eval.jsonl new file mode 100644 index 000000000..e094a657d --- /dev/null +++ b/skills/obsidian-gbrain-safe-index/routing-eval.jsonl @@ -0,0 +1,14 @@ +// Routing eval fixtures for skills/obsidian-gbrain-safe-index. +// Positive cases: realistic user phrasings that should resolve to this skill. +{"intent": "connect my Obsidian vault to gbrain", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "import my vault into gbrain without embeddings first", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "sync my Obsidian vault with gbrain", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "capture this new skill in my vault", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "embed gbrain after I update the vault", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "is gbrain synced with my Obsidian vault", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "safe way to index my notes into gbrain without spending money", "expected_skill": "obsidian-gbrain-safe-index"} +{"intent": "I want to import my markdown vault to gbrain first, no embeddings", "expected_skill": "obsidian-gbrain-safe-index"} +// Negative cases: related but owned by other skills. Assert NO route to this skill. +{"intent": "harvest this skill into gbrain", "expected_skill": null} +{"intent": "set up gbrain from scratch", "expected_skill": null} +{"intent": "migrate my notes from Notion to gbrain", "expected_skill": null} diff --git a/skills/skill-vault-capture-policy/SKILL.md b/skills/skill-vault-capture-policy/SKILL.md new file mode 100644 index 000000000..b17047b99 --- /dev/null +++ b/skills/skill-vault-capture-policy/SKILL.md @@ -0,0 +1,100 @@ +--- +name: skill-vault-capture-policy +version: 1.0.0 +description: | + Use when the user wants a durable operational learning, new agent skill, or + important environment/setup change to be captured into the Obsidian vault + instead of left only in transient chat memory. Covers the capture rule, + preferred page patterns, and index/log update obligations. +triggers: + - "save this learning to the vault" + - "capture this skill in Obsidian" + - "record this workflow in my notes" + - "put this setup change in the vault" + - "should we add this to the knowledge base" +tools: + - read_file + - search_files + - write_file + - patch +mutating: true +--- + +# Skill Vault Capture Policy + +## Contract + +This skill guarantees: + +- Durable operational learnings, newly adopted skills, and important environment/setup changes are captured into the Obsidian vault rather than left only in chat memory. +- An existing page is updated when the knowledge clearly belongs there; a new page is created only when the topic is distinct and likely to recur. +- Every new vault page is added to `index.md`. +- Every meaningful create/update appends a dated entry to `log.md`. +- Small linked pages are preferred over one giant running note. + +## Phases + +1. **Classify the learning.** + - New gbrain operating rule, cost control, or embedding/provider change. + - New agent-fork / harness / coding-tool integration fact. + - New Obsidian vault workflow or structure decision. + - New recurring agent skill that changes how the agent should operate here. + +2. **Avoid duplicates.** + - Search the vault for an existing page that already owns the topic. + - If found, update it with a new section or dated note rather than creating a near-duplicate. + +3. **Create when distinct.** + - Place new pages under the vault schema: `_meta/` for operating notes, `concepts/` for workflows, `entities/` for tools/people. + - Use YAML frontmatter and at least two `[[wikilinks]]` unless it is a short seed page. + +4. **Update navigation.** + - Add the page to `index.md` under the correct type heading. + - Append a `## [YYYY-MM-DD] create|update | subject` entry to `log.md`. + +5. **Report the capture.** + - State which files changed and whether `index.md` / `log.md` were updated. + +## Output Format + +Use a short status block: + +| Item | Status | +|---|---| +| Learning classified | type | +| Page created/updated | path | +| index.md updated | yes/no | +| log.md updated | yes/no | + +## Anti-Patterns + +- Leaving durable learnings only in chat memory. +- Creating a near-duplicate page instead of updating the existing one. +- Forgetting to update `index.md` and `log.md`. +- Writing one giant running note instead of small linked pages. +- Recording secrets, API keys, or raw credentials in the vault. + +## Tools Used + +- `read_file` — read `SCHEMA.md`, `index.md`, `log.md`, and target pages. +- `search_files` — find existing pages to avoid duplicates. +- `write_file` / `patch` — create or update vault pages and navigation. + +## Safe Commands + +```bash +# inspect vault navigation before writing +read SCHEMA.md index.md log.md +# create or update a page, then refresh catalog/log +# index.md: add [[page-slug]] under the matching type heading +# log.md: append ## [YYYY-MM-DD] create|update | subject +``` + +## Verification Checklist + +- [ ] Vault schema/read files were checked before writing. +- [ ] Existing pages were searched to avoid duplicates. +- [ ] New/updated page has frontmatter and wikilinks. +- [ ] `index.md` updated for new pages. +- [ ] `log.md` appended for meaningful actions. +- [ ] No secrets or raw credentials were written. diff --git a/skills/skill-vault-capture-policy/routing-eval.jsonl b/skills/skill-vault-capture-policy/routing-eval.jsonl new file mode 100644 index 000000000..786bc059e --- /dev/null +++ b/skills/skill-vault-capture-policy/routing-eval.jsonl @@ -0,0 +1,10 @@ +// Routing eval fixtures for skills/skill-vault-capture-policy. +{"intent": "save this learning to the vault", "expected_skill": "skill-vault-capture-policy"} +{"intent": "capture this new skill in Obsidian", "expected_skill": "skill-vault-capture-policy"} +{"intent": "record this workflow in my notes", "expected_skill": "skill-vault-capture-policy"} +{"intent": "put this setup change in the knowledge base", "expected_skill": "skill-vault-capture-policy"} +{"intent": "should we add this recurring agent skill to the wiki", "expected_skill": "skill-vault-capture-policy"} +{"intent": "I learned a new gbrain cost control, write it down", "expected_skill": "skill-vault-capture-policy"} +// Negative cases: related but owned by other skills. +{"intent": "connect my Obsidian vault to gbrain", "expected_skill": null} +{"intent": "harvest this skill into gbrain", "expected_skill": null} diff --git a/test/e2e/obsidian-gbrain-safe-index.test.ts b/test/e2e/obsidian-gbrain-safe-index.test.ts new file mode 100644 index 000000000..fd1718152 --- /dev/null +++ b/test/e2e/obsidian-gbrain-safe-index.test.ts @@ -0,0 +1,61 @@ +/** + * E2E smoke for skills/obsidian-gbrain-safe-index. + * + * Verifies the from-trigger-to-side-effect path that skillify requires: + * a real user trigger phrase routes to the skill, the resolver/check + * pipeline treats it as reachable, and the skill file exposes the + * gbrain commands the workflow actually runs. + * + * This stays local-only (no paid embedding, no external API): it asserts + * the documented safe/import path is present and parseable, not that it + * mutates a live brain. + */ + +import { describe, expect, it } from 'bun:test'; +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; + +const SKILLS = join(import.meta.dir, '..', '..', 'skills'); +const SKILL_MD = join(SKILLS, 'obsidian-gbrain-safe-index', 'SKILL.md'); +const RESOLVER = join(SKILLS, 'RESOLVER.md'); + +const TRIGGER_PHRASES = [ + 'connect my Obsidian vault to gbrain', + 'import my vault to gbrain', + 'sync vault and gbrain', + 'capture this skill in my vault', + 'embed gbrain after vault update', + 'is gbrain synced with my vault', +]; + +describe('obsidian-gbrain-safe-index E2E', () => { + it('resolver maps real trigger phrasings to the skill', () => { + const resolver = readFileSync(RESOLVER, 'utf-8'); + expect(resolver).toContain('obsidian-gbrain-safe-index/SKILL.md'); + // Each representative phrase shares a token substring with a resolver row. + const rows = resolver + .split('\n') + .filter((l) => l.includes('obsidian-gbrain-safe-index/SKILL.md')) + .join('\n'); + for (const phrase of TRIGGER_PHRASES) { + const hit = phrase + .toLowerCase() + .split(/\s+/) + .some((tok) => tok.length > 3 && rows.toLowerCase().includes(tok)); + expect(hit, `no resolver token for: ${phrase}`).toBe(true); + } + }); + + it('skill documents the gbrain import-first safe path', () => { + const body = readFileSync(SKILL_MD, 'utf-8'); + expect(body).toContain('gbrain import'); + expect(body).toContain('--no-embed'); + expect(body).toContain('gbrain config set search.mode conservative'); + // Paid gate must be explicit, not a silent default. + expect(body).toContain('gbrain embed --stale'); + }); + + it('skill is reachable from the skill tree', () => { + expect(existsSync(SKILL_MD)).toBe(true); + }); +}); diff --git a/test/e2e/skill-vault-capture-policy.test.ts b/test/e2e/skill-vault-capture-policy.test.ts new file mode 100644 index 000000000..62bf09eb5 --- /dev/null +++ b/test/e2e/skill-vault-capture-policy.test.ts @@ -0,0 +1,52 @@ +/** + * E2E smoke for skills/skill-vault-capture-policy. + * + * Verifies the from-trigger-to-side-effect path: a real capture request + * routes to the skill, and the skill documents the vault navigation + * obligations (index.md / log.md) that make a capture durable. + * + * Local-only: it asserts documented behavior, not live vault writes. + */ + +import { describe, expect, it } from 'bun:test'; +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; + +const SKILLS = join(import.meta.dir, '..', '..', 'skills'); +const SKILL_MD = join(SKILLS, 'skill-vault-capture-policy', 'SKILL.md'); +const RESOLVER = join(SKILLS, 'RESOLVER.md'); + +const TRIGGER_PHRASES = [ + 'save this learning to the vault', + 'capture this skill in Obsidian', + 'record this workflow in my notes', + 'put this setup change in the knowledge base', +]; + +describe('skill-vault-capture-policy E2E', () => { + it('resolver maps real capture phrasings to the skill', () => { + const resolver = readFileSync(RESOLVER, 'utf-8'); + expect(resolver).toContain('skill-vault-capture-policy/SKILL.md'); + const rows = resolver + .split('\n') + .filter((l) => l.includes('skill-vault-capture-policy/SKILL.md')) + .join('\n'); + for (const phrase of TRIGGER_PHRASES) { + const hit = phrase + .toLowerCase() + .split(/\s+/) + .some((tok) => tok.length > 3 && rows.toLowerCase().includes(tok)); + expect(hit, `no resolver token for: ${phrase}`).toBe(true); + } + }); + + it('skill documents index.md and log.md update obligations', () => { + const body = readFileSync(SKILL_MD, 'utf-8'); + expect(body).toContain('index.md'); + expect(body).toContain('log.md'); + }); + + it('skill is reachable from the skill tree', () => { + expect(existsSync(SKILL_MD)).toBe(true); + }); +}); diff --git a/test/obsidian-gbrain-safe-index.test.ts b/test/obsidian-gbrain-safe-index.test.ts new file mode 100644 index 000000000..920ea2bc6 --- /dev/null +++ b/test/obsidian-gbrain-safe-index.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'bun:test'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +const SKILL_DIR = join(import.meta.dir, '..', 'skills', 'obsidian-gbrain-safe-index'); +const SKILL_MD = join(SKILL_DIR, 'SKILL.md'); +const RESOLVER = join(import.meta.dir, '..', 'skills', 'RESOLVER.md'); + +function parseFrontmatter(raw: string): Record { + const m = raw.match(/^---\n([\s\S]*?)\n---/); + if (!m) throw new Error('no frontmatter'); + const out: Record = {}; + for (const line of m[1].split('\n')) { + const mm = line.match(/^([a-zA-Z_]+):\s*(.*)$/); + if (mm) out[mm[1]] = mm[2].trim(); + } + return out; +} + +describe('obsidian-gbrain-safe-index skill', () => { + it('has a SKILL.md with required frontmatter', () => { + expect(existsSync(SKILL_MD)).toBe(true); + const fm = parseFrontmatter(readFileSync(SKILL_MD, 'utf-8')); + expect(fm['name']).toBe('obsidian-gbrain-safe-index'); + expect(fm['description']).toBeTruthy(); + }); + + it('has the required conformance sections', () => { + const body = readFileSync(SKILL_MD, 'utf-8'); + for (const section of ['## Contract', '## Phases', '## Output Format', '## Anti-Patterns']) { + expect(body.includes(section), `missing ${section}`).toBe(true); + } + }); + + it('is registered in RESOLVER.md', () => { + expect(existsSync(RESOLVER)).toBe(true); + const resolver = readFileSync(RESOLVER, 'utf-8'); + expect(resolver.includes('obsidian-gbrain-safe-index/SKILL.md')).toBe(true); + }); + + it('has routing-eval fixtures that exercise real trigger phrasings', () => { + const evalPath = join(SKILL_DIR, 'routing-eval.jsonl'); + expect(existsSync(evalPath)).toBe(true); + const lines = readFileSync(evalPath, 'utf-8') + .split('\n') + .filter((l) => l.trim() && !l.trim().startsWith('//')) + .map((l) => JSON.parse(l)); + const positives = lines.filter((l) => l.expected_skill === 'obsidian-gbrain-safe-index'); + expect(positives.length).toBeGreaterThanOrEqual(5); + }); +}); diff --git a/test/skill-vault-capture-policy.test.ts b/test/skill-vault-capture-policy.test.ts new file mode 100644 index 000000000..cea47aa9b --- /dev/null +++ b/test/skill-vault-capture-policy.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'bun:test'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +const SKILL_DIR = join(import.meta.dir, '..', 'skills', 'skill-vault-capture-policy'); +const SKILL_MD = join(SKILL_DIR, 'SKILL.md'); +const RESOLVER = join(import.meta.dir, '..', 'skills', 'RESOLVER.md'); + +function parseFrontmatter(raw: string): Record { + const m = raw.match(/^---\n([\s\S]*?)\n---/); + if (!m) throw new Error('no frontmatter'); + const out: Record = {}; + for (const line of m[1].split('\n')) { + const mm = line.match(/^([a-zA-Z_]+):\s*(.*)$/); + if (mm) out[mm[1]] = mm[2].trim(); + } + return out; +} + +describe('skill-vault-capture-policy skill', () => { + it('has a SKILL.md with required frontmatter', () => { + expect(existsSync(SKILL_MD)).toBe(true); + const fm = parseFrontmatter(readFileSync(SKILL_MD, 'utf-8')); + expect(fm['name']).toBe('skill-vault-capture-policy'); + expect(fm['description']).toBeTruthy(); + }); + + it('has the required conformance sections', () => { + const body = readFileSync(SKILL_MD, 'utf-8'); + for (const section of ['## Contract', '## Phases', '## Output Format', '## Anti-Patterns']) { + expect(body.includes(section), `missing ${section}`).toBe(true); + } + }); + + it('is registered in RESOLVER.md', () => { + expect(existsSync(RESOLVER)).toBe(true); + expect(readFileSync(RESOLVER, 'utf-8').includes('skill-vault-capture-policy/SKILL.md')).toBe(true); + }); + + it('contains no private user or agent-fork names (privacy rule)', () => { + for (const file of [SKILL_MD, join(SKILL_DIR, 'routing-eval.jsonl')]) { + const body = readFileSync(file, 'utf-8'); + for (const name of [/\bAdam\b/, /\bHermes\b/, /\bHerdr\b/, /\bArk\b/, /\bWintermute\b/i]) { + expect(name.test(body), `private name ${name} in ${file}`).toBe(false); + } + } + }); + + it('has routing-eval fixtures', () => { + const evalPath = join(SKILL_DIR, 'routing-eval.jsonl'); + expect(existsSync(evalPath)).toBe(true); + const positives = readFileSync(evalPath, 'utf-8') + .split('\n') + .filter((l) => l.trim() && !l.trim().startsWith('//')) + .map((l) => JSON.parse(l)) + .filter((l) => l.expected_skill === 'skill-vault-capture-policy'); + expect(positives.length).toBeGreaterThanOrEqual(4); + }); +});