Layer-specific conventions live in .claude/rules/, loaded when you touch a matching file:
laravel-boost.md (generated by Boost, package versions and framework guidance), php.md (Pint, PHPStan, Rector), frontend.md (Vue/TS comments, generated types, npm tooling), i18n.md (translation mechanics), testing.md (coverage gate, parallelism, browser-suite internals), docs-site.md (the Starlight site).
- Activate the relevant skill whenever you work in its domain (
.claude/skills/), rather than waiting until you are stuck. - Follow the conventions already in the code. Check sibling files for structure, naming, and approach before inventing your own, and look for an existing component to reuse before writing a new one.
- Do not add or change dependencies, and do not create new base directories, without approval.
- Only create documentation files when explicitly asked.
- Do not write verification scripts or tinker one-offs for behaviour a test can prove. Unit and feature tests are worth more.
- Be concise in explanations. Focus on what matters rather than narrating the obvious.
- Releases run on two branches.
developaccumulates features and cutsvX.Y.Z-rc.Nrelease candidates;mastercuts the stable releases and is fed by promotingdevelop. Feature work therefore branches off and targetsdevelop— a PR opened againstmasterbypasses the candidate line, and nothing errors to tell you. Full flow inCONTRIBUTING.md→ Releases; the invariants are enforced bytests/Unit/ReleaseFlowTest.php. - Feature PRs merge by squash, and the squash commit subject is the PR title (
squash_merge_commit_title = PR_TITLE). So the PR title is the commit release-please reads — it MUST be a valid Conventional Commit (type: imperative subject, lower-case type, no trailing period), e.g.feat: edit last message from the composer with ↑. A PR titled like a sentence ("Edit last message…") is silently dropped from the changelog and the version bump — this is the #1 mistake here. - The subject must not start with a capital letter (acronyms included:
feat: SSO login…is rejected,feat: re-check the SSO policyis fine), must not end with a full stop, and must be at most 90 characters. That length rule is the one you cannot fix after the fact, so get it right before merging (#891). - Changelog-relevant types (from
release-please-config.json):feat→ Features (minor bump),fix→ Bug Fixes (patch),perf→ Performance,refactor→ Code Refactoring,deps→ Dependencies (non-bumping, rides along into the next release). A breaking change (feat!:/fix!:, or aBREAKING CHANGE:footer) forces a major bump.docs,test,chore,ci,buildandstyleare allowed but do not appear in the changelog and do not bump the version — only use them for PRs that genuinely ship no user-facing feature/fix. - Never hand-edit
CHANGELOG.md,VERSION, or either release-please manifest — release-please owns them, and nothing is tagged by a push: merging the release PR is what tags and publishes. - Keep individual commit messages Conventional too.
commitlintvalidates the PR's commits; it never reads the PR title, and both must be right. - Branch names are cosmetic (release-please ignores them). Never rely on one for the changelog; set the PR title.
- When opening a PR with
gh pr create, always pass a Conventional-Commit--titleand reference the issue in the body (Closes #NNN). - Hotfixes are the one exception, and only when both halves hold: a released version is broken in production and
developcannot be promoted as it stands. Then branch offmaster, PR intomasterwith afix:title, and merge the stable release PR. Everything else — including nearly allfix:work — goes throughdevelop; do not take this route because it is faster.CONTRIBUTING.md→ Hotfixes has the full path, including the mandatory back-merge intodevelop. - Promote
developtomasterwith a merge commit, never a squash. release-please reads the individual Conventional Commits, so squashing a promotion would collapse a whole release's worth of them into one changelog entry.
- Always activate the
tddskill when implementing an issue or building a feature. Drive the work test-first (red → green → refactor): write a failing test that captures the acceptance criterion, make it pass with the minimal change, then refactor. This pairs with the non-negotiable 100% coverage gate below.
- All user-visible copy must go through the translation layer — never hardcode English in a component, controller, or Blade view. Frontend copy goes through
$t/useTranslations, backend copy through__(), and the message key is the English source string. - Every new key needs its French translation in
lang/fr.jsonin the same change. The helpers, interpolation syntax and locale plumbing are in.claude/rules/i18n.md.
- Backend:
./vendor/bin/sail composer testruns Pint, PHPStan, Rector's dry-run, andphp artisan test --parallel --coverage --min=100. 100% coverage is non-negotiable; do not push or open a PR until it reportsTotal: 100.0 %. - Frontend:
./vendor/bin/sail npm run lint:check,./vendor/bin/sail npm run format:check,./vendor/bin/sail npm run types:check,./vendor/bin/sail npm run test:js(the Vitest suite, which the PHP coverage gate cannot reach) and./vendor/bin/sail npm run build. All five must pass../vendor/bin/sail composer ci:checkruns both gates at once. - Always run Node/npm tooling through Sail, never bare
npmon the host:node_moduleslives inside the Linux container and its native bindings are Linux-only, so a host run fails withCannot find native binding. - Accessibility is part of a green UI change, and the automated gate does not catch it. Run the repo's axe/contrast checks and match the patterns in the existing a11y browser tests before calling UI work done.
- Every browser you drive runs headless. Never open a visible window. This covers the Pest browser suite, interactive tooling (the Playwright MCP, the
browser-useandrunskills) and any raw Playwright/Chromium launch. A window popping open steals focus from whoever is at the keyboard, and on a machine running several worktrees at once it is a stream of them. - Drive interactive tooling headless too:
@playwright/mcp --headless,chromium.launch({ headless: true }). To see the page, take a screenshot and read it. - If you genuinely need to watch a browser, ask first and revert it before committing. What that costs inside the Pest suite, and the
->debug()trap in particular, is in.claude/rules/testing.md.
- A change an operator would have to act on is not done until
docs/is updated in the same PR — a new or changed.env/config setting, a feature toggle, or a change to install, upgrade, reverse-proxy or production-stack steps. The trigger fires on app changes, which is why it lives here; the page map and build steps are in.claude/rules/docs-site.md.
- When you discover a bug, broken tooling, or other pre-existing defect while implementing something unrelated, do not fix it inline. Keep the current change focused on its own scope.
- Check for an existing issue first (
gh issue list --state open --search "<keywords>", and closed issues too). If none exists,gh issue createone describing what's broken, how it surfaced, why it matters, and clear acceptance criteria, with a fitting label (e.g.tech-debt). - Mention the issue in the PR of the feature you're working on so the discovery is traceable, then carry on. Only fix it inline if it directly blocks you.