Architecture
cogs is one pnpm/Turborepo monorepo serving two audiences from the same source tree: Claude Code skill authors/consumers, and @cogs/* npm package consumers. Here's how the pieces fit together.
Top-level layout
| Path | What it is | In the pnpm workspace? |
|---|---|---|
packages/* | 24 @cogs/* packages (auth, email, config, react-query, react-hook-form helpers, etc.) — 22 publishable to npm, plus 2 private (send-service, supabase-sync) | Yes — pnpm-workspace.yaml globs packages/* |
skills/* | Canonical source for the 5 Claude Code skills, git-tracked | No (not an npm package tree) |
packages/skill-* | Generated mirror of skills/*, one npm package per skill | Yes, but gitignored content — never hand-edited |
infra/ | Terraform for AWS SES, Cloudflare DNS/Turnstile, remote state | No — applied directly with terraform, never via pnpm |
docs/ | Session dossiers, ADRs, runbooks, published-skills.json | N/A (plain markdown/JSON) |
bin/ | Release tooling scripts (see below) | N/A |
website/ | This documentation site (Docusaurus) | No — has its own pnpm-workspace.yaml, isolated on purpose |
Skills: canonical source vs. generated mirror
The 5 Claude Code skills are authored once, in exactly one place:
skills/kubb-react-query/
skills/nuqs-table-url-state/
skills/nuqs-url-state/
skills/shadcn-data-table-rhf/
skills/shadcn-react-hook-form-forms/
Each has a SKILL.md (the skill content Claude Code loads) and a metadata.json (version, provenance abstract). These are the files to read or edit.
bin/sync-skill-content.mjs mirrors each skills/<name>/ into a matching packages/skill-<name>/ directory so that directory can be published to npm as its own package (@cogs/skill-<name>) — for distribution channels that expect an installable package rather than a Claude Code plugin. That mirror directory is gitignored and rebuilt from skills/<name>/ on every sync; it is never the place to make an edit; it will be silently overwritten.
The 5 skills are also distributed as a single Claude Code plugin (.claude-plugin/marketplace.json, plugin name cogs), so a Claude Code user gets all 5 in one install rather than five separate npm installs. See Getting Started.
Release pipeline
Publishing a skill package or an @cogs/* library package follows the same root release script:
pnpm build # turbo run build across all packages
node bin/generate-skill-package-json.mjs --check
node bin/sync-skill-content.mjs --all # skills/* -> packages/skill-*
node bin/validate-skill-package.mjs --all
changeset publish
Ongoing releases run this in CI via GitHub Actions, publishing to the public npm @cogs scope using npm trusted publishing (OIDC) — no long-lived npm token is stored in CI.
Infra boundary
infra/ holds the Terraform for the auth/email plane this repo also ships (AWS SES identities, Cloudflare DNS, Turnstile widgets). It's deliberately outside the pnpm workspace and applied directly with terraform, gated by a manual-approval GitHub Actions environment for apply. It's scoped narrowly: infra/variables.tf validates that every domain it manages ends in .workloom.app — other domains (including this docs site's own catesworks.dev subdomain) are intentionally out of scope here, to avoid two separate Terraform states fighting over the same DNS zone.
Why two build systems for one repo
packages/* and website/ are built completely independently:
packages/*build through Turborepo (turbo.json), sharingtsconfig.base.json, Biome, and Vitest config.website/builds through Docusaurus's own toolchain, with its ownpnpm-workspace.yamlsopnpm installinsidewebsite/never touches — or is touched by — the root workspace's lockfile.
This mirrors the infra/ pattern: a subtree with a fundamentally different toolchain gets a hard boundary instead of being forced into the shared build graph.