diff --git a/.agents/skills/ai-sdk-development/SKILL.md b/.agents/skills/ai-sdk-development/SKILL.md deleted file mode 100644 index 5071ca45..00000000 --- a/.agents/skills/ai-sdk-development/SKILL.md +++ /dev/null @@ -1,483 +0,0 @@ ---- -name: ai-sdk-development -description: TRIGGER when working with ai-sdk which is Laravel official first-party AI SDK. Activate when building, editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly. -license: MIT -metadata: - author: laravel ---- - -# Developing with the Laravel AI SDK - -The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers. - -## Searching the Documentation - -This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth. - -- Use broad, simple queries that match the documentation section headings below. -- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`. -- Run multiple queries at once — the most relevant results are returned first. - -### Documentation Sections - -Use these section headings as query terms for accurate results: - -- Introduction, Installation, Configuration, Provider Support -- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration -- Images -- Audio (TTS) -- Transcription (STT) -- Embeddings: Querying Embeddings, Caching Embeddings -- Reranking -- Files -- Vector Stores: Adding Files to Stores -- Failover -- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores -- Events - -## Decision Workflow - -Determine the right entry point before writing code: - -Text generation or chat? → Agent class with `Promptable` trait -Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic) -Structured JSON output? → Agent + `HasStructuredOutput` interface -Image generation? → `Image::of()->generate()` -Audio synthesis? → `Audio::of()->generate()` -Transcription? → `Transcription::fromPath()->generate()` -Embeddings? → `Embeddings::for()->generate()` -Reranking? → `Reranking::of()->rerank()` -File storage? → `Document::fromPath()->put()` -Vector stores? → `Stores::create()` - -## Basic Usage Examples - -### Agents - -```php -use Laravel\Ai\Contracts\Agent; -use Laravel\Ai\Enums\Lab; -use Laravel\Ai\Promptable; - -class SalesCoach implements Agent -{ - use Promptable; - - public function instructions(): string - { - return 'You are a sales coach.'; - } -} - -// Prompting -$response = (new SalesCoach)->prompt('Analyze this transcript...'); -echo $response->text; - -// Container resolution with dependency injection -$agent = SalesCoach::make(user: $user); - -// Override provider, model, or timeout per-prompt -$response = (new SalesCoach)->prompt( - 'Analyze this transcript...', - provider: Lab::Anthropic, - model: 'claude-haiku-4-5-20251001', - timeout: 120, -); - -// Streaming (returns SSE response from a route) -return (new SalesCoach)->stream('Analyze this transcript...'); - -// Queueing -(new SalesCoach)->queue('Analyze this transcript...') - ->then(fn ($response) => /* ... */); - -// Anonymous agents -use function Laravel\Ai\{agent}; - -$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello'); -``` - -### Conversation Context - -Manual conversation history via the `Conversational` interface: - -```php -use Laravel\Ai\Contracts\Agent; -use Laravel\Ai\Contracts\Conversational; -use Laravel\Ai\Messages\Message; -use Laravel\Ai\Promptable; - -class SalesCoach implements Agent, Conversational -{ - use Promptable; - - public function __construct(public User $user) {} - - public function instructions(): string { return 'You are a sales coach.'; } - - public function messages(): iterable - { - return History::where('user_id', $this->user->id) - ->latest()->limit(50)->get()->reverse() - ->map(fn ($m) => new Message($m->role, $m->content)) - ->all(); - } -} -``` - -Automatic conversation persistence via the `RemembersConversations` trait: - -```php -use Laravel\Ai\Concerns\RemembersConversations; -use Laravel\Ai\Contracts\Agent; -use Laravel\Ai\Contracts\Conversational; -use Laravel\Ai\Promptable; - -class SalesCoach implements Agent, Conversational -{ - use Promptable, RemembersConversations; - - public function instructions(): string { return 'You are a sales coach.'; } -} - -// Start a new conversation -$response = (new SalesCoach)->forUser($user)->prompt('Hello!'); -$conversationId = $response->conversationId; - -// Continue an existing conversation -$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.'); -``` - -### Structured Output - -```php -use Illuminate\Contracts\JsonSchema\JsonSchema; -use Laravel\Ai\Contracts\Agent; -use Laravel\Ai\Contracts\HasStructuredOutput; -use Laravel\Ai\Promptable; - -class Reviewer implements Agent, HasStructuredOutput -{ - use Promptable; - - public function instructions(): string { return 'Review and score content.'; } - - public function schema(JsonSchema $schema): array - { - return [ - 'feedback' => $schema->string()->required(), - 'score' => $schema->integer()->min(1)->max(10)->required(), - ]; - } -} - -$response = (new Reviewer)->prompt('Review this...'); -echo $response['score']; // Access like an array -``` - -### Images - -```php -use Laravel\Ai\Image; - -$image = Image::of('A sunset over mountains') - ->landscape() - ->quality('high') - ->generate(); - -$path = $image->store(); // Store to default disk -``` - -### Audio - -```php -use Laravel\Ai\Audio; - -$audio = Audio::of('Hello from Laravel.') - ->female() - ->instructions('Speak warmly') - ->generate(); - -$path = $audio->store(); -``` - -### Transcription - -```php -use Laravel\Ai\Transcription; - -$transcript = Transcription::fromStorage('audio.mp3') - ->diarize() - ->generate(); - -echo (string) $transcript; -``` - -### Embeddings - -```php -use Laravel\Ai\Embeddings; -use Illuminate\Support\Str; - -$response = Embeddings::for(['Text one', 'Text two']) - ->dimensions(1536) - ->cache() - ->generate(); - -// Single string via Stringable -$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); -``` - -### Reranking - -```php -use Laravel\Ai\Reranking; - -$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.']) - ->limit(5) - ->rerank('PHP frameworks'); - -$response->first()->document; // "Laravel is PHP." -``` - -### Files and Vector Stores - -```php -use Laravel\Ai\Files\Document; -use Laravel\Ai\Stores; - -// Store a file with the provider -$file = Document::fromPath('/path/to/doc.pdf')->put(); - -// Create a vector store and add files -$store = Stores::create('Knowledge Base'); -$store->add($file->id); -$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step -``` - -## Agent Configuration - -### PHP Attributes - -```php -use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout}; -use Laravel\Ai\Enums\Lab; - -#[Provider(Lab::Anthropic)] -#[Model('claude-haiku-4-5-20251001')] -#[MaxSteps(10)] -#[MaxTokens(4096)] -#[Temperature(0.7)] -#[Timeout(120)] -class MyAgent implements Agent -{ - use Promptable; - // ... -} -``` - -The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. - -The `#[WithoutBroadcasting]` attribute stops the given stream event types from broadcasting (e.g. data-heavy `ToolResult` payloads that exceed the WebSocket frame limit). The events are still streamed and persisted; they just never hit the channel: - -```php -use Laravel\Ai\Attributes\WithoutBroadcasting; -use Laravel\Ai\Streaming\Events\{ToolCall, ToolResult}; - -#[WithoutBroadcasting(ToolResult::class, ToolCall::class)] -class SearchAgent implements Agent, HasTools -{ - use Promptable; - // ... -} -``` - -### Tools - -Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: - -```php -use Laravel\Ai\Contracts\HasTools; - -class MyAgent implements Agent, HasTools -{ - use Promptable; - - public function tools(): iterable - { - return [new MyCustomTool]; - } -} -``` - -### Provider Tools - -```php -use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch}; - -public function tools(): iterable -{ - return [ - (new WebSearch)->max(5)->allow(['laravel.com']), - new WebFetch, - new FileSearch(stores: ['store_id']), - ]; -} -``` - -### Conversation Memory - -```php -use Laravel\Ai\Concerns\RemembersConversations; -use Laravel\Ai\Contracts\Conversational; - -class ChatBot implements Agent, Conversational -{ - use Promptable, RemembersConversations; - // ... -} - -$response = (new ChatBot)->forUser($user)->prompt('Hello!'); -$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...'); -``` - -### Failover - -```php -$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]); -``` - -## Testing and Faking - -Each capability supports `fake()` with assertions: - -```php -use App\Ai\Agents\SalesCoach; -use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores}; - -// Agents -SalesCoach::fake(['Response 1', 'Response 2']); -SalesCoach::assertPrompted('query'); -SalesCoach::assertNotPrompted('query'); -SalesCoach::assertNeverPrompted(); -SalesCoach::fake()->preventStrayPrompts(); - -// Images -Image::fake(); -Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset')); -Image::assertNothingGenerated(); - -// Audio -Audio::fake(); -Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello')); - -// Transcription -Transcription::fake(['Transcribed text.']); -Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized()); - -// Embeddings -Embeddings::fake(); -Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel')); - -// Reranking -Reranking::fake(); -Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP')); - -// Files -Files::fake(); -Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain'); - -// Stores -Stores::fake(); -Stores::assertCreated('Knowledge Base'); -$store = Stores::get('id'); -$store->assertAdded('file_id'); -``` - -## Key Patterns - -- Namespace: `Laravel\Ai\` -- Package: `composer require laravel/ai` -- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait -- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` -- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` -- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings) -- Artisan commands: `php artisan make:agent`, `php artisan make:tool` -- Global helper: `agent()` for anonymous agents - -## OpenAI-Compatible Provider - -Point the SDK at any OpenAI-compatible endpoint (LM Studio, vLLM, Together, etc.) with the config-driven `openai-compatible` driver. Define named instances in `config/ai.php`, no code required: - -```php -'my-llm' => [ - 'driver' => 'openai-compatible', - 'url' => env('MY_LLM_URL'), // required - 'key' => env('MY_LLM_API_KEY'), // optional Bearer token - 'models' => [ - 'text' => ['default' => 'some-chat-model'], - 'embeddings' => [ - 'default' => 'some-embedding-model', - 'dimensions' => 1024, // optional; omit to use native dimensions - ], - ], -], -``` - -Reference it by config key (or `Lab::OpenAiCompatible`). A model is required via the corresponding `models` configuration or per-call `model:`: - -```php -agent()->prompt('Hello', provider: 'my-llm', model: 'some-model'); - -Embeddings::for(['Hello'])->generate( - provider: 'my-llm', - model: 'some-embedding-model', -); -``` - -It uses OpenAI-standard shapes and supports text, streaming, tools, structured output, image attachments, and text embeddings. Embedding dimensions are optional; omit them to use the model's native dimensions. For extra request-body fields, implement `HasProviderOptions` — the returned array is merged into the body. - -## Common Pitfalls - -### Wrong Namespace - -The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`. - -```php -// Correct -use Laravel\Ai\Image; -use Laravel\Ai\Contracts\Agent; -use Laravel\Ai\Promptable; - -// Wrong — these do not exist -use Illuminate\Ai\Image; -use Laravel\AI\Agent; -``` - -### Unsupported Provider Capability - -Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. - -## Provider Support - -| Feature | Providers | -| ---------- | --------------------------------------------------------------- | -| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, OpenAI-compatible | -| Images | OpenAI, Gemini, xAI | -| TTS | OpenAI, ElevenLabs | -| STT | OpenAI, ElevenLabs, Mistral | -| Embeddings | OpenAI, OpenAI-compatible, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI | -| Reranking | Cohere, Jina | -| Files | OpenAI, Anthropic, Gemini | - -Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings: - -```php -use Laravel\Ai\Enums\Lab; - -Lab::Anthropic; -Lab::OpenAI; -Lab::Gemini; -Lab::OpenAiCompatible; // configurable OpenAI-compatible endpoint -// ... -``` diff --git a/.agents/skills/cashier-stripe-development/SKILL.md b/.agents/skills/cashier-stripe-development/SKILL.md deleted file mode 100644 index 09284646..00000000 --- a/.agents/skills/cashier-stripe-development/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: cashier-stripe-development -description: "Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues." -license: MIT -metadata: - author: laravel ---- - -# Cashier Stripe Development - -## Documentation - -Use `search-docs` for detailed Cashier patterns and documentation covering subscriptions, webhooks, Stripe Checkout, invoices, payment methods, and testing. - -For deeper guidance on specific topics, read the relevant reference file before implementing: - -- `references/subscriptions.md` covers subscription creation, status checks, swapping, trials, quantities, and multiple products -- `references/webhooks.md` covers webhook setup, custom handlers, CSRF exclusion, and local development with the Stripe CLI -- `references/testing.md` covers Stripe test cards, payment method tokens, and feature test patterns - -## Basic Usage - -### Installation - -```bash -php artisan vendor:publish --tag="cashier-migrations" -php artisan migrate -php artisan vendor:publish --tag="cashier-config" -``` - -### Environment Variables - -``` -STRIPE_KEY=pk_test_... -STRIPE_SECRET=sk_test_... -STRIPE_WEBHOOK_SECRET=whsec_... -CASHIER_CURRENCY=usd -CASHIER_CURRENCY_LOCALE=en_US -``` - -### Billable Model - - -```php -use Laravel\Cashier\Billable; - -class User extends Authenticatable -{ - use Billable; -} -``` - -For a non-User model, register it in a service provider: - - -```php -// In AppServiceProvider::boot() -Cashier::useCustomerModel(Team::class); -``` - -### Creating a Subscription - - -```php -use Laravel\Cashier\Exceptions\IncompletePayment; - -try { - $user->newSubscription('default', 'price_xxxx')->create($paymentMethodId); -} catch (IncompletePayment $e) { - return redirect()->route('cashier.payment', [$e->payment->id, 'redirect' => route('home')]); -} -``` - -Always wrap subscription creation in a try/catch for `IncompletePayment`. When a card requires 3DS authentication, Cashier throws this exception. The `cashier.payment` route is auto-registered and handles the confirmation flow. - -## Verification - -1. Run migrations and confirm `stripe_id`, `pm_type`, `pm_last_four`, and `trial_ends_at` columns exist on the billable model table -2. Test the webhook endpoint with `stripe listen --forward-to localhost/stripe/webhook` if you use the default path, or swap `stripe` for your configured `CASHIER_PATH` -3. Confirm `$user->subscribed('default')` returns the expected value for active and incomplete subscriptions - -## Common Pitfalls - -- The migration publish tag is `cashier-migrations`, not `cashier`. Running `migrate` before publishing results in missing columns and tables. -- `CASHIER_CURRENCY` must be set explicitly. It defaults to USD, which silently breaks non-US apps. -- The Stripe CLI generates its own webhook signing secret. It is different from the Dashboard endpoint secret. Using the wrong one causes signature verification failures. -- The webhook route must be excluded from CSRF verification using your configured `cashier.path`. If you change `CASHIER_PATH` from `stripe` to `billing`, exclude `billing/*`, not `stripe/*`. -- `canceled()` returns true as soon as `cancel()` is called, but the user still has access during the grace period. Use `ended()` to confirm access is fully revoked. -- `subscribed()` returns true during the grace period even though the subscription is canceled. -- `subscribed()` returns false for `incomplete` and `past_due` subscriptions by default. -- Prices cannot be swapped and quantity cannot be updated while a subscription has an incomplete payment. -- When extending `WebhookController`, call `Cashier::ignoreRoutes()` in a service provider and re-register both `cashier.payment` and `cashier.webhook` under the configured `cashier.path`. -- Use `Cashier::useCustomerModel()` in a service provider to set a custom billable model. There is no `CASHIER_MODEL` env var. -- `trial_ends_at` is a local database column synced via webhooks. It will be stale if webhooks are not configured in production. -- In MySQL, the `stripe_id` column must use `utf8_bin` collation to avoid case-sensitivity issues. -- `noProrate()` has no effect when combined with `swapAndInvoice()`. That method always prorates. -- Methods like `withPromotionCode()` require the Stripe API ID such as `promo_xxxx`, not the customer-facing code. Use `findPromotionCode()` to resolve a code to its ID. -- Always use `search-docs` for the latest Cashier documentation rather than relying on this skill alone. diff --git a/.agents/skills/cashier-stripe-development/references/subscriptions.md b/.agents/skills/cashier-stripe-development/references/subscriptions.md deleted file mode 100644 index 980c0642..00000000 --- a/.agents/skills/cashier-stripe-development/references/subscriptions.md +++ /dev/null @@ -1,108 +0,0 @@ -# Subscriptions Reference - -Use `search-docs` for authoritative documentation on subscriptions. - -## Status Checks - -| Method | Returns true when | -|---|---| -| `$user->subscribed('default')` | Active or on grace period | -| `->onTrial()` | Trial period active | -| `->onGracePeriod()` | Canceled, period not yet ended | -| `->canceled()` | `ends_at` is set, may still have access | -| `->ended()` | Canceled and grace period expired | -| `->incomplete()` | Awaiting SCA/3DS confirmation | -| `->pastDue()` | Payment overdue | -| `->recurring()` | Active and not on trial | - -Check by product or price: - -```php -$user->subscribedToProduct('prod_premium', 'default'); -$user->subscribedToPrice('price_monthly', 'default'); -``` - -## Swapping Plans - -```php -$user->subscription('default')->swap('price_new'); -$user->subscription('default')->noProrate()->swap('price_new'); -$user->subscription('default')->swapAndInvoice('price_new'); -$user->subscription('default')->skipTrial()->swap('price_new'); -``` - -## Quantity - -```php -$user->subscription('default')->incrementQuantity(); -$user->subscription('default')->decrementQuantity(); -$user->subscription('default')->updateQuantity(10); -$user->subscription('default')->noProrate()->updateQuantity(10); -``` - -## Trials - -```php -$user->newSubscription('default', 'price_xxxx') - ->trialDays(14) - ->create($paymentMethodId); - -$subscription->extendTrial(now()->addDays(7)); -``` - -## Multiple Products on One Subscription - -```php -$user->newSubscription('default', ['price_monthly', 'price_chat']) - ->quantity(5, 'price_chat') - ->create($paymentMethod); - -$user->subscription('default')->addPrice('price_chat'); -$user->subscription('default')->removePrice('price_chat'); -$user->subscription('default')->swap(['price_pro', 'price_chat']); -``` - -## Multiple Subscriptions - -```php -$user->newSubscription('swimming', 'price_swimming_monthly')->create($pm); -$user->newSubscription('gym', 'price_gym_monthly')->create($pm); - -$user->subscription('swimming')->swap('price_swimming_yearly'); -$user->subscription('gym')->cancel(); -``` - -## Cancellation and Resumption - -```php -$user->subscription('default')->cancel(); // At end of billing period -$user->subscription('default')->cancelNow(); // Immediately -$user->subscription('default')->resume(); // During grace period only -``` - -## Incomplete Payment Handling - -```php -if ($user->hasIncompletePayment('default')) { - $paymentId = $user->subscription('default')->latestPayment()->id; - return redirect()->route('cashier.payment', $paymentId); -} -``` - -Opt out of default deactivation behavior: - -```php -Cashier::keepPastDueSubscriptionsActive(); -Cashier::keepIncompleteSubscriptionsActive(); -``` - -## Metered / Usage-Based Billing - -```php -$user->newSubscription('default') - ->meteredPrice('price_metered') - ->create($paymentMethodId); - -$user->reportMeterEvent('emails-sent'); -$user->reportMeterEvent('emails-sent', quantity: 15); -``` diff --git a/.agents/skills/cashier-stripe-development/references/testing.md b/.agents/skills/cashier-stripe-development/references/testing.md deleted file mode 100644 index 2d6b04cf..00000000 --- a/.agents/skills/cashier-stripe-development/references/testing.md +++ /dev/null @@ -1,52 +0,0 @@ -# Testing Reference - -Use `search-docs` for authoritative documentation on testing Cashier integrations. - -## Test Cards and Tokens - -Use card numbers for browser-based flows (Stripe.js / Checkout). Use `pm_card_*` tokens directly in feature tests that call the Stripe API. - -| Card Number | Token | Behavior | -|---|---|---| -| `4242 4242 4242 4242` | `pm_card_visa` | Succeeds immediately | -| `4000 0025 0000 3155` | `pm_card_threeDSecure2Required` | Requires SCA/3DS | -| `4000 0027 6000 3184` | `pm_card_authenticationRequired` | Requires authentication | -| `4000 0000 0000 9995` | `pm_card_chargeDeclinedInsufficientFunds` | Declined, insufficient funds | -| `4000 0000 0000 0002` | `pm_card_chargeDeclined` | Declined | - -Use expiry `12/34`, any CVC, any ZIP for card number inputs. - -## Feature Test Example - -Feature tests that hit the real Stripe test API use `pm_card_*` tokens: - -```php -public function test_user_can_subscribe(): void -{ - $user = User::factory()->create(); - - $user->newSubscription('default', 'price_xxxx') - ->create('pm_card_visa'); - - $this->assertTrue($user->subscribed('default')); -} - -public function test_incomplete_payment_is_handled(): void -{ - $user = User::factory()->create(); - - try { - $user->newSubscription('default', 'price_xxxx') - ->create('pm_card_threeDSecure2Required'); - } catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) { - $this->assertTrue($user->subscription('default')->incomplete()); - } -} -``` - -## Setup Notes - -- Use Stripe test mode keys (`sk_test_...`, `pk_test_...`) in your test environment -- Cashier does not ship a global `fake()` helper. Tests hit the real Stripe test API by default. -- Refer to `tests/Feature/` in the Cashier package itself for integration test patterns covering subscription creation, payment methods, and webhook handling -- Use `search-docs` for current guidance on mocking Stripe HTTP calls or using Stripe's test clock feature for time-sensitive scenarios diff --git a/.agents/skills/cashier-stripe-development/references/webhooks.md b/.agents/skills/cashier-stripe-development/references/webhooks.md deleted file mode 100644 index 5ebf153d..00000000 --- a/.agents/skills/cashier-stripe-development/references/webhooks.md +++ /dev/null @@ -1,132 +0,0 @@ -# Webhooks Reference - -Use `search-docs` for authoritative documentation on webhooks. - -## Auto-Registered Routes - -Cashier registers two routes automatically under the `cashier.path` prefix (`config('cashier.path')`, default `stripe`): - -- `POST /{cashier.path}/webhook` named `cashier.webhook` -- `GET /{cashier.path}/payment/{id}` named `cashier.payment` - -With the default config these are `/stripe/webhook` and `/stripe/payment/{id}`. If you set `CASHIER_PATH=billing`, they become `/billing/webhook` and `/billing/payment/{id}`. - -## CSRF Exclusion - -Use the same path prefix you configured for Cashier here. If `CASHIER_PATH=billing`, exclude `billing/*` instead of `stripe/*`. - -**Laravel 11+ (`bootstrap/app.php`, default path example):** - -```php -->withMiddleware(function (Middleware $middleware) { - $middleware->validateCsrfTokens(except: ['stripe/*']); -}) -``` - -**Laravel 10 (`app/Http/Middleware/VerifyCsrfToken.php`, default path example):** - -```php -protected $except = [ - 'stripe/*', -]; -``` - -## Local Development with Stripe CLI - -If you changed `cashier.path`, forward Stripe CLI events to that URL instead of `/stripe/webhook`. - -```bash -stripe login -stripe listen --forward-to your-app.test/stripe/webhook -stripe trigger invoice.payment_succeeded -``` - -The CLI outputs a `whsec_...` signing secret specific to that session. Set it as `STRIPE_WEBHOOK_SECRET` locally. It is not the same as the Dashboard endpoint secret. - -## Registering Events in the Stripe Dashboard - -Use the Artisan command to create the endpoint automatically with all required events: - -```bash -php artisan cashier:webhook -``` - -Cashier's `cashier:webhook` command registers these events by default: - -- `customer.subscription.created` -- `customer.subscription.updated` -- `customer.subscription.deleted` -- `customer.updated` / `customer.deleted` -- `invoice.payment_action_required` -- `invoice.payment_succeeded` -- `payment_method.automatically_updated` - -Cashier's `WebhookController` has built-in handlers for all of the above except `invoice.payment_succeeded`. For renewal hooks, prefer `WebhookReceived` / `WebhookHandled` listeners unless you intentionally add your own controller method. - -## Custom Handlers: Extending WebhookController - -Method name pattern: `handle` + StudlyCase of event type with dots replaced by underscores. - -`customer.subscription.created` becomes `handleCustomerSubscriptionCreated`. - -```php -use Laravel\Cashier\Http\Controllers\WebhookController as CashierController; - -class StripeWebhookController extends CashierController -{ - public function handleCustomerSubscriptionCreated(array $payload) - { - $response = parent::handleCustomerSubscriptionCreated($payload); - - // your logic after Cashier syncs the subscription - - return $response; - } -} -``` - -If you add a method for an event Cashier does not handle internally, such as `invoice.payment_succeeded`, do not call `parent::handle...()` unless the base controller actually defines that method. - -In a service provider, disable auto-registration and re-register both Cashier routes so the incomplete-payment flow and `cashier:webhook` command keep working: - -```php -Cashier::ignoreRoutes(); -``` - -```php -// routes/web.php -use App\Http\Controllers\StripeWebhookController; -use Illuminate\Support\Facades\Route; -use Laravel\Cashier\Http\Controllers\PaymentController; - -Route::prefix(config('cashier.path')) - ->name('cashier.') - ->group(function () { - Route::get('payment/{id}', [PaymentController::class, 'show'])->name('payment'); - Route::post('webhook', [StripeWebhookController::class, 'handleWebhook'])->name('webhook'); - }); -``` - -Keep the `cashier.webhook` route name unless you plan to pass `--url` explicitly to `php artisan cashier:webhook`. - -## Custom Handlers: Listening to Events - -The simpler option when you do not need to replace Cashier's internal logic, or when you want to react to events such as `invoice.payment_succeeded` that Cashier does not process itself: - -```php -use Laravel\Cashier\Events\WebhookReceived; -use Laravel\Cashier\Events\WebhookHandled; - -// WebhookReceived fires for every event before Cashier processes it -// WebhookHandled fires after Cashier processes it - -Event::listen(WebhookReceived::class, function (WebhookReceived $event) { - if ($event->payload['type'] === 'invoice.payment_succeeded') { - // handle renewal - } -}); -``` - -## Signature Verification - -`VerifyWebhookSignature` middleware is applied automatically when `cashier.webhook.secret` is set. No extra wiring is needed. diff --git a/.agents/skills/configure-nightwatch/SKILL.md b/.agents/skills/configure-nightwatch/SKILL.md deleted file mode 100644 index 1fc580bb..00000000 --- a/.agents/skills/configure-nightwatch/SKILL.md +++ /dev/null @@ -1,404 +0,0 @@ ---- -name: configure-nightwatch -description: Configures Laravel Nightwatch data collection, sampling rates, filtering rules, and redaction policies. Use when setting up Nightwatch, managing data volume, protecting sensitive data (PII), or optimizing event collection for production workloads. -license: MIT -metadata: - author: laravel ---- - -# Nightwatch Configuration Guide - -This skill helps configure Laravel Nightwatch data collection to balance observability, performance, and privacy. Covers sampling strategies, filtering rules, and redaction methods across all event types. - -## Documentation Reference - -The [Nightwatch Documentation](https://nightwatch.laravel.com/docs) is the definitive and up-to-date source of information for all Nightwatch configuration options. This skill provides practical guidance and common patterns, but always consult the official documentation as the primary source of truth for specific details, environment variables, and API behavior. The documentation includes comprehensive coverage of: - -- [Filtering and Configuration](https://nightwatch.laravel.com/docs/filtering) - Core concepts for sampling, filtering, and redaction -- Individual event type pages with specific configuration options: - - [Requests](https://nightwatch.laravel.com/docs/requests) - Request sampling, header handling, payload capture - - [Commands](https://nightwatch.laravel.com/docs/commands) - Command sampling and redaction - - [Queries](https://nightwatch.laravel.com/docs/queries) - Query filtering and redaction - - [Cache](https://nightwatch.laravel.com/docs/cache) - Cache event filtering by key or pattern - - [Jobs](https://nightwatch.laravel.com/docs/jobs) - Job filtering and sampling decoupling - - [Mail](https://nightwatch.laravel.com/docs/mail) - Mail event filtering - - [Notifications](https://nightwatch.laravel.com/docs/notifications) - Notification filtering by channel - - [Exceptions](https://nightwatch.laravel.com/docs/exceptions) - Exception sampling and throttling - - [Outgoing Requests](https://nightwatch.laravel.com/docs/outgoing-requests) - HTTP request filtering -- [reference.md](reference.md) - Quick lookup table by event type, production presets, and verification checklist - -## Data Collection Flow - -Nightwatch processes events through three stages: - -1. **Sampling** - Controls which entry points are captured (requests, commands, scheduled tasks) -2. **Filtering** - Excludes specific events after sampling (queries, cache, mail, etc.) -3. **Redaction** - Modifies captured data to remove/obfuscate sensitive information - -``` -Request/Command/Scheduled Task - | - v - [Sampling?] ----NO----> Drop entire trace - | YES - v - Events generated - | - v - [Filtering?] ----YES---> Drop specific event - | NO - v - [Redaction] ----------> Store modified data -``` - ---- - -## Sampling Configuration - -Sampling determines which entry points (requests, commands, scheduled tasks) trigger full trace collection. When an entry point is sampled, all related events are captured. - -### Global Sample Rates - -Configure via environment variables: - -```bash - -# Default: 100% sampling (all requests/commands captured) - -NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 # Recommended: 10% of requests - -NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0 # Capture all commands - -NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # Always capture exceptions - -``` - -**Recommendation**: Start with `0.1` (10%) for requests in production, adjust based on volume and needs. - -### Route-Based Sampling - -Apply different rates to specific routes using the `Sample` middleware: - -```php routes/web.php -use Illuminate\Support\Facades\Route; -use Laravel\Nightwatch\Http\Middleware\Sample; - -// Sample admin routes at 100% -Route::middleware(Sample::rate(1.0))->prefix('admin')->group(function () { - // All admin routes sampled fully -}); - -// Sample API routes at 5% -Route::middleware(Sample::rate(0.05))->prefix('api')->group(function () { - // API routes sampled sparingly -}); - -// Always sample critical endpoints -Route::post('/checkout', [CheckoutController::class, 'process']) - ->middleware(Sample::always()); - -// Never sample health checks -Route::get('/health', [HealthController::class, 'check']) - ->middleware(Sample::never()); -``` - -### Unmatched Route Sampling - -Handle 404/bot traffic with reduced sampling: - -```php routes/web.php -Route::fallback(fn () => abort(404)) - ->middleware(Sample::rate(0.01)); // 1% sampling for unmatched routes -``` - -### Dynamic Sampling - -Sample based on runtime conditions (user role, request attributes): - -```php app/Http/Middleware/SampleAdminRequests.php -use Closure; -use Illuminate\Http\Request; -use Laravel\Nightwatch\Facades\Nightwatch; - -class SampleAdminRequests -{ - public function handle(Request $request, Closure $next) - { - if ($request->user()?->isAdmin()) { - Nightwatch::sample(); // Always sample admin requests - } - return $next($request); - } -} -``` - -### Command Sampling - -Exclude specific commands from sampling: - -```php AppServiceProvider.php -use Illuminate\Console\Events\CommandStarting; -use Illuminate\Support\Facades\Event; -use Laravel\Nightwatch\Facades\Nightwatch; - -public function boot(): void -{ - Event::listen(function (CommandStarting $event) { - if (in_array($event->command, ['schedule:finish', 'horizon:snapshot'])) { - Nightwatch::dontSample(); - } - }); -} -``` - -### Vendor Commands - -Nightwatch automatically ignores framework/internal commands. Opt-in to capture them: - -```php -Nightwatch::captureDefaultVendorCommands(); -``` - ---- - -## Filtering Configuration - -Filtering excludes specific events from collection after sampling. Use filtering to reduce noise and quota usage. - -### Database Queries - -**Filter all queries** (disable query collection): - -```bash -NIGHTWATCH_IGNORE_QUERIES=true -``` - -**Filter specific queries** by SQL pattern: - -```php AppServiceProvider.php -use Laravel\Nightwatch\Facades\Nightwatch; -use Laravel\Nightwatch\Records\Query; - -public function boot(): void -{ - // Filter job table queries (PostgreSQL) - Nightwatch::rejectQueries(function (Query $query) { - return str_contains($query->sql, 'into "jobs"'); - }); - - // Filter cache table queries (MySQL) - Nightwatch::rejectQueries(function (Query $query) { - return str_contains($query->sql, 'from `cache`') - || str_contains($query->sql, 'into `cache`'); - }); -} -``` - -### Cache Events - -**Filter all cache events**: - -```bash -NIGHTWATCH_IGNORE_CACHE_EVENTS=true -``` - -**Filter by cache key patterns**: - -```php -Nightwatch::rejectCacheKeys([ - 'my-app:users', // Exact match - '/^my-app:posts:/', // Regex: starts with my-app:posts: - '/^[a-zA-Z0-9]{40}$/', // Regex: session IDs -]); -``` - -**Filter with callback**: - -```php -use Laravel\Nightwatch\Records\CacheEvent; - -Nightwatch::rejectCacheEvents(function (CacheEvent $cacheEvent) { - return str_starts_with($cacheEvent->key, 'temp:'); -}); -``` - -### Mail Events - -**Filter all mail**: - -```bash -NIGHTWATCH_IGNORE_MAIL=true -``` - -**Filter specific mail**: - -```php -use Laravel\Nightwatch\Records\Mail; - -Nightwatch::rejectMail(function (Mail $mail) { - return str_contains($mail->subject, 'Newsletter'); -}); -``` - -### Notification Events - -**Filter all notifications**: - -```bash -NIGHTWATCH_IGNORE_NOTIFICATIONS=true -``` - -**Filter by channel**: - -```php -use Laravel\Nightwatch\Records\Notification; - -Nightwatch::rejectNotifications(function (Notification $notification) { - return $notification->channel === 'database'; -}); -``` - -### Outgoing HTTP Requests - -**Filter all outgoing requests**: - -```bash -NIGHTWATCH_IGNORE_OUTGOING_REQUESTS=true -``` - -**Filter by URL**: - -```php -use Laravel\Nightwatch\Records\OutgoingRequest; - -Nightwatch::rejectOutgoingRequests(function (OutgoingRequest $request) { - return str_contains($request->url, 'analytics.example.com'); -}); -``` - -### Queued Jobs - -**Filter specific jobs**: - -```php -use Laravel\Nightwatch\Records\QueuedJob; - -Nightwatch::rejectQueuedJobs(function (QueuedJob $job) { - return $job->name === 'App\Jobs\LowPriorityJob'; -}); -``` - -### Decoupling Job Sampling - -Sample jobs independently from parent contexts: - -```php -use Illuminate\Support\Facades\Queue; - -public function boot(): void -{ - Queue::before(fn () => Nightwatch::sample(rate: 0.5)); -} -``` - ---- - -## Redaction Configuration - -Redaction modifies captured data to remove or obfuscate sensitive information. Unlike filtering, redaction keeps the event but sanitizes its content. - -### Request Redaction - -**Redact sensitive headers** (automatically redacts: Authorization, Cookie, X-XSRF-TOKEN): - -```bash - -# Customize redacted headers - -NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-API-Key -``` - -**Redact request payloads** (disabled by default): - -```bash - -# Enable payload capture - -NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=true - -# Customize redacted fields - -NIGHTWATCH_REDACT_PAYLOAD_FIELDS=password,password_confirmation,ssn,credit_card -``` - -**Programmatic redaction**: - -```php -use Laravel\Nightwatch\Facades\Nightwatch; -use Laravel\Nightwatch\Records\Request; - -Nightwatch::redactRequests(function (Request $request) { - $request->url = str_replace('secret', '***', $request->url); - $request->ip = preg_replace('/\d+$/', '***', $request->ip); -}); -``` - -### Query Redaction - -```php -use Laravel\Nightwatch\Records\Query; - -Nightwatch::redactQueries(function (Query $query) { - $query->sql = str_replace('secret_token', '***', $query->sql); -}); -``` - -### Cache Redaction - -```php -use Laravel\Nightwatch\Records\CacheEvent; - -Nightwatch::redactCacheEvents(function (CacheEvent $cacheEvent) { - $cacheEvent->key = str_replace('user:', 'user:***:', $cacheEvent->key); -}); -``` - -### Command Redaction - -```php -use Laravel\Nightwatch\Records\Command; - -Nightwatch::redactCommands(function (Command $command) { - $command->command = preg_replace('/--password=\S+/', '--password=***', $command->command); -}); -``` - -### Exception Redaction - -```php -use Laravel\Nightwatch\Records\Exception; - -Nightwatch::redactExceptions(function (Exception $exception) { - $exception->message = str_replace('secret', '***', $exception->message); -}); -``` - -### Mail Redaction - -```php -use Laravel\Nightwatch\Records\Mail; - -Nightwatch::redactMail(function (Mail $mail) { - $mail->subject = str_replace('Invoice #', 'Invoice ***', $mail->subject); -}); -``` - -### Outgoing Request Redaction - -```php -use Laravel\Nightwatch\Records\OutgoingRequest; - -Nightwatch::redactOutgoingRequests(function (OutgoingRequest $outgoingRequest) { - $outgoingRequest->url = preg_replace('/api_key=\w+/', 'api_key=***', $outgoingRequest->url); -}); -``` diff --git a/.agents/skills/configure-nightwatch/reference.md b/.agents/skills/configure-nightwatch/reference.md deleted file mode 100644 index b071d2f2..00000000 --- a/.agents/skills/configure-nightwatch/reference.md +++ /dev/null @@ -1,108 +0,0 @@ -# Nightwatch Configuration Reference - -## Configuration Summary by Event Type - -| Event Type | Sampling | Filtering | Redaction | -| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------- | -| **Requests** | `NIGHTWATCH_REQUEST_SAMPLE_RATE`, Route middleware | Not applicable | Headers, payload, URL, IP | -| **Commands** | `NIGHTWATCH_COMMAND_SAMPLE_RATE`, Event listener | Not applicable | Command arguments | -| **Queries** | Parent context | `rejectQueries()`, `NIGHTWATCH_IGNORE_QUERIES` | SQL statement | -| **Cache** | Parent context | `rejectCacheKeys()`, `rejectCacheEvents()`, `NIGHTWATCH_IGNORE_CACHE_EVENTS` | Cache key | -| **Jobs** | Parent context, Queue::before | `rejectQueuedJobs()` | Not applicable | -| **Mail** | Parent context | `rejectMail()`, `NIGHTWATCH_IGNORE_MAIL` | Subject | -| **Notifications** | Parent context | `rejectNotifications()`, `NIGHTWATCH_IGNORE_NOTIFICATIONS` | Not applicable | -| **Outgoing Requests** | Parent context | `rejectOutgoingRequests()`, `NIGHTWATCH_IGNORE_OUTGOING_REQUESTS` | URL | -| **Exceptions** | `NIGHTWATCH_EXCEPTION_SAMPLE_RATE` | Not applicable | Exception message | - ---- - -## Production Recommendations - -### High-Traffic Applications - -```bash - -# Conservative sampling - -NIGHTWATCH_REQUEST_SAMPLE_RATE=0.01 # 1% of requests - -NIGHTWATCH_COMMAND_SAMPLE_RATE=0.1 # 10% of commands - -NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # Always capture exceptions - -# Filter noisy events - -NIGHTWATCH_IGNORE_CACHE_EVENTS=true -NIGHTWATCH_IGNORE_QUERIES=true # Or filter specific queries programmatically - -``` - -### Privacy-Conscious Applications - -```bash - -# Disable sensitive data collection - -NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=false -NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN - -# Or use redaction in AppServiceProvider - -``` - -### Balanced Configuration (Recommended Start) - -```bash - -# Sample rates - -NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 -NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0 -NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 - -# Filter obvious noise programmatically - -# Redact PII as needed - -``` - ---- - -## Verification Checklist - -After configuration: - -- [ ] Sampling rates appropriate for traffic volume -- [ ] Noisy events filtered (cache, certain queries) -- [ ] Sensitive data redacted (PII, tokens, credentials) -- [ ] Exceptions always captured for debugging -- [ ] Test in development with `NIGHTWATCH_REQUEST_SAMPLE_RATE=1.0` -- [ ] Monitor event quota usage in Nightwatch dashboard - ---- - -## Common Patterns - -### Filter Health Checks + Reduce Sampling - -```php -Route::get('/health', fn() => ['status' => 'ok']) - ->middleware(Sample::never()); -``` - -### Exclude Internal/Vendor Queries - -```php -Nightwatch::rejectQueries(fn($q) => - str_contains($q->sql, 'telescope') || - str_contains($q->sql, 'pulse') -); -``` - -### Protect User Data in Cache Keys - -```php -Nightwatch::redactCacheEvents(fn($e) => - $e->key = preg_replace('/user:\d+/', 'user:***', $e->key) -); -``` diff --git a/.agents/skills/configuring-horizon/SKILL.md b/.agents/skills/configuring-horizon/SKILL.md deleted file mode 100644 index 68477acd..00000000 --- a/.agents/skills/configuring-horizon/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: configuring-horizon -description: "Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching." -license: MIT -metadata: - author: laravel ---- - -# Horizon Configuration - -## Documentation - -Use `search-docs` for detailed Horizon patterns and documentation covering configuration, supervisors, balancing, dashboard authorization, tags, notifications, metrics, and deployment. - -For deeper guidance on specific topics, read the relevant reference file before implementing: - -- `references/supervisors.md` covers supervisor blocks, balancing strategies, multi-queue setups, and auto-scaling -- `references/notifications.md` covers LongWaitDetected alerts, notification routing, and the `waits` config -- `references/tags.md` covers job tagging, dashboard filtering, and silencing noisy jobs -- `references/metrics.md` covers the blank metrics dashboard, snapshot scheduling, and retention config - -## Basic Usage - -### Installation - -```bash -php artisan horizon:install -``` - -### Supervisor Configuration - -Define supervisors in `config/horizon.php`. The `environments` array merges into `defaults` and does not replace the whole supervisor block: - - -```php -'defaults' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['default'], - 'balance' => 'auto', - 'minProcesses' => 1, - 'maxProcesses' => 10, - 'tries' => 3, - ], -], - -'environments' => [ - 'production' => [ - 'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3], - ], - 'local' => [ - 'supervisor-1' => ['maxProcesses' => 2], - ], -], -``` - -### Dashboard Authorization - -Restrict access in `App\Providers\HorizonServiceProvider`: - - -```php -protected function gate(): void -{ - Gate::define('viewHorizon', function (User $user) { - return $user->is_admin; - }); -} -``` - -## Verification - -1. Run `php artisan horizon` and visit `/horizon` -2. Confirm dashboard access is restricted as expected -3. Check that metrics populate after scheduling `horizon:snapshot` - -## Common Pitfalls - -- Horizon only works with the Redis queue driver. Other drivers such as database and SQS are not supported. -- Redis Cluster is not supported. Horizon requires a standalone Redis connection. -- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration. -- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it. -- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out. -- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics. -- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone. diff --git a/.agents/skills/configuring-horizon/references/metrics.md b/.agents/skills/configuring-horizon/references/metrics.md deleted file mode 100644 index 7e1aea6b..00000000 --- a/.agents/skills/configuring-horizon/references/metrics.md +++ /dev/null @@ -1,21 +0,0 @@ -# Metrics & Snapshots - -## Where to Find It - -Search with `search-docs`: -- `"horizon metrics snapshot"` for the snapshot command and scheduling -- `"horizon trim snapshots"` for retention configuration - -## What to Watch For - -### Metrics dashboard stays blank until `horizon:snapshot` is scheduled - -Running `horizon` artisan command does not populate metrics automatically. The metrics graph is built from snapshots, so `horizon:snapshot` must be scheduled to run every 5 minutes via Laravel's scheduler. - -### Register the snapshot in the scheduler rather than running it manually - -A single manual run populates the dashboard momentarily but will not keep it updated. Search `"horizon metrics snapshot"` for the exact scheduler registration syntax, which differs between Laravel 10 and 11+. - -### `metrics.trim_snapshots` is a snapshot count, not a time duration - -The `trim_snapshots.job` and `trim_snapshots.queue` values in `config/horizon.php` are counts of snapshots to keep, not minutes or hours. With the default of 24 snapshots at 5-minute intervals, that provides 2 hours of history. Increase the value to retain more history at the cost of Redis memory usage. diff --git a/.agents/skills/configuring-horizon/references/notifications.md b/.agents/skills/configuring-horizon/references/notifications.md deleted file mode 100644 index d6d3feed..00000000 --- a/.agents/skills/configuring-horizon/references/notifications.md +++ /dev/null @@ -1,21 +0,0 @@ -# Notifications & Alerts - -## Where to Find It - -Search with `search-docs`: -- `"horizon notifications"` for Horizon's built-in notification routing helpers -- `"horizon long wait detected"` for LongWaitDetected event details - -## What to Watch For - -### `waits` in `config/horizon.php` controls the LongWaitDetected threshold - -The `waits` array (e.g., `'redis:default' => 60`) defines how many seconds a job can wait in a queue before Horizon fires a `LongWaitDetected` event. This value is set in the config file, not in Horizon's notification routing. If alerts are firing too often or too late, adjust `waits` rather than the routing configuration. - -### Use Horizon's built-in notification routing in `HorizonServiceProvider` - -Configure notifications in the `boot()` method of `App\Providers\HorizonServiceProvider` using `Horizon::routeMailNotificationsTo()`, `Horizon::routeSlackNotificationsTo()`, or `Horizon::routeSmsNotificationsTo()`. Horizon already wires `LongWaitDetected` to its notification sender, so the documented setup is notification routing rather than manual listener registration. - -### Failed job alerts are separate from Horizon's documented notification routing - -Horizon's 12.x documentation covers built-in long-wait notifications. Do not assume the docs provide a `JobFailed` listener example in `HorizonServiceProvider`. If a user needs failed job alerts, treat that as custom queue event handling and consult the queue documentation instead of Horizon's notification-routing API. diff --git a/.agents/skills/configuring-horizon/references/supervisors.md b/.agents/skills/configuring-horizon/references/supervisors.md deleted file mode 100644 index b71285cf..00000000 --- a/.agents/skills/configuring-horizon/references/supervisors.md +++ /dev/null @@ -1,27 +0,0 @@ -# Supervisor & Balancing Configuration - -## Where to Find It - -Search with `search-docs` before writing any supervisor config, as option names and defaults change between Horizon versions: -- `"horizon supervisor configuration"` for the full options list -- `"horizon balancing strategies"` for auto, simple, and false modes -- `"horizon autoscaling workers"` for autoScalingStrategy details -- `"horizon environment configuration"` for the defaults and environments merge - -## What to Watch For - -### The `environments` array merges into `defaults` rather than replacing it - -The `defaults` array defines the complete base supervisor config. The `environments` array patches it per environment, overriding only the keys listed. There is no need to repeat every key in each environment block. A common pattern is to define `connection`, `queue`, `balance`, `autoScalingStrategy`, `tries`, and `timeout` in `defaults`, then override only `maxProcesses`, `balanceMaxShift`, and `balanceCooldown` in `production`. - -### Use separate named supervisors to enforce queue priority - -Horizon does not enforce queue order when using `balance: auto` on a single supervisor. The `queue` array order is ignored for load balancing. To process `notifications` before `default`, use two separately named supervisors: one for the high-priority queue with a higher `maxProcesses`, and one for the low-priority queue with a lower cap. The docs include an explicit note about this. - -### Use `balance: false` to keep a fixed number of workers on a dedicated queue - -Auto-balancing suits variable load, but if a queue should always have exactly N workers such as a video-processing queue limited to 2, set `balance: false` and `maxProcesses: 2`. Auto-balancing would scale it up during bursts, which may be undesirable. - -### Set `balanceCooldown` to prevent rapid worker scaling under bursty load - -When using `balance: auto`, the supervisor can scale up and down rapidly under bursty load. Set `balanceCooldown` to the number of seconds between scaling decisions, typically 3 to 5, to smooth this out. `balanceMaxShift` limits how many processes are added or removed per cycle. diff --git a/.agents/skills/configuring-horizon/references/tags.md b/.agents/skills/configuring-horizon/references/tags.md deleted file mode 100644 index 8234e4ad..00000000 --- a/.agents/skills/configuring-horizon/references/tags.md +++ /dev/null @@ -1,21 +0,0 @@ -# Tags & Silencing - -## Where to Find It - -Search with `search-docs`: -- `"horizon tags"` for the tagging API and auto-tagging behaviour -- `"horizon silenced jobs"` for the `silenced` and `silenced_tags` config options - -## What to Watch For - -### Eloquent model jobs are tagged automatically without any extra code - -If a job's constructor accepts Eloquent model instances, Horizon automatically tags the job with `ModelClass:id` such as `App\Models\User:42`. These tags are filterable in the dashboard without any changes to the job class. Only add a `tags()` method when custom tags beyond auto-tagging are needed. - -### `silenced` hides jobs from the dashboard completed list but does not stop them from running - -Adding a job class to the `silenced` array in `config/horizon.php` removes it from the completed jobs view. The job still runs normally. This is a dashboard noise-reduction tool, not a way to disable jobs. - -### `silenced_tags` hides all jobs carrying a matching tag from the completed list - -Any job carrying a matching tag string is hidden from the completed jobs view. This is useful for silencing a category of jobs such as all jobs tagged `notifications`, rather than silencing specific classes. diff --git a/.agents/skills/inertia-vue-development/SKILL.md b/.agents/skills/inertia-vue-development/SKILL.md deleted file mode 100644 index 2813e8cd..00000000 --- a/.agents/skills/inertia-vue-development/SKILL.md +++ /dev/null @@ -1,575 +0,0 @@ ---- -name: inertia-vue-development -description: "Develops Inertia.js v3 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,