From fbff4d2177d2cdf9b09aaf8651fd48f92c0e82e0 Mon Sep 17 00:00:00 2001 From: bubaley Date: Thu, 22 Oct 2026 23:19:17 +0200 Subject: [PATCH 1/2] refactor(agents): share skills between Claude Code and Codex Move the canonical skills to .agents/skills/ (Agent Skills layout read natively by Codex) and expose each one to Claude Code through a relative per-skill symlink in .claude/skills/. Cut the set to new-domain, design-system and sync-template: - new-domain ends with the backend self-review from domain-reviewer - design-system absorbs add-shadcn-component and the ui-reviewer pass - verify and release become always-on rules in AGENTS.md - the Claude-only domain-reviewer and ui-reviewer subagents are removed scripts/tests/test_agent_skills.py checks spec frontmatter, links and the symlink for every skill. Docs, ownership map and init output follow. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/design-system/SKILL.md | 30 +++++++++ .../skills/new-domain/SKILL.md | 15 +++-- .../skills/sync-template/SKILL.md | 2 +- .claude/agents/domain-reviewer.md | 24 ------- .claude/agents/ui-reviewer.md | 21 ------ .claude/skills/add-shadcn-component/SKILL.md | 16 ----- .claude/skills/design-system | 1 + .claude/skills/design-system/SKILL.md | 23 ------- .claude/skills/new-domain | 1 + .claude/skills/release/SKILL.md | 15 ----- .claude/skills/sync-template | 1 + .claude/skills/verify/SKILL.md | 29 --------- .gitignore | 2 +- .template-version | 2 +- AGENTS.md | 16 +++-- README.md | 6 +- docs/architecture.md | 20 +++--- docs/backend-concept.md | 2 +- docs/template-core.md | 6 +- docs/template-ownership.md | 4 +- scripts/bootstrap/init.py | 2 +- scripts/tests/test_agent_skills.py | 64 +++++++++++++++++++ 22 files changed, 140 insertions(+), 162 deletions(-) create mode 100644 .agents/skills/design-system/SKILL.md rename {.claude => .agents}/skills/new-domain/SKILL.md (51%) rename {.claude => .agents}/skills/sync-template/SKILL.md (96%) delete mode 100644 .claude/agents/domain-reviewer.md delete mode 100644 .claude/agents/ui-reviewer.md delete mode 100644 .claude/skills/add-shadcn-component/SKILL.md create mode 120000 .claude/skills/design-system delete mode 100644 .claude/skills/design-system/SKILL.md create mode 120000 .claude/skills/new-domain delete mode 100644 .claude/skills/release/SKILL.md create mode 120000 .claude/skills/sync-template delete mode 100644 .claude/skills/verify/SKILL.md create mode 100644 scripts/tests/test_agent_skills.py diff --git a/.agents/skills/design-system/SKILL.md b/.agents/skills/design-system/SKILL.md new file mode 100644 index 0000000..72cdfaa --- /dev/null +++ b/.agents/skills/design-system/SKILL.md @@ -0,0 +1,30 @@ +--- +name: design-system +description: Build or restyle frontend UI the way this template's design system requires — tokens, palettes, App* components, Russian copy — add missing shadcn-vue primitives, and visually review the result in the browser. Use for any change to screens, components, styling, colors, theming, spacing or layout in frontend/, when a screen needs a UI primitive the kit lacks (accordion, slider, command, hover card…), and before finishing UI work. +--- + +# Design system + +Rules: [frontend-concept.md](../../../docs/frontend-concept.md) — Design system, Templates, UI layers and visual review, Button and table variants. Live reference: `/ui`. + +**Build** +1. Compose screens from `AppPageView`, `AppListTemplate`/`AppDetailTemplate`, `AppSection`, `AppCard` and other `App*` components. Features never import `reka-ui` or `shared/ui/shadcn` (the architecture guard fails). +2. Missing primitive? Extend a close `App*` component first; a genuinely new one → "Add a shadcn primitive" below. +3. Colors, radii, sizes only through tokens from `app/assets/styles/theme.css` via utilities (`bg-card`, `text-success`, `h-control-md`, `z-overlay`). No hex values, no `bg-[color:var(--…)]`, no inline geometry styles. +4. One vocabulary: `variant`, `tone` (`success`/`info`/`warning`/`destructive`), `size` (`xs`–`lg`). Two or more buttons in a row → `AppTemplateActions`; a single async click → `useActionState`. +5. Data through resource descriptors and Pinia Colada, never `useFetch`/`$fetch` for API resources. Icons: `@lucide/vue` components. Dates: `useDate()`. +6. Copy: **Russian** for product screens (titles, labels, placeholders, empty states, toasts, errors, aria-labels). `/ui` is the only English screen. +7. Re-branding (palette, neutral surfaces): "Re-branding a fork" in the doc; check `/ui` → Tokens. + +**Add a shadcn primitive** — procedure: [frontend-concept.md → Adding a shadcn primitive](../../../docs/frontend-concept.md#adding-a-shadcn-primitive). +1. Confirm it is missing in `frontend/app/shared/ui/shadcn/` and `shared/ui/app/App*`. +2. `cd frontend && pnpm ui:add ` — never `pnpm dlx shadcn-vue` directly (the script fixes the `cn` import and formats). +3. Review `shared/ui/shadcn//`: `z-50` → `z-overlay`, semantic token classes only, everything exported from `/index.ts`. +4. Wrap it as `shared/ui/app/App.vue`: Russian default texts, the shared vocabulary and control sizes (`shared/ui/app/types/control.ts`). Features use only the wrapper. +5. Showcase it on `/ui` in the matching `features/ui/components/Ui*Showcase.vue` (English) with variants and states — knip fails on unused files. New building block → add it to the Templates table in the doc. + +**Review in the browser** (required; `make check` does not replace it) +1. `make run-backend` + `make run-frontend` (or reuse running servers), sign in at `/auth`. Map the diff (`git diff HEAD -- frontend/app`) to routes: `pages/**` → its route, feature views → the pages that mount them, `shared/ui/**` → `/ui` plus every screen using the component. +2. Each affected route at **390, 768 and 1440 px**, in **light and dark**, and with a non-default palette when colors changed. +3. Walk "UI layers and visual review" in the doc: empty/loading/error/retry, long text, create/edit/delete, disabled/loading buttons, modals and drawers (stacking, Escape, focus return), no horizontal page scroll, double scrollbars or overlapping fixed regions, no console errors or hydration warnings after reload. +4. Report the routes, viewports and themes you checked and what you fixed; say plainly if no browser was available. diff --git a/.claude/skills/new-domain/SKILL.md b/.agents/skills/new-domain/SKILL.md similarity index 51% rename from .claude/skills/new-domain/SKILL.md rename to .agents/skills/new-domain/SKILL.md index 3ac7cfd..736bc37 100644 --- a/.claude/skills/new-domain/SKILL.md +++ b/.agents/skills/new-domain/SKILL.md @@ -1,6 +1,6 @@ --- name: new-domain -description: Add a new business entity end to end (backend domain + frontend feature) in this FastAPI + Nuxt template. Use when the user asks to add a domain, model, entity, CRUD screen, resource or "a list of X" to the product, or runs /new-domain. Not for extending an existing domain. +description: Add a new business entity end to end (backend domain + frontend feature) in this FastAPI + Nuxt template, then self-review it against the backend rules. Use when the user asks to add a domain, model, entity, CRUD screen, resource or "a list of X" to the product, or invokes new-domain (/new-domain, $new-domain). Not for extending an existing domain. --- # New domain @@ -11,9 +11,16 @@ Scaffold first, then shape. Rules live in the docs; this is the order of work. 2. **Scaffold:** `make new-domain name= title="Счета" item_title="Счёт"` (`plural=` if the default is wrong). It refuses to overwrite; read its "Next steps". 3. **Backend fields** in `backend/app/domains//`: `models.py` (keep `organization` FK + `tenant_scope` unless the user says otherwise), `schemas.py` (separate update schema; immutable ownership fields only on create), `filters.py`. Non-CRUD logic → `services/_service.py` class. Rules: [backend-concept.md](../../../docs/backend-concept.md) (Model, Schemas, Views, Scoping, Services). 4. **Tests:** extend `tests/test_.py` for every non-obvious rule. Run `cd backend && uv run pytest app/domains/ --sqlite` (no migration exists yet). -5. **Migration:** do NOT run `make mm` / `make m`. Tell the user the domain needs a migration and ask them to run both (a hook blocks it; `ALLOW_MIGRATIONS=1` only after their explicit yes). +5. **Migration:** do NOT run `make mm` / `make m`. Tell the user the domain needs a migration and ask them to run both (`ALLOW_MIGRATIONS=1` only after their explicit yes; in Claude Code a hook enforces this). 6. **Client:** `make gen-client`, then adapt `frontend/app/features//resources/.ts` (form type, `toCreate`/`toUpdate`) and the list/detail views. UI copy Russian; App* components only — [frontend-concept.md](../../../docs/frontend-concept.md) (Resource descriptor, Templates). 7. **Navigation:** the scaffolder added an entry to `frontend/app/navigation/settings.ts` with a `Folder` icon — pick a fitting Lucide icon; set `superuserOnly` if needed. Main-app (non-settings) screens go to `layouts/default.vue` instead. 8. **Docs:** add the scenario to `docs/product.md` (what the user can see and do). -9. **Verify:** `make lint`, then the `/verify` skill (`make check`; `PYTEST_ARGS=--sqlite` until the user has migrated). For UI, the `design-system` skill's visual review; optionally the `domain-reviewer` and `ui-reviewer` subagents on the diff. -10. **Commit:** `feat(): …` — one feature per commit series. +9. **Self-review the backend diff** (`git diff HEAD -- backend/`) against [backend-concept.md](../../../docs/backend-concept.md) and the canonical `backend/app/domains/organization/`; fix every miss: + - every model has `tenant_scope` (`Scope(user=...)`, plus `organization=` for org data) or `Unscoped(reason=...)` with a real reason; no superuser bypass inside the scope; + - models inherit `BaseModel`, FK + `_id: UUID` annotation, plural `related_name`, `Meta.table` and `ordering`; + - Create / Update / Read schemas; ownership fields never in Update; `organization` from auth, not the body; + - viewset is thin (schema, permissions, queryset, filterset) and `views.py` exports `routers`; loading and rule checks live in the service (`validate_*`); + - errors via `AppException.from_attrs(ErrorCode...)`; router has a plural kebab tag, actions have return types, `openapi.json` + generated client regenerated; + - tests cover the non-obvious rules and tenant isolation, using the `auth_headers` / `*_factory` fixtures. +10. **Verify:** `make lint`, then `make check` (`PYTEST_ARGS=--sqlite` until the user has migrated). UI → the `design-system` skill's visual review. +11. **Commit:** `feat(): …` — one feature per commit series. Report the pending migration to the user. diff --git a/.claude/skills/sync-template/SKILL.md b/.agents/skills/sync-template/SKILL.md similarity index 96% rename from .claude/skills/sync-template/SKILL.md rename to .agents/skills/sync-template/SKILL.md index 56eaa20..187644f 100644 --- a/.claude/skills/sync-template/SKILL.md +++ b/.agents/skills/sync-template/SKILL.md @@ -1,6 +1,6 @@ --- name: sync-template -description: Pull improvements from the upstream FastAPI + Nuxt template into a product created from it, path by path according to the ownership map. Use when the user asks to sync, update or upgrade from the template, pull template changes, or runs /sync-template in a fork. +description: Pull improvements from the upstream FastAPI + Nuxt template into a product created from it, path by path according to the ownership map. Use when the user asks to sync, update or upgrade from the template, pull template changes, or invokes sync-template (/sync-template, $sync-template) in a fork. --- # Sync template diff --git a/.claude/agents/domain-reviewer.md b/.claude/agents/domain-reviewer.md deleted file mode 100644 index 35214a4..0000000 --- a/.claude/agents/domain-reviewer.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: domain-reviewer -description: Reviews a backend diff (new or changed domain) against this template's backend rules — thin viewsets, services, tenant_scope, schemas, OpenAPI, tests, migrations note. Use after adding or changing anything under backend/app/domains/ and before committing; give it the base ref or the list of changed files. -tools: Read, Grep, Glob, Bash ---- - -You review backend changes in a FastAPI + Tortoise + fastapi-ronin template. You do not edit files; you report findings. - -Read first: `docs/backend-concept.md` (Model, Schemas, Filters, Views, Scoping, Services, Registration, OpenAPI contract, Tests) and the canonical domain `backend/app/domains/organization/`. Get the diff with `git diff ...HEAD -- backend/` (or `git diff HEAD` for uncommitted work). - -Check every changed domain for: -1. **Scope**: every model declares `tenant_scope` (`Scope(user=...)` always; `organization=` for org data) or `Unscoped(reason=...)` with a real reason. User rule present; no superuser bypass inside the scope (only in a viewset `get_queryset`). -2. **Models**: inherit `BaseModel`; FK field + `_id: UUID` annotation; plural `related_name`; `Meta.table` and `ordering`. -3. **Schemas**: separate Create / Update / Read; ownership fields (organization, user, parent) never in the Update schema; `organization` set from auth, not the body (unless deliberately org-agnostic, like memberships). -4. **Views**: thin — schema, permissions, queryset, filterset; `views.py` exports `routers` (or `router`) so discovery picks it up; no business logic, no model loading or rule checks in the viewset: those go to a service class taking instances, with `validate_*` for loading and checks. -5. **Errors**: `AppException.from_attrs(ErrorCode...)`, never bare dicts or leaking DB messages. -6. **OpenAPI**: router has a plural kebab tag; actions have return types; `frontend/openapi.json` and `frontend/app/shared/api/generated/` regenerated in the same change (`make gen-client`). -7. **Tests**: non-obvious rules covered next to the domain (`tests/test_.py`) with the `auth_headers` / `*_factory` fixtures; tenant isolation tested for org-scoped data. -8. **Migrations**: model changes come with a note that the user must run `make mm` + `make m`; no migration was run or hand-written by an agent unless the user asked. -9. **Docs**: `docs/product.md` updated for user-visible behavior; concept docs if a pattern changed. - -Run `cd backend && uv run ruff check . && uv run pytest app/domains/ --sqlite -q -p no:logging` for the touched domains. - -Report: a short verdict, then findings ordered by severity (blocker / should fix / nit) with `path:line`, the rule broken (link the doc section) and the concrete fix. Say explicitly which checks passed. No praise, no restating the diff. diff --git a/.claude/agents/ui-reviewer.md b/.claude/agents/ui-reviewer.md deleted file mode 100644 index acd5113..0000000 --- a/.claude/agents/ui-reviewer.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: ui-reviewer -description: Walks the screens affected by a frontend diff in the browser and checks them against the design system — tokens, App* variants, Russian copy, states, responsive layout in both themes. Use after UI changes under frontend/app/ and before committing; give it the changed files or routes. ---- - -You review frontend changes in a Nuxt 4 + shadcn-vue template. You do not edit files; you report findings. - -Read first: `docs/frontend-concept.md` (Templates, Design system, UI layers and visual review, Button and table variants) and `.claude/skills/design-system/SKILL.md`. Get the diff with `git diff ...HEAD -- frontend/app` and map changed files to routes (`pages/**` → route; feature views → the pages that mount them; `shared/ui/**` → `/ui` plus every screen using the component). - -**Static pass** over the diff: -1. Only `App*` / template components in features; no `reka-ui` or `shared/ui/shadcn` imports. -2. Tokens via utilities only: no hex/rgb colors, no `bg-[color:var(--…)]`, no inline geometry styles, `z-overlay` instead of `z-50`. -3. Shared vocabulary: `variant`, `tone`, `size`; button rows through `AppTemplateActions`; async clicks through `useActionState`. -4. Copy: every user-facing string on product screens is Russian (titles, labels, placeholders, empty states, toasts, errors, aria-labels); `/ui` stays English. -5. Data through resource descriptors and Pinia Colada, never `useFetch`/`$fetch` for API resources. - -**Browser pass** (use whichever browser tools are available; servers: `make run-backend`, `make run-frontend`, sign in at `/auth`): -- Each affected route at 390, 768 and 1440 px, light and dark theme (and a non-default palette if colors changed). -- Empty, loading, error/retry states; long names; create/edit/delete; modals and drawers (stacking, Escape, focus return); no horizontal page scroll, double scrollbars or overlapping fixed regions; no console errors or hydration warnings after reload. - -Report: verdict, then findings by severity (blocker / should fix / nit) with route, viewport, theme, `path:line` when known, and the fix. List the routes and viewports you actually checked; say clearly if the browser pass could not run and why. diff --git a/.claude/skills/add-shadcn-component/SKILL.md b/.claude/skills/add-shadcn-component/SKILL.md deleted file mode 100644 index 8ee35d0..0000000 --- a/.claude/skills/add-shadcn-component/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: add-shadcn-component -description: Add a new shadcn-vue primitive to the frontend UI kit and expose it as an App* component. Use when a screen needs a UI primitive the kit lacks (e.g. accordion, slider, command, hover card) or the user asks to add a shadcn component. ---- - -# Add a shadcn component - -Procedure: [frontend-concept.md → Adding a shadcn primitive](../../../docs/frontend-concept.md#adding-a-shadcn-primitive). - -1. Check it is really missing: `frontend/app/shared/ui/shadcn/` and `shared/ui/app/App*` (extend an App* component if one is close). -2. `cd frontend && pnpm ui:add ` — never `pnpm dlx shadcn-vue` directly (the script fixes the `cn` import and formats). -3. Review `shared/ui/shadcn//`: `z-50` → `z-overlay`, semantic token classes only, everything exported from `/index.ts`. -4. Wrap it as `shared/ui/app/App.vue`: Russian default texts, the shared `variant`/`tone`/`size` vocabulary and control sizes (`shared/ui/app/types/control.ts`). Features use only the wrapper. -5. Add a showcase entry on `/ui` in the matching `features/ui/components/Ui*Showcase.vue` (English copy there) with its variants and states — knip fails on unused files. -6. Add the component to the Templates table in `docs/frontend-concept.md` if it is a new building block. -7. `make lint`, `/verify`, then the browser review from the `design-system` skill (both themes, 390/768/1440). diff --git a/.claude/skills/design-system b/.claude/skills/design-system new file mode 120000 index 0000000..ed518de --- /dev/null +++ b/.claude/skills/design-system @@ -0,0 +1 @@ +../../.agents/skills/design-system \ No newline at end of file diff --git a/.claude/skills/design-system/SKILL.md b/.claude/skills/design-system/SKILL.md deleted file mode 100644 index 13588e2..0000000 --- a/.claude/skills/design-system/SKILL.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: design-system -description: Build or restyle frontend UI the way this template's design system requires — tokens, palettes, App* components, Russian copy — and visually review it in the browser. Use for any change to screens, components, styling, colors, theming, spacing or layout in frontend/, and before finishing UI work. ---- - -# Design system - -Rules: [frontend-concept.md](../../../docs/frontend-concept.md) — Design system, Templates, UI layers and visual review, Button and table variants. Live reference: `/ui`. - -**Build** -1. Compose screens from `AppPageView`, `AppListTemplate`/`AppDetailTemplate`, `AppSection`, `AppCard` and other `App*` components. Features never import `reka-ui` or `shared/ui/shadcn` (the architecture guard fails). -2. Missing primitive? Extend an `App*` component; a genuinely new one → `add-shadcn-component` skill. -3. Colors, radii, sizes only through tokens from `app/assets/styles/theme.css` via utilities (`bg-card`, `text-success`, `h-control-md`, `z-overlay`). No hex values, no `bg-[color:var(--…)]`, no inline geometry styles. -4. One vocabulary: `variant`, `tone` (`success`/`info`/`warning`/`destructive`), `size` (`xs`–`lg`). Two or more buttons in a row → `AppTemplateActions`; a single async click → `useActionState`. -5. Icons: `@lucide/vue` components. Dates: `useDate()`. -6. Copy: **Russian** for product screens (titles, labels, empty states, toasts, errors). `/ui` is the only English screen. -7. Re-branding (palette, neutral surfaces): "Re-branding a fork" in the doc; check `/ui` → Tokens. - -**Review in the browser** (required; checks do not replace it) -1. `make run-backend` + `make run-frontend` (or use running servers); open the affected screens and `/ui`. -2. Each screen at **390, 768 and 1440 px**, in **light and dark**, and with a non-default palette when colors changed. -3. Walk the checklist in "UI layers and visual review": empty/loading/error/retry, long text, disabled/loading buttons, modals and drawers (stacking, Escape, focus return), no horizontal page scroll or double scrollbars, no console errors or hydration warnings after reload. -4. Report what you looked at and what you fixed; the `ui-reviewer` subagent can do this pass on a diff. diff --git a/.claude/skills/new-domain b/.claude/skills/new-domain new file mode 120000 index 0000000..bb2be30 --- /dev/null +++ b/.claude/skills/new-domain @@ -0,0 +1 @@ +../../.agents/skills/new-domain \ No newline at end of file diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md deleted file mode 100644 index 6746ca0..0000000 --- a/.claude/skills/release/SKILL.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: release -description: How versions, changelog and releases work in this template (conventional commits + git-cliff in CI). Use when the user asks to release, bump the version, tag, write the changelog, choose a commit message type, or asks why something did or did not release. ---- - -# Release - -Rules: [deployment.md → Releases](../../../docs/deployment.md) and [cliff.toml](../../../cliff.toml). Releases are automatic; agents never tag or bump by hand. - -1. **Commit types decide the version.** `feat` → minor; `fix`, `perf`, `revert`, `build`, `chore(deps)` → patch; `!` or a `BREAKING CHANGE:` footer → major. `docs`, `refactor`, `test`, `chore`, `ci`, `style` do not release. -2. Write conventional commits (`type(scope): summary`, imperative, English). PR titles must be conventional — they become the squash commit on `main`. -3. Never edit `version.json`, the `version` in `backend/pyproject.toml` / `frontend/package.json`, or `CHANGELOG.md`; never create `v*` tags. The CI `release` job does it after the gate passes on `main`: `chore(release): vX.Y.Z [skip ci]`, tag, GitHub Release, image publish, deploy. -4. To preview the next version locally: `uvx --from git-cliff git-cliff --bumped-version` (needs the tags fetched). -5. Nothing released? Check that the commits since the last tag have a releasable type and that the CI gate passed on `main`. -6. Rollback is a workflow (`.github/workflows/rollback.yaml`), not a revert-and-retag — see deployment.md. diff --git a/.claude/skills/sync-template b/.claude/skills/sync-template new file mode 120000 index 0000000..e0be30d --- /dev/null +++ b/.claude/skills/sync-template @@ -0,0 +1 @@ +../../.agents/skills/sync-template \ No newline at end of file diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md deleted file mode 100644 index 65a0399..0000000 --- a/.claude/skills/verify/SKILL.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: verify -description: Run and interpret the project's full check suite (make check) for this FastAPI + Nuxt template. Use before declaring any change done, before committing, when CI is red, or when the user asks to verify, test, lint or check the project. ---- - -# Verify - -`make check` is exactly what CI runs (read-only). Source of truth: the `check*` targets in the [Makefile](../../../Makefile). - -1. Fix-and-format first: `make lint` (ruff fix/format for backend + tooling, Oxfmt + Oxlint/ESLint fix for frontend). -2. Run `make check`. Postgres down (`docker compose up -d db` fails or tests cannot connect) or the change has an unapplied migration → `make check PYTEST_ARGS=--sqlite`. Say which mode you used. -3. Read the first failure, not the last line. Map it: - | Failure | Meaning / fix | - |---|---| - | `ruff check` / `ruff format --check` | Run `make lint`; remaining errors are real (fix code, do not add `noqa` without a reason) | - | `ty check` | Type error in backend code | - | pytest `frontend/openapi.json is stale` | API changed: `make gen-client` and commit `openapi.json` + `generated/` | - | pytest `DomainDiscoveryError` | A domain's `views.py` failed to import or exports no `routers` | - | pytest `ScopeConfigurationError` | A model lacks `tenant_scope` ([backend-concept.md](../../../docs/backend-concept.md#scoping)) | - | pytest relation/table missing (Postgres only) | Migration not applied: ask the user to run `make m`, use `--sqlite` meanwhile | - | `scripts/tests` | Scaffolder, `make init` or `.claude/hooks` broke; a template file they rewrite may have changed shape | - | `fmt:check` / `lint` (frontend) | `make lint`, then fix what Oxlint/ESLint still report | - | `typecheck` | vue-tsc error; after an API change check resource descriptors against the regenerated types | - | `gen:api:check` | Generated client stale: `make gen-client` | - | `check:architecture` | Import crosses UI layers ([frontend-concept.md](../../../docs/frontend-concept.md#tooling)) | - | `check:unused` (knip) | Unused file/export/dependency: delete it or use it | -4. Frontend production build is not in `make check`: run `make check-frontend BUILD=1` (or `cd frontend && pnpm build`) for routing, config or dependency changes. -5. UI changed → also the visual review in the `design-system` skill. Passing checks do not replace it. -6. Report: commands run, mode (Postgres/SQLite), pass/fail, and anything skipped. diff --git a/.gitignore b/.gitignore index e4b76ae..646a77e 100644 --- a/.gitignore +++ b/.gitignore @@ -302,6 +302,6 @@ logs .env.* !.env.example -# Claude Code: skills, agents, hooks and settings.json are committed; worktrees and personal settings are not. +# Claude Code: skill symlinks (to .agents/skills), hooks and settings.json are committed; worktrees and personal settings are not. .claude/worktrees/ .claude/settings.local.json diff --git a/.template-version b/.template-version index d90ef6d..6075fbf 100644 --- a/.template-version +++ b/.template-version @@ -1,4 +1,4 @@ -# Template this product was created from; /sync-template diffs template-owned paths from `commit` +# Template this product was created from; the sync-template skill diffs template-owned paths from `commit` # (or tag v when the commit is not in the template history) and updates both. # See docs/template-ownership.md. Written by `make init`. repository=https://github.com/bubaley/production-fastapi-docker-template diff --git a/AGENTS.md b/AGENTS.md index 7149aad..4dcbacb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,9 +8,9 @@ This file is an index. Do not copy patterns here — follow the linked docs. |---|---| | What does the product do? Which scenarios exist? | [docs/product.md](docs/product.md) (product-owned) on top of [docs/template-core.md](docs/template-core.md) (inherited capabilities). `product.md` wins on conflict | | Start a new product from the template | `make init name= title="…"` — [README.md](README.md#start-a-new-product) | -| Add a domain (backend + frontend CRUD)? | `make new-domain name= title="…"`, then the `/new-domain` skill — [docs/backend-concept.md](docs/backend-concept.md#scaffolding) | -| Which files does the template own in a fork? How to pull template updates? | [docs/template-ownership.md](docs/template-ownership.md), `.template-version`, the `/sync-template` skill | -| Which skills, subagents and hooks exist? | `.claude/skills/*/SKILL.md` (`new-domain`, `verify`, `design-system`, `add-shadcn-component`, `sync-template`, `release`), `.claude/agents/` (`domain-reviewer`, `ui-reviewer`), hooks in `.claude/settings.json` — [docs/architecture.md](docs/architecture.md#agent-tooling) | +| Add a domain (backend + frontend CRUD)? | `make new-domain name= title="…"`, then the `new-domain` skill — [docs/backend-concept.md](docs/backend-concept.md#scaffolding) | +| Which files does the template own in a fork? How to pull template updates? | [docs/template-ownership.md](docs/template-ownership.md), `.template-version`, the `sync-template` skill | +| Which skills and hooks exist? | Skills (Claude Code and Codex): `.agents/skills//SKILL.md` — `new-domain`, `design-system` (UI, shadcn primitives, browser review), `sync-template`; `.claude/skills/` are symlinks to them. Claude Code hooks: `.claude/settings.json` — [docs/architecture.md](docs/architecture.md#agent-tooling) | | How does existing code work, who calls X, what depends on Y? | MCP **codebase-memory** first (`search_graph`, `get_architecture`, `trace_path`, `get_code_snippet`). Then open the files it points to. | | Where does a new file go? What is each folder for? | [docs/architecture.md](docs/architecture.md) | | How to add a backend domain, model, viewset, service? | [docs/backend-concept.md](docs/backend-concept.md) — canonical code: `backend/app/domains/organization/` | @@ -19,7 +19,7 @@ This file is an index. Do not copy patterns here — follow the linked docs. | Auth contract (cookies, CSRF, SSR), OpenAPI conventions, `make openapi`? | [docs/backend-concept.md](docs/backend-concept.md#auth-and-tokens) and [#openapi-contract](docs/backend-concept.md#openapi-contract) | | When to write backend tests, where they live? | [docs/backend-concept.md](docs/backend-concept.md#tests) — canonical: `backend/app/domains/organization/tests/test_organization.py` | | How to run the project locally? | [README.md](README.md) | -| How is the image built, released and deployed? | [docs/deployment.md](docs/deployment.md) — `Dockerfile`, `docker/rootfs/`, `.github/workflows/ci.yaml` | +| How is the image built, released and deployed? Which commit type releases what? | [docs/deployment.md](docs/deployment.md) ([Releases](docs/deployment.md#releases)) — `Dockerfile`, `docker/rootfs/`, `.github/workflows/ci.yaml`, `cliff.toml` | | How to lint, format, test, what CI checks? | `make lint` / `make check` in [README.md](README.md#makefile); frontend tooling in [docs/frontend-concept.md](docs/frontend-concept.md#tooling) | | Known open items (upstream workarounds, CI not yet run, deferred decisions)? | [docs/follow-ups.md](docs/follow-ups.md); history of the 2026 modernization: [docs/archive/roadmap-2026.md](docs/archive/roadmap-2026.md) | @@ -27,11 +27,13 @@ If the knowledge graph is missing or stale, index the repo with codebase-memory. ## Always-on rules -- **Do not run migrations** (`make mm`, `make m`, Tortoise migrate) unless the user explicitly asks. After model changes, describe the migration needed. A hook blocks these commands; when the user explicitly asked, prefix the command with `ALLOW_MIGRATIONS=1`. +- **Do not run migrations** (`make mm`, `make m`, Tortoise migrate) unless the user explicitly asks. After model changes, describe the migration needed. In Claude Code a hook blocks these commands; when the user explicitly asked, prefix the command with `ALLOW_MIGRATIONS=1`. - **Do not duplicate.** Extend existing viewsets, UI primitives, repos, and helpers before creating new ones. - Comments in code: English only, and only when the logic is non-obvious. - User-facing UI copy is **Russian**. Do not add English labels, titles, or toasts in product screens. The one exception is the `/ui` component catalogue: its section titles and sample content are English so they read alongside the prop names they demonstrate. -- After a non-trivial change, run **`make lint`** (fixes and formats both sides), then **`make check`** (what CI runs: format check, lint, types, tests, architecture guard; read-only). Fix what `make check` reports — do not leave it red. Without local Postgres: `make check PYTEST_ARGS=--sqlite`. +- After a non-trivial change, run **`make lint`** (fixes and formats both sides), then **`make check`** (what CI runs: format check, lint, types, tests, architecture guard; read-only) before declaring the work done. Fix the first failure `make check` reports — do not leave it red. Without local Postgres, or with a migration the user has not applied yet: `make check PYTEST_ARGS=--sqlite` (say which mode you used). Frontend build is not in it: `make check-frontend BUILD=1` after routing, config or dependency changes. +- Commits are conventional (`type(scope): summary`, English); the type decides the release: `feat` → minor, `fix`/`perf`/`revert`/`build`/`chore(deps)` → patch, `!` or `BREAKING CHANGE:` → major, other types do not release — [docs/deployment.md](docs/deployment.md#releases). +- Never tag, bump versions (`version.json`, `pyproject.toml`, `package.json`) or edit `CHANGELOG.md` by hand: the CI `release` job does it on `main`. - Complex backend behavior needs tests — see [docs/backend-concept.md](docs/backend-concept.md#tests). Canonical: `backend/app/domains/organization/tests/test_organization.py`. ## Keep docs current @@ -42,7 +44,7 @@ A change is incomplete if the matching doc still describes the old product or pa |---|---| | User-facing behavior or scenario | [docs/product.md](docs/product.md); an inherited capability of every product (template repo only) → [docs/template-core.md](docs/template-core.md) | | A new template path, or who owns a path in forks | [docs/template-ownership.md](docs/template-ownership.md) | -| How a skill, subagent or hook works | its file under `.claude/`; the overview in [docs/architecture.md](docs/architecture.md#agent-tooling) | +| How a skill or hook works | the skill in `.agents/skills//SKILL.md` (a new skill also gets its `.claude/skills/` symlink), hooks under `.claude/`; the overview in [docs/architecture.md](docs/architecture.md#agent-tooling) | | Folder roles, request flow, where new files go | [docs/architecture.md](docs/architecture.md) | | How to add/grow a backend domain (model, schema, viewset, scope, tests) | [docs/backend-concept.md](docs/backend-concept.md) | | How to add/grow a frontend feature (resource descriptor, view, templates, data layer) | [docs/frontend-concept.md](docs/frontend-concept.md) | diff --git a/README.md b/README.md index 003fe61..7acd7b6 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ make run-frontend # terminal 2 → http://lo `make init` (`scripts/bootstrap/init.py`) renames the project everywhere it is greppable (compose project, database, package names, API title, UI name, image names), writes `.env` from `.env.example` with a fresh `SECRET_KEY` and `ENCRYPTION_KEY`, resets `version.json`/`CHANGELOG.md` and package versions to 0.1.0, replaces [docs/product.md](docs/product.md) with the product skeleton and records the template version in `.template-version`. It refuses a dirty tree (`force=1` overrides) and is safe to run again. If the copy kept the template's `v*` tags, delete them before the first push (it tells you how). -Then describe the product in `docs/product.md` (purpose, personas, scenarios, non-goals) — agents build only what is written there or in [docs/template-core.md](docs/template-core.md). To pull later template improvements into the product, run the `/sync-template` skill ([docs/template-ownership.md](docs/template-ownership.md)). +Then describe the product in `docs/product.md` (purpose, personas, scenarios, non-goals) — agents build only what is written there or in [docs/template-core.md](docs/template-core.md). To pull later template improvements into the product, run the `sync-template` skill ([docs/template-ownership.md](docs/template-ownership.md)). ## Adding a domain @@ -43,11 +43,11 @@ Then describe the product in `docs/product.md` (purpose, personas, scenarios, no make new-domain name=invoice title="Счета" item_title="Счёт" ``` -Generates an organization-scoped CRUD entity end to end — backend domain (model with `tenant_scope`, schemas, filters, thin viewset, tests) and frontend feature (resource descriptor, list and detail views, settings pages, drawer entry) — mirroring the canonical `organization` code. Models and routers are discovered automatically. Then fill in the real fields, run the migration (`make mm`, `make m`), `make gen-client`, `make check`, and update `docs/product.md`. Details: [docs/backend-concept.md](docs/backend-concept.md#scaffolding); agents follow the `/new-domain` skill. +Generates an organization-scoped CRUD entity end to end — backend domain (model with `tenant_scope`, schemas, filters, thin viewset, tests) and frontend feature (resource descriptor, list and detail views, settings pages, drawer entry) — mirroring the canonical `organization` code. Models and routers are discovered automatically. Then fill in the real fields, run the migration (`make mm`, `make m`), `make gen-client`, `make check`, and update `docs/product.md`. Details: [docs/backend-concept.md](docs/backend-concept.md#scaffolding); agents follow the `new-domain` skill. ## Agent tooling -[AGENTS.md](AGENTS.md) is the index agents read first (`CLAUDE.md` includes it). `.claude/` is committed: skills (`new-domain`, `verify`, `design-system`, `add-shadcn-component`, `sync-template`, `release`), review subagents (`domain-reviewer`, `ui-reviewer`) and hooks that block migration commands (prefix `ALLOW_MIGRATIONS=1` when you ask an agent to run one) and format every edited file. Editor setup: copy `.vscode/settings.json.example` to `.vscode/settings.json` and install the recommended extensions. Overview: [docs/architecture.md](docs/architecture.md#agent-tooling). +[AGENTS.md](AGENTS.md) is the index agents read first (Codex reads it directly, `CLAUDE.md` includes it for Claude Code). Skills live once in `.agents/skills/` — `new-domain`, `design-system`, `sync-template` — and work in Claude Code (`/new-domain`, through the symlinks in `.claude/skills/`) and Codex (`$new-domain`). `.claude/settings.json` adds Claude Code hooks that block migration commands (prefix `ALLOW_MIGRATIONS=1` when you ask an agent to run one) and format every edited file. On Windows, enable Developer Mode and `git config --global core.symlinks true` before cloning so Claude Code sees the skills. Editor setup: copy `.vscode/settings.json.example` to `.vscode/settings.json` and install the recommended extensions. Overview: [docs/architecture.md](docs/architecture.md#agent-tooling). --- diff --git a/docs/architecture.md b/docs/architecture.md index 7bc53bf..3dbd0f6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -247,12 +247,13 @@ Where new infrastructure goes: The template is the seed of products that agents extend; this tooling keeps that fast and deterministic. ``` -AGENTS.md, CLAUDE.md # Agent index (CLAUDE.md includes AGENTS.md) +AGENTS.md, CLAUDE.md # Agent index: Codex reads AGENTS.md, CLAUDE.md includes it for Claude Code +.agents/ + skills//SKILL.md # Canonical skills for Claude Code and Codex: new-domain, design-system, sync-template .claude/ - settings.json # Hooks (committed); personal overrides in settings.local.json (ignored) + skills/ # Relative symlinks -> ../../.agents/skills/ (Claude Code discovery path) + settings.json # Claude Code hooks (committed); personal overrides in settings.local.json (ignored) hooks/ # block_migrations.py (PreToolUse Bash), format_file.py (PostToolUse Edit|Write) - skills//SKILL.md # new-domain, verify, design-system, add-shadcn-component, sync-template, release - agents/ # domain-reviewer, ui-reviewer (subagents) scripts/ scaffold/ # make new-domain: new_domain.py + templates/*.tmpl (string.Template) bootstrap/ # make init: init.py + product.md.tmpl @@ -266,10 +267,9 @@ scripts/ | Domain discovery | `backend/app/core/discovery.py`: no registration step for models or routers ([backend-concept.md](backend-concept.md#registration)) | | `make new-domain` | Scaffolds backend domain + frontend feature + drawer entry; never overwrites, never migrates ([backend-concept.md](backend-concept.md#scaffolding)) | | `make init` | Turns a template copy into a product ([README.md](../README.md#start-a-new-product)) | -| Migration hook | Blocks `make m/mm/migrate/make-migrations` and `tortoise migrate/makemigrations/upgrade/downgrade` with a message to ask the user. `ALLOW_MIGRATIONS=1 ` passes, only after the user explicitly asked | -| Format hook | Formats only the edited file: Ruff for `backend/**/*.py`, `vp fmt` for `frontend/**`; no-op elsewhere; reports formatter problems as a note and never fails the edit | -| Skills | Short checklists that link to these docs; they do not copy rules | -| Subagents | Read-only reviewers of a diff: backend rules (`domain-reviewer`), browser pass over affected screens (`ui-reviewer`) | -| Forks | [template-ownership.md](template-ownership.md), `.template-version`, `/sync-template` | +| Migration hook (Claude Code) | Blocks `make m/mm/migrate/make-migrations` and `tortoise migrate/makemigrations/upgrade/downgrade` with a message to ask the user. `ALLOW_MIGRATIONS=1 ` passes, only after the user explicitly asked | +| Format hook (Claude Code) | Formats only the edited file: Ruff for `backend/**/*.py`, `vp fmt` for `frontend/**`; no-op elsewhere; reports formatter problems as a note and never fails the edit | +| Skills | Short checklists that link to these docs; they do not copy rules. One copy in `.agents/skills/` ([Agent Skills](https://agentskills.io/specification) format: `name` = folder name, `description`, no tool-specific fields), read natively by Codex; Claude Code reads `.claude/skills/`, where each skill is a relative symlink to it. `new-domain` ends with a backend self-review; `design-system` covers new shadcn primitives and the browser review. Checking (`make check`) and releasing (commit types) are always-on rules in `AGENTS.md`, not skills | +| Forks | [template-ownership.md](template-ownership.md), `.template-version`, the `sync-template` skill | -Where new things go: a recurring agent workflow → `.claude/skills//SKILL.md` plus a row in `AGENTS.md`; hooks stay small Python 3.9+ stdlib scripts reading the hook JSON on stdin, with tests in `scripts/tests/`. A change to a file that `make init` or `make new-domain` rewrites (Makefile image line, `app.config.ts`, the organization templates, `navigation/settings.ts`) must keep their patterns — `scripts/tests` fails when one goes missing. +Where new things go: a recurring agent workflow → `.agents/skills//SKILL.md`, its symlink (`ln -s ../../.agents/skills/ .claude/skills/`) and a row in `AGENTS.md` — `scripts/tests/test_agent_skills.py` checks the pair and the frontmatter; a rule for every change belongs in `AGENTS.md`, not in a skill. On Windows the symlinks need Developer Mode + `git config --global core.symlinks true` before cloning (or WSL); otherwise they check out as plain text files and only Codex sees the skills; hooks stay small Python 3.9+ stdlib scripts reading the hook JSON on stdin, with tests in `scripts/tests/`. A change to a file that `make init` or `make new-domain` rewrites (Makefile image line, `app.config.ts`, the organization templates, `navigation/settings.ts`) must keep their patterns — `scripts/tests` fails when one goes missing. diff --git a/docs/backend-concept.md b/docs/backend-concept.md index 41826e8..652dae1 100644 --- a/docs/backend-concept.md +++ b/docs/backend-concept.md @@ -232,7 +232,7 @@ Domains are discovered, not registered (`app/core/discovery.py`): | Backend | `domains//` `models.py` (`organization` FK, `tenant_scope` via organization), `schemas.py` (create/update/read), `filters.py` (`name` icontains), `views.py` (thin viewset, `routers`), `services/`, `tests/test_.py` (CRUD + tenant isolation) | | Frontend | `features//resources/.ts`, `views/ListView.vue` + `DetailView.vue`, `pages/settings//index.vue` + `[id].vue`, an entry in `app/navigation/settings.ts` | -It validates the name (singular snake_case, up to 24 characters, not a kernel domain), refuses to overwrite anything and runs no migrations. Then: replace the placeholder field with the real ones, ask the user for `make mm` + `make m`, `make gen-client`, run the new tests (`--sqlite` until the migration exists), update [product.md](product.md). The `/new-domain` skill (`.claude/skills/new-domain/`) walks through it. +It validates the name (singular snake_case, up to 24 characters, not a kernel domain), refuses to overwrite anything and runs no migrations. Then: replace the placeholder field with the real ones, ask the user for `make mm` + `make m`, `make gen-client`, run the new tests (`--sqlite` until the migration exists), update [product.md](product.md). The `new-domain` skill (`.agents/skills/new-domain/`) walks through it. --- diff --git a/docs/template-core.md b/docs/template-core.md index ebd741e..8d1c39e 100644 --- a/docs/template-core.md +++ b/docs/template-core.md @@ -1,6 +1,6 @@ # Template core -Capabilities every product built from this template inherits. **Template-owned**: forks receive changes to this file through `/sync-template` ([template-ownership.md](template-ownership.md)) and do not edit it — product-specific changes go to [product.md](product.md), which wins when the two disagree. +Capabilities every product built from this template inherits. **Template-owned**: forks receive changes to this file through the `sync-template` skill ([template-ownership.md](template-ownership.md)) and do not edit it — product-specific changes go to [product.md](product.md), which wins when the two disagree. Behavior only; how it is built: [architecture.md](architecture.md), [backend-concept.md](backend-concept.md), [frontend-concept.md](frontend-concept.md), [deployment.md](deployment.md). UI copy is **Russian**; code comments English. @@ -51,5 +51,5 @@ Behavior only; how it is built: [architecture.md](architecture.md), [backend-con ## Agent tooling -- `make init name=…` turns a template copy into a product; `make new-domain name=…` scaffolds a CRUD entity end to end; `/sync-template` pulls template changes into a fork. -- Claude Code project settings block migration commands and format edited files; skills and review subagents live in `.claude/` ([architecture.md](architecture.md#agent-tooling)). +- `make init name=…` turns a template copy into a product; `make new-domain name=…` scaffolds a CRUD entity end to end; the `sync-template` skill pulls template changes into a fork. +- Agent skills shared by Claude Code and Codex live in `.agents/skills/`; Claude Code project hooks block migration commands and format edited files ([architecture.md](architecture.md#agent-tooling)). diff --git a/docs/template-ownership.md b/docs/template-ownership.md index b6d3f42..43ca7b7 100644 --- a/docs/template-ownership.md +++ b/docs/template-ownership.md @@ -1,6 +1,6 @@ # Template ownership -Products are created from this template (`make init`) and keep receiving its improvements through `/sync-template` (`.claude/skills/sync-template/`). This map decides, per path, who owns a file in a fork and what a sync does with it. **Template-owned** (this file included). +Products are created from this template (`make init`) and keep receiving its improvements through the `sync-template` skill (`.agents/skills/sync-template/`). This map decides, per path, who owns a file in a fork and what a sync does with it. **Template-owned** (this file included). `.template-version` records where a fork stands: `repository`, `version` (template `version.json` at that point) and `commit` (template commit). `make init` writes it; every sync updates it. @@ -25,7 +25,7 @@ First matching row wins (top to bottom). | `frontend/nuxt.config.ts`, `frontend/knip.jsonc` | shared | 3-way merge; product route rules and ignores stay | | `backend/pyproject.toml`, `frontend/package.json` | shared | 3-way merge; product name, version and extra dependencies win | | `.env.example`, `Makefile`, `README.md`, `compose*.yaml`, `Dockerfile`, `.github/**` | shared | 3-way merge; values written by `make init` (names, image, title) win | -| `.claude/skills/**`, `.claude/agents/**` | template | Template skills and agents are taken; a product adds its own under new names (prefix them, e.g. `.claude/skills/acme-billing/`) and those are never touched | +| `.agents/skills/**`, `.claude/skills/**` | template | Template skills and their `.claude/skills/` symlinks are taken; a skill deleted upstream is deleted with its symlink. A product adds its own under new names (prefix them, e.g. `.agents/skills/acme-billing/` + symlink) and those are never touched | | everything else (`backend/app/{core,shared,scripts}/**`, `backend/app/conftest.py`, `backend/manage.py`, `frontend/app/{shared,plugins,middleware,assets}/**`, `frontend/{scripts,modules,tests}/**`, `docs/**` except `product.md`, `scripts/**`, `.claude/{hooks,settings.json}`, `AGENTS.md`, `CLAUDE.md`, configs) | template | Taken from the template | Product code is product-owned; a fork adds files, it does not edit template-owned ones. Tests a product adds for template code go next to its own domain. diff --git a/scripts/bootstrap/init.py b/scripts/bootstrap/init.py index 1e27b5d..52a9c83 100644 --- a/scripts/bootstrap/init.py +++ b/scripts/bootstrap/init.py @@ -77,7 +77,7 @@ def read_template_version(root: Path) -> dict[str, str]: def write_template_version(root: Path, values: dict[str, str]) -> str: return ( - '# Template this product was created from; /sync-template diffs template-owned paths from `commit`\n' + '# Template this product was created from; the sync-template skill diffs template-owned paths from `commit`\n' '# (or tag v when the commit is not in the template history) and updates both.\n' '# See docs/template-ownership.md. Written by `make init`.\n' f'repository={values.get("repository", "")}\n' diff --git a/scripts/tests/test_agent_skills.py b/scripts/tests/test_agent_skills.py new file mode 100644 index 0000000..cd2fa45 --- /dev/null +++ b/scripts/tests/test_agent_skills.py @@ -0,0 +1,64 @@ +import os +import re + +import pytest +from conftest import ROOT + +yaml = pytest.importorskip('yaml') + +SKILLS = ROOT / '.agents' / 'skills' +CLAUDE_SKILLS = ROOT / '.claude' / 'skills' +NAME = re.compile(r'^[a-z0-9]+(-[a-z0-9]+)*$') +# Agent Skills spec fields; anything else is tool-specific and may break the other agent. +ALLOWED_FIELDS = {'name', 'description', 'license', 'compatibility', 'metadata', 'allowed-tools'} +LINK = re.compile(r'\]\(([^)#\s]+)(?:#[^)]*)?\)') + + +def _skill_dirs(): + return sorted(p for p in SKILLS.iterdir() if p.is_dir()) + + +def _frontmatter(path): + text = path.read_text() + assert text.startswith('---\n'), f'{path}: frontmatter must open on the first line' + block, _, body = text[4:].partition('\n---\n') + return yaml.safe_load(block), body + + +def test_skills_exist(): + assert _skill_dirs(), 'no skills under .agents/skills' + + +@pytest.mark.parametrize('skill', _skill_dirs(), ids=lambda p: p.name) +def test_frontmatter_follows_agent_skills_spec(skill): + meta, body = _frontmatter(skill / 'SKILL.md') + assert isinstance(meta, dict) + assert set(meta) <= ALLOWED_FIELDS, f'tool-specific fields: {set(meta) - ALLOWED_FIELDS}' + assert meta.get('name') == skill.name + assert NAME.match(meta['name']) and len(meta['name']) <= 64 + description = meta.get('description') + assert isinstance(description, str) and 0 < len(description) <= 1024 + assert body.strip() + + +@pytest.mark.parametrize('skill', _skill_dirs(), ids=lambda p: p.name) +def test_relative_links_resolve(skill): + _, body = _frontmatter(skill / 'SKILL.md') + for target in LINK.findall(body): + if '://' in target: + continue + assert (skill / target).exists(), f'{skill.name}: broken link {target}' + + +@pytest.mark.parametrize('skill', _skill_dirs(), ids=lambda p: p.name) +def test_claude_code_sees_skill_through_relative_symlink(skill): + link = CLAUDE_SKILLS / skill.name + assert link.is_symlink(), f'missing symlink: ln -s ../../.agents/skills/{skill.name} .claude/skills/{skill.name}' + assert os.readlink(link) == f'../../.agents/skills/{skill.name}' + assert (link / 'SKILL.md').is_file() + + +def test_no_claude_only_skills(): + for entry in CLAUDE_SKILLS.iterdir(): + assert entry.is_symlink(), f'{entry}: put the skill in .agents/skills/ and symlink it' + assert entry.resolve().parent == SKILLS.resolve(), f'{entry}: dangling or foreign symlink' From b0fcaee04a72b63e4357088e34fc097ba47f5c19 Mon Sep 17 00:00:00 2001 From: bubaley Date: Thu, 22 Oct 2026 23:19:57 +0200 Subject: [PATCH 2/2] ci: run the tooling tests when skills change Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dca3d79..7962313 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,6 +49,9 @@ jobs: - 'scripts/**' - '.claude/hooks/**' - '.claude/settings.json' + # Skills are checked by scripts/tests/test_agent_skills.py. + - '.agents/**' + - '.claude/skills/**' - '.python-version' - 'Makefile' - '.github/workflows/ci.yaml'