Skip to content

Sponsor

Laravel Nightwatch Integration

The self-healer's built-in listeners only see failures that happen inside your own process — a queue job that throws, a scheduled task that fails. Laravel Nightwatch sees production. It groups exceptions and performance problems into issues and fires a signed webhook when one opens.

Point that webhook at Tackle and the healer runs on production signal.

Nightwatch does the part the healer cannot do for itself: it dedupes thousands of occurrences into a single issue, so you get one pull request per problem rather than one per exception.

Configuration

env
TACKLE_NIGHTWATCH_ENABLED=true
TACKLE_NIGHTWATCH_SECRET=whsec_...   # Nightwatch → Issues settings → Webhooks → Edit

This registers POST /tackle/nightwatch/webhook. Add that URL in the Nightwatch dashboard under your application's Issues settings → Webhooks, then make sure a worker is consuming the healer queue:

bash
php artisan queue:work --queue=healer

php artisan tackle:health verifies the route, the secret, and reminds you about the worker.

The signing secret is generated by Nightwatch

You copy it from the dashboard into your .env — you do not choose it. If the two do not match, every delivery is rejected with a 403.

Performance issues, not just exceptions

This is what an exception-only integration cannot reach. Nightwatch reports slow routes, jobs, commands, and scheduled tasks as issues with the same open/resolved lifecycle as exceptions, so a regression arrives with its measured duration and the threshold it broke:

slow-route  GET|POST checkout/{order}  →  2400ms against a 1000ms threshold

The agent is pointed at the usual suspects in order — N+1 queries, missing indexes, work inside a loop, uncached external calls — and told explicitly not to change observable behaviour. If it cannot find a cause it is confident in, it is told to say so rather than guess.

Slow-route detection is opt-in, per route

Nightwatch has no default duration threshold. Until you create one for a specific route, no performance issue will ever open for it, no matter how slow it gets — and nothing will reach Tackle.

Create one under Settings → your application → Thresholds → Edit threshold, pick the route, and set a duration in milliseconds. This is the single most common reason a correctly-configured integration appears to do nothing.

Exceptions

Nightwatch sends the class, message, and file:line — but not a stack trace. That is enough, because unlike a hosted fixer the agent is already inside the codebase the issue points at and reads the rest itself. The prompt says so, and asks it to reproduce the condition in a failing test before changing anything.

Choosing which issues get healed

Every gate lives in config/tackle.php under nightwatch:

OptionEnvDefaultDescription
enabledTACKLE_NIGHTWATCH_ENABLEDfalseRegister the webhook route
secretTACKLE_NIGHTWATCH_SECRETSigning secret. Without it, every delivery is refused
pathTACKLE_NIGHTWATCH_PATHtackle/nightwatch/webhookRoute URI
middleware[]Extra middleware. Do not add the web group — CSRF rejects every delivery
eventsissue.opened, issue.reopenedissue.resolved and issue.ignored are ignored — nothing to fix
issue_typesexception, performanceNarrow to one if you only want exceptions healed
environments[] (all)e.g. ['production'] to ignore staging noise
min_priorityTACKLE_NIGHTWATCH_MIN_PRIORITYnoneFloor: none | low | medium | high
handled_exceptionsTACKLE_NIGHTWATCH_HANDLED_EXCEPTIONSfalseHeal exceptions the app caught. Off — usually deliberate
cooldownTACKLE_NIGHTWATCH_COOLDOWN86400Seconds before the same issue can be healed again

Environment names come from Nightwatch, not APP_ENV

A Nightwatch environment named "Production" does not have to correspond to APP_ENV=production. environments matches the Nightwatch name, which is why it defaults to empty (all environments) rather than guessing.

Security

Every request must carry a valid Nightwatch-Signature — HMAC-SHA256 over the raw request body, compared with hash_equals(). Unsigned or mis-signed requests get a 403 and are logged.

If no secret is configured the endpoint refuses everything rather than dispatching an agent on unauthenticated input.

Rejections that are deliberate — an event you do not heal, an issue below your priority floor — return 200, because a non-2xx tells the sender to retry and there is nothing to retry about a decision.

Everything else is the same as any other heal: worktree isolation, budget caps, PathGuard, and a full test run before the PR.

Testing it without waiting for production to break

Sign a payload yourself and post it at the endpoint:

bash
SECRET="whsec_..."   # same value as TACKLE_NIGHTWATCH_SECRET

BODY='{"event":"issue.opened","payload":{"issue":{"id":"local-test-1","ref":1,"type":"performance","title":"Slow route: /checkout","status":"open","priority":"high","url":"https://nightwatch.laravel.com/issues/1","details":{"type":"slow-route","methods":["GET"],"path":"checkout","action":"App\\Http\\Controllers\\CheckoutController@show","duration":2400,"threshold":1000}},"environment":{"name":"production"}}}'

SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)

curl -sS -X POST http://localhost:8000/tackle/nightwatch/webhook \
  -H "Content-Type: application/json" \
  -H "Nightwatch-Signature: $SIG" \
  --data-binary "$BODY"

A {"status":"queued"} response means the healer picked it up. Change the id between runs, or the cooldown will skip the second one.

Gotchas

queue:work caches config at boot

Changing AI_CODE_HEALING_MODE, AI_CODE_PROVIDER, or AI_CODE_BUDGET in .env has no effect on a worker that is already running. Restart the worker after any config change, or it will keep healing with what it booted with.

LOG_CHANNEL=nightwatch sends logs to Nightwatch only

Nothing lands in storage/logs, which makes a failing heal hard to diagnose locally. Stack the channels instead:

env
LOG_CHANNEL=stack
LOG_STACK=single,nightwatch

One webhook per application. If yours already points at Slack, you will need an endpoint that fans out to both.

Disable ingest in tests. Set NIGHTWATCH_ENABLED=false in phpunit.xml so the agent's own sandbox test runs do not pollute production metrics.

Closing the loop

Tackle opens the PR; Nightwatch confirms the fix. Once it deploys and the issue stops recurring, Nightwatch marks it resolved on its own — and for a performance issue, the duration graph is what tells you the change actually worked, not the diff.

Nightwatch also ships an MCP server for browsing issues and updating their status from an interactive session. This integration covers the unattended direction: no human in the loop at all.

Released under the MIT License.