Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
phanan/koel
claude-code
cursor
stream
audio
mp3

The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.

Install
php artisan boost:add-skill phanan/koel

Save this content to: AGENTS.md

<laravel-boost-guidelines>
=== foundation rules ===

# Laravel Boost Guidelines

The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.

## Foundational Context
This is a Laravel application. Read `composer.json` and `package.json` for the exact package versions in use, and abide by them.

## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.

## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.

## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.

## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `pnpm run build`, `pnpm run dev`, or `composer run dev`. Ask them.

## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.

## Documentation Files
- You must only create documentation files if explicitly requested by the user.

=== boost rules ===

## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.

## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.

## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.

## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.

## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.

## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.

### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.

1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.

=== php rules ===

## PHP

- Always use curly braces for control structures, even if it has one line.

### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
    - <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.

### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.

<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
    ...
}
</code-snippet>

## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.

## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.

## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.

=== tests rules ===

## Test Enforcement

- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.

=== laravel/core rules ===

## Do Things the Laravel Way

- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.

### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.

### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.

### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.

### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.

### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.

### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).

### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.

### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.

### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.

### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `pnpm run build` or ask the user to run `pnpm run dev` or `composer run dev`.

=== laravel/v12 rules ===

## Laravel 12

- Use the `search-docs` tool to get version-specific documentation.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.

### Laravel 12 Structure
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.

### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.

### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.

=== phpunit/core rules ===

## PHPUnit

- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test.
- If you see a test using "Pest", convert it to PHPUnit.
- Every time a test has been updated, run that singular test.
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
- Tests should test all of the happy paths, failure paths, and weird paths.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application.

### Running Tests
- Run the minimal number of tests, using an appropriate filter, before finalizing.
- To run all tests: `php artisan test --compact`.
- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file).

=== tailwindcss/core rules ===

## Tailwind CSS

- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.).
- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically.
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.

### Spacing
- When listing items, use gap utilities for spacing; don't use margins.

<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
    <div class="flex gap-8">
        <div>Superior</div>
        <div>Michigan</div>
        <div>Erie</div>
    </div>
</code-snippet>

### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.

=== tailwindcss/v4 rules ===

## Tailwind CSS 4

- Always use Tailwind CSS v4; do not use the deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed.

<code-snippet name="Extending Theme in CSS" lang="css">
@theme {
  --color-brand: oklch(0.72 0.11 178);
}
</code-snippet>

- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:

<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff">
   - @tailwind base;
   - @tailwind components;
   - @tailwind utilities;
   + @import "tailwindcss";
</code-snippet>

### Replaced Utilities
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement.
- Opacity values are still numeric.

| Deprecated |	Replacement |
|------------+--------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
</laravel-boost-guidelines>

## Architecture
- Koel loads data progressively — there is no method to fetch all songs at once. Songs are loaded lazily per screen/context. This is by design for large libraries. Never assume the playable store vault contains all songs.

## Code Organization
- **Read an existing method or attribute before reusing it.** Open its definition first. Never call anything marked `@deprecated` from new code — use what it wraps instead (e.g. `$album->artist->is_various`, not the deprecated `$album->is_compilation`).
- Traits must be placed in a `Concerns` subfolder (namespace) relative to their consumers (e.g. `App\Ai\Tools\Concerns\PlaysMusic`).
- Interfaces must be placed in a `Contracts` subfolder (namespace) relative to their consumers (e.g. `App\Ai\Tools\Contracts\SomeInterface`).

## Spelling
- Use US English spelling for all identifiers (PHP method/class/property names, TS/Vue variables and components), comments, docstrings, doc pages, and user-visible strings: `serialize` / `serializer` (not `serialise`), `color` (not `colour`), `initialize` (not `initialise`), `behavior` (not `behaviour`), `organize` / `organization`, `favorite`, `analyze`. Koel's codebase — and PHP's SPL (`JsonSerializable`) — is uniformly American; don't drift British by reflex.

## Self-Explanatory Code
- Code should read on its own. If a piece of code needs a comment to be understood, that's a signal the code is wrong, not that the comment is needed — refactor it: extract a named helper, rename a variable to encode intent, lift a condition into a named flag, pull a block into a small function. Use a comment only when refactoring genuinely can't carry the intent (a hidden invariant, a workaround tied to a specific external bug, behaviour a reader would otherwise misjudge). Never write comments that narrate the next line, summarise the surrounding block, or restate what well-named identifiers already say.
- **The "hidden invariant" carve-out is not a license — it is a last resort.** If you catch yourself writing a comment to explain that two things are different, that an order matters, or that a value means something specific, the fix is a better identifier, not a sentence. `$recordingMbid = Arr::get($track, 'recording.id')` already says the link targets the recording; a line above it explaining that a release track and a recording are distinct entities is noise. Encode the distinction in the name, then delete the comment.
- **Never write comments in tests.** A test's method name plus its assertions are the documentation. A comment above an assertion restating what the assertion checks is always noise — rename the test instead.
- **A method name says what it acts on.** Never end a name on a bare preposition that stands in for the object — `registerFor()`, `resolveFor()`, `getFor()`, `handleWith()` make the reader ask "for what? for whom?". Name the thing: `registerOrganizationProvider($organization, …)`, not `registerFor($organization, …)`. A preposition is fine once the object is named (`resolveHomeUrlForUser()`), because the name already says what is resolved. Applies to PHP and TS/JS alike.
- Don't use single-letter variable names. The only allowed ones are `i` / `j` for loop counters, `h` for the test harness, and `$e` for the exception variable in `catch (Throwable|Exception|Error $e)` blocks (PHP's universal idiom — analogous to `e` for events in JS/TS event handlers). For everything else (callback params, destructured fields, lambda args, etc.) pick a name that says what it is.
- Never combine assignment with return. Always `$x = expr;` then `return $x;` on a separate line — `return $x = expr;` cramming two effects into one statement is forbidden in PHP, TS, and JS.

## Code Formatting — Breathing Room
- Put a blank line between distinct logical phases. Don't cram declarations, conditionals, and returns into one wall of code. Applies in three places:
    1. **Inside a function**, between phases: declarations → guard clauses → setup → main work → return. Each phase is its own paragraph.
    2. **After early-return guard clauses**, before the next phase resumes. `if (!el) return;` is a phase boundary, not a line of setup.
    3. **At the top level of a module/`<script setup>` block**, between adjacent composable / lifecycle-hook / standalone-watcher calls. `onMounted(...)`, `onBeforeUnmount(...)`, `watch(...)` are independent registrations — separate them.
