Operating instructions for Claude Code (and other AI agents) working in this repository. Audience is the agent, not human contributors — for human-facing documentation see README.md.
When the instructions here conflict with the user's global ~/.claude/CLAUDE.md,
this file wins — project-specific rules override the global defaults.
ai-lib is a shared catalog of AI assistants for the Danish public sector.
The application is a Symfony 8 web app built on the ITK Dev Docker
development setup.
- PHP 8.4 running under
phpfpm(Symfony 8 skeleton, PSR-4App\\atsrc/). - Nginx in front of
phpfpm, served via the shared Traefik proxy athttps://ai-lib.local.itkdev.dk. - MariaDB for persistence.
- Mailpit for outbound mail capture at
https://mail-ai-lib.local.itkdev.dk. - ITK Dev Docker template
symfony-8provides the container orchestration.
bin/ Symfony console entry
config/ Symfony configuration (bundles, packages, routes, services)
public/ Web root (public/index.php)
src/ Application code (PSR-4 namespace App\)
assets/ Frontend entry points (placeholder until a stack is picked)
.docker/ nginx config and templates
.github/workflows/ CI workflows (do NOT edit locally — see "Workflows" below)
docs/adr/ Architecture Decision Records (see "ADRs")
All language/build tooling runs inside containers. Never invoke php,
composer, node, npm, npx, prettier, or similar on the host.
Preferred order:
task <name>— the project'sTaskfile.ymlis the entry point for everyday commands. Runtask --listto see what's available.task compose -- <args>/task compose-exec -- <args>— pass-through helpers when no dedicated target exists.itkdev-docker-compose <command>— for cross-project ITK Dev tooling not wrapped by the project Taskfile (e.g.traefik:start).docker compose --profile dev run --rm <service> <args>— direct fallback for the dev-only tooling (prettier,markdownlint) when going around the Taskfile is justified.
# Lifecycle
task start # pull, up, composer install
task down # tear the stack down
# Composer / PHP / Symfony console
task composer -- <command> # e.g. task composer -- require foo/bar
task compose-exec -- phpfpm php <command>
task console -- <command> # e.g. task console -- cache:clear
# Coding standards (check / apply pairs)
task coding-standards-php-check
task coding-standards-php-apply
task coding-standards-twig-check
task coding-standards-twig-apply
task coding-standards-yaml-check
task coding-standards-yaml-apply
task coding-standards-markdown-check
task coding-standards-markdown-apply
task coding-standards-composer-check
task coding-standards-composer-apply
# Run every check at once
task coding-standards-check
# Tests
task test # PHPUnit, no coverage
task test-coverage # PHPUnit + Xdebug coverage, enforces 100% gateThe coverage gate is 100% and is enforced by the Tests GitHub
Actions workflow on every pull request — see .github/workflows/tests.yaml.
Run the matching check before committing changes in that area. For
commands without a dedicated task, fall back to task compose -- <args>
or itkdev-docker-compose <args>.
Config files live at the repo root:
.php-cs-fixer.dist.php— PHP CS Fixer (Symfony ruleset)..twig-cs-fixer.dist.php— Twig CS Fixer..prettierrc.yaml— Prettier (YAML, CSS/SCSS, JS)..markdownlint.jsonc+.markdownlintignore— Markdown lint.
These come from the symfony-8 template — don't edit them without a reason.
If a project-specific override is needed, override via the template's
documented mechanism (e.g. .php-cs-fixer.php next to .php-cs-fixer.dist.php).
Controllers handle routes and template/response rendering only — no business
logic. Push logic into a service class. A controller action looks like:
inject service → call service method → return render() / Response /
RedirectResponse.
Do not add PHPDoc to controllers. The class name, route attribute, action name, parameter types, and return type already describe what an action does; class- and method-level docblocks duplicate that. Push the explanatory prose into the (fully documented) service the controller delegates to. If a controller is so unusual that it needs a docblock to explain itself, that's the signal it's doing too much.
Every service class method (public, protected, private) carries a PHPDoc block
with a one-line summary, a description of intent, @param per parameter,
@return, and @throws for every exception that can be raised.
Do not edit, rename, delete, or skip files under tests/ (or any other
test files) without explicit user approval — even when a failure looks like
a stale assertion. If a change you're making appears to require test
updates, stop and describe to the user, briefly:
- Which test files / test methods need to change.
- What the change is (assertion update, fixture change, new case, removal).
- Why it's needed (production behavior changed, contract widened, etc.).
Wait for the user to approve before touching the files. The 100% coverage gate (see "Common commands") means test edits have real consequences; the user decides whether the production change or the test is wrong.
The .github/workflows/*.yaml files are mirrored from
itk-dev/devops_itkdev-docker
and carry a Do not edit this file! header. If a workflow needs to change,
open a PR upstream rather than patching locally.
- Base branch for feature work:
develop.mainis the release/stable line. - Branch name:
feature/issue-<n>-<short-slug>(e.g.feature/issue-5-claude-md). - One issue per branch where possible. Reference the issue number in the branch name and PR.
- PR target:
develop. - A PR must:
- Link the issue with
Fixes #<n>(orCloses #<n>for non-bug issues). UseRefs #<n>when coverage is partial. - Pass all required CI checks before merging.
- Carry a
CHANGELOG.mdupdate under## [Unreleased]for any user-visible change.
- Link the issue with
- If a PR carries the
do-not-mergelabel, the PR description must spell out what blocks the merge and why (e.g. waiting on upstream change, dependent PR, unresolved decision). Keep this up to date — remove or rewrite the block reason as blockers resolve.
Use Conventional Commits:
feat:new featurefix:bug fixdocs:documentation onlychore:tooling, build, deps, repo housekeepingrefactor:code change that neither adds a feature nor fixes a bugtest:tests only
Keep subject lines under ~70 characters. Use the body for the why.
CHANGELOG.md follows Keep a Changelog.
Add an entry to ## [Unreleased] under the right section (Added, Changed,
Fixed, Removed, Deprecated, Security) for every meaningful change.
Pre-release rule: while the project has no tagged releases yet,
everything is Added — there is no prior released version for a
change to be Changed, Fixed, Removed, Deprecated, or Security
relative to. Keep those sections empty (or omit them) and fold the
entry into Added, even when the work edits or replaces material that
already exists in [Unreleased]. Before adding to any non-Added
section, check git tag (or the GitHub releases page) and confirm at
least one release exists; if none does, use Added. Once the first
release is cut, the standard Keep a Changelog sections apply normally
from the next [Unreleased] onward. See PR #57 for the prior
consolidation that established this convention.
Every issue must have its native issue type set to one of:
- Bug — something is broken.
- Feature — new user-visible capability.
- Task — everything else: chores, tooling, documentation, infrastructure, refactors, ADRs, etc.
Documentation-only work is tracked as a Task type plus the
documentation label. The type classifies the nature of the work,
labels add orthogonal context.
The current gh CLI (≤ 2.92) does not expose --type. To set a type,
fall back to the REST API (PATCH /repos/{owner}/{repo}/issues/{n} with
type=<Name>) when available, otherwise ask the user to set it in the
UI. Labels can always be set with gh issue create --label.
When creating an issue, use the repository's issue template at
.github/ISSUE_TEMPLATE/issue.md. Preserve its structure — every heading
and HTML comment marker stays in its original order — and fill each
section from the available context. Pass it via gh issue create --body-file (or --body with the rendered content) rather than hand-
rolling a description.
SSH keys aren't available to the Claude session. Push one-off via HTTPS:
git push https://github.com/itk-dev/ai-lib.git HEAD:<branch>Do not change the origin remote URL — SSH is wanted for normal use outside
Claude.
Architectural decisions are recorded as ADRs in docs/adr/. Create and manage
them via the itkdev-adr skill (see issue #11). Open an ADR for decisions
that:
- Change the runtime architecture (storage, integrations, deployment).
- Choose between two viable options with non-trivial trade-offs.
- Establish a convention other contributors must follow.
Small implementation choices belong in code review, not in an ADR.
Do not include a "Follow-up Actions" (or similarly named) checklist inside an ADR. Track follow-up work as GitHub issues and reference the ADR from each issue, not the other way around. The ADR records the decision; the issues track the work derived from it. The one-time cleanup of existing sections is tracked in #44.
- Assistant — a configured AI persona/prompt bundle that can be exported, shared, and re-imported.
- Catalog — the searchable collection of assistants surfaced by the app.
- OpenWebUI export format — the JSON schema used by OpenWebUI for importing/exporting assistants; the canonical interchange format for this project.
- Share/upload flow — the moderated path by which a user submits an assistant to the catalog (metadata + review).
- Tags / categories — taxonomy applied to assistants for filtering and discovery.
- Moderation — validation and review of submitted assistants before they appear in the catalog.
- Prefer an existing pattern in the codebase over inventing a new one.
- For non-trivial decisions, write an ADR or ask the user — don't silently pick.
- For destructive git operations (
reset --hard,push --force, branch deletion), stop and ask the user — these are deny-listed globally for good reason.