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
- A job fails → Laravel fires the
JobFailedevent. - Tackle's
JobFailureListenerpicks it up and dispatches aHealJobFailurejob to thehealerqueue (a separate queue from your normal workers). - A dedicated queue worker picks up
HealJobFailure. It:- Creates an isolated git worktree on a fresh branch (
tackle/heal-{id}). - Spins up a
HealingAgentpointed 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.
- Creates an isolated git worktree on a fresh branch (
- After the agent finishes:
prmode (default): pushes the branch to GitHub and opens a pull request with the agent's reasoning as the description.patchmode: merges the fix back into your main workspace branch and re-dispatches the original job.
- 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
healerqueue (see below). - For PR mode, a GitHub personal access token is required.
- For
patchmode, the working tree must be clean when healing runs.
Enabling the healer
Publish and run the migration, then enable via .env:
php artisan vendor:publish --tag="tackle-migrations"
php artisan migrateAI_CODE_HEALING_ENABLED=trueThe 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:
php artisan queue:work --queue=healerRun 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:
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:
GITHUB_TOKENin.env(or thetackle.healing.github_tokenconfig key)- GitHub CLI (
~/.config/gh/hosts.yml) — if you haveghinstalled and authenticated, Tackle reads your token automatically with no extra config. - 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.
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=falseto 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.jsonis 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].
| Option | Env var | Default | Description |
|---|---|---|---|
enabled | AI_CODE_HEALING_ENABLED | false | Enable or disable the healer |
mode | AI_CODE_HEALING_MODE | pr | pr = open a pull request; patch = apply directly |
model | AI_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 |
provider | AI_CODE_HEALING_PROVIDER | (falls back to tackle.provider) | Provider heals run on |
queue | AI_CODE_HEALING_QUEUE | healer | Queue name for the HealJobFailure job |
threshold | AI_CODE_HEALING_THRESHOLD | 1 | Number of failures before healing triggers |
base_branch | AI_CODE_HEALING_BASE_BRANCH | main | Branch PRs are opened against |
branch_prefix | AI_CODE_HEALING_BRANCH_PREFIX | tackle/heal- | Prefix for fix branches |
baseline | AI_CODE_HEALING_BASELINE | true | Run the suite before the fix so the gate keys on new failures, not a fully green suite. Set false for very slow suites |
static_analysis | AI_CODE_HEALING_STATIC_ANALYSIS | true | Run Larastan/PHPStan over the changed files and gate on it (skipped if not installed) |
max_files | AI_CODE_HEALING_MAX_FILES | 20 | A heal touching more files is held back from auto-apply and flagged for review |
max_diff_lines | AI_CODE_HEALING_MAX_DIFF_LINES | 400 | A 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_token | GITHUB_TOKEN | — | GitHub token for opening PRs |
telescope | AI_CODE_HEALING_TELESCOPE | true | Use 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:
AI_CODE_HEALING_THRESHOLD=3PR mode vs patch mode
pr (default) | patch | |
|---|---|---|
| Human review required | Yes — merge the PR | No — merged automatically |
| Tests must pass | No (PR opened regardless) | Yes (only merges on green) |
| Job re-dispatched | No | Yes, after merge |
| Best for | Production / sensitive code | CI 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:
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:
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:
php artisan tackle:healing-logThe 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:
# 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=50The audit log requires the migration to have been run:
php artisan vendor:publish --tag="tackle-migrations"
php artisan migrateIf 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
patchmode, 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.