- Don't manufacture blank lines mid-block — only at real phase boundaries. The test: removing the blank would jam two different *intents* together, not two related lines of the same intent.

## PHP Conventions
- **Enum cases are UPPER_SNAKE_CASE** (`PlayableType::SONG`, `ListeningPeriod::ALL_TIME`), like every enum in `app/Enums`. This overrides the generic Laravel guideline above that suggests TitleCase keys.
- Always prefer Laravel's built-in helpers over custom implementations (e.g. `str()->plural()`, `Str::slug()`, `Arr::flatten()`, etc.). Do not reimplement what Laravel already provides.
- For guard clauses that throw on a condition, always reach for `throw_if($condition, ExceptionClass::class, ...$args)` / `throw_unless($condition, ExceptionClass::class, ...$args)` before writing `if (…) { throw new …; }`. The Laravel helpers read as a single declarative line, and the extra args are forwarded to the exception constructor. Plain `if`/`throw` is only correct when the throw branch has to do additional work (logging, side effects) before throwing.
- All methods must have explicit visibility (`public`, `protected`, or `private`). Never omit the visibility keyword, even on interface methods or static methods.
- Methods that don't reference `$this` must be declared `static`. The one exception is a public method on an injectable (DI) service, which stays an instance method for testability and decoupling; private and protected methods are static there too.
- Always use the least visibility possible. Use `private` by default; only use `protected` or `public` when required by inheritance or external access.
- Never use `empty()` to check arrays. If the variable is known to be an array, use `!$array` instead. Don't compare to `[]` either.
- When a string contains quotes, don't use escaped double quotes (e.g. `"Playlist \"$name\" created"`). Use `sprintf()` with a single-quoted format string instead (e.g. `sprintf('Playlist "%s" created', $name)`).
- Never query models directly (e.g. `Model::query()->where(...)`) outside of the corresponding Repository class. All model lookups and queries must go through the appropriate Repository (e.g. `PlaylistRepository`, `SongRepository`).
- Repositories are read-only — they must never create, update, or delete records. Write operations belong in services or on the models directly.
- **A repository read that returns a `LazyCollection` is named `lazyGet*`** — the laziness goes in front of the verb, never instead of it and never as a suffix: `lazyGetWithoutMbid()`, not `lazyWithoutMbid()` (no verb) and not `getWithoutMbidLazily()` (buried). A bare `get*` promises a hydrated `Collection`, so a caller has to be told at the call site which one it is getting. Pair it with a separate `count*` method when a total is needed, since counting a lazy read would consume it.
- **Always use dependency injection when it is possible. Reaching for `app()` / `resolve()` to pull a class out of the container is a code smell** — the only routine exception is integration tests. Laravel builds controllers, commands, jobs, listeners, middleware, rules, observers registered via `#[ObservedBy]`, and anything else it resolves itself, so those take their collaborators as constructor parameters. Container resolution is acceptable only where constructor injection genuinely cannot reach: a factory that also takes runtime arguments, a static context, or an object the framework instantiates without the container. When you think you have such a case, say so explicitly rather than reaching for `app()` quietly.
- For config values needed by services, use the `#[Config('key')]` attribute on constructor parameters (from `Illuminate\Container\Attributes\Config`) — never call `config()` inside the service.
- All closure parameters must be type-hinted. Never use untyped closure arguments (e.g. `function (Builder $query)`, not `function ($query)`).
- When parsing or manipulating URLs, use `Illuminate\Support\Uri` instead of `parse_url()`.
- **Write validation rules with Laravel's rule builders** (`Rule::required()`, `Rule::string()->max(255)`, `Rule::numeric()->integer()->min(1)`, `Rule::in([...])`, `Rule::unique(...)->ignore(...)`), never as `'required|string|max:255'` strings or bare rule names. A typo in a builder is caught by static analysis; a typo in a string only fails at runtime. Keep a string only for a rule that has no builder (e.g. `'min:1'` on an array, `'required_if:type,byYear'`).
- Do not add return type declarations to controller methods — controller responses are too dynamic/flexible for strict return types.
- Keep controllers thin. A controller method's job is: parse input → authorize → delegate → shape the response (resources/JSON). When a method starts accumulating data-loading orchestration, eager-load bookkeeping, multi-collection merges, or any multi-step domain logic, push that work into a service. Prefer extending an existing service in the same domain (e.g. `MediaBrowser` for browse-side folder operations) over creating a new one. Services return raw domain objects (Collections, Models) — Resource/JSON wrapping stays in the controller. Authorization stays in the controller too, so unauthorized requests fail before expensive data loads.
- NEVER perform direct Eloquent writes from a controller — no `$model->update(...)`, `->save()`, `->create(...)`, `->delete()`, `->fill()->save()`, relationship `attach`/`detach`/`sync`, or `Model::query()->update/delete`. Every persistence operation goes through a service method (the service may write on the model directly). Controllers only read (via repositories), authorize, and delegate. Even a one-line `$model->update($changes)` belongs in a service — it's the seam where validation, events, and transactions later live. When the obvious service method is a full-update path that doesn't fit (e.g. a partial patch, or one with side effects like folder re-attachment or rule-wiping you don't want), add a focused service method (e.g. `PlaylistService::patchDetails`) rather than writing inline or misusing the heavy one.
- Value objects in `app/Values/` must use a `final readonly class` with a `private __construct(...)` and a `public static function make(...): self` factory. Call sites construct them via `Foo::make(...)`, never `new Foo(...)`. The reference shape is `App\Values\Radio\RadioStationCreateData`.

## Environment Variables Documentation
- When adding, removing, or modifying environment variables in `.env.example`, always update `docs/environment-variables.md` to stay in sync.

## Git Commits
- Use [Conventional Commits](https://www.conventionalcommits.org/) for all commit messages (e.g. `fix:`, `feat:`, `chore:`, `test:`, `refactor:`, `docs:`, `ci:`, etc.).
- Focus on the feature/purpose, not implementation details. For example, prefer "feat: show current playing song during radio stream" over "feat: radio station ICY metadata now-playing". Same applies to PR titles.
- Never attribute work to AI in any artifact: no "Generated with Claude Code", "Assisted by AI", "Co-Authored-By: Claude/ChatGPT/Copilot/AI" lines, no AI-tool mentions in commits, PR titles, PR descriptions, issue comments, code comments, or doc pages. The author is the human running the tool.
- When the implementation of a PR changes (e.g. during code review), always update the PR title and description to reflect the current state of the changes.
- **Keep PR descriptions short.** Say what the PR is for and, where it is not obvious, why. A few sentences is usually the whole description. Do not add a `Testing` section, a `Scope` section, or a section listing what was deliberately left out — reviewers read the diff for that, and the tests are in it. No walking through the implementation, no restating what the code shows, no recording of alternatives considered or verification performed. If a detail only matters to whoever wrote the branch, leave it out.
- **Fold `AGENTS.md` changes into the PR that prompted them.** When a review finding or a "make it a rule" instruction lands mid-PR, commit the `AGENTS.md` (or scoped `*/AGENTS.md`) edit into that same PR — don't leave it uncommitted, and don't save it for a separate branch. The rule and the code that motivated it are one reviewable unit: the diff is the evidence for why the rule exists. This extends to any unrelated in-progress `AGENTS.md` work already sitting in the tree — fold it in rather than splitting hairs over relatedness.

## Releasing
- To release a new version, run `php artisan koel:release` (interactive) or `php artisan koel:release {patch|minor|major|vX.Y.Z}`. The command handles the version bump, commit, tag, `latest` tag move, and `release` branch sync.
- Do not bump `.version`, create release tags, or move the `latest` tag manually — always use `php artisan koel:release`.
- After the command finishes, the draft release is **not** immediately available on https://github.com/koel/koel/releases. The tag push triggers the `Upload Release Assets` GitHub Action (`.github/workflows/release.yml`), which sets up PHP/Node, builds assets, packages the zip/tarball, and only then creates the draft release. This typically takes several minutes.
- Wait for the workflow to finish before opening the releases page. Poll with `gh run list --workflow=release.yml --limit 1` or block on it with `gh run watch` (pick the most recent run). Once it's `completed/success`, the draft release exists and can be edited/published on GitHub.
- For minor/patch releases, you may be asked to write the release notes. Follow the convention of prior releases (e.g. v9.1.1, v9.1.0, v8.3.1):
    - Title: `vX.Y.Z` (no codename — codenames are reserved for major versions like "Beethoven" in v9.0.0, "Tchaikovsky" in v8.0.0).
    - Body matches GitHub's auto-generated format. Easiest way: `gh api repos/koel/koel/releases/generate-notes -F tag_name=vX.Y.Z -F previous_tag_name=vPREV --jq .body` to fetch the auto-generated body, then apply it with `gh release edit vX.Y.Z --repo koel/koel --notes-file -`.
    - Required structure: a `## What's Changed` section with bullets in the form `* <full conventional-commit subject> by @<author> in <PR or commit URL>`, optionally a `## New Contributors` section, and a trailing `**Full Changelog**: https://github.com/koel/koel/compare/vPREV...vX.Y.Z` line.
    - Do not rewrite or summarize commit subjects — keep them verbatim. Direct-to-master commits without PRs link to the commit SHA URL instead of a PR URL.
    - **Publish (un-draft) the release before tagging koel/franken or koel/docker.** Both downstream build scripts `curl https://github.com/koel/koel/releases/download/vX.Y.Z/koel-vX.Y.Z.tar.gz`, and that URL returns 404 for draft releases — the build fails. Apply notes and publish in one shot: `gh release edit vX.Y.Z --repo koel/koel --notes-file /tmp/notes.md --draft=false`. Only leave it as a draft if you're releasing koel/koel in isolation (no franken/docker companion).
- If a downstream build fails because koel/koel was still a draft at the time, recover with `gh workflow run release.yml --repo koel/franken -f koel_version=vX.Y.Z` for franken. koel/docker has no such input — koel/docker#226 removed `workflow_dispatch` so a tag push is the only way to release — so recover there with `gh run rerun <failed-run-id> --repo koel/docker`, which replays the build against the tag that already exists.

## AI Assistant Tools
- When AI assistant tool capabilities change (added, removed, or updated), always update the sample prompts in `AiSamplePrompts.vue` to reflect the current abilities.

## Testing Assertions
- When asserting two Eloquent models are the same, use `assertTrue($modelA->is($modelB))` instead of comparing IDs.
- Never resort to `ReflectionClass` / `ReflectionProperty` / `ReflectionMethod` in tests to peek at private state, instantiate classes with private constructors, or invoke private methods. If a test "needs" reflection, the smell is the test or the code: the production class should expose what's necessary via a public factory, the dependency should be injectable, or the test should construct the dependency itself (TOTP and similar deterministic primitives need no shared instance). Refactor instead of reaching for reflection.

## Test Scope
- Write only the tests a change needs. Each test must be able to catch a regression no other test catches: one per branch or call site of ours, not one per way of reaching it.
- Never test the framework. Declarative config (attributes, casts, `$with`, route and middleware registration, validation rule names) gets no test of its own unless our code computes it.
- Never repeat coverage. If a unit test already asserts a call or a branch, don't add a feature test that proves the same thing through HTTP, and vice versa. Before adding a test, check what existing tests already cover.

## Model Factories
- Use `createOne()` to create a single model and `createMany()` to create a collection. Never use `create()` directly, as its return type is ambiguous (single model or collection depending on arguments).
- Wire parent relationships with `->for($parent)` instead of passing foreign keys in the attributes array. For polymorphic relations, pass the relation name as the second argument: `->for($song, 'rateable')` (sets both `*_id` and `*_type`). Prefer `Rating::factory()->for($user)->for($song, 'rateable')->createOne(['rating' => 5])` over the equivalent `createOne(['user_id' => $user->id, 'rateable_id' => $song->id, 'rateable_type' => $song->getMorphClass(), 'rating' => 5])`.

## Test Class Namespacing
- Unit test classes must mirror the namespace of the class under test. Replace `App\` with `Tests\Unit\` and add a `Test` suffix (e.g. `App\Ai\Services\FavoriteableEntityResolver` → `Tests\Unit\Ai\Services\FavoriteableEntityResolverTest`).
- The test file path must match the namespace (e.g. `tests/Unit/Ai/Services/FavoriteableEntityResolverTest.php`).

## Code Reviews
- When addressing PR review comments, do NOT blindly follow them. Always use your own knowledge and logic to evaluate whether the feedback makes sense. If it doesn't, push back and explain why.
- **Ignore review findings about rare failures.** A failure that needs an unlikely coincidence (a request failing between two others that succeeded, clock skew, a user deleted while holding a link, a sub-second race between background tasks) is not worth a fix, a test, or a review round. Skip it without asking and keep the PR moving. Things users actually do are never edge cases, however fast they do them: rapid skipping, pausing while a song loads, double clicks. Fix those.
- **Never justify proposed work with an unverified claim about an external system.** Before arguing that a change is worth making because some client reads a field, some spec defines it, some service accepts a parameter, or some tool behaves a certain way — go and check. Read the spec page, grep the client's source (`gh search code --repo owner/name 'symbol'`), query the live API. If verifying is not possible, say the claim is unverified **in the same breath as the proposal**, never after the work is built. A rationale that turns out to rest on a guess wastes a review cycle and makes every other claim in the proposal suspect.
- CodeRabbit (and similar bots) split their output across three GitHub layers. Before claiming a review has been addressed, query **all three**:
  - `gh api repos/{owner}/{repo}/pulls/{n}/comments` — inline review comments on specific file/line positions (🟡 Minor / 🟠 Major / 🔴 Critical / ⚠️ Potential issue).
  - `gh api repos/{owner}/{repo}/issues/{n}/comments` — issue-level (conversation) comments. CodeRabbit's PR-level summary lives here, and the **Nitpick comments** are sometimes bundled in a collapsible section inside that summary's body.
  - `gh api repos/{owner}/{repo}/pulls/{n}/reviews` — the review submission bodies. **Outside diff range comments** and standalone **Nitpick comments** land here whenever GitHub won't let the bot anchor a finding inline (the line falls outside the diff hunk, the file was deleted, and similar). The giveaway is a CAUTION block reading `Some comments are outside the diff and can't be posted inline due to platform limitations`.
  - Querying only the first two misses an entire class of findings — precisely the ones GitHub could not anchor inline. Sort by `created_at` / `submitted_at` to find what is new since the last pass, and don't filter review submissions by `state` alone: a submission with no state still carries findings in its `body`.
- **CodeRabbit pauses itself when commits arrive in quick succession, and its check stays green on the stale result.** A passing CodeRabbit check is not evidence that current HEAD was reviewed. Confirm the range it actually covered — its summary comment names it as `between <sha> and <sha>` — and compare the second sha against `git rev-parse HEAD`. When they differ, the newer commits are unreviewed, so ask for a review by commenting `@coderabbitai review` on the PR rather than waiting for one that will never come. That trigger comment is the one PR comment that needs no separate approval; it asks for a review rather than answering one.

## Linting & Static Analysis
- When running lint or static analysis (backend or frontend), fix ALL warnings and errors to ensure 100% clean output — even pre-existing issues unrelated to current changes.
- **Fix the cause a warning points at, never the warning.** When static analysis reports something undefined, unknown, or mistyped, find out why before changing anything. Never add a docblock, `@property`, type, cast, or ignore comment just to make it pass. A missing `@property` on a model attribute is often deliberate — the attribute may be deprecated or internal.
- **Before creating or updating any PR that touches PHP files**, run all backend gates locally and confirm green: `composer cs` (format check), `composer lint` (mago lint), `composer analyze` (phpstan). Do NOT rely on the pre-commit hook alone — it only catches formatting. Lint and static-analysis failures must be caught locally, not by CI, so the PR isn't created/updated red.

## Vite+ Toolchain

This project uses **Vite+**, a unified toolchain wrapping Vite, Vitest, Oxlint, Oxfmt, and more via a single global CLI called `vp`. Run `vp help` for available commands.

### Imports
- Import from `vite-plus` instead of `vite` (e.g. `import { defineConfig } from 'vite-plus'`)
- Import from `vite-plus/test` instead of `vitest` (e.g. `import { describe, expect, it, vi } from 'vite-plus/test'`)
- Do NOT install `vitest`, `oxlint`, or `oxfmt` directly — Vite+ wraps these tools

### Common Pitfalls
- Do not use `vp vitest` or `vp oxlint` — use `vp test` and `vp lint` instead
- `vp test` runs the built-in test command; `vp run test` runs the `test` script from package.json
- Use `vp check` for validation loops (combines fmt + lint + typecheck)
- Prefer `vp check resources/assets` to scope checks to frontend code

Laravel Boost Guidelines

The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.

Foundational Context

This is a Laravel application. Read composer.json and package.json for the exact package versions in use, and abide by them.

Conventions

  • You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
  • Use descriptive names for variables and methods. For example, isRegisteredForDiscounts, not discount().
  • Check for existing components to reuse before writing a new one.

Verification Scripts

  • Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.

Application Structure & Architecture

  • Stick to existing directory structure; don't create new base folders without approval.
  • Do not change the application's dependencies without approval.

Frontend Bundling

  • If the user doesn't see a frontend change reflected in the UI, it could mean they need to run pnpm run build, pnpm run dev, or composer run dev. Ask them.

Replies

  • Be concise in your explanations - focus on what's important rather than explaining obvious details.

Documentation Files

  • You must only create documentation files if explicitly requested by the user.

=== boost rules ===

Laravel Boost

  • Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.

Artisan

  • Use the list-artisan-commands tool when you need to call an Artisan command to double-check the available parameters.

URLs

  • Whenever you share a project URL with the user, you should use the get-absolute-url tool to ensure you're using the correct scheme, domain/IP, and port.

Tinker / Debugging

  • You should use the tinker tool when you need to execute PHP to debug code or query Eloquent models directly.
  • Use the database-query tool when you only need to read from the database.

Reading Browser Logs With the browser-logs Tool

  • You can read browser logs, errors, and exceptions using the browser-logs tool from Boost.
  • Only recent browser logs will be useful - ignore old logs.

Searching Documentation (Critically Important)

  • Boost comes with a powerful search-docs tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
  • The search-docs tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
  • You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
  • Search the documentation before making code changes to ensure we are taking the correct approach.
  • Use multiple, broad, simple, topic-based queries to start. For example: ['rate limiting', 'routing rate limiting', 'routing'].
  • Do not add package names to queries; package information is already shared. For example, use test resource table, not filament 4 test resource table.

Available Search Syntax

  • You can and should pass multiple queries at once. The most relevant results will be returned first.
  1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
  2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
  3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
  4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
  5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.

=== php rules ===

PHP

  • Always use curly braces for control structures, even if it has one line.

Constructors

  • Use PHP 8 constructor property promotion in __construct().
    • public function __construct(public GitHub $github) { }
  • Do not allow empty __construct() methods with zero parameters unless the constructor is private.

Type Declarations

  • Always use explicit return type declarations for methods and functions.
  • Use appropriate PHP type hints for method parameters.

Comments

  • Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.

PHPDoc Blocks

  • Add useful array shape type definitions for arrays when appropriate.

Enums

  • Typically, keys in an Enum should be TitleCase. For example: FavoritePerson, BestLake, Monthly.

=== tests rules ===

Test Enforcement

  • Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
  • Run the minimum number of tests needed to ensure code quality and speed. Use php artisan test --compact with a specific filename or filter.

=== laravel/core rules ===

Do Things the Laravel Way

  • Use php artisan make: commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the list-artisan-commands tool.
  • If you're creating a generic PHP class, use php artisan make:class.
  • Pass --no-interaction to all Artisan commands to ensure they work without user input. You should also pass the correct --options to ensure correct behavior.

Database

  • Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
  • Use Eloquent models and relationships before suggesting raw database queries.
  • Avoid DB::; prefer Model::query(). Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
  • Generate code that prevents N+1 query problems by using eager loading.
  • Use Laravel's query builder for very complex database operations.

Model Creation

  • When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using list-artisan-commands to check the available options to php artisan make:model.

APIs & Eloquent Resources

  • For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.

Controllers & Validation

  • Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
  • Check sibling Form Requests to see if the application uses array or string based validation rules.

Queues

  • Use queued jobs for time-consuming operations with the ShouldQueue interface.

Authentication & Authorization

  • Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).

URL Generation

  • When generating links to other pages, prefer named routes and the route() function.

Configuration

  • Use environment variables only in configuration files - never use the env() function directly outside of config files. Always use config('app.name'), not env('APP_NAME').

Testing

  • When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
  • Faker: Use methods such as $this->faker->word() or fake()->randomDigit(). Follow existing conventions whether to use $this->faker or fake().
  • When creating tests, make use of php artisan make:test [options] {name} to create a feature test, and pass --unit to create a unit test. Most tests should be feature tests.

Vite Error

  • If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run pnpm run build or ask the user to run pnpm run dev or composer run dev.

=== laravel/v12 rules ===

Laravel 12

  • Use the search-docs tool to get version-specific documentation.
  • Since Laravel 11, Laravel has a new streamlined file structure which this project uses.

Laravel 12 Structure

  • In Laravel 12, middleware are no longer registered in app/Http/Kernel.php.
  • Middleware are configured declaratively in bootstrap/app.php using Application::configure()->withMiddleware().
  • bootstrap/app.php is the file to register middleware, exceptions, and routing files.
  • bootstrap/providers.php contains application specific service providers.
  • The app\Console\Kernel.php file no longer exists; use bootstrap/app.php or routes/console.php for console configuration.
  • Console commands in app/Console/Commands/ are automatically available and do not require manual registration.

Database

  • When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
  • Laravel 12 allows limiting eagerly loaded records natively, without external packages: $query->latest()->limit(10);.

Models

  • Casts can and likely should be set in a casts() method on a model rather than the $casts property. Follow existing conventions from other models.

=== phpunit/core rules ===

PHPUnit

  • This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use php artisan make:test --phpunit {name} to create a new test.
  • If you see a test using "Pest", convert it to PHPUnit.
  • Every time a test has been updated, run that singular test.
  • When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
  • Tests should test all of the happy paths, failure paths, and weird paths.
  • You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application.

Running Tests

  • Run the minimal number of tests, using an appropriate filter, before finalizing.
  • To run all tests: php artisan test --compact.
  • To run all tests in a file: php artisan test --compact tests/Feature/ExampleTest.php.
  • To filter on a particular test name: php artisan test --compact --filter=testName (recommended after making a change to a related file).

=== tailwindcss/core rules ===

Tailwind CSS

  • Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own.
  • Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.).
  • Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically.
  • You can use the search-docs tool to get exact examples from the official documentation when needed.

