Skill Architecture — Fundamentals + Stack Specialization
Purpose: Canonical explanation of how coding-os organizes skills. Two-layer model: agent-agnostic fundamentals in src/core/skills/ + stack-specific specializations in src/templates/<stack>/skills/. A stack skill declares depends_on: [fundamentals...] in its frontmatter so the agent loads both and gets DRY guidance that specializes where it matters.
Read when: adding a new stack · adding a new skill · debugging why enforce-skill.sh loaded the wrong skill · asking "is this a shared concern or a stack-specific one?".
Nav: Section Index | Docs Index
Aligned with: Claude Certified Architect Foundations (TS 3.2 — skill frontmatter composition; TS 3.3 — path-scoped rules).
The problem composition solves
Before extraction, src/templates/django/skills/python-django/SKILL.md, src/templates/fastapi/skills/python-fastapi/SKILL.md, and src/templates/go-fiber/skills/go-fiber/SKILL.md each restated the same cross-cutting patterns: service/selector split, error envelopes, idempotency, N+1 avoidance, migration discipline, auth middleware shape. Seven backend concerns × three stacks = 21 copies that must stay in sync when any one pattern evolves.
Composition pulls those concerns into one SSOT (src/core/skills/backend-fundamentals/SKILL.md) and leaves each stack skill to cover only what's framework-specific: Django Meta, FastAPI Depends, Fiber middleware chain, etc.
The two layers
src/core/skills/ ← agent-agnostic, stack-agnostic
├── clean-code/SKILL.md ← fail-closed errors, no PII in logs, typed exceptions
├── thinking_os/SKILL.md ← Complexity Gate, Zoom cycle (always-active via rule)
├── codebase-explorer/SKILL.md ← trace-flow, dependency-map
├── backend-fundamentals/SKILL.md ← services, idempotency, envelopes, migrations, N+1
└── frontend-fundamentals/SKILL.md ← loading/error/empty states, hydration, a11y, SEO
src/templates/<stack>/skills/ ← framework-specific extensions
├── django/skills/python-django/ ← Django ORM, DRF, Celery, services+selectors layout
├── fastapi/skills/python-fastapi/ ← Pydantic, Depends, async handlers
├── go/skills/go-patterns/ ← idiomatic Go, error wrapping, table-driven tests
├── go-fiber/skills/go-fiber/ ← Fiber v2 handlers, middleware, validator
└── nextjs/skills/{nextjs-react,frontend-design}/ ← RSC, hydration, design system
How composition works
Each stack SKILL.md declares its dependencies in frontmatter:
---
name: python-django
description: Use when creating or modifying Python files in src/backend/ — Django models, DRF views, services, selectors, Celery tasks, migrations, tests.
globs: "src/backend/**/*.py"
context: fork
depends_on:
- clean-code
- backend-fundamentals
allowed-tools:
- Read
- Grep
- Glob
- Edit
- Write
---
When enforce-skill.sh matches a file to python-django, the agent loads python-django AND transitively loads every depends_on entry. The resulting context is: universal code quality (clean-code) + cross-stack backend patterns (backend-fundamentals) + Django specifics (python-django). No duplication at source; one skill per task at load time.
Why this beats install-time concatenation: install-time composition (pre-concatenating backend-fundamentals into python-django) would save one skill-load round-trip but breaks skill independence. If the agent is reviewing a non-Django backend project and needs backend-fundamentals alone, it can still load it standalone.
Decision rule — shared vs specific
When adding a rule, ask: does this hold for every stack, or only for ?
| Rule class | Goes in |
|---|---|
| Code quality, fail-closed errors, typed exceptions | src/core/skills/clean-code |
| Service/selector split, idempotency, envelopes, N+1 avoidance, migration discipline | src/core/skills/backend-fundamentals |
| Client/server component split, hydration safety, loading-error-empty-state pattern, SEO basics | src/core/skills/frontend-fundamentals |
Django Meta.ordering, DRF serializer Meta.fields |
src/templates/django/skills/python-django |
FastAPI Depends() chains, Pydantic model_validator |
src/templates/fastapi/skills/python-fastapi |
Next.js App Router conventions, use client rules |
src/templates/nextjs/skills/nextjs-react |
If a rule starts stack-specific and later turns out to hold across stacks, promote it up (move to fundamentals, remove from the specific skill, bump the depends_on only if it wasn't already there).
Why not one monolithic skill per stack (status quo before 2026-04-18)
- Duplication grows linearly with stack count. 5 backend stacks × 1 monolith each × N shared concerns = 5N copies to update when a concern changes.
- Cross-stack reviews suffer. Reviewing a go-fiber codebase while having
python-djangoworldview loaded produces noisy suggestions. - Testing is weaker. No way to test "does our shared backend guidance hold" — only per-stack monoliths exist.
Why not install-time concatenation (Option B from analysis)
- Install-time requires a regen step. Miss the step and stack skills drift from
src/core/skills/backend-fundamentals. - Breaks skill independence — can't invoke
backend-fundamentalsalone. make regen-rulesandmake regen-adapter-templatesalready exist; adding a third regen step raises the bar for contributors.
Runtime composition via depends_on gives the same DRYness with zero regen step.
Interaction with enforce-skill.sh
The hook reads src/core/rules/skill-enforcement.md — a table mapping file globs to primary + secondary skills. That table is generated from src/templates/*/stack.yaml by make regen-rules. Today the table lists:
| Globs | Primary | Secondary |
|---|---|---|
src/backend/**/*.py |
python-django |
clean-code |
src/backend/**/*.py |
python-fastapi |
clean-code, api-design |
src/backend/**/*.go |
go-patterns |
clean-code |
src/backend/**/*.go |
go-fiber |
clean-code |
src/frontend/**/*.{ts,tsx} |
nextjs-react |
clean-code, frontend-design |
Once backend-fundamentals and frontend-fundamentals ship, the stack.yaml files should declare them as additional secondaries (or the stack skills declare depends_on: and the hook resolves transitively). This keeps skill-enforcement.md readable while the full dependency graph is computed by the hook.
Public skill standard + trusted import (TASK-369)
Anyone can author or import a skill; the standard keeps the catalog safe and coherent. Three trust tiers:
| Tier | Origin | Scripts execution |
|---|---|---|
core |
ships in src/core/skills/ |
trusted |
stack |
ships in src/templates/<id>/skills/ |
trusted |
community |
imported via cos skill add <path|git-url> into ~/.coding-os/skills/ |
requires recorded consent (cos skill consent <name>) |
Authoring — cos skill new <name> scaffolds a spec-compliant skill
(frontmatter: name, description ("Use when …" + triggers), tier,
domain[], globs, optional depends_on[]/context/license, body with
purpose/when-to-read/anti-patterns); cos skill lint <path> validates it
through the SAME loader the runtime uses (schema + enum checks), so lint-pass
⇒ loadable. The scaffold passes lint out of the box.
Import gate — cos skill add runs, in order:
- Normalize: a vanilla Agent-Skills-format skill (name+description only)
gains the coding-os taxonomy fields (
tier/domain/globsdefaults when absent). The taxonomytier:describes WHAT the skill is; TRUST is recorded in provenance only — a file can never claim a trust level. - Security scan: SKILL.md +
scripts//assets/are pattern-scanned for exfiltration and destructive shapes (remote POSTs of env/keys, piped shell-from-curl, base64+eval,rm -rf /, reverse-shell idioms). Findings BLOCK the import and are printed by name — this is defense-in-depth and honest about being static; consent (below) is the second layer. - License check: a
licensefrontmatter field or LICENSE file is recorded; absence is a warning, not a block. - Provenance:
<name>/.provenance.jsonrecords source, sha256 of every file, import date and trust tier —cos skill listsurfaces it. - Consent: community skills with
scripts/install withscripts_consent: false; executing their scripts is forbidden untilcos skill consent <name>records explicit consent.
A community skill may never shadow a core or stack skill name.
Per-project extras (TASK-370) — .coding-os.yaml::extra_skills is the
SSOT (written by init/wizard; no second file). cos skill enable|disable <name> mutates it and, for community skills, maintains symlinks in every
installed adapter's skills dir (core/stack skills are already wholesale-linked
by the adapter install). cos skill project lists stack-provided vs extra
with provenance. The Hub Config → Skills tab toggles the same list via
PATCH /api/config/skills/{name} — one shared mutator
(cli.skill_commands.set_project_skill) backs both. cos update never prunes
a symlinked skill dir: it's an enable-managed link, and descending through it
would delete the user's source files.
Per-stack skill groups — onboarding SSOT (TASK-352)
The onboarding wizard ("which skills will this stack install?") and the Config
tab need a per-stack grouping that is distinct from the taxonomy tier: in
SKILL.md frontmatter. The grouping is derived, never duplicated — there is
no extra data file:
| Group | Derivation (all from stack.yaml + SKILL.md frontmatter) |
|---|---|
required |
primary_skill + skills: (what the stack explicitly declares) |
recommended |
union of skill_enforcement[].secondary minus required |
optional |
remaining core skills (universal catalog) |
Single SSOT function: cli.skills_list.collect_stack_skill_groups(stack_id) —
consumed by BOTH cos skills-list --stack <id> and the hub endpoints
GET /api/hub/stacks/{id}/skills (grouped) / GET /api/hub/skills
(global core+stack catalog with provenance: core|stack:<id> and schema
validated flag). Each entry carries {name, tier, domain, description, provenance, validated}; a skill a stack references but no skills dir ships
yet appears with validated: false and an empty description — visible, not
silently dropped. Skill profile resolution order: the stack's own
src/templates/<id>/skills/ first, then src/core/skills/.
Path-scoped rules (distinct from skills)
A skill is invoked on demand (Skill skill: "python-django"); a rule loads automatically based on file path. For universal policies (e.g., all Python uses ruff), prefer a rule in src/core/rules/ with paths: ["**/*.py"] frontmatter over a skill load. The exam guide's TS 3.3 favors path-scoped rules for "conventions that span multiple directories regardless of stack" — rules < skills in context cost, so pick rules when the policy is tiny and universal.
Testing a new fundamentals skill
- Write the skill with a clear
descriptionthat says WHEN to use it. - Add
depends_on:from at least one stack skill so it's actually exercised. - Manually trigger: edit a relevant file in this repo via an agent; confirm
enforce-skill.shlogsfundamentals skill loadedin.coding-os/.hooks.log. - Add a test in
tests/test_skills.py(or the skills-specific test file) that loads the SKILL.md and asserts required frontmatter keys.
References
- src/core/rules/skill-enforcement.md — generated globs → skills table
- src/core/hooks/enforce-skill.sh — the gating hook
- docs/governance/agent-workflow.md § Hook & Skill Enforcement
- docs/engineering/hooks-reference.md — all hooks catalog
- Claude Certified Architect Foundations → Task Statement 3.2 and 3.3