Verification: P0.6.2 ε Skill Registry CRUD

Task: P0.6.2 — ε Skill Registry CRUD (repository + migration + loader + MCP tool) Branch: feature/p0-6-2-skill-crud Rebased onto: origin/main @ 0a10d85a (post-P0.7.2) Verify commit: TBD (this file is part of Step 5 commit)


1. Rebase + migration renumber

Migration collision resolved:

  • P0.7.2 landed 003_thought_records.sql in main after this branch was created
  • This branch’s 003_skills.sql was renamed to 004_skills.sql (commit fa52ce97)
  • src/db/schema.sql updated to reference 004_skills.sql (conflict resolved in rebase)
  • src/db/migrations/ now has: 001_init.sql, 002_tasks.sql, 003_thought_records.sql, 004_skills.sql

2. Circular import fix (startup.ts)

The P0.7.2 pattern registers tools at bootstrap() time using getDb() lazy-resolve. loadSkillsFromDisk is called from startup.ts Phase 2 — not from bootstrap().

A static import of loadSkillsFromDisk from startup.ts would create a circular chain: server.ts (IIFE)startup.tsrepository.tsserver.ts (deadlock).

Fix: dynamic await import('./domains/skills/repository.js') at Phase 2 call-time, same pattern as loadServerModule(). A loadSkillsFn injection seam was added to StartupOptions so tests can bypass the real loader with a no-op.

registerSkillTools(ctx) was updated to use getDb() lazily (same as thought tools) and is called from bootstrap() alongside registerThoughtTools(ctx).


3. Implementation files

File Change
src/db/migrations/004_skills.sql ε skills table + idx_skills_greek index
src/domains/skills/repository.ts loadSkillsFromDisk, getSkill, listSkills, registerSkillTools
src/server.ts Added registerSkillTools import + call in bootstrap()
src/startup.ts Added Phase 2 skills loader via dynamic import; loadSkillsFn seam in StartupOptions
src/db/schema.sql Updated to reference 004_skills.sql; added ε section

4. Test evidence

New test file: src/__tests__/domains/skills/repository.test.ts New tests: 35

Test categories covered:

  • migration 004_skills.sql sanity (2 tests)
  • loadSkillsFromDisk single skill (2 tests)
  • loadSkillsFromDisk optional fields (2 tests)
  • loadSkillsFromDisk multiple skills (1 test)
  • loadSkillsFromDisk idempotency (2 tests)
  • loadSkillsFromDisk pruning (1 test)
  • loadSkillsFromDisk error handling (4 tests)
  • getSkill (5 tests)
  • listSkills — empty DB (1 test)
  • listSkills — single skill (1 test)
  • listSkills — ordering (1 test)
  • listSkills — search filter (5 tests)
  • listSkills — capability filter (3 tests)
  • round-trip (2 tests)
  • total count verification (1 test)
  • registerSkillTools registration (2 tests)

Coverage on src/domains/skills/repository.ts:

  • Statements: 91.2%
  • Branches: 84.21%
  • Functions: 88.23%
  • Lines: 91.76%

Uncovered lines (acceptable per P0.7.2 precedent):

  • Line 142: defensive capabilities array parse fallback (JSON error on non-array)
  • Line 231: defensive same path in decodeRow (JSON parse error fallback)
  • Lines 441–455: MCP handler closure body — getDb() cannot be called in unit tests without a live DB singleton; this branch is covered by integration tests

Modified existing tests (no regressions):

  • src/__tests__/startup.test.ts: added loadSkillsFn no-op to defaultOptions and 2 direct startup() calls that used fake DB handles
  • src/__tests__/tools/health.test.ts: added loadSkillsFn no-op to one startupOpts that used a partial fake DB

5. Gate output

npm test

Test Suites: 13 passed, 13 total
Tests:       677 passed, 677 total
Snapshots:   0 total
Time:        17.518 s
Ran all test suites.

(35 new tests in domains/skills/repository.test.ts; 642 pre-existing passing)

npm run lint

(exit 0 — no output)

npm run build

(exit 0 — tsc clean)

6. Wiring status

Feature Status
Migration 004_skills.sql ✓ Applied via initDb migrations loader
loadSkillsFromDisk in startup Phase 2 ✓ Wired via dynamic import + loadSkillsFn injection seam
registerSkillTools in bootstrap ✓ Registered alongside registerThoughtTools
skill_list MCP tool ✓ Available post-bootstrap, lazy-resolves getDb()

7. Ordering note

listSkills orders by name ASC (skills table has name as PK — unique kebab-case). This is deterministic and does not suffer from the created_at ms-precision tie issue documented in the P0.7.2 lesson. No ORDER BY rowid needed for skills.


8. Residual risks

  • registerSkillTools handler calls getDb() lazily; if a tool call arrives before Phase 2 completes (impossible per P0.2.3 contract but not statically enforced), getDb() throws. This is shared behavior with registerThoughtTools.
  • loadSkillsFromDisk logs + skips malformed SKILL.md files (does not crash startup). The repo-facing-polish skill in .agents/skills/ triggers one skip log at startup (its name field doesn’t match the kebab-case constraint). This is a known corpus issue, not a code bug.
  • skill_get, skill_reload, and hot-reload are Phase 1 features (not Phase 0). Deferred per s17 §1 + P0.6.2 contract.

9. Compliance

  • No main checkout edits ✓
  • No force-push ✓
  • No skipped hooks ✓
  • All pre-commit checks pass ✓
  • Worktree: .worktrees/claude/p0-6-2-skill-crud
  • Branch pushed as new name feature/p0-6-2-skill-crud-rebased (rebased history) ✓
  • Old PR #127 closed with comment ✓

*Verification generated by T3 executor (Claude Sonnet 4.6) R75 Phase 0 Wave E 2026-04-17*

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.