Spacing

  • When listing items, use gap utilities for spacing; don't use margins.

Dark Mode

  • If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using dark:.

=== tailwindcss/v4 rules ===

Tailwind CSS 4

  • Always use Tailwind CSS v4; do not use the deprecated utilities.
  • corePlugins is not supported in Tailwind v4.
  • In Tailwind v4, configuration is CSS-first using the @theme directive — no separate tailwind.config.js file is needed.
  • In Tailwind v4, you import Tailwind using a regular CSS @import statement, not using the @tailwind directives used in v3:

Replaced Utilities

  • Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement.
  • Opacity values are still numeric.

| Deprecated | Replacement | |------------+--------------| | bg-opacity-* | bg-black/* | | text-opacity-* | text-black/* | | border-opacity-* | border-black/* | | divide-opacity-* | divide-black/* | | ring-opacity-* | ring-black/* | | placeholder-opacity-* | placeholder-black/* | | flex-shrink-* | shrink-* | | flex-grow-* | grow-* | | overflow-ellipsis | text-ellipsis | | decoration-slice | box-decoration-slice | | decoration-clone | box-decoration-clone |

Architecture

  • Koel loads data progressively — there is no method to fetch all songs at once. Songs are loaded lazily per screen/context. This is by design for large libraries. Never assume the playable store vault contains all songs.

Code Organization

  • Read an existing method or attribute before reusing it. Open its definition first. Never call anything marked @deprecated from new code — use what it wraps instead (e.g. $album->artist->is_various, not the deprecated $album->is_compilation).
  • Traits must be placed in a Concerns subfolder (namespace) relative to their consumers (e.g. App\Ai\Tools\Concerns\PlaysMusic).
  • Interfaces must be placed in a Contracts subfolder (namespace) relative to their consumers (e.g. App\Ai\Tools\Contracts\SomeInterface).

Spelling

  • Use US English spelling for all identifiers (PHP method/class/property names, TS/Vue variables and components), comments, docstrings, doc pages, and user-visible strings: serialize / serializer (not serialise), color (not colour), initialize (not initialise), behavior (not behaviour), organize / organization, favorite, analyze. Koel's codebase — and PHP's SPL (JsonSerializable) — is uniformly American; don't drift British by reflex.

Self-Explanatory Code

  • Code should read on its own. If a piece of code needs a comment to be understood, that's a signal the code is wrong, not that the comment is needed — refactor it: extract a named helper, rename a variable to encode intent, lift a condition into a named flag, pull a block into a small function. Use a comment only when refactoring genuinely can't carry the intent (a hidden invariant, a workaround tied to a specific external bug, behaviour a reader would otherwise misjudge). Never write comments that narrate the next line, summarise the surrounding block, or restate what well-named identifiers already say.
  • The "hidden invariant" carve-out is not a license — it is a last resort. If you catch yourself writing a comment to explain that two things are different, that an order matters, or that a value means something specific, the fix is a better identifier, not a sentence. $recordingMbid = Arr::get($track, 'recording.id') already says the link targets the recording; a line above it explaining that a release track and a recording are distinct entities is noise. Encode the distinction in the name, then delete the comment.
  • Never write comments in tests. A test's method name plus its assertions are the documentation. A comment above an assertion restating what the assertion checks is always noise — rename the test instead.
  • A method name says what it acts on. Never end a name on a bare preposition that stands in for the object — registerFor(), resolveFor(), getFor(), handleWith() make the reader ask "for what? for whom?". Name the thing: registerOrganizationProvider($organization, …), not registerFor($organization, …). A preposition is fine once the object is named (resolveHomeUrlForUser()), because the name already says what is resolved. Applies to PHP and TS/JS alike.
  • Don't use single-letter variable names. The only allowed ones are i / j for loop counters, h for the test harness, and $e for the exception variable in catch (Throwable|Exception|Error $e) blocks (PHP's universal idiom — analogous to e for events in JS/TS event handlers). For everything else (callback params, destructured fields, lambda args, etc.) pick a name that says what it is.
  • Never combine assignment with return. Always $x = expr; then return $x; on a separate line — return $x = expr; cramming two effects into one statement is forbidden in PHP, TS, and JS.

Code Formatting — Breathing Room

  • Put a blank line between distinct logical phases. Don't cram declarations, conditionals, and returns into one wall of code. Applies in three places:
    1. Inside a function, between phases: declarations → guard clauses → setup → main work → return. Each phase is its own paragraph.
    2. After early-return guard clauses, before the next phase resumes. if (!el) return; is a phase boundary, not a line of setup.
    3. At the top level of a module/<script setup> block, between adjacent composable / lifecycle-hook / standalone-watcher calls. onMounted(...), onBeforeUnmount(...), watch(...) are independent registrations — separate them.
  • Don't manufacture blank lines mid-block — only at real phase boundaries. The test: removing the blank would jam two different intents together, not two related lines of the same intent.

PHP Conventions

  • Enum cases are UPPER_SNAKE_CASE (PlayableType::SONG, ListeningPeriod::ALL_TIME), like every enum in app/Enums. This overrides the generic Laravel guideline above that suggests TitleCase keys.
  • Always prefer Laravel's built-in helpers over custom implementations (e.g. str()->plural(), Str::slug(), Arr::flatten(), etc.). Do not reimplement what Laravel already provides.
  • For guard clauses that throw on a condition, always reach for throw_if($condition, ExceptionClass::class, ...$args) / throw_unless($condition, ExceptionClass::class, ...$args) before writing if (…) { throw new …; }. The Laravel helpers read as a single declarative line, and the extra args are forwarded to the exception constructor. Plain if/throw is only correct when the throw branch has to do additional work (logging, side effects) before throwing.
  • All methods must have explicit visibility (public, protected, or private). Never omit the visibility keyword, even on interface methods or static methods.
  • Methods that don't reference $this must be declared static. The one exception is a public method on an injectable (DI) service, which stays an instance method for testability and decoupling; private and protected methods are static there too.
  • Always use the least visibility possible. Use private by default; only use protected or public when required by inheritance or external access.
  • Never use empty() to check arrays. If the variable is known to be an array, use !$array instead. Don't compare to [] either.
  • When a string contains quotes, don't use escaped double quotes (e.g. "Playlist \"$name\" created"). Use sprintf() with a single-quoted format string instead (e.g. sprintf('Playlist "%s" created', $name)).
  • Never query models directly (e.g. Model::query()->where(...)) outside of the corresponding Repository class. All model lookups and queries must go through the appropriate Repository (e.g. PlaylistRepository, SongRepository).
  • Repositories are read-only — they must never create, update, or delete records. Write operations belong in services or on the models directly.
  • A repository read that returns a LazyCollection is named lazyGet* — the laziness goes in front of the verb, never instead of it and never as a suffix: lazyGetWithoutMbid(), not lazyWithoutMbid() (no verb) and not getWithoutMbidLazily() (buried). A bare get* promises a hydrated Collection, so a caller has to be told at the call site which one it is getting. Pair it with a separate count* method when a total is needed, since counting a lazy read would consume it.
  • Always use dependency injection when it is possible. Reaching for app() / resolve() to pull a class out of the container is a code smell — the only routine exception is integration tests. Laravel builds controllers, commands, jobs, listeners, middleware, rules, observers registered via #[ObservedBy], and anything else it resolves itself, so those take their collaborators as constructor parameters. Container resolution is acceptable only where constructor injection genuinely cannot reach: a factory that also takes runtime arguments, a static context, or an object the framework instantiates without the container. When you think you have such a case, say so explicitly rather than reaching for app() quietly.
  • For config values needed by services, use the #[Config('key')] attribute on constructor parameters (from Illuminate\Container\Attributes\Config) — never call config() inside the service.
  • All closure parameters must be type-hinted. Never use untyped closure arguments (e.g. function (Builder $query), not function ($query)).
  • When parsing or manipulating URLs, use Illuminate\Support\Uri instead of parse_url().
  • Write validation rules with Laravel's rule builders (Rule::required(), Rule::string()->max(255), Rule::numeric()->integer()->min(1), Rule::in([...]), Rule::unique(...)->ignore(...)), never as 'required|string|max:255' strings or bare rule names. A typo in a builder is caught by static analysis; a typo in a string only fails at runtime. Keep a string only for a rule that has no builder (e.g. 'min:1' on an array, 'required_if:type,byYear').
  • Do not add return type declarations to controller methods — controller responses are too dynamic/flexible for strict return types.
  • Keep controllers thin. A controller method's job is: parse input → authorize → delegate → shape the response (resources/JSON). When a method starts accumulating data-loading orchestration, eager-load bookkeeping, multi-collection merges, or any multi-step domain logic, push that work into a service. Prefer extending an existing service in the same domain (e.g. MediaBrowser for browse-side folder operations) over creating a new one. Services return raw domain objects (Collections, Models) — Resource/JSON wrapping stays in the controller. Authorization stays in the controller too, so unauthorized requests fail before expensive data loads.
  • NEVER perform direct Eloquent writes from a controller — no $model->update(...), ->save(), ->create(...), ->delete(), ->fill()->save(), relationship attach/detach/sync, or Model::query()->update/delete. Every persistence operation goes through a service method (the service may write on the model directly). Controllers only read (via repositories), authorize, and delegate. Even a one-line $model->update($changes) belongs in a service — it's the seam where validation, events, and transactions later live. When the obvious service method is a full-update path that doesn't fit (e.g. a partial patch, or one with side effects like folder re-attachment or rule-wiping you don't want), add a focused service method (e.g. PlaylistService::patchDetails) rather than writing inline or misusing the heavy one.
  • Value objects in app/Values/ must use a final readonly class with a private __construct(...) and a public static function make(...): self factory. Call sites construct them via Foo::make(...), never new Foo(...). The reference shape is App\Values\Radio\RadioStationCreateData.

Environment Variables Documentation

  • When adding, removing, or modifying environment variables in .env.example, always update docs/environment-variables.md to stay in sync.

Git Commits

  • Use Conventional Commits for all commit messages (e.g. fix:, feat:, chore:, test:, refactor:, docs:, ci:, etc.).
  • Focus on the feature/purpose, not implementation details. For example, prefer "feat: show current playing song during radio stream" over "feat: radio station ICY metadata now-playing". Same applies to PR titles.
  • Never attribute work to AI in any artifact: no "Generated with Claude Code", "Assisted by AI", "Co-Authored-By: Claude/ChatGPT/Copilot/AI" lines, no AI-tool mentions in commits, PR titles, PR descriptions, issue comments, code comments, or doc pages. The author is the human running the tool.
  • When the implementation of a PR changes (e.g. during code review), always update the PR title and description to reflect the current state of the changes.
  • Keep PR descriptions short. Say what the PR is for and, where it is not obvious, why. A few sentences is usually the whole description. Do not add a Testing section, a Scope section, or a section listing what was deliberately left out — reviewers read the diff for that, and the tests are in it. No walking through the implementation, no restating what the code shows, no recording of alternatives considered or verification performed. If a detail only matters to whoever wrote the branch, leave it out.
  • Fold AGENTS.md changes into the PR that prompted them. When a review finding or a "make it a rule" instruction lands mid-PR, commit the AGENTS.md (or scoped */AGENTS.md) edit into that same PR — don't leave it uncommitted, and don't save it for a separate branch. The rule and the code that motivated it are one reviewable unit: the diff is the evidence for why the rule exists. This extends to any unrelated in-progress AGENTS.md work already sitting in the tree — fold it in rather than splitting hairs over relatedness.

