Verification — Skills root resolution + diagnostic surface
Setup
- Branch:
feature/fix-skills-root-env - Worktree:
E:/AMS/.worktrees/claude/fix-skills-root-env - Implementation commit:
2872b70c - Audit commit:
642f76bf· Contract:44f56ab3· Packet:24eda617 - Origin base:
86e430fb(origin/main)
Gate 1 — npm run build
> colibri@0.0.1 build
> tsc
(no output, exit 0)
tsc accepts the new COLIBRI_SKILLS_ROOT field on Config, the new
skill_count field on HealthPayload, the new countSkills export, and
all six modified test files. Schema/types are sound.
Gate 2 — npm run lint
> colibri@0.0.1 lint
> eslint src
(no output, exit 0)
Initially failed with two @typescript-eslint/consistent-type-imports
errors on the dynamic import() type annotations in
src/__tests__/startup.test.ts. Resolved by hoisting the type via a
top-level import type * as StartupModuleType from '../startup.js' +
type StartupModule = typeof StartupModuleType declaration. Lint now
clean.
Gate 3 — npm test
Test Suites: 30 passed, 30 total
Tests: 1384 passed, 1384 total
Snapshots: 0 total
Time: 23.116 s
Pre-change baseline at 86e430fb was 1357 tests across 30 suites (per
memory R83 close). Post-change is 1384 = baseline + 27 new tests
across:
config.test.ts+5 cases (absolute path, relative path, undefined, empty rejection, freeze).startup.test.ts+12 cases (2 resolution-chain, 2 mocked-config resolution, 6 WARN-gating, plus the eager-import deletion was a non-test refactor not a count change).tools/health.test.ts+10 cases (7countSkillsunit, 3 payload, plus 2 E2E forskill_count; minus 0 from the 6→7 field assertion which is an in-place edit not a count change).
Coverage on the touched files:
| File | % stmts | % branch | % funcs | % lines |
|---|---|---|---|---|
src/config.ts |
100% | 80% | 100% | 100% |
src/tools/health.ts |
100% | 100% | 100% | 100% |
src/startup.ts |
93.65% | 92.18% | 85.71% | 93.54% |
(startup.ts uncovered lines are pre-existing branches in the shutdown
- subprocess paths, untouched by this change.)
Acceptance evidence vs. task spec
| Criterion | How verified |
|---|---|
1. COLIBRI_SKILLS_ROOT optional in config schema with TSDoc |
src/config.ts shows the field at L48-71 with TSDoc explaining override semantics + AMS_* rejection + accepted values. loadConfig parses it as optional via z.string().min(1).optional(). Three positive cases + one rejection case in config.test.ts. |
2. Resolution chain options.skillsRoot ?? config.COLIBRI_SKILLS_ROOT ?? cwd default |
src/startup.ts:299-302 shows the new ?? chain. The options.skillsRoot wins… test in startup.test.ts confirms the test-seam priority. The passes config.COLIBRI_SKILLS_ROOT… test (mocked config) confirms the env-var path. The uses cwd default… test confirms the fallback. |
3. skill_count on server_health payload |
src/tools/health.ts:88-95 schema field. src/tools/health.ts:135-149 countSkills helper. src/tools/health.ts:163 payload assembly. E2E test returns skill_count reflecting a populated skills table over the SDK envelope confirms the field surfaces through the full SDK envelope. |
4. WARN log on loaded=0 AND total_on_disk=0 AND mode=FULL |
src/startup.ts:316-326 shows the conditional. Six tests in startup — empty-skills WARN diagnostic exercise every gating branch (FULL fires; READONLY/TEST/MINIMAL silent; non-zero total_on_disk silent; non-zero loaded silent). |
| 5. New test surfaces for config/startup/health | Counted above (+5/+12/+10). |
6. npm run build && npm run lint && npm test all green |
Gates above. |
Constraint compliance
- 14-tool surface unchanged.
skill_countis a payload field on the existingserver_healthtool. No new MCP tool registered. The_registeredToolNames.has('server_health')assertion in the existing tests (which counts unique MCP tool names) is unaffected. - Worktree only. Every edit landed in
E:/AMS/.worktrees/claude/fix-skills-root-env. Main checkout (E:/AMS) untouched. - No push, no merge. Local commits on
feature/fix-skills-root-envonly.git pushnot invoked. - No new migration. The
skillstable already exists atsrc/db/migrations/004_skills.sql:18-29.countSkillsreads from it viaSELECT COUNT(*) AS c FROM skills.
Design decisions
-
Path resolution:
COLIBRI_SKILLS_ROOTis consumed verbatim. We do NOT callpath.resolve()to absolutize it —loadSkillsFromDiskalready callspath.resolve(skillsRoot)internally (seesrc/domains/skills/repository.ts:238), so a relative value gets resolved againstprocess.cwd()at the call site, identical to how the bare.agents/skillsdefault works today. This keeps debugging predictable: the value the operator sets is the valuefssees. -
Empty-string rejection:
z.string().min(1).optional()rejects an empty string at module-load time. An operator who exportsCOLIBRI_SKILLS_ROOT=""gets a validation error immediately, not a silent zero-skills boot. -
WARN gating: restricted-mode deploys (READONLY/TEST/MINIMAL) may legitimately ship without skills. A populated disk inventory with
loaded=0is a parser-error scenario already logged inside the loader. The diagnostic targets only thetotal_on_disk === 0 && loaded === 0 && mode === 'FULL'shape — that’s the misconfiguration the code review surfaced. -
WARN line text frozen: the line
[Startup] WARNING: 0 skills loaded — check COLIBRI_SKILLS_ROOT or process.cwd()is intentionally stable. Operators may grep for it. Future changes to the gating conditions must keep this exact substring. -
Test mocking strategy: the env-var-to-startup chain is normally tested via subprocess (mirroring the existing
subprocess smokepattern), but on Windows + tsx cold-start, Phase 2 (DB integrity check + 6 migrations + skills load) routinely exceeds 15 s — well beyond Jest’s per-test budget. Switched tojest.unstable_mockModuleto stub theconfigimport, then dynamically re-importedstartupso it picks up the mock. Importedjestfrom@jest/globals(already used indb-init.test.ts); cast for the unstable API (not in@types/jest@29.5.14).
Test fixture impact
src/__tests__/tools/health.test.ts:299-306— pre-existing 6-field key-set assertion inbuildHealthPayload’s “complete payload shape” test. Updated to 7 fields includingskill_count.- No other existing test fixtures needed updating; the existing no-op
loadSkillsFn: () => ({ loaded: 0, skipped: 0, pruned: 0, total_on_disk: 0 })stub is now a default that trips the new WARN under the defaultmode: 'FULL'fixture, but the WARN is benign noise — no existing test asserts log absence (verified via grep acrossstartup.test.ts).
Residual risks
- First subprocess run latency: the existing
startup — subprocess smoketest (startup.test.ts:1041) usestimeout: 3000and only asserts on Phase 1 logs. It still passes reliably; no regression. - No live ζ writeback: no MCP client attached to this session. The
task
task_update+thought_recordwriteback lives in the PR body- memory file per CLAUDE.md §4 fallback path.
- Pre-existing
@types/jestgap:unstable_mockModuleis not typed; the cast pattern is documented inline.
Rollback plan
If the WARN line proves noisy in production for non-FULL operators with skills configured (we don’t believe so — the gating is strict), the single-line revert is:
// src/startup.ts — drop the if-block at L316-326
COLIBRI_SKILLS_ROOT itself is purely additive and safe to keep. The
skill_count payload field is similarly additive.