text:
Script completed
Wall time 0.1 seconds
Output:

text:
specs/02-API-ROUTES.md:3:This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.
specs/02-API-ROUTES.md:71:| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |
specs/02-API-ROUTES.md:78:| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |
specs/02-API-ROUTES.md:171:| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |
specs/02-API-ROUTES.md:192:| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
specs/02-API-ROUTES.md:193:| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |
specs/02-API-ROUTES.md:203:These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).
specs/02-API-ROUTES.md:425:    C->>API: POST /checkouts (cart_id, email)
specs/02-API-ROUTES.md:428:    C->>API: PUT /checkouts/{id}/address
specs/02-API-ROUTES.md:429:    API-->>C: 200 Checkout (status: addressed, available_shipping_methods)
specs/02-API-ROUTES.md:431:    C->>API: PUT /checkouts/{id}/shipping-method
specs/02-API-ROUTES.md:435:        C->>API: POST /checkouts/{id}/apply-discount
specs/02-API-ROUTES.md:439:    C->>API: POST /checkouts/{id}/pay (payment_method + details)
specs/02-API-ROUTES.md:441:    API->>MockPSP: charge(checkout, paymentMethodData)
specs/02-API-ROUTES.md:458:#### POST /api/storefront/v1/checkouts
specs/02-API-ROUTES.md:460:Create a checkout from a cart.
specs/02-API-ROUTES.md:463:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:477:| `email` | string | Yes | Valid email address. |
specs/02-API-ROUTES.md:489:  "shipping_address_json": null,
specs/02-API-ROUTES.md:490:  "billing_address_json": null,
specs/02-API-ROUTES.md:524:#### GET /api/storefront/v1/checkouts/{checkoutId}
specs/02-API-ROUTES.md:526:Retrieve the current checkout state and computed totals.
specs/02-API-ROUTES.md:528:- **Authentication:** None (checkout ID acts as token)
specs/02-API-ROUTES.md:529:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:533:Returns the full checkout object (same schema as POST response, with updated fields reflecting the current state).
specs/02-API-ROUTES.md:541:#### PUT /api/storefront/v1/checkouts/{checkoutId}/address
specs/02-API-ROUTES.md:543:Set the shipping and/or billing address on a checkout.
specs/02-API-ROUTES.md:546:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:552:  "shipping_address": {
specs/02-API-ROUTES.md:555:    "address1": "123 Main St",
specs/02-API-ROUTES.md:556:    "address2": "Apt 4B",
specs/02-API-ROUTES.md:565:  "billing_address": null,
specs/02-API-ROUTES.md:570:**Shipping address fields:**
specs/02-API-ROUTES.md:574:| `shipping_address` | object | Yes (if any line requires shipping) | See fields below |
specs/02-API-ROUTES.md:575:| `shipping_address.first_name` | string | Yes | Max: 255 |
specs/02-API-ROUTES.md:576:| `shipping_address.last_name` | string | Yes | Max: 255 |
specs/02-API-ROUTES.md:577:| `shipping_address.address1` | string | Yes | Max: 500 |
specs/02-API-ROUTES.md:578:| `shipping_address.address2` | string | No | Max: 500 |
specs/02-API-ROUTES.md:579:| `shipping_address.city` | string | Yes | Max: 255 |
specs/02-API-ROUTES.md:580:| `shipping_address.province` | string | No | Max: 255 |
specs/02-API-ROUTES.md:581:| `shipping_address.province_code` | string | No | Max: 10 |
specs/02-API-ROUTES.md:582:| `shipping_address.country` | string | Yes | Max: 255 |
specs/02-API-ROUTES.md:583:| `shipping_address.country_code` | string | Yes | Valid ISO 3166-1 alpha-2 |
specs/02-API-ROUTES.md:584:| `shipping_address.postal_code` | string | Yes | Max: 20 |
specs/02-API-ROUTES.md:585:| `shipping_address.phone` | string | No | Max: 50 |
specs/02-API-ROUTES.md:586:| `billing_address` | object | No | Same fields as shipping_address |
specs/02-API-ROUTES.md:587:| `use_shipping_as_billing` | boolean | No | Default: true. If true, billing_address is ignored and copied from shipping. |
specs/02-API-ROUTES.md:591:Returns the full checkout object. Status transitions to `addressed`. The response includes `available_shipping_methods` populated based on the shipping address and store's shipping zones.
specs/02-API-ROUTES.md:596:  "status": "addressed",
specs/02-API-ROUTES.md:597:  "shipping_address_json": {
specs/02-API-ROUTES.md:600:    "address1": "123 Main St",
specs/02-API-ROUTES.md:601:    "address2": "Apt 4B",
specs/02-API-ROUTES.md:610:  "billing_address_json": {
specs/02-API-ROUTES.md:613:    "address1": "123 Main St",
specs/02-API-ROUTES.md:614:    "address2": "Apt 4B",
specs/02-API-ROUTES.md:657:- `422` - Validation errors (missing required fields, invalid country code, address not in any shipping zone)
specs/02-API-ROUTES.md:661:#### PUT /api/storefront/v1/checkouts/{checkoutId}/shipping-method
specs/02-API-ROUTES.md:663:Select a shipping method for the checkout.
specs/02-API-ROUTES.md:666:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:678:| `shipping_method_id` | integer | Yes | Must be one of the available shipping methods for this checkout's address. |
specs/02-API-ROUTES.md:682:Returns the full checkout object. Status transitions to `shipping_selected`. Totals are recalculated including the shipping cost and taxes (if applicable).
specs/02-API-ROUTES.md:717:- `422` - Invalid shipping method (not available for this address/checkout)
specs/02-API-ROUTES.md:721:#### PUT /api/storefront/v1/checkouts/{checkoutId}/payment-method
specs/02-API-ROUTES.md:723:Select a payment method for the checkout. This step records the chosen method without processing payment.
specs/02-API-ROUTES.md:726:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:732:  "payment_method": "credit_card"
specs/02-API-ROUTES.md:738:| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |
specs/02-API-ROUTES.md:742:Returns the full checkout object. Status transitions to `payment_selected`.
specs/02-API-ROUTES.md:747:  "status": "payment_selected",
specs/02-API-ROUTES.md:748:  "payment_method": "credit_card",
specs/02-API-ROUTES.md:763:- `422` - Invalid payment method or checkout not in `shipping_selected` state
specs/02-API-ROUTES.md:767:#### POST /api/storefront/v1/checkouts/{checkoutId}/apply-discount
specs/02-API-ROUTES.md:769:Apply a discount code to the checkout.
specs/02-API-ROUTES.md:772:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:829:#### DELETE /api/storefront/v1/checkouts/{checkoutId}/discount
specs/02-API-ROUTES.md:831:Remove the applied discount code from the checkout.
specs/02-API-ROUTES.md:834:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:838:Returns the full checkout object with discount removed and totals recalculated.
specs/02-API-ROUTES.md:848:#### POST /api/storefront/v1/checkouts/{checkoutId}/pay
specs/02-API-ROUTES.md:850:Process payment for a checkout using the selected payment method. All payment processing happens in-process via the Mock PSP - no external API calls, no redirects, no webhooks.
specs/02-API-ROUTES.md:853:- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md:859:  "payment_method": "credit_card",
specs/02-API-ROUTES.md:871:  "payment_method": "paypal"
specs/02-API-ROUTES.md:879:  "payment_method": "bank_transfer"
specs/02-API-ROUTES.md:885:| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |
specs/02-API-ROUTES.md:886:| `card_number` | string | Conditional | Required when `payment_method = credit_card`. Digits only (spaces stripped). |
specs/02-API-ROUTES.md:887:| `card_expiry` | string | Conditional | Required when `payment_method = credit_card`. Format: MM/YY. |
specs/02-API-ROUTES.md:888:| `card_cvc` | string | Conditional | Required when `payment_method = credit_card`. 3-4 digits. |
specs/02-API-ROUTES.md:889:| `card_holder` | string | Conditional | Required when `payment_method = credit_card`. Max 255 chars. |
specs/02-API-ROUTES.md:895:  "checkout_id": 7,
specs/02-API-ROUTES.md:902:    "payment_method": "credit_card",
specs/02-API-ROUTES.md:909:**Success response (Bank Transfer - deferred payment):** `200 OK`
specs/02-API-ROUTES.md:913:  "checkout_id": 7,
specs/02-API-ROUTES.md:920:    "payment_method": "bank_transfer",
specs/02-API-ROUTES.md:936:- `409` - Checkout not in a valid state for payment (must be `payment_selected`)
specs/02-API-ROUTES.md:937:- `422` - Validation errors (missing card fields, invalid payment method)
specs/02-API-ROUTES.md:999:  "shipping_address": {
specs/02-API-ROUTES.md:1002:    "address1": "123 Main St",
specs/02-API-ROUTES.md:1186:| `events[].type` | string | Yes | One of: `page_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. |
specs/02-API-ROUTES.md:1955:    "billing_address_json": {
specs/02-API-ROUTES.md:1958:      "address1": "123 Main St",
specs/02-API-ROUTES.md:1963:    "shipping_address_json": {
specs/02-API-ROUTES.md:1966:      "address1": "123 Main St",
specs/02-API-ROUTES.md:1989:    "payments": [
specs/02-API-ROUTES.md:1994:        "provider_payment_id": "mock_abc123",
specs/02-API-ROUTES.md:2113:    "payment_id": 401,
specs/02-API-ROUTES.md:2554:- `422` - Theme has validation errors preventing publish
specs/02-API-ROUTES.md:2801:      "checkout_started_count": 420,
specs/02-API-ROUTES.md:2813:        "checkout_started_count": 35
specs/02-API-ROUTES.md:2899:> **Note:** No inbound webhook endpoints are needed. All payment processing happens in-process via the Mock PSP -- there are no external payment provider callbacks. The webhook infrastructure defined in this section is reserved for **outbound** app webhooks (see Section 5, OAuth / App Ecosystem).
specs/02-API-ROUTES.md:3004:| `ShareErrorsFromSession` | Flash validation errors |
specs/02-API-ROUTES.md:3015:| `verified` | Require verified email address |
specs/02-API-ROUTES.md:3080:| `login` | 5 requests | 1 minute | IP address | Admin and customer login attempts |
specs/02-API-ROUTES.md:3082:| `api.storefront` | 120 requests | 1 minute | IP address | Storefront REST API (carts, etc.) |
specs/02-API-ROUTES.md:3083:| `checkout` | 10 requests | 1 minute | Session ID | Checkout creation and mutation endpoints |
specs/02-API-ROUTES.md:3084:| `search` | 30 requests | 1 minute | IP address | Search and autocomplete endpoints |
specs/02-API-ROUTES.md:3085:| `analytics` | 60 requests | 1 minute | IP address | Analytics event ingestion |
specs/02-API-ROUTES.md:3086:| `webhooks` | 100 requests | 1 minute | IP address | Reserved for future outbound app webhook endpoints |
specs/02-API-ROUTES.md:3117:| `POST /api/storefront/v1/checkouts*` | `checkout` |
specs/02-API-ROUTES.md:3118:| `GET /api/storefront/v1/checkouts*` | `checkout` |
specs/02-API-ROUTES.md:3119:| `PUT /api/storefront/v1/checkouts*` | `checkout` |
specs/02-API-ROUTES.md:3120:| `DELETE /api/storefront/v1/checkouts*` | `checkout` |
specs/02-API-ROUTES.md:3121:| `POST /api/storefront/v1/checkouts/*/pay` | `checkout` |
specs/02-API-ROUTES.md:3136:The checkout follows a strict state machine with explicit transitions:
specs/02-API-ROUTES.md:3140:    [*] --> started : POST /checkouts (create from cart)
specs/02-API-ROUTES.md:3141:    started --> addressed : PUT /checkouts/{id}/address
specs/02-API-ROUTES.md:3142:    addressed --> shipping_selected : PUT /checkouts/{id}/shipping-method
specs/02-API-ROUTES.md:3143:    shipping_selected --> payment_selected : PUT /checkouts/{id}/payment-method
specs/02-API-ROUTES.md:3144:    payment_selected --> completed : POST /checkouts/{id}/pay (instant capture: credit_card/paypal)
specs/02-API-ROUTES.md:3145:    payment_selected --> completed : POST /checkouts/{id}/pay (bank_transfer: deferred, order created as pending)
specs/02-API-ROUTES.md:3146:    payment_selected --> shipping_selected : Payment declined, customer retries
specs/02-API-ROUTES.md:3148:    addressed --> expired : 24-hour TTL reached
specs/02-API-ROUTES.md:3150:    payment_selected --> expired : 24-hour TTL reached
specs/02-API-ROUTES.md:3158:| `started` | `addressed` | PUT /checkouts/{id}/address with valid address |
specs/02-API-ROUTES.md:3159:| `addressed` | `shipping_selected` | PUT /checkouts/{id}/shipping-method with valid method |
specs/02-API-ROUTES.md:3160:| `shipping_selected` | `payment_selected` | PUT /checkouts/{id}/payment-method with valid method |
specs/02-API-ROUTES.md:3161:| `payment_selected` | `completed` | POST /checkouts/{id}/pay - Mock PSP processes payment in-process (instant for card/paypal, deferred for bank_transfer) |
specs/02-API-ROUTES.md:3162:| `payment_selected` | `shipping_selected` | Payment declined by Mock PSP (magic card number), customer retries |
specs/02-API-ROUTES.md:3165:Applying or removing a discount does not change the checkout status but triggers totals recalculation at the current state.
specs/05-BUSINESS-LOGIC.md:80:products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/05-BUSINESS-LOGIC.md:208:| `quantity_reserved` | Units held for active checkouts or pending orders |
specs/05-BUSINESS-LOGIC.md:230:        B -->|Commit on payment| C[Deducted]
specs/05-BUSINESS-LOGIC.md:240:| **Reserve** | Checkout enters `payment_selected` | No change | +quantity | If policy=deny, verify available >= quantity. Throw `InsufficientInventoryException` if not. |
specs/05-BUSINESS-LOGIC.md:291:**Add line - validations (in order):**
specs/05-BUSINESS-LOGIC.md:300:8. Set `line_discount_amount = 0` (recalculated by discount engine during checkout).
specs/05-BUSINESS-LOGIC.md:330:line_discount_amount = 0  (updated by discount engine during checkout)
specs/05-BUSINESS-LOGIC.md:343:- Releases any reserved inventory associated with active checkouts for these carts.
specs/05-BUSINESS-LOGIC.md:405:- After each calculation, store the full result in `checkouts.totals_json` as a JSON snapshot.
specs/05-BUSINESS-LOGIC.md:406:- Recalculate on every significant state change: address change (may affect tax/shipping zones), shipping method change, discount code change.
specs/05-BUSINESS-LOGIC.md:415:**Enum:** `App\Enums\CheckoutStatus` - values: `started`, `addressed`, `shipping_selected`, `payment_selected`, `completed`, `expired`
specs/05-BUSINESS-LOGIC.md:420:    started --> addressed : Address provided
specs/05-BUSINESS-LOGIC.md:421:    addressed --> shipping_selected : Shipping method chosen
specs/05-BUSINESS-LOGIC.md:422:    shipping_selected --> payment_selected : Payment method selected
specs/05-BUSINESS-LOGIC.md:423:    payment_selected --> completed : Payment confirmed
specs/05-BUSINESS-LOGIC.md:424:    payment_selected --> expired : Timeout (24h)
specs/05-BUSINESS-LOGIC.md:427:    addressed --> expired : Timeout (24h)
specs/05-BUSINESS-LOGIC.md:430:    note right of addressed : Triggers pricing recalculation
specs/05-BUSINESS-LOGIC.md:432:    note right of payment_selected : Inventory reserved
specs/05-BUSINESS-LOGIC.md:441:#### Transition: started -> addressed
specs/05-BUSINESS-LOGIC.md:443:**Method:** `setAddress(checkout, addressData)`
specs/05-BUSINESS-LOGIC.md:446:- `email` - valid email address
specs/05-BUSINESS-LOGIC.md:447:- `shipping_address` with required fields:
specs/05-BUSINESS-LOGIC.md:453:| `address1` | Yes | string | |
specs/05-BUSINESS-LOGIC.md:454:| `address2` | No | string | |
specs/05-BUSINESS-LOGIC.md:464:1. Validate address fields using a dedicated Form Request (`SetCheckoutAddressRequest`).
specs/05-BUSINESS-LOGIC.md:465:2. Save email and shipping_address_json on the checkout.
specs/05-BUSINESS-LOGIC.md:466:3. Copy shipping address to billing_address_json by default (can be overridden later).
specs/05-BUSINESS-LOGIC.md:467:4. Recalculate pricing (shipping zones and tax regions may change based on address).
specs/05-BUSINESS-LOGIC.md:468:5. Transition status to `addressed`.
specs/05-BUSINESS-LOGIC.md:471:#### Transition: addressed -> shipping_selected
specs/05-BUSINESS-LOGIC.md:473:**Method:** `setShippingMethod(checkout, shippingRateId)`
specs/05-BUSINESS-LOGIC.md:476:- A valid shipping_rate_id that belongs to a shipping zone matching the checkout's shipping address.
specs/05-BUSINESS-LOGIC.md:480:1. Resolve available shipping zones for the address (see Section 9).
specs/05-BUSINESS-LOGIC.md:482:3. Save shipping_method_id on the checkout.
specs/05-BUSINESS-LOGIC.md:487:#### Transition: shipping_selected -> payment_selected
specs/05-BUSINESS-LOGIC.md:489:**Method:** `selectPaymentMethod(checkout, paymentMethod)` - validates and stores the selected payment method
specs/05-BUSINESS-LOGIC.md:492:- All previous data is valid (address, shipping method).
specs/05-BUSINESS-LOGIC.md:494:- `paymentMethod` must be one of: `credit_card`, `paypal`, `bank_transfer`.
specs/05-BUSINESS-LOGIC.md:497:1. Store `payment_method` on the checkout record.
specs/05-BUSINESS-LOGIC.md:499:3. Set `expires_at` on the checkout to current time + 24 hours.
<?php

use App\Enums\CheckoutStatus;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use App\Models\Checkout;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use App\Services\InventoryService;
use Database\Seeders\ShopSeeder;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;

uses(LazilyRefreshDatabase::class);

beforeEach(function (): void {
    config(['cache.default' => 'array']);
    $this->seed(ShopSeeder::class);
    $this->store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $this->store);
    $this->variant = Product::query()->where('handle', 'classic-cotton-t-shirt')->firstOrFail()->variants()->firstOrFail();
});

test('checkout addresses validate every required nested field', function (): void {
    $cart = $this->postJson('http://shop.test/api/storefront/v1/carts')->assertCreated()->json();
    $this->postJson("http://shop.test/api/storefront/v1/carts/{$cart['id']}/lines", [
        'variant_id' => $this->variant->getKey(),
        'quantity' => 1,
        'cart_version' => 1,
    ])->assertCreated();
    $checkout = $this->postJson('http://shop.test/api/storefront/v1/checkouts', [
        'cart_id' => $cart['id'],
        'email' => 'address-validation@example.test',
    ])->assertCreated()->json();

    $this->putJson("http://shop.test/api/storefront/v1/checkouts/{$checkout['id']}/address", [
        'shipping_address' => ['first_name' => 'Incomplete'],
    ])->assertUnprocessable()->assertJsonValidationErrors([
        'shipping_address.last_name',
        'shipping_address.address1',
        'shipping_address.city',
        'shipping_address.country',
        'shipping_address.country_code',
        'shipping_address.postal_code',
    ]);

    $this->putJson("http://shop.test/api/storefront/v1/checkouts/{$checkout['id']}/address", [
        'shipping_address' => [
            'first_name' => 'Jane',
            'last_name' => 'Doe',
            'address1' => '123 Main St',
            'city' => 'Berlin',
            'country' => 'Germany',
            'country_code' => 'DE',
            'postal_code' => '10115',
        ],
        'use_shipping_as_billing' => false,
    ])->assertUnprocessable()->assertJsonValidationErrors([
        'billing_address',
        'billing_address.first_name',
    ]);
});

test('credit card payments require all card fields while other payment methods do not', function (): void {
    $cart = $this->postJson('http://shop.test/api/storefront/v1/carts')->assertCreated()->json();
    $this->postJson("http://shop.test/api/storefront/v1/carts/{$cart['id']}/lines", [
        'variant_id' => $this->variant->getKey(),
        'quantity' => 1,
        'cart_version' => 1,
    ])->assertCreated();
    $checkout = $this->postJson('http://shop.test/api/storefront/v1/checkouts', [
        'cart_id' => $cart['id'],
        'email' => 'card-validation@example.test',
    ])->assertCreated()->json();
    $address = [
        'first_name' => 'Jane',
        'last_name' => 'Doe',
        'address1' => '123 Main St',
        'city' => 'Berlin',
        'country' => 'Germany',
        'country_code' => 'DE',
        'postal_code' => '10115',
    ];
    $this->putJson("http://shop.test/api/storefront/v1/checkouts/{$checkout['id']}/address", ['shipping_address' => $address])->assertOk();
    $rate = ShippingRate::query()->firstOrFail();
    $this->putJson("http://shop.test/api/storefront/v1/checkouts/{$checkout['id']}/shipping-method", ['shipping_method_id' => $rate->getKey()])->assertOk();

    $this->postJson("http://shop.test/api/storefront/v1/checkouts/{$checkout['id']}/pay", [
        'payment_method' => 'credit_card',
        'card_number' => '4242 4242 4242 4242',
    ])->assertUnprocessable()->assertJsonValidationErrors([
        'card_expiry',
        'card_cvc',
        'card_holder',
    ]);
});

test('abandoned cart cleanup does not release inventory owned by another checkout', function (): void {
    $carts = app(CartService::class);
    $inventory = InventoryItem::query()->where('variant_id', $this->variant->getKey())->firstOrFail();

    $expiredCart = $carts->create($this->store);
    $carts->addLine($expiredCart, $this->variant->getKey(), 1);
    $expiredCheckout = Checkout::withoutGlobalScopes()->create([
        'store_id' => $this->store->getKey(),
        'cart_id' => $expiredCart->getKey(),
        'email' => 'expired@example.test',
        'status' => CheckoutStatus::PaymentSelected,
        'expires_at' => now()->subMinute(),
    ]);
    app(InventoryService::class)->reserve($inventory, 1);

    app(ExpireAbandonedCheckouts::class)->handle(app(InventoryService::class));

    $activeCart = $carts->create($this->store);
    $carts->addLine($activeCart, $this->variant->getKey(), 1);
    $activeCheckout = Checkout::withoutGlobalScopes()->create([
        'store_id' => $this->store->getKey(),
        'cart_id' => $activeCart->getKey(),
        'email' => 'active@example.test',
        'status' => CheckoutStatus::PaymentSelected,
        'expires_at' => now()->addHour(),
    ]);
    app(InventoryService::class)->reserve($inventory->refresh(), 1);
    DB::table('carts')->where('id', $expiredCart->getKey())->update(['updated_at' => now()->subDays(15)]);

    app(CleanupAbandonedCarts::class)->handle(app(InventoryService::class));

    expect($expiredCheckout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and($activeCheckout->refresh()->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($expiredCart->refresh()->status->value)->toBe('abandoned')
        ->and($inventory->refresh()->quantity_reserved)->toBe(1);
});

test('commerce rate limiters match the specification', function (): void {
    $request = Request::create('/api/storefront/v1/search', 'GET', [], [], [], ['REMOTE_ADDR' => '192.0.2.1']);
    $search = RateLimiter::limiter('search');
    $webhooks = RateLimiter::limiter('webhooks');

    expect($search)->not->toBeNull()
        ->and($webhooks)->not->toBeNull()
        ->and($search($request)->maxAttempts)->toBe(30)
        ->and($webhooks($request)->maxAttempts)->toBe(100)
        ->and($search($request)->decaySeconds)->toBe(60)
        ->and($webhooks($request)->decaySeconds)->toBe(60);
});
<?php

use App\Models\Customer;
use App\Models\Store;
use App\Models\Theme;
use App\Models\ThemeSetting;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use App\Services\WebhookService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Schema;

uses(LazilyRefreshDatabase::class);

test('theme settings use a one-to-one JSON contract and preserve model relations', function (): void {
    $columns = collect(Schema::getColumns('theme_settings'))->keyBy('name');
    $foreignKeys = collect(DB::select("PRAGMA foreign_key_list('theme_settings')"));

    expect($columns->keys()->all())->toBe(['theme_id', 'settings_json', 'updated_at'])
        ->and($columns['theme_id']['nullable'])->toBeFalse()
        ->and($columns['settings_json']['default'])->toBe("'{}'")
        ->and($foreignKeys->pluck('table')->all())->toContain('themes');

    $theme = Theme::factory()->create();
    $setting = $theme->themeSettings()->create([
        'settings_json' => ['primary_color' => '#1a1a2e', 'dark_mode' => true],
    ]);

    expect($setting)->toBeInstanceOf(ThemeSetting::class)
        ->and($setting->getKey())->toBe($theme->getKey())
        ->and($theme->fresh()->settingsRows->settings_json)->toBe([
            'primary_color' => '#1a1a2e',
            'dark_mode' => true,
        ]);
});

test('customer password reset tokens are non-null foreign-keyed and store scoped', function (): void {
    $columns = collect(Schema::getColumns('customer_password_reset_tokens'))->keyBy('name');
    $foreignKeys = collect(DB::select("PRAGMA foreign_key_list('customer_password_reset_tokens')"));

    expect($columns['store_id']['nullable'])->toBeFalse()
        ->and($foreignKeys->pluck('table')->all())->toContain('stores');

    $firstStore = Store::factory()->create();
    $secondStore = Store::factory()->create();
    $firstCustomer = Customer::factory()->create(['store_id' => $firstStore->getKey(), 'email' => 'same@example.test']);
    $secondCustomer = Customer::factory()->create(['store_id' => $secondStore->getKey(), 'email' => 'same@example.test']);

    app()->instance('current_store', $firstStore);
    $firstToken = Password::broker('customers')->createToken($firstCustomer);

    expect(DB::table('customer_password_reset_tokens')->where('store_id', $firstStore->getKey())->where('email', $firstCustomer->email)->exists())->toBeTrue();

    app()->instance('current_store', $secondStore);

    expect(Password::broker('customers')->tokenExists($secondCustomer, $firstToken))->toBeFalse();

    $secondToken = Password::broker('customers')->createToken($secondCustomer);

    expect(Password::broker('customers')->tokenExists($secondCustomer, $secondToken))->toBeTrue();
});

test('security defaults enable encrypted sessions, CORS credentials, and ninety-day audit retention', function (): void {
    $sessionEncryptEnvironment = $_ENV['SESSION_ENCRYPT'] ?? null;
    $sessionEncryptServer = $_SERVER['SESSION_ENCRYPT'] ?? null;
    $hasEnvironmentValue = array_key_exists('SESSION_ENCRYPT', $_ENV);
    $hasServerValue = array_key_exists('SESSION_ENCRYPT', $_SERVER);

    unset($_ENV['SESSION_ENCRYPT'], $_SERVER['SESSION_ENCRYPT']);
    putenv('SESSION_ENCRYPT');
    $sessionConfig = require config_path('session.php');

    if ($hasEnvironmentValue) {
        $_ENV['SESSION_ENCRYPT'] = $sessionEncryptEnvironment;
    }

    if ($hasServerValue) {
        $_SERVER['SESSION_ENCRYPT'] = $sessionEncryptServer;
    }

    expect($sessionConfig['encrypt'])->toBeTrue()
        ->and(config('cors.supports_credentials'))->toBeTrue()
        ->and(config('logging.channels.audit.days'))->toBe(90);
});

test('webhook secrets are encrypted under the contract name and sign the exact JSON request', function (): void {
    Http::fake(['https://hooks.test/*' => Http::response(['ok' => true], 200)]);
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    $subscription = WebhookSubscription::create([
        'event' => 'order.created',
        'target_url' => 'https://hooks.test/orders',
        'signing_secret_encrypted' => 'test-secret',
        'status' => 'active',
    ]);

    expect($subscription->signing_secret_encrypted)->toBe('test-secret')
        ->and($subscription->getHidden())->toContain('signing_secret_encrypted')
        ->and(DB::table('webhook_subscriptions')->where('id', $subscription->getKey())->value('signing_secret_encrypted'))->not->toBe('test-secret');

    (new WebhookService)->dispatch($store, 'order.created', ['order_id' => 1001]);

    Http::assertSent(function ($request): bool {
        $timestamp = $request->header('X-Platform-Timestamp')[0];
        $body = $request->body();

        return $request->header('Content-Type')[0] === 'application/json'
            && $request->header('X-Platform-Event')[0] === 'order.created'
            && $request->header('X-Platform-Signature')[0] === hash_hmac('sha256', $timestamp.'.'.$body, 'test-secret');
    });

    expect(WebhookDelivery::query()->where('webhook_subscription_id', $subscription->getKey())->firstOrFail()->status)->toBe('delivered');
});


exec

status: completed

input:
const patch = "*** Begin Patch\n*** Update File: /Users/wesner/Herd/shop/app/Http/Requests/SetCheckoutAddressRequest.php\n@@\n namespace App\\Http\\Requests;\n \n+use App\\Models\\Checkout;\n use Illuminate\\Foundation\\Http\\FormRequest;\n@@\n     public function rules(): array\n     {\n-        return [\n-            'shipping_address' => ['required', 'array'],\n-            'shipping_address.first_name' => ['required', 'string', 'max:100'],\n-            'shipping_address.last_name' => ['required', 'string', 'max:100'],\n-            'shipping_address.address1' => ['required', 'string', 'max:255'],\n-            'shipping_address.city' => ['required', 'string', 'max:100'],\n-            'shipping_address.country_code' => ['required', 'string', 'size:2'],\n-            'shipping_address.province_code' => ['nullable', 'string', 'max:10'],\n-            'shipping_address.postal_code' => ['required', 'string', 'max:20'],\n-            'shipping_address.phone' => ['nullable', 'string', 'max:30'],\n-            'billing_address' => ['nullable', 'array'],\n-            'use_shipping_as_billing' => ['nullable', 'boolean'],\n-        ];\n+        $requiresShipping = $this->checkoutRequiresShipping();\n+        $useShippingAsBilling = $this->boolean('use_shipping_as_billing', true);\n+        $rules = [\n+            'use_shipping_as_billing' => ['sometimes', 'boolean'],\n+            ...$this->addressRules('shipping_address', $requiresShipping),\n+        ];\n+\n+        if (! $useShippingAsBilling) {\n+            $rules = [...$rules, ...$this->addressRules('billing_address', true)];\n+        }\n+\n+        return $rules;\n     }\n+\n+    /**\n+     * @return array<string, array<int, string>>\n+     */\n+    private function addressRules(string $key, bool $required): array\n+    {\n+        $presence = $required ? 'required' : 'sometimes';\n+        $root = $required ? 'required' : 'nullable';\n+\n+        return [\n+            $key => [$root, 'array'],\n+            \"{$key}.first_name\" => [$presence, 'string', 'max:255'],\n+            \"{$key}.last_name\" => [$presence, 'string', 'max:255'],\n+            \"{$key}.address1\" => [$presence, 'string', 'max:500'],\n+            \"{$key}.address2\" => ['sometimes', 'nullable', 'string', 'max:500'],\n+            \"{$key}.company\" => ['sometimes', 'nullable', 'string', 'max:255'],\n+            \"{$key}.city\" => [$presence, 'string', 'max:255'],\n+            \"{$key}.province\" => ['sometimes', 'nullable', 'string', 'max:255'],\n+            \"{$key}.province_code\" => ['sometimes', 'nullable', 'string', 'max:10'],\n+            \"{$key}.country\" => [$presence, 'string', 'max:255'],\n+            \"{$key}.country_code\" => [$presence, 'string', 'regex:/^[A-Z]{2}$/'],\n+            \"{$key}.postal_code\" => [$presence, 'string', 'max:20'],\n+            \"{$key}.phone\" => ['sometimes', 'nullable', 'string', 'max:50'],\n+        ];\n+    }\n+\n+    private function checkoutRequiresShipping(): bool\n+    {\n+        $checkoutId = $this->route('checkoutId');\n+\n+        if (! is_numeric($checkoutId)) {\n+            return true;\n+        }\n+\n+        $checkout = Checkout::query()->with('cart.lines.variant')->find((int) $checkoutId);\n+\n+        return $checkout?->cart?->lines?->contains(fn ($line): bool => (bool) $line->variant?->requires_shipping) ?? true;\n+    }\n }\n*** Update File: /Users/wesner/Herd/shop/app/Http/Controllers/Api/StorefrontCheckoutController.php\n@@\n use App\\Exceptions\\InsufficientInventoryException;\n use App\\Exceptions\\InvalidDiscountException;\n use App\\Http\\Controllers\\Controller;\n+use App\\Http\\Requests\\ApplyDiscountRequest;\n+use App\\Http\\Requests\\SetCheckoutAddressRequest;\n@@\n-    public function address(Request $request, int $checkoutId): JsonResponse\n+    public function address(SetCheckoutAddressRequest $request, int $checkoutId): JsonResponse\n@@\n-        $rules = [\n-            'use_shipping_as_billing' => ['sometimes', 'boolean'],\n-            ...$this->addressValidationRules('shipping_address', $requiresShipping),\n-        ];\n-\n-        if (! $useShippingAsBilling) {\n-            $rules = [...$rules, ...$this->addressValidationRules('billing_address', $requiresShipping)];\n-        }\n-\n-        $data = $request->validate($rules);\n+        $data = $request->validated();\n@@\n-    public function applyDiscount(Request $request, int $checkoutId): JsonResponse\n+    public function applyDiscount(ApplyDiscountRequest $request, int $checkoutId): JsonResponse\n     {\n-        $data = $request->validate(['code' => ['required', 'string', 'max:64']]);\n+        $data = $request->validated();\n@@\n-    /**\n-     * @return array<string, array<int, string>>\n-     */\n-    private function addressValidationRules(string $key, bool $required): array\n-    {\n-        $presence = $required ? 'required' : 'sometimes';\n-        $root = $required ? 'required' : 'nullable';\n-\n-        return [\n-            $key => [$root, 'array'],\n-            \"{$key}.first_name\" => [$presence, 'string', 'max:255'],\n-            \"{$key}.last_name\" => [$presence, 'string', 'max:255'],\n-            \"{$key}.address1\" => [$presence, 'string', 'max:500'],\n-            \"{$key}.address2\" => ['sometimes', 'nullable', 'string', 'max:500'],\n-            \"{$key}.company\" => ['sometimes', 'nullable', 'string', 'max:255'],\n-            \"{$key}.city\" => [$presence, 'string', 'max:255'],\n-            \"{$key}.province\" => ['sometimes', 'nullable', 'string', 'max:255'],\n-            \"{$key}.province_code\" => ['sometimes', 'nullable', 'string', 'max:10'],\n-            \"{$key}.country\" => [$presence, 'string', 'max:255'],\n-            \"{$key}.country_code\" => [$presence, 'string', 'regex:/^[A-Z]{2}$/'],\n-            \"{$key}.postal_code\" => [$presence, 'string', 'max:20'],\n-            \"{$key}.phone\" => ['sometimes', 'nullable', 'string', 'max:50'],\n-        ];\n-    }\n-\n*** End Patch";
text(await tools.apply_patch(patch));


type: FileChange

type: update

unified_diff:
@@ -4,2 +4,3 @@

+use App\Models\Checkout;
 use Illuminate\Foundation\Http\FormRequest;
@@ -23,16 +24,53 @@
     {
+        $requiresShipping = $this->checkoutRequiresShipping();
+        $useShippingAsBilling = $this->boolean('use_shipping_as_billing', true);
+        $rules = [
+            'use_shipping_as_billing' => ['sometimes', 'boolean'],
+            ...$this->addressRules('shipping_address', $requiresShipping),
+        ];
+
+        if (! $useShippingAsBilling) {
+            $rules = [...$rules, ...$this->addressRules('billing_address', true)];
+        }
+
+        return $rules;
+    }
+
+    /**
+     * @return array<string, array<int, string>>
+     */
+    private function addressRules(string $key, bool $required): array
+    {
+        $presence = $required ? 'required' : 'sometimes';
+        $root = $required ? 'required' : 'nullable';
+
         return [
-            'shipping_address' => ['required', 'array'],
-            'shipping_address.first_name' => ['required', 'string', 'max:100'],
-            'shipping_address.last_name' => ['required', 'string', 'max:100'],
-            'shipping_address.address1' => ['required', 'string', 'max:255'],
-            'shipping_address.city' => ['required', 'string', 'max:100'],
-            'shipping_address.country_code' => ['required', 'string', 'size:2'],
-            'shipping_address.province_code' => ['nullable', 'string', 'max:10'],
-            'shipping_address.postal_code' => ['required', 'string', 'max:20'],
-            'shipping_address.phone' => ['nullable', 'string', 'max:30'],
-            'billing_address' => ['nullable', 'array'],
-            'use_shipping_as_billing' => ['nullable', 'boolean'],
+            $key => [$root, 'array'],
+            "{$key}.first_name" => [$presence, 'string', 'max:255'],
+            "{$key}.last_name" => [$presence, 'string', 'max:255'],
+            "{$key}.address1" => [$presence, 'string', 'max:500'],
+            "{$key}.address2" => ['sometimes', 'nullable', 'string', 'max:500'],
+            "{$key}.company" => ['sometimes', 'nullable', 'string', 'max:255'],
+            "{$key}.city" => [$presence, 'string', 'max:255'],
+            "{$key}.province" => ['sometimes', 'nullable', 'string', 'max:255'],
+            "{$key}.province_code" => ['sometimes', 'nullable', 'string', 'max:10'],
+            "{$key}.country" => [$presence, 'string', 'max:255'],
+            "{$key}.country_code" => [$presence, 'string', 'regex:/^[A-Z]{2}$/'],
+            "{$key}.postal_code" => [$presence, 'string', 'max:20'],
+            "{$key}.phone" => ['sometimes', 'nullable', 'string', 'max:50'],
         ];
     }
+
+    private function checkoutRequiresShipping(): bool
+    {
+        $checkoutId = $this->route('checkoutId');
+
+        if (! is_numeric($checkoutId)) {
+            return true;
+        }
+
+        $checkout = Checkout::query()->with('cart.lines.variant')->find((int) $checkoutId);
+
+        return $checkout?->cart?->lines?->contains(fn ($line): bool => (bool) $line->variant?->requires_shipping) ?? true;
+    }
 }

unified_diff:
@@ -8,2 +8,4 @@
 use App\Http\Controllers\Controller;
+use App\Http\Requests\ApplyDiscountRequest;
+use App\Http\Requests\SetCheckoutAddressRequest;
 use App\Models\Cart;
@@ -50,3 +52,3 @@

-    public function address(Request $request, int $checkoutId): JsonResponse
+    public function address(SetCheckoutAddressRequest $request, int $checkoutId): JsonResponse
     {
@@ -55,12 +57,3 @@
         $useShippingAsBilling = $request->boolean('use_shipping_as_billing', true);
-        $rules = [
-            'use_shipping_as_billing' => ['sometimes', 'boolean'],
-            ...$this->addressValidationRules('shipping_address', $requiresShipping),
-        ];
-
-        if (! $useShippingAsBilling) {
-            $rules = [...$rules, ...$this->addressValidationRules('billing_address', $requiresShipping)];
-        }
-
-        $data = $request->validate($rules);
+        $data = $request->validated();
         try {
@@ -86,5 +79,5 @@

-    public function applyDiscount(Request $request, int $checkoutId): JsonResponse
+    public function applyDiscount(ApplyDiscountRequest $request, int $checkoutId): JsonResponse
     {
-        $data = $request->validate(['code' => ['required', 'string', 'max:64']]);
+        $data = $request->validated();
         $checkout = $this->checkout($checkoutId);
@@ -162,27 +155,2 @@
         return $checkout;
-    }
-
-    /**
-     * @return array<string, array<int, string>>
-     */
-    private function addressValidationRules(string $key, bool $required): array
-    {
-        $presence = $required ? 'required' : 'sometimes';
-        $root = $required ? 'required' : 'nullable';
-
-        return [
-            $key => [$root, 'array'],
-            "{$key}.first_name" => [$presence, 'string', 'max:255'],
-            "{$key}.last_name" => [$presence, 'string', 'max:255'],
-            "{$key}.address1" => [$presence, 'string', 'max:500'],
-            "{$key}.address2" => ['sometimes', 'nullable', 'string', 'max:500'],
-            "{$key}.company" => ['sometimes', 'nullable', 'string', 'max:255'],
-            "{$key}.city" => [$presence, 'string', 'max:255'],
-            "{$key}.province" => ['sometimes', 'nullable', 'string', 'max:255'],
-            "{$key}.province_code" => ['sometimes', 'nullable', 'string', 'max:10'],
-            "{$key}.country" => [$presence, 'string', 'max:255'],
-            "{$key}.country_code" => [$presence, 'string', 'regex:/^[A-Z]{2}$/'],
-            "{$key}.postal_code" => [$presence, 'string', 'max:20'],
-            "{$key}.phone" => ['sometimes', 'nullable', 'string', 'max:50'],
-        ];
     }

status: completed

stdout:
Success. Updated the following files:
M /Users/wesner/Herd/shop/app/Http/Requests/SetCheckoutAddressRequest.php
M /Users/wesner/Herd/shop/app/Http/Controllers/Api/StorefrontCheckoutController.php


text:
Script completed
Wall time 0.0 seconds
Output:


status: completed

input:
const patch = "*** Begin Patch\n*** Update File: /Users/wesner/Herd/shop/app/Http/Controllers/Api/StorefrontCheckoutController.php\n@@\n     public function address(SetCheckoutAddressRequest $request, int $checkoutId): JsonResponse\n     {\n         $checkout = $this->checkout($checkoutId);\n-        $requiresShipping = $checkout->cart->lines->contains(fn ($line): bool => (bool) $line->variant?->requires_shipping);\n         $useShippingAsBilling = $request->boolean('use_shipping_as_billing', true);\n         $data = $request->validated();\n*** End Patch";
text(await tools.apply_patch(patch));


unified_diff:
@@ -55,3 +55,2 @@
         $checkout = $this->checkout($checkoutId);
-        $requiresShipping = $checkout->cart->lines->contains(fn ($line): bool => (bool) $line->variant?->requires_shipping);
         $useShippingAsBilling = $request->boolean('use_shipping_as_billing', true);

stdout:
Success. Updated the following files:
M /Users/wesner/Herd/shop/app/Http/Controllers/Api/StorefrontCheckoutController.php


text:
Script completed
Wall time 0.0 seconds
Output:


type: AgentMessage

type: Text

text: The checkout address and discount endpoints now use their dedicated Form Requests, including the spec’s conditional shipping/billing rules. I’m checking that this preserves the existing API behavior, especially for digital-only carts and nested validation errors.

phase: commentary


type: message

role: assistant


status: completed

input:
const r = await tools.exec_command({
  cmd: "vendor/bin/pint --dirty --format agent && php artisan test --compact tests/Feature/CommerceValidationAndCleanupTest.php tests/Feature/ContractBehaviorTest.php",
  workdir: "/Users/wesner/Herd/shop",
  yield_time_ms: 1000,
  max_output_tokens: 12000
});
text(JSON.stringify(r));


text:
Script completed
Wall time 1.2 seconds
Output:

text: {"chunk_id":"d8842b","wall_time_seconds":1.002838291,"session_id":92761,"original_token_count":5,"output":"{\"result\":\"pass\"}"}


type: CommandExecution

command: vendor/bin/pint --dirty --format agent && php artisan test --compact tests/Feature/CommerceValidationAndCleanupTest.php tests/Feature/ContractBehaviorTest.php

type: unknown

stdout:
{"result":"pass"}
  ........

  Tests:    8 passed (55 assertions)
  Duration: 0.82s


status: completed

input:
const r = await tools.write_stdin({session_id: 92761, chars: "", yield_time_ms: 1000, max_output_tokens: 12000});
text(r.output);


text:
Script completed
Wall time 0.0 seconds
Output:

text:
  ........

  Tests:    8 passed (55 assertions)
  Duration: 0.82s


status: completed

input:
const r = await tools.exec_command({
  cmd: "php artisan test --compact",
  workdir: "/Users/wesner/Herd/shop",
  yield_time_ms: 1000,
  max_output_tokens: 16000
});
text(JSON.stringify(r));


text:
Script completed
Wall time 1.2 seconds
Output:

text: {"chunk_id":"3f19cc","wall_time_seconds":1.000251666,"session_id":47605,"original_token_count":4,"output":"\n  ............."}


status: completed

input:
const r = await tools.write_stdin({session_id: 47605, chars: "", yield_time_ms: 1000, max_output_tokens: 16000});
text(r.output);


command:
/bin/zsh
-lc
php artisan test --compact

cmd: php artisan test --compact


text:
Script completed
Wall time 2.1 seconds
Output:

text:
...............................................................
  ......

  Tests:    82 passed (265 assertions)
  Duration: 5.68s


status: completed

input:
const matches = ALL_TOOLS.filter(x => /agent|task|subagent/i.test(x.name + " " + x.description));
text(matches);


text:
Script completed
Wall time 0.0 seconds
Output:

text:
Warning: truncated output (original token count: 11929)
Total output lines: 1

[{"name":"create_goal","description":"Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks.\nSet token_budget only when an explicit token budget is requested. Fails if an unfinished goal exists; use update_goal only for status.\n\nexec tool declaration:\n```ts\ndeclare const tools: { create_goal(args: {\n  // Required. The concrete objective to start pursuing. This starts a new active goal when no goal exists or replaces the current goal when it is complete.\n  objective: string;\n  // Positive token budget for the new goal. Omit unless explicitly requested.\n  token_budget?: number;\n}): Promise<unknown>; };\n```"},{"name":"mcp__codex_apps__codex_document_control_execute_d_7437ad2e4ffa","description":"Use Codex Document Control to find connected document sessions, inspect the tools supported by a selected session, and execute one supported tool against that session. Call `list_document_sessions` first to choose the intended connected session, call `get_document_tool_schemas` before constructing tool arguments, then call `execute_document_command` with a caller-stable `idempotency_key`. Use this only for connected Codex document control; do not use it for general spreadsheet, presentation, or document tasks without a connected document session.\n\nExecute one supported surface-specific tool against a connected Codex document session. First call `list_document_sessions` to choose the intended `executor_session_id` and `supported_tools[].name`, then call `get_document_tool_schemas` for the selected `surface`, that `supported_tools[].name` as `tool_name`, and `version` before constructing `args`. `idempotency_key` must be a caller-stable key that you reuse verbatim only when retrying the same logical document-control command; use a new key for a different command. This tool is part of plugin `Spreadsheets`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__codex_document_control_execute_d_7437ad2e4ffa(args: {\n  // JSON object of arguments matching the selected tool's `input_schema` from `get_document_tool_schemas`.\n  args: { [key: string]: unknown; };\n  // Exact `executor_session_id` copied from the selected Codex document session returned by `list_document_sessions`.\n  executor_session_id: string;\n  // Caller-stable idempotency key for this logical document-control command. Reuse the exact same key only for retries of the same command.\n  idempotency_key: string;\n  // Exact selected session `supported_tools[].name` copied from `list_document_sessions`.\n  tool_name: string;\n}): Promise<CallToolResult<{\n  // The server's response to a tool call.\n  result: { _meta?: { [key: string]: unknown; } | null; content: Array<{ _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; text: string; type: \"text\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"image\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"audio\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; description?: string | null; icons?: Array<{ mimeType?: string | null; sizes?: Array<string> | null; src: string; }> | null; mimeType?: string | null; name: string; size?: number | null; title?: string | null; type: \"resource_link\"; uri: string; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; resource: { _meta?: { [key: string]: unknown; } | null; mimeType?: string | null; text: string; uri: string; } | { _meta?: { [key: string]: unknown; } | null; blob: string; mimeType?: string | null; uri: string; }; type: \"resource\"; }>; isError?: boolean; structuredContent?: { [key: string]: unknown; } | null; };\n}>>; };\n```"},{"name":"mcp__codex_apps__codex_document_control_get_docum_83c7f0565c0f","description":"Use Codex Document Control to find connected document sessions, inspect the tools supported by a selected session, and execute one supported tool against that session. Call `list_document_sessions` first to choose the intended connected session, call `get_document_tool_schemas` before constructing tool arguments, then call `execute_document_command` with a caller-stable `idempotency_key`. Use this only for connected Codex document control; do not use it for general spreadsheet, presentation, or document tasks without a connected document session.\n\nFetch the concrete input schemas for tools supported by a selected Codex document session before constructing `execute_document_command.args`. First call `list_document_sessions`, then pass the exact `surface`, selected `supported_tools[].name` as `tool_name`, and `version` values from that session's `supported_tools` records. This tool is part of plugin `Spreadsheets`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__codex_document_control_get_docum_83c7f0565c0f(args: {\n  // Exact tool schema lookup keys from Codex document session discovery, keyed by `surface`, `supported_tools[].name` passed as `tool_name`, and `version`.\n  items: Array<{\n  // Document surface. Use `excel` for Excel workbooks, `powerpoint` for PowerPoint presentations, or `sheets` for Google Sheets spreadsheets.\n  surface: \"excel\" | \"powerpoint\" | \"sheets\";\n  // Exact `supported_tools[].name` copied from the selected session.\n  tool_name: string;\n  // Exact `supported_tools[].version` copied from the selected session.\n  version: string;\n}>;\n}): Promise<CallToolResult<{\n  // The server's response to a tool call.\n  result: { _meta?: { [key: string]: unknown; } | null; content: Array<{ _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; text: string; type: \"text\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"image\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"audio\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; description?: string | null; icons?: Array<{ mimeType?: string | null; sizes?: Array<string> | null; src: string; }> | null; mimeType?: string | null; name: string; size?: number | null; title?: string | null; type: \"resource_link\"; uri: string; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; resource: { _meta?: { [key: string]: unknown; } | null; mimeType?: string | null; text: string; uri: string; } | { _meta?: { [key: string]: unknown; } | null; blob: string; mimeType?: string | null; uri: string; }; type: \"resource\"; }>; isError?: boolean; structuredContent?: { [key: string]: unknown; } | null; };\n}>>; };\n```"},{"name":"mcp__codex_apps__codex_document_control_list_document_sessions","description":"Use Codex Document Control to find connected document sessions, inspect the tools supported by a selected session, and execute one supported tool against that session. Call `list_document_sessions` first to choose the intended connected session, call `get_document_tool_schemas` before constructing tool arguments, then call `execute_document_command` with a caller-stable `idempotency_key`. Use this only for connected Codex document control; do not use it for general spreadsheet, presentation, or document tasks without a connected document session.\n\nList the user's currently connected Codex document sessions and the surface-specific tools each session supports. Call this before executing a document-control command so you can choose the intended `executor_session_id` and `supported_tools[].name`. This tool is part of plugin `Spreadsheets`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__codex_document_control_list_document_sessions(args: {\n  // Optional document surface filter. Use `excel` for Excel workbooks, `powerpoint` for PowerPoint presentations, or `sheets` for Google Sheets spreadsheets. Omit to list connected Codex document sessions across all supported surfaces.\n  surface?: \"excel\" | \"powerpoint\" | \"sheets\" | null;\n}): Promise<CallToolResult<{\n  // The server's response to a tool call.\n  result: { _meta?: { [key: string]: unknown; } | null; content: Array<{ _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; text: string; type: \"text\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"image\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"audio\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; description?: string | null; icons?: Array<{ mimeType?: string | null; sizes?: Array<string> | null; src: string; }> | null; mimeType?: string | null; name: string; size?: number | null; title?: string | null; type: \"resource_link\"; uri: string; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; resource: { _meta?: { [key: string]: unknown; } | null; mimeType?: string | null; text: string; uri: string; } | { _meta?: { [key: string]: unknown; } | null; blob: string; mimeType?: string | null; uri: string; }; type: \"resource\"; }>; isError?: boolean; structuredContent?: { [key: string]: unknown; } | null; };\n}>>; };\n```"},{"name":"mcp__codex_apps__figma_generate_diagram","description":"Turn code into editable design\n\nCreate a flowchart, decision tree, gantt chart, sequence diagram, state diagram, or entity relationship diagram in FigJam, using Mermaid.js. Generated diagrams should be simple, unless a user asks for details. This tool also does not support generating Figma designs, class diagrams, timelines, venn diagrams, or other Mermaid.js diagram types. This tool also does not support font changes, or moving individual shapes around -- if a user asks for those changes to an existing diagram, encourage them to open the diagram in Figma. If the tool is unable to complete the user's task, reference the error that is passed back. Do not use the create_new_file tool prior to creating a diagram using this tool; generate_diagram creates its own files. This tool is part of plugin `Figma`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__figma_generate_diagram(args: {\n  // Optional. The key of an existing FigJam file to add the diagram to. Extract from a Figma URL like figma.com/board/{fileKey}/... When provided, the diagram is placed directly in this file instead of creating a new one. The user must have edit access to the file.\n  fileKey?: string;\n  // Mermaid.js code for the diagram. Keep diagrams simple, unless the user has detailed requirements. Only the following diagram types are supported: graph, flowchart, sequenceDiagram, stateDiagram, stateDiagram-v2, gantt, and erDiagram. Make sure to use correct Mermaid.js syntax. For graph, flowchart, or entity relationship diagrams, use LR direction by default and put all shape and edge text in quotes (eg. [\"Text\"], -->|\"Edge Text\"|, --\"Edge Text\"-->). Do not use emojis in the Mermaid.js code. Do not use\n  // to represent new lines. Feel free to use the full range of shapes and connectors that Mermaid.js syntax offers. For graph and flowchart diagrams only, you can use color styling--but do so sparingly unless the user asks for it. In gantt charts, do not use color styling. In sequence diagrams, do not use notes. Do not use the word \"end\" in classNames.\n  mermaidSyntax: string;\n  // A human-readable title for the diagram. Keep it short, but descriptive.\n  name: string;\n  // The team or organization key (e.g. \"team::1234567890\" or \"organization::1234567890\"). Use the `key` field verbatim from one of the user's plans. If the user has more than one plan, ask which one to use before calling.\n  planKey?: string;\n  // Optional. Indicates whether to use the same plan for future generations. Do not provide this parameter unless the user specifically requests it, and a planKey is also provided.\n  savePlanKey?: boolean;\n  // Optional. To generate a diagram using the software architecture layout, pass the code from the architecture-diagram-instructions resource. Omit this parameter for standard diagrams.\n  useArchitectureLayoutCode?: string;\n  // A description of what the user is trying to accomplish with this tool call. Important: Do not add extraneous information other than what the user provides.\n  userIntent?: string;\n}): Promise<CallToolResult>; };\n```"},{"name":"mcp__codex_apps__figma_get_screenshot","description":"Turn code into editable design\n\nGenerate a screenshot for a given node or the currently selected node in the Figma desktop app. Works on Figma design files (URL path `/design/`), FigJam boards (`/board/`), and Figma Slides (`/slides/`). The optional `maxDimension` parameter (positive integer, max 65536, default 1024) caps the longer edge of the rendered PNG in pixels — increase it when you need to inspect fine detail, decrease it for thumbnails or to save context. The JSON metadata entry in the response includes both `width`/`height` (the rendered PNG size) and `original_width`/`original_height` (the node's natural canvas size before any clamping), so callers can decide whether to re-request at a higher `maxDimension`. Use the nodeId parameter to specify a node id. nodeId parameter is REQUIRED. Use the fileKey parameter to specify the file key. fileKey parameter is REQUIRED. If a URL is provided, extract the file key and node id from the URL. For example, if given the URL https://figma.com/design/pqrs/ExampleFile?node-id=1-2 the extracted fileKey would be `pqrs` and the extracted nodeId would be `1:2`. If the URL does not include `node-id`, ask the user for a node-specific URL. Do not pass an empty or guessed nodeId. This tool is not supported for Figma Make Files (URLs containing `/make/`). By default this tool returns a short-lived URL to the screenshot plus curl instructions for downloading the PNG — the URL+curl path is strongly preferred because it uses far fewer tokens than embedding the image inline. The `enableBase64Response` parameter defaults to `false`. Only set `enableBase64Response: true` when the agent cannot fetch URLs (no shell access, no HTTP client, or a sandboxed environment that blocks outbound requests); when set, an inline base64 image entry is appended to the response in addition to the URL and curl instructions. If the URL is of the format https://figma.com/design/:fileKey/branch/:branchKey/:fileName then use the branchKey as the fileKey. This tool is part of plugin `Figma`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__figma_get_screenshot(args: {\n  // When true, renders the node in isolation — floating/overlapping content (e.g. connectors parented to the page that visually sit above a section) is excluded. Defaults to false so screenshots match what the user sees on the canvas. Only set to true if the caller specifically needs the isolated render.\n  contentsOnly?: boolean;\n  // When true, the response also includes the screenshot inline as a base64-encoded image entry, in addition to the short-lived URL and curl instructions. Defaults to false. Set to true ONLY if the agent cannot fetch URLs (no shell access, no HTTP client, or a sandboxed environment that blocks outbound requests)\n  enableBase64Response?: boolean;\n  // The key of the Figma file to use. If the URL is provided, extract the file key from the URL. The given URL must be in the format https://figma.com/design/:fileKey/:fileName?node-id=:int1-:int2. The extracted fileKey would be `:fileKey`.\n  fileKey: string;\n  // Optional. Maximum pixel size of the longer edge of the rendered screenshot — the server scales the node so that max(width, height) ≤ maxDimension while preserving aspect ratio. Defaults to 1024. Must be a positive integer; values above 65536 are rejected. Increase when the agent will visually inspect fine detail; decrease for thumbnails or to save context.\n  maxDimension?: number;\n  // The ID of the node in the Figma document, eg. \"123:456\" or \"123-456\". This should be a valid node ID in the Figma document. Do not pass an empty string for node_id.\n  nodeId: string;\n}): Promise<CallToolResult>; };\n```"},{"name":"mcp__codex_apps__figma_weave_list_tools","description":"Turn code into editable design\n\nLists the authenticated user's Weave tools — published Weave workflows they can run — in their active Weave workspace. Here \"tool\" means a Weave tool (a published Weave workflow), not an agent/MCP tool. Use this when the user wants to see, browse, or choose from their Weave tools. Returns each tool's name, creation date, and a link to open it in Weave. If the user already pasted a specific Weave URL (app.weavy.ai/tool/<id> or app.weavy.ai/flow/<id>), you already have its recipeId — that `<id>` — so skip this and go straight to weave_get_tool_inputs/weave_run_tool instead of opening a browser. This tool is part of plugin `Figma`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__figma_weave_list_tools(args: { [key: string]: unknown; }): Promise<CallToolResult>; };\n```"},{"name":"mcp__codex_apps__plugin_management_get_app_permissions","description":"Manage plugins, settings, permissions, and connections. Prefer available built-in tools or connected plugins when they fit the task. Proactively search for plugins when an external app, account, or service would materially help, even if the user did not request a plugin. Search before claiming a service is unavailable or suggesting manual workarounds. Do not suggest plugins for native web search, image generation, memory, or sites unless a specific external provider or missing capability is needed.\n\nInspect one named ChatGPT plugin's global/default and plugin-specific permission settings. Use when the user asks what the plugin may read, write, or do, whether it must ask first, or whether it inherits the default. For a missing/broad target such as my plugins, all, or Google, make no call and ask which plugin. Never pass global. Do not use for OAuth/admin scopes, install/connect/undo requests, ordinary plugin use, or npm/Chrome/code plugins. This tool is part of plugin `Plugin Management`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__plugin_management_get_app_permissions(args: {\n  // ChatGPT plugin reference to inspect. May be a plugin id, connector id, platform slug, or unambiguous user-facing plugin name. It must identify one plugin; never pass all, global, Google, or another broad/generic target.\n  app_id: string;\n}): Promise<CallToolResult<{\n  // The server's response to a tool call.\n  result: { _meta?: { [key: string]: unkno…1929 tokens truncated…known; } | null; };\n}>>; };\n```"},{"name":"mcp__codex_apps__plugin_management_update_app_permissions","description":"Manage plugins, settings, permissions, and connections. Prefer available built-in tools or connected plugins when they fit the task. Proactively search for plugins when an external app, account, or service would materially help, even if the user did not request a plugin. Search before claiming a service is unavailable or suggesting manual workarounds. Do not suggest plugins for native web search, image generation, memory, or sites unless a specific external provider or missing capability is needed.\n\nUpdate global ChatGPT plugin permissions or a plugin-specific override. Omit app_id for global-only updates and provide it for plugin-specific updates. Map Always ask to always_ask, Any changes to ask_before_writes, Important actions to review_important_actions, Never ask to full_access, and Use my default to inherit. For plugin-specific changes, a missing/broad target such as Google, a vague mode such as tighter/more permissive, conflicting intent such as less access plus Never ask, or a choice left to you requires a question and no tool call; explicit global/default changes need no app_id. Never infer a mode or probe with get_app_permissions. One call may include both global_permissions and app_permissions with app_id; the global change is applied first. For several plugins call once per target and complete every requested update. This tool is part of plugin `Plugin Management`.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__codex_apps__plugin_management_update_app_permissions(args: {\n  // Optional ChatGPT plugin identifier. Required for app_permissions updates; omit for global_permissions-only updates. May be a plugin id, connector id, platform slug, or unambiguous user-facing plugin name. Never pass Google or another broad/generic target.\n  app_id?: string | null;\n  // Optional user-visible reason for changing permissions.\n  reason?: string | null;\n  // Permission updates to apply. A call may contain global_permissions, app_permissions, or both; app_permissions requires app_id.\n  updates: {\n  // Plugin-specific permission updates to apply.\n  app_permissions?: Array<{\n  // Permission setting to update. This field is optional; omit it unless needed. If provided, use permission_mode.\n  setting?: \"permission_mode\";\n  // New value for the plugin-specific permission setting. Options: inherit (UI label: Use default or follow global; clear this plugin's override), always_ask (UI label: Always ask; ask before reading or making changes with this plugin), ask_before_writes (UI label: Allow read actions; read without asking but ask before making changes with this plugin), review_important_actions (UI label: Allow low-risk actions; automatically approve low-risk actions with this plugin but may deny actions involving sensitive information), and full_access (UI label: Allow all actions; read or take action with this plugin without asking; elevated risk).\n  value: \"inherit\" | \"always_ask\" | \"ask_before_writes\" | \"review_important_actions\" | \"full_access\";\n}> | null;\n  // Global default permission updates to apply.\n  global_permissions?: Array<{\n  // Permission setting to update. This field is optional; omit it unless needed. If provided, use permission_mode.\n  setting?: \"permission_mode\";\n  // New value for the global permission setting. Options: always_ask (UI label: Always ask; ask before reading or making changes), ask_before_writes (UI label: Allow read actions; read without asking but ask before making changes), review_important_actions (UI label: Allow low-risk actions; automatically approve low-risk actions but may deny actions involving sensitive information), and full_access (UI label: Allow all actions; read or take action without asking; elevated risk and may be unavailable globally when the feature gate hides it).\n  value: \"always_ask\" | \"ask_before_writes\" | \"review_important_actions\" | \"full_access\";\n}> | null;\n};\n}): Promise<CallToolResult<{\n  // The server's response to a tool call.\n  result: { _meta?: { [key: string]: unknown; } | null; content: Array<{ _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; text: string; type: \"text\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"image\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; data: string; mimeType: string; type: \"audio\"; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; description?: string | null; icons?: Array<{ mimeType?: string | null; sizes?: Array<string> | null; src: string; }> | null; mimeType?: string | null; name: string; size?: number | null; title?: string | null; type: \"resource_link\"; uri: string; } | { _meta?: { [key: string]: unknown; } | null; annotations?: { audience?: Array<\"user\" | \"assistant\"> | null; priority?: number | null; } | null; resource: { _meta?: { [key: string]: unknown; } | null; mimeType?: string | null; text: string; uri: string; } | { _meta?: { [key: string]: unknown; } | null; blob: string; mimeType?: string | null; uri: string; }; type: \"resource\"; }>; isError?: boolean; structuredContent?: { [key: string]: unknown; } | null; };\n}>>; };\n```"},{"name":"mcp__laravel_boost__record_rule","description":"Laravel ecosystem MCP server offering database schema access, error logs, semantic documentation search, and more. Boost helps with code generation.\n\nRecord a durable project rule so the next agent or teammate inherits it instead of working it out again. Use it for a settled decision (why the project does something a certain way), a non-obvious trap, or a standing constraint that must always be followed. Pass a glob for the files it applies to (e.g. app/Http/Controllers/**) and Boost files it into a shared, committed markdown note grouped by area. Keep it to a few lines; only record what you would want to read in three months. Do not record secrets, transient state, or anything already obvious from the code.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__laravel_boost__record_rule(args: {\n  // Glob for the files this rule applies to, for example \"app/Http/Controllers/**\" or \"app/Models/*.php\". This routes the rule into a shared area file and is how agents find it later.\n  glob: string;\n  // A few lines stating the rule plainly. No essays.\n  note: string;\n  // A short, specific heading, for example \"Extend BaseController for tenant scoping\".\n  title: string;\n}): Promise<CallToolResult>; };\n```"},{"name":"multi_agent_v1__close_agent","description":"Tools for spawning and managing sub-agents.\n\nClose an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Completed agents remain open and count toward the concurrency limit until closed. Don't keep agents open for too long if they are not needed anymore.\n\nexec tool declaration:\n```ts\ndeclare const tools: { multi_agent_v1__close_agent(args: {\n  // Agent id to close (from spawn_agent).\n  target: string;\n}): Promise<{\n  // The agent status observed before shutdown was requested.\n  previous_status: \"pending_init\" | \"running\" | \"interrupted\" | \"shutdown\" | \"not_found\" | { completed: string | null; } | { errored: string; };\n}>; };\n```"},{"name":"multi_agent_v1__resume_agent","description":"Tools for spawning and managing sub-agents.\n\nResume a previously closed agent by id so it can receive send_input and wait_agent calls.\n\nexec tool declaration:\n```ts\ndeclare const tools: { multi_agent_v1__resume_agent(args: {\n  // Agent id to resume.\n  id: string;\n}): Promise<{ status: \"pending_init\" | \"running\" | \"interrupted\" | \"shutdown\" | \"not_found\" | { completed: string | null; } | { errored: string; }; }>; };\n```"},{"name":"multi_agent_v1__send_input","description":"Tools for spawning and managing sub-agents.\n\nSend a message to an existing agent. Use interrupt=true to redirect work immediately. You should reuse the agent by send_input if you believe your assigned task is highly dependent on the context of a previous task.\n\nexec tool declaration:\n```ts\ndeclare const tools: { multi_agent_v1__send_input(args: {\n  // True interrupts the current task and handles this message immediately; false or omitted queues it.\n  interrupt?: boolean;\n  // Structured input items. Use this to pass explicit mentions (for example app:// connector paths).\n  items?: Array<{\n  // Audio data URL when type is audio.\n  audio_url?: string;\n  // Image URL when type is image.\n  image_url?: string;\n  // Display name when type is skill or mention.\n  name?: string;\n  // Path when type is local_image/local_audio/skill, or structured mention target such as app://<connector-id> or plugin://<plugin-name>@<marketplace-name> when type is mention.\n  path?: string;\n  // Text content when type is text.\n  text?: string;\n  // Input item type: text, image, local_image, audio, local_audio, skill, or mention.\n  type?: string;\n}>;\n  // Legacy plain-text message to send to the agent. Use either message or items.\n  message?: string;\n  // Agent id to message (from spawn_agent).\n  target: string;\n}): Promise<{\n  // Identifier for the queued input submission.\n  submission_id: string;\n}>; };\n```"},{"name":"multi_agent_v1__spawn_agent","description":"Tools for spawning and managing sub-agents.\n\n\n        \n        Available model overrides (optional; inherited parent model is preferred):\n- `gpt-5.6-sol`: Latest frontier agentic coding model. Reasoning efforts: low (default), medium, high, xhigh, max, ultra. Service tiers: priority.\n- `gpt-5.6-terra`: Balanced agentic coding model for everyday work. Reasoning efforts: low, medium (default), high, xhigh, max, ultra. Service tiers: priority.\n- `gpt-5.6-luna`: Fast and affordable agentic coding model. Reasoning efforts: low, medium (default), high, xhigh, max. Service tiers: priority.\n- `gpt-5.5`: Frontier model for complex coding, research, and real-world work. Reasoning efforts: low, medium (default), high, xhigh. Service tiers: priority.\n- `gpt-5.4`: Strong model for everyday coding. Reasoning efforts: low, medium (default), high, xhigh. Service tiers: priority.\n        Spawn a sub-agent for a well-scoped task. Returns the spawned agent id plus the user-facing nickname when available. Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed.\nThis spawn_agent tool provides you access to sub-agents that inherit your current model by default. Do not set the `model` field unless the user explicitly asks for a different model or there is a clear task-specific reason. You should follow the rules and guidelines below to use this tool.\n\nDo not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.\nRequests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.\nAgent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself.\n\n### When to delegate vs. do the subtask yourself\n- First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it.\n- Use a subagent when a subtask is easy enough for it to handle and can run in parallel with your local work. Prefer delegating concrete, bounded sidecar tasks that materially advance the main task without blocking your immediate next local step.\n- Do not delegate urgent blocking work when your immediate next step depends on that result. If the very next action is blocked on that task, the main rollout should usually do it locally to keep the critical path moving.\n- Keep work local when the subtask is too difficult to delegate well and when it is tightly coupled, urgent, or likely to block your immediate next step.\n\n### Designing delegated subtasks\n- Subtasks must be concrete, well-defined, and self-contained.\n- Delegated subtasks must materially advance the main task.\n- Do not duplicate work between the main rollout and delegated subtasks.\n- Avoid issuing multiple delegate calls on the same unresolved thread unless the new delegated task is genuinely different and necessary.\n- Narrow the delegated ask to the concrete output you need next.\n- For coding tasks, prefer delegating concrete code-change worker subtasks over read-only explorer analysis when the subagent can make a bounded patch in a clear write scope.\n- When delegating coding work, instruct the submodel to edit files directly in its forked workspace and list the file paths it changed in the final answer.\n- For code-edit subtasks, decompose work so each delegated task has a disjoint write set.\n\n### After you delegate\n- Call wait_agent very sparingly. Only call wait_agent when you need the result immediately for the next critical-path step and you are blocked until it returns.\n- Do not redo delegated subagent tasks yourself; focus on integrating results or tackling non-overlapping work.\n- While the subagent is running in the background, do meaningful non-overlapping work immediately.\n- Do not repeatedly wait by reflex.\n- When a delegated coding task returns, quickly review the uploaded changes, then integrate or refine them.\n\n### Parallel delegation patterns\n- Run multiple independent information-seeking subtasks in parallel when you have distinct questions that can be answered independently.\n- Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap.\n- Delegate verification only when it can run in parallel with ongoing implementation and is likely to catch a concrete risk before final integration.\n- The key is to find opportunities to spawn multiple independent subtasks in parallel within the same round, while ensuring each subtask is well-defined, self-contained, and materially advances the main task.\n\nexec tool declaration:\n```ts\ndeclare const tools: { multi_agent_v1__spawn_agent(args: {\n  // True forks the current thread history into the new agent; false or omitted starts with only the initial prompt.\n  fork_context?: boolean;\n  // Structured input items. Use this to pass explicit mentions (for example app:// connector paths).\n  items?: Array<{\n  // Audio data URL when type is audio.\n  audio_url?: string;\n  // Image URL when type is image.\n  image_url?: string;\n  // Display name when type is skill or mention.\n  name?: string;\n  // Path when type is local_image/local_audio/skill, or structured mention target such as app://<connector-id> or plugin://<plugin-name>@<marketplace-name> when type is mention.\n  path?: string;\n  // Text content when type is text.\n  text?: string;\n  // Input item type: text, image, local_image, audio, local_audio, skill, or mention.\n  type?: string;\n}>;\n  // Initial plain-text task for the new agent. Use either message or items.\n  message?: string;\n  // Model override for the new agent. Omit unless an explicit override is needed.\n  model?: string;\n  // Reasoning effort override for the new agent. Omit to inherit the parent effort.\n  reasoning_effort?: string;\n  // Service tier override for the new agent. Omit unless explicitly requested.\n  service_tier?: string;\n}): Promise<{\n  // Thread identifier for the spawned agent.\n  agent_id: string;\n  // User-facing nickname for the spawned agent when available.\n  nickname: string | null;\n}>; };\n```"},{"name":"multi_agent_v1__wait_agent","description":"Tools for spawning and managing sub-agents.\n\nWait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status.\n\nexec tool declaration:\n```ts\ndeclare const tools: { multi_agent_v1__wait_agent(args: {\n  // Agent ids to wait on. Pass multiple ids to wait for whichever finishes first.\n  targets: Array<string>;\n  // Timeout in milliseconds. Defaults to 30000, min 10000, max 3600000. Prefer longer waits (minutes) to avoid busy polling.\n  timeout_ms?: number;\n}): Promise<{\n  // Final statuses keyed by agent id.\n  status: { [key: string]: \"pending_init\" | \"running\" | \"interrupted\" | \"shutdown\" | \"not_found\" | { completed: string | null; } | { errored: string; }; };\n  // Whether the wait call returned due to timeout before any agent reached a final status.\n  timed_out: boolean;\n}>; };\n```"},{"name":"update_goal","description":"Update the existing goal.\nUse this tool only to mark the goal achieved or genuinely blocked.\nSet status to `complete` only when the objective has actually been achieved and no required work remains.\nSet status to `blocked` only when the same blocking condition has repeated for at least three consecutive goal turns, counting the original/user-triggered turn and any automatic continuations, and the agent cannot make meaningful progress without user input or an external-state change.\nIf the user resumes a goal that was previously marked `blocked`, treat the resumed run as a fresh blocked audit. If the same blocking condition then repeats for at least three consecutive resumed goal turns, set status to `blocked` again.\nOnce the blocked threshold is satisfied, do not keep reporting that you are still blocked while leaving the goal active; set status to `blocked`.\nDo not use `blocked` merely because the work is hard, slow, uncertain, incomplete, or would benefit from clarification.\nDo not mark a goal complete merely because its budget is nearly exhausted or because you are stopping work.\nYou cannot use this tool to pause, resume, budget-limit, or usage-limit a goal; those status changes are controlled by the user or system.\nWhen marking a budgeted goal achieved with status `complete`, report the final token usage from the tool result to the user.\n\nexec tool declaration:\n```ts\ndeclare const tools: { update_goal(args: {\n  // Required. Set to `complete` only when the objective is achieved and no required work remains. Set to `blocked` only after the same blocking condition has recurred for at least three consecutive goal turns and the agent is at an impasse. After a previously blocked goal is resumed, the resumed run starts a fresh blocked audit.\n  status: \"complete\" | \"blocked\";\n}): Promise<unknown>; };\n```"},{"name":"update_plan","description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n\n\nexec tool declaration:\n```ts\ndeclare const tools: { update_plan(args: {\n  // Optional explanation for this plan update.\n  explanation?: string;\n  // The list of steps\n  plan: Array<{\n  // Step status.\n  status: \"pending\" | \"in_progress\" | \"completed\";\n  // Task step text.\n  step: string;\n}>;\n}): Promise<unknown>; };\n```"}]


text: The focused and full Pest suites are green: 82 tests, 265 assertions. I’m sending one final independent read-only audit across the specs and implementation while I prepare the release checks and progress record.


status: completed

input:
const r = await tools.multi_agent_v1__spawn_agent({
  fork_context: true,
  message: "Perform a final independent read-only audit of /Users/wesner/Herd/shop against specs/01–09 and the current implementation. Do not edit files. Focus on critical/major correctness, security, schema, route, tenant-isolation, and UI/runtime gaps that are not covered by the passing tests. Inspect the actual code and report only actionable findings with file paths and severity; if no critical/major issues remain, say so explicitly.",
  reasoning_effort: "high"
});
text(r);


type: CollabAgentToolCall

status: completed

agent_nickname: Darwin

prompt: Perform a final independent read-only audit of /Users/wesner/Herd/shop against specs/01–09 and the current implementation. Do not edit files. Focus on critical/major correctness, security, schema, route, tenant-isolation, and UI/runtime gaps that are not covered by the passing tests. Inspect the actual code and report only actionable findings with file paths and severity; if no critical/major issues remain, say so explicitly.

reasoning_effort: high


text:
Script completed
Wall time 0.1 seconds
Output:

text: {"agent_id":"01a0219c-6293-7b40-b5e1-7376c4c0fe0d","nickname":"Darwin"}


status: completed

input:
const r = await tools.exec_command({
  cmd: "sed -n '1,260p' specs/progress.md && printf '\\n--- migrations ---\\n' && php artisan migrate:status --no-interaction && printf '\\n--- modified PHP lint ---\\n' && find app database routes config bootstrap tests -type f -name '*.php' -print0 | xargs -0 -n1 php -l >/tmp/shop_php_lint.log && tail -n 5 /tmp/shop_php_lint.log",
  workdir: "/Users/wesner/Herd/shop",
  yield_time_ms: 1000,
  max_output_tokens: 16000
});
text(JSON.stringify(r));


text:
Script completed
Wall time 1.2 seconds
Output:

text: {"chunk_id":"5ed617","wall_time_seconds":1.001429208,"session_id":89271,"original_token_count":965,"output":"# Implementation Progress\n\nThe self-contained multi-tenant shop is implemented across the database, commerce domain, storefront, admin, API, authentication, seed data, and delivery integrations described in specs 01–09.\n\n## Completed\n\n- [x] Foundation: SQLite configuration, tenant schema/models, store resolution, global tenant scope, roles, policies, and Fortify authentication.\n- [x] Catalog: products, variants, options, inventory, collections, media, status transitions, search indexing, and deterministic tenant fixtures.\n- [x] Storefront: home, collections, product detail, variant selection, responsive layouts, cart, cart drawer, search, static pages, customer accounts, and order history.\n- [x] Checkout: session/customer carts, optimistic versions, addresses, shipping rates, taxes, discounts, payment methods, expiry, order snapshots, confirmation, and saved addresses.\n- [x] Commerce operations: payment idempotency, inventory reservations, cancellation, refunds with line allocations/restocking, fulfillment guards, and webhook delivery with signatures/retries.\n- [x] Admin: dashboard metrics, product and collection CRUD, inventory, orders, customers, discounts, pages, navigation, themes, settings, analytics, apps, search settings, and developer tokens/webhooks.\n- [x] API/security: session and Sanctum authentication, token abilities, tenant ownership checks, rate limiting, CORS, scoped customer password reset tokens, audit logging, and error pages.\n- [x] Test data: canonical `DatabaseSeeder` plus isolated `ShopSeeder` compatibility fixtures, factories, and idempotency assertions.\n\n## Verification\n\n- `php artisan test --compact`: 71 passing tests, 208 assertions.\n- `vendor/bin/pint --dirty --format agent`: passing.\n- `php artisan view:cache --no-interaction`: passing.\n- `npm run build`: passing.\n- Playwright browser smoke checks: storefront home, product detail, cart drawer, collections, search, cart, responsive mobile layout, admin login/dashboard/products/orders/developers/settings, Flux domain modal, and mobile admin navigation; no application console errors observed.\n\n## Final audit\n\n- Independent read-only audit is in progress against specs 01–09. Any remaining findings will be recorded here before the final commit.\n\n--- migrations ---\n\n  Migration name .............................................. Batch / Status  \n  0001_01_01_000000_create_users_table ............................... [1] Ran  \n  0001_01_01_000001_create_cache_table ............................... [1] Ran  \n  0001_01_01_000002_create_jobs_table ................................ [1] Ran  \n  2025_08_14_170933_add_two_factor_columns_to_users_table ............ [1] Ran  \n  2026_08_20_000000_create_shop_schema ............................... [1] Ran  \n  2026_08_20_210057_add_enum_defaults_to_tenancy_tables .............. [1] Ran  \n  2026_08_20_210057_add_settings_json_to_store_settings_table ........ [1] Ran  \n  2026_08_20_210057_add_tls_mode_to_store_domains_table .............. [1] Ran  \n  2026_08_20_220000_add_search_and_auth_support ...................... [1] Ran  \n  2026_08_20_220001_add_webhook_delivery_status ...................... [1] Ran  \n  2026_08_20_220002_add_spec_compatibility_columns ................... [1] Ran  \n  2026_08_20_220003_add_discount_code_to_carts ....................... [1] Ran  \n  2026_08_20_223045_add_remaining_schema_contract_columns ............ [2] Ran  \n  2026_08_20_224526_add_line_allocations_to_refunds_table ............ [3] Ran  \n  2026_08_20_230144_add_remaining_spec_contract_fields ............... [4] Ran  \n  2026_08_20_231709_scope_customer_password_reset_tokens_by_store .... [5] Ran  \n  2026_08_20_234417_align_theme_settings_password_reset_and_webhook_contracts  [6] Ran  \n  2026_08_20_235245_remove_legacy_settings_from_themes_table ......... [7] Ran  \n\n\n--- modified PHP lint ---\n"}


status: completed

input:
const r = await tools.write_stdin({session_id: 89271, chars: "", yield_time_ms: 1000, max_output_tokens: 16000});
text(r.output);


text:
Script completed
Wall time 5.0 seconds
Output:


command: sed -n '1,260p' specs/progress.md && printf '\n--- migrations ---\n' && php artisan migrate:status --no-interaction && printf '\n--- modified PHP lint ---\n' && find app database routes config bootstrap tests -type f -name '*.php' -print0 | xargs -0 -n1 php -l >/tmp/shop_php_lint.log && tail -n 5 /tmp/shop_php_lint.log

stdout:
No syntax errors detected in tests/Feature/ContractBehaviorTest.php
No syntax errors detected in tests/Feature/CommerceFlowTest.php
No syntax errors detected in tests/Feature/SeedDataTest.php
No syntax errors detected in tests/Pest.php
No syntax errors detected in tests/TestCase.php


status: completed

input:
const r = await tools.write_stdin({session_id: 89271, chars: "", yield_time_ms: 1000, max_output_tokens: 16000});
text(r.output);


text:
Script completed
Wall time 0.0 seconds
Output:

text:
No syntax errors detected in tests/Feature/ContractBehaviorTest.php
No syntax errors detected in tests/Feature/CommerceFlowTest.php
No syntax errors detected in tests/Feature/SeedDataTest.php
No syntax errors detected in tests/Pest.php
No syntax errors detected in tests/TestCase.php


status: completed

input:
const patch = "*** Begin Patch\n*** Update File: /Users/wesner/Herd/shop/specs/progress.md\n@@\n-## Verification\n-\n-- `php artisan test --compact`: 71 passing tests, 208 assertions.\n-- `vendor/bin/pint --dirty --format agent`: passing.\n-- `php artisan view:cache --no-interaction`: passing.\n-- `npm run build`: passing.\n-- Playwright browser smoke checks: storefront home, product detail, cart drawer, collections, search, cart, responsive mobile layout, admin login/dashboard/products/orders/developers/settings, Flux domain modal, and mobile admin navigation; no application console errors observed.\n+## Verification\n+\n+- `php artisan test --compact`: 82 passing tests, 265 assertions.\n+- `vendor/bin/pint --dirty --format agent`: passing.\n+- PHP lint across application, database, routes, configuration, bootstrap, and tests: passing.\n+- `php artisan migrate:status --no-interaction`: all migrations applied, including tenant-scoped password reset tokens, webhook contract alignment, and normalized theme settings.\n+- `php artisan view:cache --no-interaction`: passing.\n+- `npm run build`: passing.\n+- Playwright MCP browser smoke checks: storefront home, product detail, cart drawer, collections, search, cart, responsive mobile layout, admin login/dashboard/products/orders/developers/settings, Flux domain modal, and mobile admin navigation; no application console errors observed.\n+- The Pest browser plugin is not installed and dependencies were intentionally left unchanged; the browser coverage above was executed manually through Playwright MCP.\n@@\n-- Independent read-only audit is in progress against specs 01–09. Any remaining findings will be recorded here before the final commit.\n+- Independent read-only audit is in progress against specs 01–09. Any remaining findings will be recorded here before the final commit.\n*** End Patch";
text(await tools.apply_patch(patch));


unified_diff:
@@ -17,7 +17,10 @@

-- `php artisan test --compact`: 71 passing tests, 208 assertions.
+- `php artisan test --compact`: 82 passing tests, 265 assertions.
 - `vendor/bin/pint --dirty --format agent`: passing.
+- PHP lint across application, database, routes, configuration, bootstrap, and tests: passing.
+- `php artisan migrate:status --no-interaction`: all migrations applied, including tenant-scoped password reset tokens, webhook contract alignment, and normalized theme settings.
 - `php artisan view:cache --no-interaction`: passing.
 - `npm run build`: passing.
-- Playwright browser smoke checks: storefront home, product detail, cart drawer, collections, search, cart, responsive mobile layout, admin login/dashboard/products/orders/developers/settings, Flux domain modal, and mobile admin navigation; no application console errors observed.
+- Playwright MCP browser smoke checks: storefront home, product detail, cart drawer, collections, search, cart, responsive mobile layout, admin login/dashboard/products/orders/developers/settings, Flux domain modal, and mobile admin navigation; no application console errors observed.
+- The Pest browser plugin is not installed and dependencies were intentionally left unchanged; the browser coverage above was executed manually through Playwright MCP.

stdout:
Success. Updated the following files:
M /Users/wesner/Herd/shop/specs/progress.md


text:
Script completed
Wall time 0.0 seconds
Output:


status: completed

input:
const results = await Promise.all([
  tools.exec_command({cmd: "npm run build", workdir: "/Users/wesner/Herd/shop", yield_time_ms: 1000, max_output_tokens: 12000}),
  tools.exec_command({cmd: "php artisan view:cache --no-interaction", workdir: "/Users/wesner/Herd/shop", yield_time_ms: 1000, max_output_tokens: 8000}),
  tools.exec_command({cmd: "git diff --check", workdir: "/Users/wesner/Herd/shop", yield_time_ms: 1000, max_output_tokens: 8000})
]);
results.forEach((r, i) => text(JSON.stringify({i, ...r})));


command:
/bin/zsh
-lc
git diff --check

cmd: git diff --check


command:
/bin/zsh
-lc
php artisan view:cache --no-interaction

cmd: php artisan view:cache --no-interaction

stdout: INFO  Blade templates cached successfully.


command:
/bin/zsh
-lc
npm run build

cmd: npm run build

stdout:
vite v7.3.5 building client environment for production...
(node:17521) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
transforming...
✓ 2 modules transformed.
Generated an empty chunk: "app".
rendering chunks...
computing gzip size...
public/build/manifest.json              0.33 kB │ gzip:  0.16 kB
public/build/assets/app-DME_u4E9.css  233.05 kB │ gzip: 31.84 kB
public/build/assets/app-l0sNRNKZ.js     0.00 kB │ gzip:  0.02 kB
✓ built in 389ms


text:
Script completed
Wall time 0.8 seconds
Output:

text: {"i":0,"chunk_id":"76b31c","wall_time_seconds":0.610893458,"exit_code":0,"original_token_count":153,"output":"\n> build\n> vite build\n\nvite v7.3.5 building client environment for production...\n(node:17521) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.\n(Use `node --trace-deprecation ...` to show where the warning was created)\ntransforming...\n✓ 2 modules transformed.\nGenerated an empty chunk: \"app\".\nrendering chunks...\ncomputing gzip size...\npublic/build/manifest.json              0.33 kB │ gzip:  0.16 kB\npublic/build/assets/app-DME_u4E9.css  233.05 kB │ gzip: 31.84 kB\npublic/build/assets/app-l0sNRNKZ.js     0.00 kB │ gzip:  0.02 kB\n✓ built in 389ms\n"}

text: {"i":1,"chunk_id":"c3bac7","wall_time_seconds":0.380294083,"exit_code":0,"original_token_count":13,"output":"\n\n   INFO  Blade templates cached successfully.  \n\n"}

text: {"i":2,"chunk_id":"adae31","wall_time_seconds":0.000001708,"exit_code":0,"original_token_count":0,"output":""}


status: completed

input:
const r = await tools.exec_command({
  cmd: "sed -n '1,360p' routes/api.php && sed -n '1,300p' app/Http/Middleware/ResolveStore.php && sed -n '1,220p' app/Http/Middleware/EnsureApiAbility.php && sed -n '1,220p' config/auth.php",
  workdir: "/Users/wesner/Herd/shop",
  yield_time_ms: 10000,
  max_output_tokens: 40000
});
text(r.output);