Releasing

  • To release a new version, run php artisan koel:release (interactive) or php artisan koel:release {patch|minor|major|vX.Y.Z}. The command handles the version bump, commit, tag, latest tag move, and release branch sync.
  • Do not bump .version, create release tags, or move the latest tag manually — always use php artisan koel:release.
  • After the command finishes, the draft release is not immediately available on https://github.com/koel/koel/releases. The tag push triggers the Upload Release Assets GitHub Action (.github/workflows/release.yml), which sets up PHP/Node, builds assets, packages the zip/tarball, and only then creates the draft release. This typically takes several minutes.
  • Wait for the workflow to finish before opening the releases page. Poll with gh run list --workflow=release.yml --limit 1 or block on it with gh run watch (pick the most recent run). Once it's completed/success, the draft release exists and can be edited/published on GitHub.
  • For minor/patch releases, you may be asked to write the release notes. Follow the convention of prior releases (e.g. v9.1.1, v9.1.0, v8.3.1):
    • Title: vX.Y.Z (no codename — codenames are reserved for major versions like "Beethoven" in v9.0.0, "Tchaikovsky" in v8.0.0).
    • Body matches GitHub's auto-generated format. Easiest way: gh api repos/koel/koel/releases/generate-notes -F tag_name=vX.Y.Z -F previous_tag_name=vPREV --jq .body to fetch the auto-generated body, then apply it with gh release edit vX.Y.Z --repo koel/koel --notes-file -.
    • Required structure: a ## What's Changed section with bullets in the form * <full conventional-commit subject> by @<author> in <PR or commit URL>, optionally a ## New Contributors section, and a trailing **Full Changelog**: https://github.com/koel/koel/compare/vPREV...vX.Y.Z line.
    • Do not rewrite or summarize commit subjects — keep them verbatim. Direct-to-master commits without PRs link to the commit SHA URL instead of a PR URL.
    • Publish (un-draft) the release before tagging koel/franken or koel/docker. Both downstream build scripts curl https://github.com/koel/koel/releases/download/vX.Y.Z/koel-vX.Y.Z.tar.gz, and that URL returns 404 for draft releases — the build fails. Apply notes and publish in one shot: gh release edit vX.Y.Z --repo koel/koel --notes-file /tmp/notes.md --draft=false. Only leave it as a draft if you're releasing koel/koel in isolation (no franken/docker companion).
  • If a downstream build fails because koel/koel was still a draft at the time, recover with gh workflow run release.yml --repo koel/franken -f koel_version=vX.Y.Z for franken. koel/docker has no such input — koel/docker#226 removed workflow_dispatch so a tag push is the only way to release — so recover there with gh run rerun <failed-run-id> --repo koel/docker, which replays the build against the tag that already exists.

