P0.6.1 — Step 1 Audit
Inventory of the worktree against the task spec for P0.6.1 ε Skill Schema (Intelligence axis, first of three ε sub-tasks). Scope: the Zod schema that defines a Colibri skill frontmatter shape, plus a parser that reads the SKILL.md files already shipping in .agents/skills/colibri-*/.
Baseline: worktree E:/AMS/.worktrees/claude/p0-6-1-skill-schema/ at commit 3ebbe419 (P0.2.2 SQLite init merged as PR #122, on top of Wave A α server bootstrap at 40cd679d).
§1. Surface being added
Targets this task creates:
src/domains/skills/schema.ts— new module. Holds the Zod schema for the SKILL.md frontmatter (SkillFrontmatterSchema), a derived TypeScript type (SkillFrontmatter), a parser function (parseSkillFile(path) → ParsedSkill), and supporting constants (kebab-case regex, capability enum, greek-letter set). Does not yet exist.src/__tests__/skill-schema.test.ts— new test file, co-located with other Phase-0 tests per the Wave A lock (Jestroots: ['<rootDir>/src']). The task spec liststests/domains/skills/schema.test.ts; this is overridden — see §4b.src/domains/skills/directory — new directory root.index.tsbarrel not in scope (P0.6.2 owns the module wiring).
A worktree scan confirms absence of the authoring targets:
ls src/domains/→ “No such file or directory”grep -rn "SkillFrontmatter\|parseSkillFile\|kebab-case\|capabilities.*read.*write" src/→ zero matches- No existing import path touches
.agents/skills/*/SKILL.mdfrom TypeScript.
This is a greenfield module set. P0.6.1 authors two files (schema + test) under a brand-new src/domains/ tree.
§2. The existing SKILL.md corpus (fixture for the primary acceptance test)
2a. File count
Task prompt claims “22 existing SKILL.md files”. Actual count on 3ebbe419 is 21. See reference_skills_inventory.md in MEMORY for the pre-existing “22 canonical skills” figure — that figure tracked 22 including colibri-memory-context/ which still exists, but one of the other colibri-* directories cited in CLAUDE.md §9.2 never had an individual SKILL.md (ams-* redirect stubs under .claude/skills/ are MIRROR-zone and not part of .agents/skills/).
Enumerating .agents/skills/colibri-*/SKILL.md:
.agents/skills/colibri-audit-memory/SKILL.md.agents/skills/colibri-audit-proof/SKILL.md.agents/skills/colibri-autonomous/SKILL.md.agents/skills/colibri-docs-check/SKILL.md.agents/skills/colibri-docs-sync/SKILL.md.agents/skills/colibri-executor/SKILL.md.agents/skills/colibri-greek-nav/SKILL.md.agents/skills/colibri-growth-strategy/SKILL.md.agents/skills/colibri-gsd/SKILL.md.agents/skills/colibri-gsd-execution/SKILL.md.agents/skills/colibri-mcp-server/SKILL.md.agents/skills/colibri-memory-context/SKILL.md.agents/skills/colibri-observability/SKILL.md.agents/skills/colibri-obsidian-integration/SKILL.md.agents/skills/colibri-pm/SKILL.md.agents/skills/colibri-roadmap-progress/SKILL.md.agents/skills/colibri-roadmaps-tasks/SKILL.md.agents/skills/colibri-task-management/SKILL.md.agents/skills/colibri-tier1-chains/SKILL.md.agents/skills/colibri-truthing/SKILL.md.agents/skills/colibri-verification/SKILL.md
Deviation from spec: corpus is 21, not 22. The primary acceptance test parses all 21 and asserts zero schema errors. This deviation is recorded in the packet (§P4) and verification (§V2).
2b. Frontmatter key inventory (observed across the 21 files)
Every file has a YAML block delimited by two --- lines at the top. Keys observed:
| Key | Present in | Shape | Notes |
|---|---|---|---|
name |
21 / 21 | string, kebab-case (all match /^[a-z][a-z0-9-]+$/) |
Universal. |
description |
21 / 21 | string (some quoted) | Universal. Some files use "…" around multi-sentence descriptions with embedded punctuation; a handwritten parser must handle this correctly. |
round |
15 / 21 | string (e.g. R74.5) |
Present on the Phase 0 skills authored/refreshed in R74; absent on the older ones (colibri-autonomous, colibri-docs-sync, colibri-executor, colibri-mcp-server, colibri-truthing, colibri-verification). |
updated |
15 / 21 | date string YYYY-MM-DD |
Co-varies with round. |
status |
8 / 21 | string (heritage) |
Only on HERITAGE skills — colibri-audit-memory, colibri-gsd, colibri-gsd-execution, colibri-memory-context, colibri-observability, colibri-obsidian-integration, colibri-roadmap-progress, colibri-roadmaps-tasks. |
model |
0 / 21 | — | The skill tool description block lists model as a typical key but no SKILL.md in this corpus declares it. Absent. |
Keys in the P0.6.1 schema spec that are absent from every file in the corpus:
version— 0 / 21entrypoint— 0 / 21capabilities— 0 / 21greekLetter— 0 / 21
The Greek: <letter> identifier appears inside the description text for many skills (e.g. Greek: β (beta)), but not as a structured frontmatter key.
2c. Frontmatter parsing hazards
Direct observation of three representative descriptions:
description: "Colibri Project Manager (T2) skill for Phase 0 pipeline management … Trigger phrases: 'PM mode', 'manage pipeline' …"
- Embedded single quotes inside a double-quoted YAML scalar (
colibri-pm,colibri-tier1-chains). - Long descriptions with em-dashes, parentheses, backticks, and colons (
colibri-memory-context,colibri-roadmap-progress). - Unicode Greek letters (
β,α,ε,η,π) in unquoted body text (colibri-growth-strategy,colibri-verification).
These are normal for a spec-grade YAML parser. A handwritten regex-only parser will fail on at least colibri-pm, colibri-memory-context, and colibri-roadmap-progress. This audit finding forces the dep decision in §4c.
§3. Adjacent code that the new module must integrate with
3a. src/config.ts (95 lines — P0.1.4, commit 3bd154a7)
Establishes the idiom for Zod-schema modules in the Phase 0 tree:
- Zod 3 (
"zod": "^3.23.8"inpackage.json) — not Zod 4 despite CLAUDE.md §1 stack line. Schema syntax must be Zod 3 (z.enum([...])withas consttuple,.optional()for absent,z.object({...}).passthrough()for tolerant extra keys).z.literal.*.or(...)plusz.union([...])behave as Zod 3 expects. - Pure factory pattern —
loadConfig(env)is pure,configis the frozen eager snapshot. The skill parser mirrors this:parseSkillFile(path)is pure (no module-load side-effect), the schema export is a plain constant. - Throw on parse failure with a clear error message.
config.tsre-wraps Zod errors with.flatten(); the skill parser does the same.
3b. src/modes.ts (185 lines — P0.4.1, commit a64d7349)
Establishes the as const tuple + derived type + frozen record pattern:
as consttuple["FULL", "READONLY", "MINIMAL"]→z.enum(RUNTIME_MODES). The skill schema reuses this shape forcapabilities(tuple["read","write","spawn","audit","admin"]) and the greek-letter set (15-element tuple).- Frozen exports via
Object.freeze(...)for capability matrices. The skill schema does not need a matrix — just an enum — but the idiom is the house style. - Test style: one file per source module, organized by
describe()per exported symbol. See §3d.
3c. src/db/index.ts (319 lines — P0.2.2, commit 3ebbe419)
Not a direct dependency of this task. Relevant only because:
- P0.6.2 (next task) builds the
skillstable and CRUD on top of this. P0.6.1 is the schema layer — pure Zod + parser, no DB access.importof./db/index.jsis forbidden insrc/domains/skills/schema.ts. - The
AuditSinkhook insrc/server.tsis where P0.6.2 will registerskill_listas an MCP tool. Not in scope for P0.6.1.
3d. src/__tests__/config.test.ts (206 lines) and src/__tests__/db-init.test.ts
Test conventions:
- Filename pattern
<module>.test.tsco-located insrc/__tests__/. Nottests/. Wave A lock, confirmed in Sigma packet guidance. describe('SkillFrontmatterSchema', () => { ... })per exported symbol;it('rejects X', () => { ... })for each acceptance criterion.expect(() => fn(bad)).toThrow(/pattern/)for schema rejection paths.expect(result).toMatchObject({...})for happy-path parse assertions.- File-system reads use
node:fs+ absolute paths resolved viaimport.meta.urlwhen the test needs to locate repo root from the compiledsrc/__tests__/location.
No jest.isolateModulesAsync is needed — the schema module has no eager side-effect, no env reads, no DB reads. Pure data.
3e. package.json + jest.config.ts + tsconfig.json
tsconfig.jsonhasexactOptionalPropertyTypes: true— anyoptional()field on the Zod schema must produce a TS type where the key is absent or present, notT | undefined. Solution: usez.string().optional()(Zod 3 outputsstring | undefined) and accept that consumers doif (meta.greekLetter)checks. This is the same treatmentconfig.tsalready uses.tsconfig.jsonhasnoUncheckedIndexedAccess: true— array access returnsT | undefined. The corpus test iterates an array of paths; each access must be narrowed before use.jest.config.tscollectCoverageFromalready coverssrc/**/*.ts— the newsrc/domains/skills/schema.tswill appear in the coverage report automatically.- ESLint
curly: "all"+eqeqeq: "always"+@typescript-eslint/no-explicit-any: "warn"+consistent-type-imports: "error". Everytypeimport usesimport type { … }.
3f. What this task MUST NOT touch
Per the dispatch prompt’s collision avoidance list:
src/server.ts— P0.2.3 owns the server-side edit; Wave C sibling.src/db/schema.sql,src/db/migrations/*,src/db/index.ts— theskillstable is P0.6.2.src/config.ts,src/modes.ts— unrelated.package.json— editable only to add the frontmatter-parsing dep (see §4c).jest.config.ts,tsconfig.json— no config changes needed for pure data module + pure test.src/domains/tasks/*,src/domains/trail/*— Wave C siblings (P0.3.1, P0.7.1).
§4. Deviations from the spec + open decisions
4a. greekLetter optional (and corpus has zero)
Spec: { name, description, version, entrypoint, capabilities[], greekLetter? }. The ? on greekLetter is explicit in both the task-breakdown acceptance line and in the dispatch prompt.
Reality: no SKILL.md in the 21-file corpus has any of version, entrypoint, capabilities, or greekLetter as a frontmatter key. If the schema declares these as required, the corpus test fails 21/21 immediately.
Resolution (packet §P2 + contract §C1):
namerequired, kebab-case. Corpus: 21/21 ✅descriptionrequired, non-empty string. Corpus: 21/21 ✅versionoptional, string (no semver enforcement — extraction does not mandate it). Corpus: 0/21, but the schema accepts it when present. Tests cover the “present + valid” path with a synthetic fixture.entrypointoptional, string (free-form path). Corpus: 0/21, same treatment.capabilitiesoptional, array of enum values (["read","write","spawn","audit","admin"]). Corpus: 0/21, same treatment.greekLetteroptional, one of the 15 greek letters. Corpus: 0/21, same treatment.
The contract documents each deviation with the acceptance line it traces back to. The test suite has (a) the corpus test that parses all 21 files with the permissive schema, (b) unit tests that exercise each optional field in isolation with synthetic YAML.
The spec says “preferred” behavior is to add the missing fields to the corpus if trivial — adding version, entrypoint, capabilities to 21 files is not trivial; each requires a real semantic decision (which capabilities does colibri-docs-check expose? what’s the entrypoint of colibri-gsd when the whole point is it’s heritage?). Attempting those assignments would exceed scope and produce 21 placeholder values that P0.6.3 (Capability Index) would then have to rewrite. Option (b) — schema tolerance — is the honest choice.
4b. Test path override
Spec says tests/domains/skills/schema.test.ts. Wave A load-bearing lock says src/__tests__/<module>.test.ts because jest.config.ts line 14 declares roots: ['<rootDir>/src']. Moving to tests/ would require a jest config change (out of scope, see §3f). Actual path: src/__tests__/skill-schema.test.ts.
4c. Dep choice — gray-matter vs depless
The dispatch prompt lists three options. Audit analysis:
| Option | Verdict | Rationale |
|---|---|---|
| Depless (handwritten) | Reject | Per §2c, at least three corpus files use double-quoted YAML strings with embedded single quotes, em-dashes, parens, and colons. A regex split on --- + line-by-line key: value will mis-parse colibri-pm (truncates description at the first ') and colibri-memory-context (line-wraps inside a scalar). Shipping a parser that can’t handle shipped files fails the primary acceptance test. |
js-yaml direct |
Acceptable | Full YAML compliance. Small (~40kb). Transitively safe. Requires the caller to split --- delimiters manually. |
gray-matter |
Chosen | Wraps js-yaml + delimiter split in one call. matter(content) returns { data, content }. Battle-tested (~15M weekly downloads), <10kb wire, all deps are standard (js-yaml, kind-of, section-matter, strip-bom-string, extend-shallow). The packet includes a dep-justification section. |
gray-matter is added as a runtime dependency (not devDependency) because P0.6.2 will use it too (src/domains/skills/repository.ts). Version pinned to ^4.0.3 (current stable).
4d. Primary acceptance test shape
The spec’s acceptance line reads: “Test: parse ALL 22 existing skill files, assert zero schema errors”.
With 21 files in the actual corpus (§2a), the test implementation is:
// src/__tests__/skill-schema.test.ts (excerpt — full shape in packet §P3)
describe('SkillFrontmatterSchema – corpus compatibility', () => {
const repoRoot = path.resolve(__dirname, '..', '..');
const skillDir = path.join(repoRoot, '.agents', 'skills');
const skillFiles = glob('colibri-*/SKILL.md', { cwd: skillDir });
it('finds the expected corpus count', () => {
expect(skillFiles.length).toBe(21);
});
it.each(skillFiles)('parses %s with zero schema errors', (rel) => {
const abs = path.join(skillDir, rel);
expect(() => parseSkillFile(abs)).not.toThrow();
});
it('every parsed skill has a name matching kebab-case', () => {
for (const rel of skillFiles) {
const parsed = parseSkillFile(path.join(skillDir, rel));
expect(parsed.frontmatter.name).toMatch(/^[a-z][a-z0-9-]+$/);
}
});
});
Using it.each gives per-file assertion rows — if one breaks, the output names the file.
4e. No SKILL.md corpus edits
Per §4a, the audit concludes that amending 21 files to add placeholder version/entrypoint/capabilities/greekLetter is out-of-scope and would produce low-signal values. This PR edits zero SKILL.md files. The contract (§C2) records this decision.
4f. Parser return shape
The dispatch prompt specifies parseSkillFile(path): SkillMeta (abstractly). The audit resolves this to:
interface ParsedSkill {
frontmatter: SkillFrontmatter; // Zod-validated frontmatter
body: string; // the Markdown after the closing `---`
path: string; // absolute path the file was read from
}
function parseSkillFile(absPath: string): ParsedSkill;
path on the return keeps errors easy to attribute in aggregate scans (P0.6.2 callsite).
§5. Acceptance criteria traceability
| Spec line | How it’s satisfied | Where |
|---|---|---|
{ name, description, version, entrypoint, capabilities[], greekLetter? } Zod schema |
Exported SkillFrontmatterSchema with name + description required, rest optional (§4a) |
schema.ts |
name kebab-case /^[a-z][a-z0-9-]+$/ |
z.string().regex(KEBAB_CASE); test with 3 invalid names + corpus sweep |
schema.ts + test |
capabilities enum ["read","write","spawn","audit","admin"] |
z.array(z.enum(CAPABILITIES)); test with valid array + invalid member rejection |
schema.ts + test |
greekLetter one of α β γ δ ε ζ η θ ι κ λ μ ν ξ π |
z.enum(GREEK_LETTERS) with the exact 15-char tuple; optional; test rejects non-greek chars and upper-case |
schema.ts + test |
| SKILL.md parser: reads frontmatter + body | parseSkillFile(abs) via gray-matter; returns { frontmatter, body, path } |
schema.ts + test |
| Corpus test: parse all existing files with zero errors | it.each(skillFiles) with corpus iteration; 21 rows |
test |
§6. Out of scope
- Loading SKILL.md files into a DB — P0.6.2.
skill_listMCP tool registration — P0.6.2.- Capability-based filtering — P0.6.3.
- Hot-reload,
skill_get,skill_reload— Phase 1 per extraction heritage note. - Adding the
skillstable tosrc/db/migrations/— P0.6.2. - δ Model Router, agent spawning via skills — Phase 1.5 per ADR-005.
Audit complete. Step 2 (Contract) follows immediately, grounded in §4’s deviations + §5’s traceability matrix.