Skip to content

Sponsor

Self-Healing

When enabled, Tackle listens for failed queue jobs and failed scheduled commands, dispatches an AI agent to diagnose the exception, patch the code, verify the fix with your test suite, and either open a pull request or apply the fix directly — all without you lifting a finger.

The same runtime also accepts production issues pushed in by Laravel Nightwatch, which extends it past failures in this process — and past exceptions, to slow routes, jobs, commands, and scheduled tasks.

How it works

  1. A job fails → Laravel fires the JobFailed event.
  2. Tackle's JobFailureListener picks it up and dispatches a HealJobFailure job to the healer queue (a separate queue from your normal workers).
  3. A dedicated queue worker picks up HealJobFailure. It:
    • Creates an isolated git worktree on a fresh branch (tackle/heal-{id}).
    • Spins up a HealingAgent pointed at that worktree.
    • Feeds the agent the exception class, message, stack trace, and (if Telescope is installed) the full Telescope exception entry.
    • The agent reads the failing code, applies a minimal fix via EditFile, and runs your test suite to verify.
  4. After the agent finishes:
    • pr mode (default): pushes the branch to GitHub and opens a pull request with the agent's reasoning as the description.
    • patch mode: merges the fix back into your main workspace branch and re-dispatches the original job.
  5. The worktree is cleaned up regardless of outcome.

Prerequisites

  • Your project must be a git repository with a remote named origin.
  • A queue worker must be running the healer queue (see below).
  • For PR mode, a GitHub personal access token is required.
  • For patch mode, the working tree must be clean when healing runs.

Enabling the healer

Publish and run the migration, then enable via .env:

bash
php artisan vendor:publish --tag="tackle-migrations"
php artisan migrate
env
AI_CODE_HEALING_ENABLED=true

The event listeners register automatically once this is set to true.

Starting the healer worker

The healer runs on a dedicated queue to avoid competing with your normal workers:

bash
php artisan queue:work --queue=healer

Run this alongside your existing workers. In production (Supervisor, Forge, etc.) add a separate process group for the healer queue.

For local development, the healer slots neatly into a @laravel/multiplex tab next to the rest of your stack:

bash
npx @laravel/multiplex \
  'server,php artisan serve' \
  'queue,php artisan queue:listen' \
  'vite,npm run dev' \
  'healer@green,php artisan queue:work --queue=healer'

When a job throws in the queue tab, watch the healer tab diagnose it, patch the code, and post the PR link — your dev environment healing itself. (Multiplex spawns commands without stdin, so it suits the healer and ai:run; the interactive ai:code and ai:fix sessions need a real terminal.)

queue:work caches config at boot

A running worker will not see changes to AI_CODE_HEALING_MODE, AI_CODE_PROVIDER, or AI_CODE_BUDGET in .env. Restart it after any config change, or it keeps healing with whatever it booted with.

GitHub token setup

For PR mode, Tackle needs a GitHub token with the repo scope.

Resolution order:

  1. GITHUB_TOKEN in .env (or the tackle.healing.github_token config key)
  2. GitHub CLI (~/.config/gh/hosts.yml) — if you have gh installed and authenticated, Tackle reads your token automatically with no extra config.
  3. If no token is found, the branch is pushed but the PR is not opened. A log entry records that you need to configure a token.
env
GITHUB_TOKEN=ghp_...

Configuration

All healer options live under the healing key in config/tackle.php:

Verification gate