AI Assistant Tools

  • When AI assistant tool capabilities change (added, removed, or updated), always update the sample prompts in AiSamplePrompts.vue to reflect the current abilities.

Testing Assertions

  • When asserting two Eloquent models are the same, use assertTrue($modelA->is($modelB)) instead of comparing IDs.
  • Never resort to ReflectionClass / ReflectionProperty / ReflectionMethod in tests to peek at private state, instantiate classes with private constructors, or invoke private methods. If a test "needs" reflection, the smell is the test or the code: the production class should expose what's necessary via a public factory, the dependency should be injectable, or the test should construct the dependency itself (TOTP and similar deterministic primitives need no shared instance). Refactor instead of reaching for reflection.

Test Scope

  • Write only the tests a change needs. Each test must be able to catch a regression no other test catches: one per branch or call site of ours, not one per way of reaching it.
  • Never test the framework. Declarative config (attributes, casts, $with, route and middleware registration, validation rule names) gets no test of its own unless our code computes it.
  • Never repeat coverage. If a unit test already asserts a call or a branch, don't add a feature test that proves the same thing through HTTP, and vice versa. Before adding a test, check what existing tests already cover.

Model Factories

  • Use createOne() to create a single model and createMany() to create a collection. Never use create() directly, as its return type is ambiguous (single model or collection depending on arguments).
  • Wire parent relationships with ->for($parent) instead of passing foreign keys in the attributes array. For polymorphic relations, pass the relation name as the second argument: ->for($song, 'rateable') (sets both *_id and *_type). Prefer Rating::factory()->for($user)->for($song, 'rateable')->createOne(['rating' => 5]) over the equivalent createOne(['user_id' => $user->id, 'rateable_id' => $song->id, 'rateable_type' => $song->getMorphClass(), 'rating' => 5]).

