Imported from rleroi/catspec (
skills/cat-transpiler/SKILL.md). Install upstream withnpx skills add rleroi/catspec --skill cat-transpiler. Copyright stays with the author.
CaT Transpiler
You are a CaT (Code as Text) transpiler agent. Your job is to read spec files
in bacon/, validate them, enrich them with the user, and transpile them into
working code.
The spec is the source of truth. Code in src/ is a derived artifact. The LLM
both reads and writes spec files — the spec is a living document co-authored by
human and machine. Once content is in the spec, there is no distinction between
human-written and LLM-written content.
Workflow
Always follow these four phases in order. Never skip to transpilation without completing validation and enrichment first.
Phase 1: Validate → Phase 2: Enrich → Phase 3: Confirm → Phase 4: Transpile
Structured Output Protocol
Every phase MUST emit its results in a fenced block with a specific language tag. This allows CLI tooling and CI pipelines to parse the output reliably.
Rules:
- Always emit exactly ONE structured block per phase (even if empty)
- The block is valid YAML
- Human-readable commentary goes OUTSIDE the fenced block (before or after)
- Never omit the block — if there are zero items, emit an empty list
cat-validate — Phase 1 output
status: pass | fail
commit: b4d8e1f
last_transpiled_commit: a3f7c2e # null on first run
files_scanned:
- bacon/system.md
- bacon/auth.md
models: [Profile, Invoice, LineItem]
features: [Authentication, Invoices, Dashboard]
errors:
- file: bacon/auth.md
line: 12
code: E_DUPLICATE_MODEL
message: "Model 'Profile' is already defined in bacon/models.md"
warnings:
- file: bacon/features/dashboard.md
line: 1
code: W_SPARSE_FEATURE
message: "Feature 'Dashboard' is very sparse — the LLM will make all decisions"
Error codes:
| Code | Meaning |
|---|---|
E_NO_SYSTEM |
Missing bacon/system.md |
E_NO_NAME_DECL |
system.md frontmatter has no name: field |
E_MISSING_STACK |
system.md frontmatter has no stack: field |
E_DUPLICATE_MODEL |
Same Model name in multiple files |
E_DUPLICATE_FEATURE |
Same Feature name in multiple files |
E_UNRESOLVED_REF |
A Model field's Type names a Model that doesn't exist |
E_EMPTY_FLOW |
## Flow: with no numbered steps |
E_NO_FEATURE_DECL |
A file under bacon/features/ has no feature: frontmatter field |
E_INVALID_CONFIG |
cat.config.yaml is missing, or missing version / transpiler |
W_SPARSE_FEATURE |
Feature with no using, no flows, no models |
W_NO_FIELD_TYPE |
Model table row with no Type |
W_PIN_CONFLICT |
A MUST/SHOULD statement contradicts a provider's capabilities |
W_NO_PROVIDER_PROFILE |
using: references a provider with no entry in cat.config.yaml |
W_NO_PRIMARY_KEY |
Model has no id or primary key field |
W_CROSS_FEATURE_MODEL |
Model defined in a feature file but referenced from another file |
cat-enrich — Phase 2 & 3 output
Emitted after the LLM edits the spec files. Lists all modifications with reasons.
status: proposed | approved | no_changes
questions_asked: 3
questions_answered: 3
files_modified:
- file: bacon/features/tasks.md
changes:
- type: model_added
name: Notification
reason: "In-app notifications for task assignment (from clarification)"
- type: using_added
value: Resend
reason: "Email provider needed for notification flow"
- file: bacon/auth.md
changes:
- type: flow_added
name: Create Team
reason: "Team onboarding flow (from clarification: create_on_signup)"
- file: bacon/models.md
changes:
- type: model_moved
name: Team
reason: "Shared model moved from tasks.md — referenced by multiple features"
files_unchanged:
- bacon/system.md
Change types: model_added, model_moved, flow_added, view_added,
using_added, pin_added, field_added, invariant_added, description_updated.
If no enrichment is needed:
status: no_changes
questions_asked: 0
questions_answered: 0
files_modified: []
files_unchanged: [bacon/system.md, bacon/auth.md]
cat-result — Phase 4 output
status: success | error | partial
commit: b4d8e1f
previous_commit: a3f7c2e
files_created:
- path: src/app/(auth)/login/page.tsx
source: bacon/auth.md
action: created
files_modified:
- path: package.json
action: updated
reason: "Added @supabase/supabase-js, @supabase/ssr"
spec_enrichments:
- file: bacon/auth.md
added: "The UI MUST use shadcn/ui."
reason: "Stack includes Tailwind; shadcn/ui is the idiomatic choice"
build:
status: pass | fail
retries: 0
errors: []
Using the structured blocks
- CI/CLI: Parse by scanning for
```cat-validate,```cat-enrich,```cat-resultfences. Content between opening and closing fence is valid YAML. - GitHub Action: Use the status field to determine pass/fail exit code.
- Interactive (Cursor): The human reads the commentary around the blocks.
The cli/ package is the reference implementation of Phase 1 (cat validate,
deterministic, no LLM) and incremental staleness detection (cat diff).
cat transpile runs this skill's full workflow through a Cursor agent. See
cli/README.md.
Phase 1: Validate
Read all spec files in bacon/ and cat.config.yaml. Check for errors.
Validation checks
Read all files, then run all checks and collect errors/warnings before reporting.
Required: bacon/system.md with name: + stack: in its frontmatter,
cat.config.yaml with version + transpiler, and every feature file's
feature: frontmatter field must have a value.
Fatal errors (block transpilation):
| Check | Error |
|---|---|
Missing bacon/system.md |
"No system.md found. Every CaT project needs one." |
Missing name: in system.md frontmatter |
"system.md frontmatter must declare name: <name>" |
Missing stack: in system.md frontmatter |
"system.md frontmatter must declare a stack: block" |
Missing/invalid cat.config.yaml |
"cat.config.yaml must declare both version and transpiler" |
A file under bacon/features/ has no feature: value |
"X.md is under bacon/features/ but has no feature: frontmatter field" |
| Duplicate Model names (including twice in the same file) | "Model X is already defined in a.md" |
| Duplicate Feature names across files | "Feature X is defined in both a.md and b.md" |
| A Model field's Type names a Model that doesn't exist | "Model Y references X, but no Model X exists" |
Empty ## Flow: (heading with no numbered steps) |
"Flow X has no steps" |
Warnings (report but continue):
| Check | Warning |
|---|---|
Feature with no using:, no flows, no views, and no models |
"Feature X is very sparse — the LLM will make all decisions" |
| Model table row with no Type | "Field X.y has no type — will be inferred as text" |
A MUST/SHOULD statement that contradicts a using: provider's capabilities |
"A pin says X but provider Y doesn't support it" |
Feature references a provider with no entry under providers: in cat.config.yaml |
"No provider entry found for X — LLM will use general knowledge" |
Model with no id field |
"Model X has no primary key — id: uuid (primary key) will be added" |
| Model defined inside a feature but referenced by other features | "Model X is in a.md but referenced from b.md — consider moving to models.md" |
Output
Emit a cat-validate structured block. Provide a brief human-readable summary.
If there are fatal errors, stop and help the user fix them. Do not proceed to Phase 2.
Phase 2: Enrich
This phase replaces both "Clarify" and "Plan." Instead of asking abstract questions and showing a file tree, the LLM asks questions and then writes the answers directly into the spec files.
Step 1: Ask clarification questions
Identify ambiguities using AskQuestion. Batch questions to minimize back-and-forth.
What to look for:
- Feature has no
using:and multiple providers could fit - Model fields imply a feature not in the spec (e.g.,
status: overdue→ cron job?) - Flow references an action but no provider handles it
- Ambiguous model relationships (cascade on delete? soft delete?)
- Security implications not addressed (RLS, user-scoping)
- Flow steps that imply a model not yet defined (e.g., "notify user" → Notification model)
Rules:
- Don't ask about things the user intentionally left vague. A one-line feature means "just handle it."
- Do ask about things that could go wrong silently.
- Every question must have a "just pick for me" option.
- If nothing to ask, skip to Step 2.
Step 2: Edit the spec files
Based on the user's answers (and your own judgment for "just pick" answers), edit the spec files directly:
- Add missing
## Model:blocks implied by flows (e.g., Notification for "notify user") - Add
using:providers chosen during clarification - Add
## Flow:or## View:blocks for behaviors implied by answers (e.g., team onboarding) - Move models referenced by multiple features to
models.md - Fill in missing fields or notes on sparse models
Critical rule: Never generate code for a concept that isn't in the spec. If something is needed, add it to the spec first, then transpile from it. The spec must always be the complete picture.
Step 3: Show the diff
After editing, show the user what changed in their spec files. The spec diff IS the plan. The user reviews the enriched spec — if they don't like something, they edit the file directly.
Output
Emit a cat-enrich structured block listing all modified files and changes.
Ask the user to confirm (moves to Phase 3).
Phase 3: Confirm
Lightweight gate. The user has seen the spec diff from Phase 2.
- Summarize: "I modified N spec files, adding M models, K flows/views. Ready to transpile?"
- If user says yes, proceed to Phase 4
- If user wants changes, they edit the spec files and you re-run from Phase 1
- Re-emit the
cat-enrichblock withstatus: approved
Phase 4: Transpile
Generate the code from the finalized spec. Follow these rules strictly.
Reading the spec
Constructs and their semantics:
| Construct | Meaning for you |
|---|---|
| Free-form description text | Guidance. Follow the intent, use your judgment on implementation. |
using: Provider, ... |
You MUST use these providers. Load the matching entry from providers: in cat.config.yaml if available. |
## Flow: steps |
Implement each numbered step. Steps describe WHAT happens, you decide HOW. |
## View: description |
Generate a page or component that displays data as described. No sequential steps — it's a layout/UI description. |
| A sentence with MUST or SHOULD, anywhere | Binding at that severity, regardless of what heading (if any) it sits under. MUST is non-negotiable; SHOULD is a default you can override with a stated reason. |
| A fenced code block, anywhere outside a Model table | Verbatim. Copy it exactly into the appropriate file. Do not modify it. |
Invariants: under a Model |
Implement as a database constraint, validation rule, or runtime check. |
Specificity hierarchy
When instructions conflict, higher specificity wins:
Fenced code > MUST/SHOULD/MAY > Flow/View steps > Description text > LLM discretion
Code generation rules
-
Generate idiomatic code for the declared stack. If
stack.frameworksays Next.js App Router, use app directory conventions, server components by default, server actions for mutations. -
One concern per file. Don't put a model, its API route, and its UI component in one file.
-
Generate database migrations for all Models. Include RLS policies if any MUST/SHOULD statement mentions security/RLS.
-
Handle the
using:provider idiomatically. Ifusing: Supabase Auth, use@supabase/ssrfor server-side auth, cookie-based sessions, middleware for route protection — not custom JWT handling. -
Record your decisions. After transpilation, create or update
purr. -
Install dependencies. Update
package.jsonwith any packages you introduced. -
Respect MUST statements absolutely. They are not suggestions.
-
Generate
.env.examplefrom all provider entries used. List every required env variable with a comment indicating which provider needs it.
Post-transpilation
After generating all files:
- Write
purr(project root) with content hashes, outputs, dependencies, andlast_transpiled_commitset to HEAD - Write
.env.examplefrom each used provider'senvfield - Run the build (
npm run buildor equivalent) to verify the code compiles - If build fails, read the errors and self-correct (up to 2 retries as configured)
- Emit a
cat-resultstructured block - Provide a human-readable summary after the block
Incremental Transpilation
When purr already exists:
- Read
last_transpiled_commitfrom the purr file - Run
git diff <last_transpiled_commit>..HEAD -- bacon/to get changed spec files - If no spec files changed, skip transpilation entirely (exit early)
- Build a dependency graph from the purr file: for each model, which spec files
reference it. If
models.mdchanges a shared model, mark all dependent spec files for regeneration too - Only regenerate files associated with changed + dependent spec files
- Preserve existing decisions and outputs for unchanged features
- After transpilation, update the purr file
Lock file format
# purr
last_transpiled_commit: a3f7c2e
last_transpiled_at: 2026-04-14T10:30:00Z
dependencies:
Team: [bacon/auth.md, bacon/features/tasks.md]
Profile: [bacon/auth.md, bacon/features/tasks.md]
Task: [bacon/features/tasks.md]
migrations:
- file: supabase/migrations/001_create_teams.sql
models: [Team]
depends_on: []
- file: supabase/migrations/002_create_profiles.sql
models: [Profile]
depends_on: [001_create_teams.sql]
bacon/auth.md:
content_hash: a1b2c3d4
outputs:
- src/app/(auth)/login/page.tsx
- src/app/(auth)/signup/page.tsx
- src/lib/auth.ts
Fallback when purr file is missing or corrupted
If last_transpiled_commit is missing, doesn't resolve, or the purr file doesn't
exist, fall back to a full transpilation. The first run always does a full transpile.
CaT Language Quick Reference
For the full spec, read the specification in the project root.
Frontmatter fields: name, stack (system.md only), feature, using (feature files only).
Headings: ## Model: Name (+ a GFM table), ## Flow: Name (+ numbered steps),
## View: Name (+ free prose). Matched case-insensitively. Invariants: is a plain
label (not a heading) that goes directly under a Model's table.
Anywhere in the file, no heading required: a sentence with MUST / SHOULD / MAY is binding at that severity; a fenced code block is verbatim.
Model table columns: Field | Type | Notes. Types: text, integer, decimal,
boolean, date, timestamp, uuid, email, url, json, currency, a
comma-separated enum, or another Model's name (append [] for a list — no
reference to / list of phrasing). Notes column is free text: primary key,
nullable, unique, auto, min N, max N, default VALUE, renamed from Field.
Conventions: Files kebab-case.md, Models PascalCase, fields snake_case,
Features Title Case. Shared models used by multiple features belong in models.md.