A heal is only auto-applied (in patch mode) when it passes a gate the harness runs itself — the agent cannot declare itself done:

  • Regression-test-first. The healer is instructed to add a test that reproduces the failure before fixing, then show it green. New test files are detected and reported.
  • No new failures. The suite is baselined before the fix, so the gate requires that the fix introduce no new failures — not that the whole suite is green (real apps carry pre-existing failures). Set baseline=false to fall back to "suite green" on very slow suites.
  • The regression test is proven. When a heal is otherwise clean, the added test is run with the fix reverted (must fail) then restored (must pass) — a red→green proof that it actually reproduces the bug, surfaced in the PR evidence.
  • A fix must change code. A heal that touches only test files (or nothing) is treated as incomplete — never auto-applied, and opened as a PR tagged [incomplete]. This stops an agent from "passing" a performance heal by adding a green test and changing no code.
  • Static analysis must be clean. Larastan/PHPStan runs over the heal's changed files; new errors hold it back from auto-apply and flag the PR [needs review]. Skipped if PHPStan isn't installed. The heal's changes are also auto-formatted with Pint before the PR.
  • Blast-radius limits. A heal that touches too many files, changes too many lines, or modifies a migration / config/* / composer.json is never auto-applied — it opens a PR flagged [needs review]. (Adding a new migration is fine.)

Every heal PR carries a Heal evidence block — new failures (if any), pre-existing failures, whether a regression test was added, files touched, and diff size — so review is quick and the "fixed" claim is backed rather than asserted. A PR that failed the gate is titled [tests failing] or [needs review].

OptionEnv varDefaultDescription
enabledAI_CODE_HEALING_ENABLEDfalseEnable or disable the healer
modeAI_CODE_HEALING_MODEprpr = open a pull request; patch = apply directly
modelAI_CODE_HEALING_MODEL(falls back to tackle.model)Model heals run on. Heals run unattended on the queue, so pinning a cheaper model here than your interactive tackle.model is common
providerAI_CODE_HEALING_PROVIDER(falls back to tackle.provider)Provider heals run on
queueAI_CODE_HEALING_QUEUEhealerQueue name for the HealJobFailure job
thresholdAI_CODE_HEALING_THRESHOLD1Number of failures before healing triggers
base_branchAI_CODE_HEALING_BASE_BRANCHmainBranch PRs are opened against
branch_prefixAI_CODE_HEALING_BRANCH_PREFIXtackle/heal-Prefix for fix branches
baselineAI_CODE_HEALING_BASELINEtrueRun the suite before the fix so the gate keys on new failures, not a fully green suite. Set false for very slow suites
static_analysisAI_CODE_HEALING_STATIC_ANALYSIStrueRun Larastan/PHPStan over the changed files and gate on it (skipped if not installed)
max_filesAI_CODE_HEALING_MAX_FILES20A heal touching more files is held back from auto-apply and flagged for review
max_diff_linesAI_CODE_HEALING_MAX_DIFF_LINES400A heal changing more lines is held back from auto-apply
protected_from_healing(config only)migrations, config/*, composer.json/.lock, .env*Modifying (not adding) these forces human review
github_tokenGITHUB_TOKENGitHub token for opening PRs
telescopeAI_CODE_HEALING_TELESCOPEtrueUse Telescope context if available

Failure threshold

By default (threshold=1) the healer triggers on the first failure. If you want the healer to wait until a job has failed a certain number of times before intervening (e.g. to let transient failures resolve themselves), set:

env
AI_CODE_HEALING_THRESHOLD=3

PR mode vs patch mode

pr (default)patch
Human review requiredYes — merge the PRNo — merged automatically
Tests must passNo (PR opened regardless)Yes (only merges on green)
Job re-dispatchedNoYes, after merge
Best forProduction / sensitive codeCI environments / trusted agents

Laravel Telescope integration

If Laravel Telescope is installed in your application, Tackle uses it to give the agent richer context: the full exception entry including class, message, and stack frames. No extra configuration is needed — Tackle detects Telescope automatically and degrades gracefully if it is not present.

Scheduled command healing

Tackle also listens to the ScheduledTaskFailed event, which Laravel fires when a task registered in App\Console\Kernel::schedule() (or a Schedule class) throws an exception.

The healing flow is identical to queue jobs — an isolated git worktree, an AI agent, a test run, then a PR or patch. The one difference: scheduled tasks are not re-dispatched after a patch (they run on their own schedule). The fix simply takes effect the next time the task runs.

No extra configuration is needed beyond AI_CODE_HEALING_ENABLED=true.

Production issues from Laravel Nightwatch

The listeners above only fire for failures inside your own process. To heal what production sees — including performance regressions, which no exception-based integration reaches — wire up the Laravel Nightwatch webhook:

env
TACKLE_NIGHTWATCH_ENABLED=true
TACKLE_NIGHTWATCH_SECRET=whsec_...

Nightwatch groups occurrences into a single issue and fires once when it opens, so you get one pull request per problem rather than one per exception. See the Nightwatch integration page for the gates, the signature scheme, and the setup steps that are easy to miss.

Per-class opt-out

Some jobs should never be auto-patched — payment processors, email senders, anything where an untested change would be worse than the failure. Use the #[Healable(false)] attribute to opt out:

php
use Tackle\Attributes\Healable;

#[Healable(false)]
class ChargeSubscription implements ShouldQueue
{
    public function handle(): void
    {
        // Tackle will skip this job entirely — even when AI_CODE_HEALING_ENABLED=true.
    }
}

The listener checks for the attribute via reflection before dispatching a heal job. Jobs without the attribute, or with #[Healable(true)], are healed normally.

Audit log

Every healing attempt — successful or not — is written to the tackle_healing_log table. View recent entries with:

bash
php artisan tackle:healing-log

The table output shows when, what failed, whether tests passed, the outcome, and a link to the PR or branch:

+-------------+----------------+--------------------+-----------+-------+------------+
| When        | Type           | Subject            | Tests     | Out.  | PR / Branch|
+-------------+----------------+--------------------+-----------+-------+------------+
| 2 mins ago  | job            | BrokenJob          | ✗         | PR    | github.com/|
| 1 hour ago  | scheduled_task | SendWeeklyReport   | ✓         | patched| tackle/... |
+-------------+----------------+--------------------+-----------+-------+------------+

Filters:

bash
# Show only job failures
php artisan tackle:healing-log --type=job

# Show only scheduled task failures
php artisan tackle:healing-log --type=scheduled_task

# Show only successful patches
php artisan tackle:healing-log --outcome=patched

# Show only PR-mode results
php artisan tackle:healing-log --outcome=pr_opened

# Show more entries
php artisan tackle:healing-log --limit=50

The audit log requires the migration to have been run:

bash
php artisan vendor:publish --tag="tackle-migrations"
php artisan migrate

If the migration has not been run, healing continues normally — the log write degrades gracefully.

Replaying a healing attempt

Use tackle:replay to re-dispatch a previous healing attempt after adjusting config or fixing something manually.

Healer limitations

  • The healer targets code bugs — logic errors the AI can diagnose and fix. It is not designed for infrastructure issues (database down, disk full, etc.).
  • The fix branch is pushed to origin — your CI pipeline will run on it and can catch anything the local test run missed.
  • In patch mode, if tests fail the healer falls back to PR mode automatically so nothing is merged without verification.
  • The healer never modifies .env, vendor/, storage/, or .git/ — the same path guards apply as in interactive mode.
  • Healer jobs have $tries = 1. A failing healer does not create a healing loop.

Released under the MIT License.