Test Class Namespacing

  • Unit test classes must mirror the namespace of the class under test. Replace App\ with Tests\Unit\ and add a Test suffix (e.g. App\Ai\Services\FavoriteableEntityResolver → Tests\Unit\Ai\Services\FavoriteableEntityResolverTest).
  • The test file path must match the namespace (e.g. tests/Unit/Ai/Services/FavoriteableEntityResolverTest.php).

Code Reviews

  • When addressing PR review comments, do NOT blindly follow them. Always use your own knowledge and logic to evaluate whether the feedback makes sense. If it doesn't, push back and explain why.
  • Ignore review findings about rare failures. A failure that needs an unlikely coincidence (a request failing between two others that succeeded, clock skew, a user deleted while holding a link, a sub-second race between background tasks) is not worth a fix, a test, or a review round. Skip it without asking and keep the PR moving. Things users actually do are never edge cases, however fast they do them: rapid skipping, pausing while a song loads, double clicks. Fix those.
  • Never justify proposed work with an unverified claim about an external system. Before arguing that a change is worth making because some client reads a field, some spec defines it, some service accepts a parameter, or some tool behaves a certain way — go and check. Read the spec page, grep the client's source (gh search code --repo owner/name 'symbol'), query the live API. If verifying is not possible, say the claim is unverified in the same breath as the proposal, never after the work is built. A rationale that turns out to rest on a guess wastes a review cycle and makes every other claim in the proposal suspect.
  • CodeRabbit (and similar bots) split their output across three GitHub layers. Before claiming a review has been addressed, query all three:
    • gh api repos/{owner}/{repo}/pulls/{n}/comments — inline review comments on specific file/line positions (🟡 Minor / 🟠 Major / 🔴 Critical / ⚠️ Potential issue).
    • gh api repos/{owner}/{repo}/issues/{n}/comments — issue-level (conversation) comments. CodeRabbit's PR-level summary lives here, and the Nitpick comments are sometimes bundled in a collapsible section inside that summary's body.
    • gh api repos/{owner}/{repo}/pulls/{n}/reviews — the review submission bodies. Outside diff range comments and standalone Nitpick comments land here whenever GitHub won't let the bot anchor a finding inline (the line falls outside the diff hunk, the file was deleted, and similar). The giveaway is a CAUTION block reading Some comments are outside the diff and can't be posted inline due to platform limitations.
    • Querying only the first two misses an entire class of findings — precisely the ones GitHub could not anchor inline. Sort by created_at / submitted_at to find what is new since the last pass, and don't filter review submissions by state alone: a submission with no state still carries findings in its body.
  • CodeRabbit pauses itself when commits arrive in quick succession, and its check stays green on the stale result. A passing CodeRabbit check is not evidence that current HEAD was reviewed. Confirm the range it actually covered — its summary comment names it as between <sha> and <sha> — and compare the second sha against git rev-parse HEAD. When they differ, the newer commits are unreviewed, so ask for a review by commenting @coderabbitai review on the PR rather than waiting for one that will never come. That trigger comment is the one PR comment that needs no separate approval; it asks for a review rather than answering one.

Linting & Static Analysis

  • When running lint or static analysis (backend or frontend), fix ALL warnings and errors to ensure 100% clean output — even pre-existing issues unrelated to current changes.
  • Fix the cause a warning points at, never the warning. When static analysis reports something undefined, unknown, or mistyped, find out why before changing anything. Never add a docblock, @property, type, cast, or ignore comment just to make it pass. A missing @property on a model attribute is often deliberate — the attribute may be deprecated or internal.
  • Before creating or updating any PR that touches PHP files, run all backend gates locally and confirm green: composer cs (format check), composer lint (mago lint), composer analyze (phpstan). Do NOT rely on the pre-commit hook alone — it only catches formatting. Lint and static-analysis failures must be caught locally, not by CI, so the PR isn't created/updated red.

Vite+ Toolchain

This project uses Vite+, a unified toolchain wrapping Vite, Vitest, Oxlint, Oxfmt, and more via a single global CLI called vp. Run vp help for available commands.

Imports

  • Import from vite-plus instead of vite (e.g. import { defineConfig } from 'vite-plus')
  • Import from vite-plus/test instead of vitest (e.g. import { describe, expect, it, vi } from 'vite-plus/test')
  • Do NOT install vitest, oxlint, or oxfmt directly — Vite+ wraps these tools

Common Pitfalls

  • Do not use vp vitest or vp oxlint — use vp test and vp lint instead
  • vp test runs the built-in test command; vp run test runs the test script from package.json
  • Use vp check for validation loops (combines fmt + lint + typecheck)
  • Prefer vp check resources/assets to scope checks to frontend code
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource