jordandalton/laravel-tackle

An interactive, terminal-based AI coding assistant for Laravel — a Claude Code native to your Laravel app.

Maintainers

Package info

github.com/JordanDalton/laravel-tackle

pkg:composer/jordandalton/laravel-tackle

Transparency log

Statistics

Installs: 206

Dependents: 1

Suggesters: 2

Stars: 42

Open Issues: 0


README

An AI agent harness for Laravel.

📚 Full documentation: tackle.jordandalton.com

Tackle is the runtime layer that lets AI agents operate inside your Laravel application — reading code, executing tools, running tests, and taking action, with safety boundaries enforced at the framework level.

Think of it the way you think of Claude Code, Codex, or GitHub Copilot — but purpose-built for Laravel and installed directly into your app via Composer. The harness ships with three built-in agents and a full tool infrastructure you can extend or build on top of:

  • ai:code — an interactive coding agent that reads your codebase, edits files, runs tests, and formats code. Supports plan mode (approve a read-only plan before any edits), project-defined slash commands, and automatic context compaction for long sessions.
  • ai:run — the same agent with no terminal attached: one task, a JSON result, and an exit code, for CI and cron
  • ai:fix — a focused fix session: paste an exception, point it at a Sentry issue (--sentry=ID) or GitHub issue (--issue=N), and the agent diagnoses, patches, and verifies the fix. Runs in worktree mode by default.
  • ai:review — a read-only agent that reviews git diffs and surfaces real issues with severity levels. Point it at a GitHub pull request (--pr=42 --comment) and it posts the findings as inline PR review comments — drop it into a pull_request workflow and every PR gets reviewed automatically.
  • ai:respond — acts on a /tackle comment left on a pull request: applies the requested change, pushes it to the PR branch, and replies in the thread. Wire it to a workflow and reviewers can type /tackle fix this under any finding.
  • ai:explain — explains what a file, class, or method does in plain English
  • ai:test — generates a Pest test file for any class or method
  • ai:upgrade — safe major version upgrades for Composer dependencies: audits what is upgradable, plans from the package's upgrade guide, resolves constraints, fixes the breaking changes, and verifies with your test suite — in an isolated worktree, delivered as a PR
  • Self-healer — an autonomous agent that listens for failed jobs and scheduled tasks, diagnoses the exception, patches the code, and opens a PR or applies the fix — without you lifting a finger

Every agent runs through the same tool infrastructure and safety layer. You can add your own tools, write new agents, and swap the default agent entirely — all without forking the package. And the terminal isn't the only way to drive it: Tackle Remote puts the same harness in your phone's browser, approval prompts and all.

Built on top of laravel/ai — and provider-agnostic like it: Tackle runs on any provider laravel/ai supports with tool calling. Anthropic (Claude) is the default; OpenAI, Gemini, Groq, and fully local models via Ollama are two env vars away. See Changing the model or provider.

Contents

How the harness works

Tackle has three layers:

Layer What it is
Tools Action primitives agents can call — ReadFile, EditFile, RunTests, RunShell, etc. Every tool goes through PathGuard and shell policy before executing.
Agents Classes implementing CodingAgent that receive a prompt, call tools, and return a result. Tackle ships three; you can add your own.
Safety PathGuard blocks reads/writes outside the workspace and protected paths. BudgetTracker aborts the session when estimated spend exceeds the limit. Shell modes gate command execution. All enforced in PHP — not advisory.

The self-healer adds a fourth piece: an event-driven runtime that spins up an agent autonomously in an isolated git worktree whenever a job or scheduled task fails. It is the same harness, running unattended.

Before you start

Run through this checklist once before your first session:

  • Commit or stash any in-progress work — the agent will modify files, and a clean git state is your undo button.
  • Set ANTHROPIC_API_KEY in .env (or the key for your chosen provider).
  • Publish the laravel/ai config: php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
  • Publish the Tackle config: php artisan vendor:publish --tag="tackle-config" (publishes as config/tackle.php)
  • Run php artisan ai:code and type a small test task to confirm everything connects.

Requirements

  • PHP ^8.3
  • Laravel ^12.0
  • laravel/ai >=0.1 <0.11 (see Known Risks)
    • laravel/ai 0.1.x itself requires PHP ^8.4; on PHP 8.3 Composer will resolve 0.2 or newer

Installation

composer require jordandalton/laravel-tackle
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan vendor:publish --tag="tackle-config"

The service provider and ai:code command register automatically via Laravel package auto-discovery.

API key setup

Tackle uses Anthropic (Claude) by default. Add your key to .env:

ANTHROPIC_API_KEY=sk-ant-...

The config/ai.php published above already includes the anthropic provider block — just add the env var and you're ready.

Prefer another provider? Any provider in config/ai.php works, e.g. OpenAI:

AI_CODE_PROVIDER=openai
AI_CODE_MODEL=gpt-4o
OPENAI_API_KEY=sk-...

Budget rates resolve automatically from Tackle's built-in model catalog (Anthropic, plus common OpenAI, Gemini, and Grok models — run /model in ai:code to see them with their rates). For models the catalog doesn't know, pin rates explicitly so budget enforcement stays meaningful — e.g. fully local via Ollama, no API key, no cost:

AI_CODE_PROVIDER=ollama
AI_CODE_MODEL=qwen3-coder
AI_CODE_PRICE_INPUT=0
AI_CODE_PRICE_OUTPUT=0

You can also teach the catalog new models (or correct a stale built-in rate) in config/tackle.php under pricing.models. Non-Anthropic built-in rates are best-effort snapshots — verify against your provider's pricing page when the budget matters.

Have a ChatGPT subscription? The tackle-codex add-on runs every Tackle agent on OpenAI's Codex models using your ChatGPT plan — no per-token billing, usage records as $0 against the session budget:

php artisan tackle:install codex   # then: AI_CODE_PROVIDER=codex

Prefer Grok? The tackle-grok add-on runs the agents on xAI's Grok models — an xAI API key (the recommended path) or your grok.com plan:

php artisan tackle:install grok    # then: AI_CODE_PROVIDER=grok

Switch models per session with ai:code --model=... [--provider=...] (also on ai:run), or mid-session with /model.

Agent quality tracks the model: the coding and review agents lean heavily on tool calling, so weaker models produce weaker results. The plumbing is neutral; our recommendation is Claude.

Environment variables

All config options can be set via .env. Nothing requires editing a PHP file.

Variable Default Description
ANTHROPIC_API_KEY Your Anthropic API key (required for the default provider)
AI_CODE_PROVIDER anthropic Provider name — must match a key in config/ai.php
AI_CODE_MODEL claude-sonnet-4-6 Model to use
AI_CODE_MAX_STEPS 40 Tool-call ceiling for ai:run — cannot exceed the agent's own #[MaxSteps]
AI_CODE_BUDGET 1.00 Hard spend limit in USD per session
AI_CODE_COMPACTION_THRESHOLD 60000 Conversation size (chars) that triggers automatic history compaction
AI_CODE_COMPACTION_KEEP 4 Recent messages kept verbatim when compacting
AI_CODE_PRICE_INPUT auto Input price per million tokens for budget estimation. Unset = resolved from the built-in model catalog (falls back to 3.00 for unknown models)
AI_CODE_PRICE_OUTPUT auto Output price per million tokens for budget estimation. Unset = resolved from the built-in model catalog (falls back to 15.00 for unknown models)
AI_CODE_SHELL approve Shell mode: off | allowlist | approve | yolo. Can be set per-environment in config/tackle.php — production defaults to off.
AI_CODE_WORKTREE false Enable worktree isolation (production defaults to true).
AI_CODE_MEMORY file Session persistence: file (resume on next run) | none
AI_CODE_HEALING_ENABLED false Enable the self-healing queue worker feature
AI_CODE_HEALING_MODE pr Healing mode: pr | patch
AI_CODE_HEALING_QUEUE healer Queue name for the HealJobFailure job
AI_CODE_HEALING_THRESHOLD 1 Failures before healing is triggered
AI_CODE_HEALING_BASE_BRANCH main Base branch for fix pull requests
GITHUB_TOKEN GitHub token for opening pull requests (pr mode)

Usage

php artisan ai:code

Type a task at the prompt. The agent maintains full conversation history within a session, so you can follow up, ask questions, and give corrections naturally.

Type exit or quit to end the session.

For CI, cron, or anything without a terminal, use ai:run — the same agent, run once, with a structured result and an exit code.

Shell mode flag

Pass --shell to override the configured shell mode for a single session without touching your config or .env:

# Safe read-only exploration — no commands will run at all
php artisan ai:code --shell=off
php artisan ai:code --off          # shorthand

# Require your approval before every shell command (config default)
php artisan ai:code --shell=approve
php artisan ai:code --approve      # shorthand

# Only allow commands from shell_allowlist, no prompt
php artisan ai:code --shell=allowlist
php artisan ai:code --allowlist    # shorthand

# No restrictions, no prompts — CI or fully-trusted environments only
php artisan ai:code --shell=yolo
php artisan ai:code --yolo         # shorthand

The flag is session-scoped and does not persist to config.

Worktree mode

Worktree mode runs the agent against an isolated git worktree rather than your live files. All edits land in a temp directory; nothing touches your working tree until you open a PR.

php artisan ai:code --worktree      # force on for this session
php artisan ai:code --no-worktree   # force off for this session

When active, the intro line shows · worktree: on and a note box explains that live files are untouched. After each turn, the git diff stat is labelled "Worktree changes (live files untouched)" so it's clear no production code has been modified.

Production environments default to worktree: on (see Configuration). Worktrees are cleaned up automatically when the session ends. Use tackle:prune to remove any that were left behind by interrupted sessions.

Plan mode

Have the agent think before it touches anything. In plan mode a read-only planning agent investigates the codebase and streams a numbered implementation plan — files, changes, risks. Nothing is edited until you approve it.

php artisan ai:code --plan     # every task plans first

Or plan a single task from inside the REPL:

> /plan add soft deletes to the Invoice model

After the plan streams you choose: Execute (the coding agent follows the approved plan), Revise (describe what to change; the planner tries again), or Cancel. One approval replaces a session of per-edit vigilance.

Slash commands

The ai:code prompt understands commands. Type / to autocomplete them:

Command What it does
/plan <task> Plan first, edit only after your approval
/model [name] Switch the model mid-session — no name shows a picker listing known models with their per-MTok rates. /model <provider> <model> switches provider too. Budget rates update automatically for known models.
/compact Summarize older session history to free context
/clear Forget the session history entirely
/sessions List saved sessions and how to resume them
/help List all commands, including your project's own
/<name> [args] Run a custom command from .tackle/commands

Custom commands (.tackle/commands)

Reusable prompts your whole team shares, checked into the repo. Drop a markdown file in .tackle/commands/ and its name becomes a command:

<!-- .tackle/commands/deploy-check.md -->
Review everything changed since the last tag for deploy risk: migrations that
lock tables, config that needs new env vars, breaking API changes. Focus on: $ARGUMENTS
> /deploy-check the billing module

$ARGUMENTS is replaced with whatever follows the command name (or the arguments are appended when the template has no placeholder). Custom commands work headlessly too:

php artisan ai:run "/deploy-check the billing module"

Context compaction

Long sessions re-send their whole history every turn — slower, costlier, and eventually over the context limit. When the conversation exceeds AI_CODE_COMPACTION_THRESHOLD (default 60,000 characters), Tackle summarizes the older exchanges and keeps the last AI_CODE_COMPACTION_KEEP messages verbatim, automatically. Force it any time with /compact, or start fresh with /clear.

Images

Drag an image into the terminal (which pastes its path), or type the path, or @-mention one in the workspace — Tackle detects it, attaches the image to the prompt, and the model sees the actual pixels:

> the header is misaligned, see /Users/me/Desktop/Screen\ Shot.png — fix the CSS
> build this component @docs/mock.webp

PNG, JPEG, GIF, and WebP are recognized; quoted paths and escaped spaces from drag-and-drop both work. Requires a vision-capable model (the default Claude models are; some local models are not).

Interactive UX

ai:code uses Laravel Prompts throughout for a fully interactive terminal experience:

  • suggest() — the task prompt shows your previous tasks as autocomplete suggestions. Use ↑↓ to browse history.
  • stream() — AI text responses stream to the terminal in real time, token by token.
  • title() — the terminal tab title updates dynamically as the agent works: "Tackle — Thinking…", "Tackle — Reading files", "Tackle — Running tests", "Tackle — Ready".
  • select() / multiselect() — when the agent calls AskUser, you're presented with a styled selection list rather than a raw text prompt.
  • confirm() — when the agent calls ConfirmAction before a destructive operation, you see a styled yes/no prompt.
  • note() — after each turn a git diff --stat is shown as a note block so you can see what changed.
  • warning() — a styled warning appears when you approach 80% of your session budget.
  • error() — styled errors on agent failures or budget overruns.
  • intro() / outro() — session start and end use styled banners showing the model, budget, and shell mode.

Example session

 ┌──────────────────────────────────────────────────────────────┐
 │  Laravel Tackle  ·  claude-sonnet-4-6  ·  $1.00  ·  approve │
 └──────────────────────────────────────────────────────────────┘

 ┌ What should I work on? ─────────────────────────────────────┐
 │ Add a slug field to the Post model                          │
 └─────────────────────────────────────────────────────────────┘

  🔍 searching for Post model
  📖 reading app/Models/Post.php
  📝 creating database/migrations/2024_01_01_add_slug_to_posts.php
  ✓ File saved
  ✏️  editing app/Models/Post.php
  ✓ File saved
  🧪 running tests
  ✓ Done

 Migration created, `$fillable` updated, and all tests pass.

 ╭─────────────────────────────────────────────────────────────╮
 │  app/Models/Post.php | 2 +-                                 │
 │  1 migration file    | 15 +++++++++++++++                   │
 ╰─────────────────────────────────────────────────────────────╯

 ┌ What should I work on? ─────────────────────────────────────┐
 │ Make the slug auto-generate from the title on creation  ▲   │
 │ Add a slug field to the Post model                      ▼   │
 └─────────────────────────────────────────────────────────────┘

Tips for better results

Be specific about what you want. Vague tasks produce vague results. The more context you give upfront, the less back-and-forth is needed.

Instead of… Try…
"Add a feature" "Add a published_at timestamp to Post with a scope for published posts and a migration"
"Fix the bug" "The UserController@store is returning a 500 when email is null — find out why and fix it"
"Refactor this" "The OrderService class is doing too much — extract the payment logic into a PaymentService"

Use --off for questions and exploration. If you just want to understand the codebase without making changes, --off mode prevents the agent from running any commands, so you can ask freely.

php artisan ai:code --off
# "How is authentication handled in this app?"
# "What does the Job queue setup look like?"

Point it at the right place. If you know which file or module is relevant, say so. "Look at app/Services/BillingService.php" is faster than letting it search from scratch.

Correct it mid-session. If the agent does something wrong, just say so in the next prompt. It reads your correction in context and adjusts. You don't need to restart the session.

Keep tasks focused. One clear task per session works better than a long list. Once a task is done, review the diff, commit it, then start a new session for the next task.

Review before you move on. After each task the agent shows a git diff --stat. Look at it before typing your next task. If something looks wrong, say so or discard with git checkout -- ..

Project instructions (TACKLE.md)

Every Tackle agent — ai:code, ai:fix, ai:review, ai:explain, ai:test, ai:upgrade, and the self-healer — loads a TACKLE.md file from your project root at the start of each session and follows it. It's the place to record project conventions, boundaries, and gotchas once, instead of repeating them in every prompt.

Generate a starter file:

php artisan tackle:init

This scans your project (composer.json, test framework, Pint/Larastan presence, app/ structure) and writes a scaffold with ## Conventions, ## Boundaries, and ## Gotchas sections for you to fill in. Use --force to overwrite an existing file.

Example content:

## Conventions

- All money values are integer cents — never floats.
- New endpoints validate through Form Requests, never inline `validate()`.

## Boundaries

- Never modify files under `app/Legacy/` — scheduled for deletion.
- Do not add new composer dependencies without asking first.

## Gotchas

- `User::active()` excludes soft-deleted AND suspended users.

Notes:

  • If no TACKLE.md exists, Tackle falls back to AGENTS.md, then CLAUDE.md — so instructions you already maintain for other AI tools work out of the box.
  • Content is capped at 20,000 characters to protect your context window and session budget; anything beyond that is truncated.
  • Project instructions never override Tackle's safety layer — protected paths, shell modes, and allowlists are enforced in PHP regardless of what the file says.

Session memory

The memory config controls what happens to conversation history when you exit.

Mode Behaviour
file (Default) The transcript is saved to storage/ai-code/ after every turn. Re-running ai:code resumes where you left off.
none History is lost when the session ends. Every php artisan ai:code starts fresh.

With file mode you'll see this on your next run:

Resumed session 'default' — 12 messages of history. Type /clear to start fresh.

Named sessions

--session keeps separate histories for separate streams of work:

php artisan ai:code --session=billing-refactor
php artisan ai:code --session=bugfixes

ai:run joins a persisted session only when --session is passed — anonymous one-shot runs never pollute your interactive history. That means a cron'd ai:run --session=nightly accumulates context across nights.

Notes:

  • Transcripts are JSON under storage/ai-code/ — delete a file (or /clear in the REPL) to forget a session; gitignore the directory to keep history out of your repository.
  • Context compaction applies to resumed sessions too: a long transcript is summarized before it eats your context window.
  • Image attachments are not persisted — re-attach an image if a later session needs it.
  • Text is persisted, not tool output: a resumed session remembers what was said and done, and the agent re-reads files as needed.

Subagents

The main coding agent can delegate self-contained work to subagents — separate agents that run the task in their own fresh context with their own (usually narrower) toolset, and hand back only their final report. Exploration happens in the child; conclusions come back. Long sessions stay coherent because reading twenty files to answer "how does billing work?" no longer costs the main conversation twenty files of context.

Two subagents ship enabled:

Name What it does
explorer Read-only codebase exploration — locates files, traces how a feature works across classes, reports back with precise file references.
test-writer Writes a Pest test file for a class or behaviour and runs it.

The agent decides when to delegate (its instructions steer it toward broad research and away from small lookups), calling the Delegate tool with a subagent name and a complete brief. You'll see the call in the session like any other tool call.

Guarantees

  • Shared budget. Subagent token usage records into the same BudgetTracker as the parent session — delegation cannot exceed your spend limit, and a subagent is stopped mid-task if it exhausts it.
  • Same safety layer. Subagent tools go through PathGuard, allowlists, hooks, and ToolCalling/ToolCalled events — exactly like the parent's.
  • One level deep. A subagent cannot delegate further.
  • No user prompts. Subagents never ask questions; they make judgment calls and note them in the report.
  • Fail-soft. A subagent that crashes returns an error message to the parent agent, which continues the session.

Adding your own

Register any Tackle\Contracts\CodingAgent implementation in config/tackle.php — including agents you've written (see Swapping the agent entirely):

'subagents' => [
    'explorer' => [
        'agent' => \Tackle\Agents\ExplorerAgent::class,
        'description' => 'Read-only codebase exploration...',
    ],
    'schema-expert' => [
        'agent' => \App\Ai\SchemaExpertAgent::class,
        'description' => 'Answers questions about the database schema, migrations, and model relationships.',
    ],
],

The description is what the delegating model reads when deciding where to send work — write it like a tool description. Set subagents to an empty array to remove the Delegate tool entirely.

Hooks

Hooks are deterministic commands that run around agent activity — enforced in PHP and shell, not by the model. Use them to audit every tool call, block specific commands with your own policy, rewrite tool arguments before they run, or trigger follow-up work (formatting, notifications) after edits.

Declare them in config/tackle.php:

'hooks' => [
    'pre_tool' => [
        // Shell hook: guard every RunShell call with your own script.
        ['match' => 'RunShell', 'run' => 'scripts/tackle/guard-shell.sh'],

        // Class hook: audit every tool call.
        ['match' => '*', 'using' => \App\Hooks\AuditToolCalls::class],
    ],
    'post_tool' => [
        // Format after every file edit.
        ['match' => ['EditFile', 'WriteFile'], 'run' => 'vendor/bin/pint --dirty'],
    ],
    'session_start' => [],
    'session_end' => [],
],

Four events fire:

Event When Can block? Can rewrite arguments?
pre_tool Before a tool executes Yes Yes
post_tool After a tool executes No No
session_start An agent session begins No No
session_end An agent session ends No No

Each hook takes either run (a shell command) or using (a class name), plus optional match (a tool-name glob or array of globs — 'Run*', ['EditFile', 'WriteFile']; default '*') and timeout (seconds, default 10). Hooks run in declaration order; the first block wins, and argument rewrites chain into the next hook.

Shell hooks speak a stable JSON protocol, so they can be written in any language:

  • The event payload arrives on stdin: {"event":"pre_tool","tool":"RunShell","arguments":{"command":"ls"}} (post_tool adds result and duration_ms).
  • Exit 0 allows the call. For pre_tool, stdout may contain {"arguments": {...}} to rewrite the tool's arguments.
  • Exit 2 blocks the call — stderr becomes the refusal message the agent sees, so make it instructive: echo "Use RunTests instead" 1>&2; exit 2.
  • Any other exit code, a timeout, or a crash is logged and ignored — a broken hook never bricks a session.

Class hooks implement Tackle\Contracts\ToolHook (or are plain invokables). Return null to allow, false to block, a string to block with that message, or an array (pre_tool only) to replace the arguments:

namespace App\Hooks;

use Tackle\Contracts\ToolHook;

class AuditToolCalls implements ToolHook
{
    public function handle(array $payload): null|false|string|array
    {
        logger()->info("Tackle called {$payload['tool']}", $payload['arguments']);

        return null; // observe only
    }
}

Notes:

  • Hooks apply everywhere tools run through the agent harness — ai:code, ai:run, ai:fix, and the self-healer. They do not apply to tools served over tackle:mcp (the connected MCP client is the policy layer there).
  • Hooks complement the existing Laravel events: ToolCalling listeners can also veto calls in pure PHP. Reach for hooks when you want config-declared, ordered policy or non-PHP tooling; reach for listeners when you're already living in the event system.
  • Hooks are policy on top of the safety layer, not a replacement for it — PathGuard, the artisan allowlist, and shell modes still apply first.

Configuration

After publishing the config, edit config/tackle.php. All values can be set via environment variables — see the Environment variables table above.

return [
    // laravel/ai provider name — must match a key in config/ai.php
    'provider' => env('AI_CODE_PROVIDER', 'anthropic'),

    // Model to use
    'model' => env('AI_CODE_MODEL', 'claude-sonnet-4-6'),

    // Tool-call ceiling for ai:run — a cap, not a grant; it cannot raise
    // the agent's own #[MaxSteps] attribute
    'max_steps' => env('AI_CODE_MAX_STEPS', 40),

    // Hard spend limit for the session in USD — aborts when exceeded
    'budget_usd' => env('AI_CODE_BUDGET', 1.00),

    // Shell execution policy — string or per-environment array.
    // String form (backward-compatible): applies to all environments.
    // Array form: keyed by environment name; production defaults to 'off'.
    'shell' => [
        'local'      => env('AI_CODE_SHELL', 'approve'),
        'staging'    => env('AI_CODE_SHELL', 'approve'),
        'production' => env('AI_CODE_SHELL', 'off'),
    ],

    'shell_allowlist' => ['composer', 'npm', 'php artisan'],

    // Artisan commands the agent may run without confirmation — per environment.
    // Flat array form is still accepted for backward compatibility.
    'artisan_allowlist' => [
        'local'      => ['make:*', 'migrate:*', 'db:seed', 'route:list', 'test'],
        'staging'    => ['migrate', 'route:list'],
        'production' => ['route:list'],
    ],

    // Artisan commands that require an interactive confirmation before running.
    'artisan_destructive' => [
        'local'      => ['migrate:fresh', 'migrate:reset', 'migrate:refresh', 'db:wipe'],
        'staging'    => [],
        'production' => [],
    ],

    // Worktree isolation — edits go to a temp worktree instead of live files.
    // Production defaults to true; other environments default to false.
    'worktree' => [
        'local'      => env('AI_CODE_WORKTREE', false),
        'staging'    => env('AI_CODE_WORKTREE', false),
        'production' => env('AI_CODE_WORKTREE', true),
    ],

    // Glob patterns (relative to workspace) the agent can never read or write
    'protected_paths' => ['.env', '.env.*', 'storage/*', 'vendor/*', '.git/*'],

    // Root directory for the agent — null defaults to base_path()
    'workspace' => null,

    // Session memory: file (default, resumes across runs) | none
    'memory' => env('AI_CODE_MEMORY', 'file'),
];

Shell modes

Mode Behaviour
off RunShell refuses everything. Use RunArtisan / RunTests instead.
allowlist Only commands whose first token matches shell_allowlist run unattended.
approve Default. Every command shows a confirmation prompt before running. Choosing "always allow this exact command" saves it to .tackle/permissions.json, and it runs without asking from then on.
yolo Runs anything, no prompt. Dangerous — CI or fully-trusted environments only.

Shell mode can be set as a plain string (applies to all environments) or as a per-environment array (shown above). The production key defaults to off.

Artisan allowlist and destructive list

artisan_allowlist controls which commands the agent may run freely. artisan_destructive lists commands that require an interactive terminal confirmation before running. Commands in neither list are refused outright. Both support glob patterns (make:* covers make:model, make:controller, etc.) and can be a flat array (all environments) or a per-environment keyed array.

RunTests also respects the allowlist — if test is not in the allowlist for the current environment, the tool is refused.

Protected paths

The protected_paths globs prevent the agent from reading or writing sensitive files regardless of what it is asked to do. This is enforced in PHP, not via prompting. Add your own patterns here if your project has additional secrets.

Built-in tools

These tools are available to the agent in every session.

Filesystem

Tool What it does
ReadFile Reads a file's contents. Always runs through PathGuard first.
Glob Lists files matching a pattern. Protected paths are excluded from results.
SearchCode Grep-style search returning file + line + snippet. Capped at 50 results.
EditFile str_replace edit — old_str must appear exactly once or the edit is refused.
WriteFile Creates a new file. Refuses if the path already exists.

Execution

Tool What it does
RunArtisan Runs php artisan <command> in a subprocess. Allowlist-gated.
RunTests Runs Pest or php artisan test in a subprocess. Returns full output.
RunPint Runs Laravel Pint to format files. Called before finishing a task.
RunLarastan Runs PHPStan / Larastan static analysis and returns the findings. Accepts an optional path and level override. No-ops gracefully if vendor/bin/phpstan is not present.
RunShell General shell — governed by the shell config mode.
RunComposer Composer with the dangerous parts fenced off — a fixed set of read-only and mutating subcommands, mutations always --no-scripts, scripts re-enabled only by a human at the terminal. Used by ai:upgrade.
ReadPackageDocs Reads an installed package's upgrade guide, changelog, or composer.json from vendor/ — a docs-only carve-out of the vendor/* protected path; package code stays unreadable. Used by ai:upgrade.

Observability

Tool What it does
ReadLog Returns the last N lines of storage/logs/laravel.log. Accepts an optional filter string.
QueryDatabase Runs a read-only SELECT query and returns results as JSON. Capped at 100 rows.
ListRoutes Returns a formatted table of all registered routes with method, URI, name, and action.
GitDiff Shows a git diff — supports staged, a specific commit, a branch range, or a path.
ReadTelescopeEntry Reads Telescope exception entries. Pass a job UUID for a specific lookup, or omit to return recent exceptions. No-ops gracefully if Telescope is not installed.
ReadSentryIssue Fetches a Sentry issue by ID — exception, stacktrace, breadcrumbs, and request context. Omit the ID to list recent unresolved issues for the configured project. No-ops gracefully if SENTRY_AUTH_TOKEN / SENTRY_ORG are not set.
ReadGitHubIssue Fetches a GitHub issue by number — title, body, labels, and all comments. Omit the number to list recent open issues. No-ops gracefully if GITHUB_TOKEN / GITHUB_REPO are not set.
ReadPullRequest Fetches a GitHub pull request by number — title, body, branch name (head ref), base branch, state, author, and comments. Use this (not ReadGitHubIssue) when the user references a PR number, especially when the branch name is needed for CommitAndPush.
CreateGitHubIssue Opens a new GitHub issue with a title and body.
CreatePullRequest Creates a branch, commits all worktree changes, pushes to origin, and opens a GitHub pull request.
CommitAndPush Stages all changes, fetches the remote branch tip, rebases onto it, commits, and pushes via HEAD:<branch> — without checking out the branch. Use this to add follow-up commits to an existing PR from a worktree session. Always pass the branch parameter (get it from ReadPullRequest).
AskUser Presents the user with a select() or multiselect() prompt and returns their choice. The agent calls this when there are multiple valid paths and it wants the user to decide.
ConfirmAction Presents the user with a confirm() prompt before a destructive or irreversible operation. Returns "confirmed" or "cancelled".
Delegate Runs a self-contained task in a subagent — a separate agent with its own fresh context and narrower toolset — and returns only its final report. Present only when tackle.subagents is non-empty.

All file reads happen in-process. Everything that executes code runs as a subprocess, so a broken generated file cannot crash the agent session.

Headless mode (CI, cron, scripts)

ai:code is a REPL and needs a terminal. ai:run is the same agent, the same tools, and the same safety layer — run once, to completion, with nothing attached to stdin.

php artisan ai:run "Add a scopeActive to the Subscription model and a test for it"

It streams a plain-text log while it works and prints a summary at the end.

Machine-readable output

--output=json prints one JSON document on stdout. Diagnostics go to stderr, so you can pipe stdout straight into jq without filtering.

php artisan ai:run "Fix the failing SubscriptionTest" --output=json | jq -r '.pr_url'
{
  "ok": true,
  "outcome": "completed",
  "text": "Added the scope and a test covering both branches.",
  "steps": 12,
  "files_changed": ["app/Models/Subscription.php", "tests/Feature/SubscriptionTest.php"],
  "diff_stat": "2 files changed, 24 insertions(+)",
  "interactions_denied": 0,
  "usage": { "input_tokens": 41233, "output_tokens": 2210, "estimated_cost_usd": 0.1563 },
  "budget_usd": 1.0,
  "worktree": "/tmp/tackle-worktree-9f2ab1c4",
  "pr_url": "https://github.com/acme/app/pull/218",
  "events": [
    { "type": "tool_call", "tool": "EditFile", "args": { "path": "app/Models/Subscription.php" } }
  ]
}

Exit codes

Code Meaning
0 Completed
1 Agent or provider error, or an invalid option
2 Stopped — the spend limit was reached
3 A confirmation was auto-denied (only with --fail-on-denied)
4 Hit the step ceiling without finishing

Confirmations without a user

Five tools ask before they act: AskUser, ConfirmAction, RunArtisan (for destructive commands), RunShell (under shell=approve), and CommitAndPush. With no terminal there is nobody to answer, so every confirmation is denied by default — nothing that would have needed a human "yes" happens without one.

AskUser is the exception, because it is a choice rather than a confirmation: the agent is told to pick the option it judges best, say which it chose and why, and carry on. A denied choice would just stall the run.

Pass --yes to approve automatically instead. Only do that where you would have clicked through the prompts yourself — it green-lights destructive Artisan commands and pushes.

--fail-on-denied turns any auto-denial into exit code 3, for pipelines that would rather fail loudly than get a partial result. Either way, the count is in the JSON as interactions_denied.

Shell in unattended runs

shell=approve — the config default for local and staging — has no meaning with no one to approve. Rather than silently promote it to yolo, ai:run refuses those commands and says why. For a run that genuinely needs a shell, choose the policy deliberately:

php artisan ai:run "..." --allowlist   # only shell_allowlist commands
php artisan ai:run "..." --yolo        # unrestricted — trusted environments only

Bounding a run

--budget and --max-steps override budget_usd and max_steps for one run. Both are hard stops: the run aborts and reports the outcome rather than continuing past the limit.

php artisan ai:run "..." --budget=0.50 --max-steps=25

max_steps is a ceiling, not a grant. Each agent also declares its own #[MaxSteps] attribute, which laravel/ai reads by reflection and which cannot be raised at runtime — setting max_steps above it has no effect.

In GitHub Actions

Worktree mode is worth forcing here: the agent edits an isolated copy and the live checkout is never touched.

name: Tackle
on:
  workflow_dispatch:
    inputs:
      task:
        description: What should Tackle do?
        required: true

jobs:
  run:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-interaction --prefer-dist
      - name: Run Tackle
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPO: ${{ github.repository }}
        run: |
          php artisan ai:run "${{ inputs.task }}" \
            --output=json \
            --worktree \
            --allowlist \
            --budget=2.00 \
            --max-steps=60 > result.json
      - run: jq -r '.text' result.json >> $GITHUB_STEP_SUMMARY
        if: always()

The job fails on any non-zero exit, so a run that blows the budget or hits the step ceiling fails the workflow rather than reporting success with half the work done.

Tackle Remote (drive it from your phone)

The terminal isn't the only way in. The companion package laravel-tackle-remote serves a mobile-first browser UI for the same harness:

php artisan tackle:install remote     # composer-requires the package (--no-dev to add to require)
php artisan tackle:remote --host=0.0.0.0

Scan the QR code printed in your terminal and your phone is driving the agent — send tasks (including photos as context), watch it work tool-by-tool, and answer approval prompts from a bottom sheet: Deny / Allow once / Always allow. Slash commands and @-file mentions work like they do in the terminal, "always allow" writes to the same permission store, and every safety guarantee in this README still applies — it's the same agents, tools, and enforcement underneath, with a web page where the terminal used to be.

Pairing links are single-use, sessions are signed with a per-run secret, and the recommended remote path is a Tailscale tailnet rather than an exposed port — see the security model for details.

MCP server

Tackle's tools aren't only for Tackle's agents. tackle:mcp serves them over the Model Context Protocol (stdio), so any MCP client — Claude Code, Cursor, Zed — can use Laravel-aware tools like ListRoutes, QueryDatabase, ReadTelescopeEntry, and RunLarastan against your app, with Tackle's safety layer still enforced in PHP: protected paths, the artisan allowlist, and SELECT-only database queries all apply exactly as they do for Tackle's own agents.

Register it with Claude Code from your app directory:

claude mcp add tackle -- php artisan tackle:mcp

Or add it to .mcp.json manually:

{
  "mcpServers": {
    "tackle": {
      "command": "php",
      "args": ["artisan", "tackle:mcp"]
    }
  }
}

The exposed tool set is controlled by config('tackle.mcp.tools') and defaults to read/inspect and analysis tools only — no file writes, no shell. Add write tools (EditFile, WriteFile, RunPint, …) to the list if you trust the connected client. Interactive tools (AskUser, ConfirmAction) are refused outright: an MCP client has no terminal to answer their prompts. Avoid tools that ask for terminal confirmation, such as CommitAndPush — they would hang the stdio session.

GitHub Issues integration

When GITHUB_TOKEN and GITHUB_REPO are set, the ReadGitHubIssue tool becomes active. The agent can fetch any issue by number — title, description, labels, and all comments — giving it full context before starting work.

Configuration

Add these to your .env:

GITHUB_TOKEN=ghp_...        # personal access token with repo scope
GITHUB_REPO=owner/repo      # e.g. acme/my-app

The GITHUB_TOKEN is shared with the self-healer (PR mode), so no extra setup is needed if healing is already configured. Generate a token at GitHub → Settings → Developer settings → Personal access tokens with repo scope (or a fine-grained token with Issues: read permission).

How it works

Ask the agent naturally:

> implement issue #42
> what are the open GitHub issues?

When given an issue number, the tool fetches the issue body plus all comments and returns them as a single block of context. When no number is given, it returns a summary list of recent open issues (pull requests are filtered out automatically).

Health check

php artisan tackle:health

Reports ✓ GitHub configured (owner/repo) — ReadGitHubIssue tool is active when both vars are present.

Sentry integration

When SENTRY_AUTH_TOKEN and SENTRY_ORG are set, the ReadSentryIssue tool becomes active. The agent can fetch the latest event for any Sentry issue — including the full exception, stacktrace, breadcrumbs, and HTTP request context — and use it as additional context when diagnosing bugs.

Configuration

Add these to your .env:

SENTRY_AUTH_TOKEN=sntrys_...   # auth token with issue:read scope
SENTRY_ORG=your-org-slug       # visible in your Sentry URL (sentry.io/organizations/<slug>/)
SENTRY_PROJECT=your-project    # project slug — required for listing recent issues

These are the same env vars used by the Sentry CLI, so no extra setup is needed if you already use it.

Generate a token at Sentry → Settings → Account → API → Auth Tokens with the issue:read scope.

How it works

Ask the agent naturally:

> there's a DivisionByZeroError in Sentry (#4821) — can you fix it?
> what are my recent unresolved Sentry issues?

When given an issue ID, the tool calls GET /api/0/organizations/{org}/issues/{id}/events/latest/ and returns the exception type, message, stacktrace (top 15 frames, most recent first), breadcrumbs (last 10), and request method/URL.

When no ID is given, it calls GET /api/0/projects/{org}/{project}/issues/ and returns a summary list of recent unresolved issues.

Health check

php artisan tackle:health

Reports ✓ Sentry configured — ReadSentryIssue tool is active when credentials are present, or a warning with setup instructions if they are missing.

Self-healing queue workers

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.

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:

php artisan vendor:publish --tag="tackle-migrations"
php artisan migrate
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:

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:

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.)

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.
GITHUB_TOKEN=ghp_...

Configuration

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

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
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
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=3

PR 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.

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-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:

# 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:

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.

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.

Fix an issue

php artisan ai:fix opens a focused fix session. It loads context from a Sentry issue, a GitHub issue, or a pasted exception — then fires the agent immediately, without you having to describe the task. Worktree mode is on by default so live files are never touched until you open a PR.

# Paste or describe the exception at the prompt
php artisan ai:fix

# Load context from a Sentry issue
php artisan ai:fix --sentry=4821

# Load context from a GitHub issue
php artisan ai:fix --issue=42

After the agent applies the fix, the session stays open for follow-up:

> add a regression test for this
> open a pull request
> exit

All shell and worktree flags from ai:code are supported:

php artisan ai:fix --sentry=4821 --no-worktree   # edit live files directly
php artisan ai:fix --issue=42 --yolo              # skip shell approval prompts

Code review

php artisan ai:review feeds your git diff to a read-only AI agent that reads the surrounding codebase for context, then surfaces real issues grouped by file with severity levels.

# Review everything since your last commit (staged + unstaged)
php artisan ai:review

# Review only staged changes
php artisan ai:review --staged

# PR-style review — your branch vs. another branch
php artisan ai:review --against=main

# Review a specific commit
php artisan ai:review --commit=abc1234

# Tell the agent what to prioritise
php artisan ai:review --against=main --focus=security,performance

# Review a GitHub pull request (diff fetched via the GitHub API)
php artisan ai:review --pr=42

# …and post the findings back to the PR as inline review comments
php artisan ai:review --pr=42 --comment

Output format

Findings are grouped by file with three severity levels:

Level Meaning
🔴 Critical Bugs that will cause failures, security vulnerabilities, data loss risks
🟡 Warning Edge cases, missing error handling, performance concerns, breaking changes
🟢 Suggestion Improvements worth considering but not blocking

The review ends with a one-line verdict: LGTM / LGTM with minor notes / Needs changes.

How it works

The ReviewAgent is a read-only agent — it has access to ReadFile, Glob, and SearchCode but no editing tools. Before commenting on any changed function or class it reads the full file for context, so findings are grounded in the actual codebase rather than the diff alone.

Focus areas

Pass --focus with a comma-separated list to direct the agent's attention:

php artisan ai:review --focus=security
php artisan ai:review --focus=performance,tests
php artisan ai:review --staged --focus=bugs,security

Any plain-language description works — security, performance, n+1 queries, missing tests, breaking changes, etc.

Reviewing pull requests

--pr=N reviews a GitHub pull request instead of a local diff. The diff is fetched from the GitHub API, so it works regardless of which branches exist in your local checkout — the checkout is only used by the agent's read tools to understand the surrounding code. Requires GITHUB_TOKEN and GITHUB_REPO (see GitHub Issues integration); the token needs Pull requests: read permission, or read & write when using --comment.

# Print the review to the terminal
php artisan ai:review --pr=42

# Post it to the PR as a single review with inline comments
php artisan ai:review --pr=42 --comment

With --comment, each finding is anchored to the exact file and line as an inline review comment (🔴/🟡/🟢 severity included), under a summary body with the overall verdict. Findings that reference lines outside the diff are folded into the summary instead of being dropped.

Incremental re-reviews

Re-running ai:review --pr on a PR Tackle has already reviewed does not repeat the whole review. Each posted review embeds an invisible marker recording the head commit it covered; on the next run Tackle finds it and:

  • reviews only the changes pushed since the last review (via the GitHub compare API),
  • tells the agent what it already reported, so findings aren't repeated,
  • labels the posted review as a follow-up,
  • and exits early with "Nothing new to review" when the head commit is unchanged.

If the previously reviewed commit was force-pushed away and the compare can't be resolved, Tackle falls back to a full review automatically. Pass --full to force a full re-review at any time:

php artisan ai:review --pr=42 --comment --full

This makes the pull_request-triggered workflow below cheap to run on synchronize: each push reviews only its own delta instead of the entire PR again.

Gating CI on the review

--fail-on makes the command exit non-zero when findings at or above the given severity exist, so a workflow can block a merge:

php artisan ai:review --pr=42 --comment --fail-on=critical   # fail on critical only
php artisan ai:review --pr=42 --fail-on=warning              # fail on critical or warning

--fail-on also works for local scopes (--staged, --against=main, …) — useful in pre-push hooks.

Machine-readable review output

As with ai:run, --output=json prints one JSON document on stdout — diagnostics (including the review prose) go to stderr — so stdout pipes straight into jq. JSON mode always requests the structured findings block, so verdict and findings are populated even without --comment or --fail-on. Exit codes are unchanged.

php artisan ai:review --pr=42 --output=json | jq -r '.verdict'
{
  "ok": true,
  "outcome": "completed",
  "error": null,
  "verdict": "needs_changes",
  "findings": [
    { "path": "app/Models/Subscription.php", "line": 42, "severity": "critical", "message": "Unchecked null." }
  ],
  "text": "The review prose, without the findings block.",
  "head_sha": "9f2ab1c4…",
  "pr_number": 42,
  "usage": { "input_tokens": 41233, "output_tokens": 2210, "estimated_cost_usd": 0.1563 }
}

outcome is completed, nothing_to_review, findings_gate_failed (--fail-on tripped), or error. For local scopes, head_sha and pr_number are null.

Reviewing every PR automatically

The easiest path is php artisan tackle:install review, which scaffolds .github/workflows/tackle-review.yml using the tackle-review action — add the ANTHROPIC_API_KEY secret to the repository and every PR gets reviewed. The generated workflow wraps the whole job in one step:

name: Tackle Review
on:
  pull_request:

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: JordanDalton/tackle-review@v1
        with:
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
          fail-on: critical   # optional — omit for an advisory review

Or hand-roll the equivalent workflow in .github/workflows/tackle-review.yml:

name: Tackle Review
on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-interaction --prefer-dist
      - name: Review the pull request
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPO: ${{ github.repository }}
        run: |
          php artisan ai:review \
            --pr=${{ github.event.pull_request.number }} \
            --comment \
            --fail-on=critical

Notes:

  • pull-requests: write is what lets the default GITHUB_TOKEN post the review.
  • Drop --fail-on=critical if you want the review to be advisory rather than blocking.
  • The review agent is read-only and the diff comes from the API, so the job needs no database and no --yes-style approval flags.

Acting on review comments (/tackle)

ai:respond closes the loop from found to fixed. When a reviewer replies to a finding (or leaves any PR comment) asking Tackle to act, the command loads the comment, its thread, and the diff context, runs the coding agent against the instruction, pushes the resulting commit to the PR branch, and replies in the thread:

php artisan ai:respond --pr=42 --comment-id=123456 --comment-type=review

# Machine-readable result — one JSON document on stdout, diagnostics on stderr
php artisan ai:respond --pr=42 --comment-id=123456 --output=json
  • --comment-type=review for inline review comments (the usual case), --comment-type=issue for comments in the PR conversation tab.
  • --output=json reports ok, outcome, error, pr_number, comment_id, reply_posted, pushed, and usage — the same stdout/stderr discipline and usage shape as ai:run. Exit codes are unchanged.
  • If the comment asks a question rather than requesting a change, the agent answers in the thread and touches nothing.
  • The reply always arrives — success (with the pushed SHA and diff stat), no-op, or a clear failure message. Threads never dangle.

Guardrails, enforced in PHP:

  • Fork PRs are refused — Tackle never pushes to a branch in someone else's repository; it replies explaining why instead.
  • The checkout must match the PR head — the agent edits the working tree and the result is pushed, so a mismatched checkout aborts before the agent runs. In CI, check out refs/pull/<n>/head.
  • Confirmations are auto-denied as in ai:run; pass --yes only where you would have approved them yourself.

The easiest way to wire this to GitHub is the respond action — see tackle-review. Gate the workflow on author_association so only maintainers can trigger it.

Explain code

php artisan ai:explain reads a file or class and explains what it does in plain English — inputs, outputs, side effects, and any non-obvious behaviour. The agent reads the full file and any closely-related classes before responding.

# Explain a whole file
php artisan ai:explain app/Services/BillingService.php

# Focus on a specific method
php artisan ai:explain app/Services/BillingService.php --method=charge

Generate tests

php artisan ai:test reads a class, checks your existing test conventions, and writes a Pest test file covering the happy path, edge cases, and error conditions. It runs the tests after writing to confirm they pass.

# Generate tests for a class
php artisan ai:test app/Services/BillingService.php

# Focus on a single method
php artisan ai:test app/Services/BillingService.php --method=charge

# Force a feature or unit test
php artisan ai:test app/Http/Controllers/UserController.php --feature
php artisan ai:test app/Services/BillingService.php --unit

Test type is inferred from the path when no flag is given — controllers, jobs, commands, listeners, and middleware default to Feature; everything else defaults to Unit.

Upgrade a dependency

php artisan ai:upgrade performs a major version upgrade of a Composer package — Laravel itself included — the way a careful human would, with the safety boundaries enforced in PHP.

# What could be upgraded, and what blocks each one? Deterministic, no AI involved.
php artisan ai:upgrade --audit

# Same audit, mirrored to a GitHub issue — built for the scheduler
php artisan ai:upgrade --audit --issue

# Upgrade one package across a major version
php artisan ai:upgrade laravel/framework

# Upgrade several — sequential isolated sessions, one PR each
php artisan ai:upgrade pestphp/pest spatie/laravel-permission

# No package? Multi-select from the audit interactively.
php artisan ai:upgrade

The session follows a fixed playbook:

  1. Auditcomposer outdated establishes what is installed and what is available; composer why-not names the packages whose constraints block the jump.
  2. Plan — the agent reads the package's UPGRADE.md / CHANGELOG.md from vendor/ (a narrow, docs-only carve-out of the vendor/* protected path), searches your code for actual usages, and presents a plan covering only the breaking changes that affect your app. Nothing mutates until you confirm.
  3. Resolve — the constraint is bumped and composer update --with-all-dependencies runs. Solver conflicts are diagnosed with why-not and the blockers raised iteratively.
  4. Fix — the code and config changes the upgrade guide requires, as minimal edits.
  5. Verify — your test suite, Larastan if installed, a boot smoke check, and Pint.
  6. Deliver — an honest summary (including which upgrade-guide items did not apply, and what your tests do not cover) and an offer to open a PR.

What makes it safe:

  • Worktree by default. The whole upgrade — lockfile, vendor/, code edits — happens in an isolated git worktree. A failed resolution can never leave your live checkout broken. Opt out with --no-worktree.
  • Lifecycle scripts stay off. Every composer mutation runs --no-scripts (composer scripts are arbitrary project PHP — the same path ComposerScriptGuard blocks). Scripts run only when a human approves them in the terminal, after the lockfile change is reviewable.
  • Composer is fenced. The agent's composer tool permits a fixed set of subcommands and refuses --working-dir / --global; run-script and exec are not available at all.
  • Green ≠ proven. The final summary is required to say what the test suite did not exercise, rather than declaring the upgrade safe on a thin green run.

One major per session. For a framework major that forces ecosystem packages to move together, the plan lists the full set before anything changes — that is one atomic change and one PR. Independent majors are a different case: pass several packages (or multi-select from the audit) and each runs as its own sequential session with a fresh agent context, fresh worktree, and fresh budget, delivering one PR per package — so a bad upgrade stays individually reviewable, bisectable, and revertable. Each session's prompt fences the scope to its own package, and every upgrade PR touches composer.lock, so after merging one, rebase the next and re-run composer update on its branch.

Major upgrades are long sessions — consider raising AI_CODE_BUDGET beyond the default $1 before starting one; in a batch the budget applies per package, not to the batch as a whole.

Scheduled dependency watch

The audit needs no AI, no TTY, and costs nothing, so it is safe to run on the scheduler. With --issue it maintains exactly one GitHub issue mirroring the audit (requires GITHUB_TOKEN + GITHUB_REPO):

// routes/console.php
Schedule::command('ai:upgrade --audit --issue')->daily();
  • The first time majors appear, an issue titled "Composer major upgrades available" is opened (labelled tackle-upgrade-audit), listing each package with its why-not blockers.
  • When the audit changes, the issue body is updated in place. When it hasn't, nothing is written — no daily notification spam.
  • When no major upgrades remain (you upgraded, or constraints resolved), the issue is commented on and closed.

The issue is the reminder; a human stays the trigger — read it and run php artisan ai:upgrade <package> to turn it into a PR, or wire up the unattended mode below.

Unattended upgrades (--headless)

php artisan ai:upgrade pestphp/pest --headless --output=json --ref-issue=42

Headless mode runs the same playbook with no terminal: the plan-confirmation step is folded into the PR body, verification still gates delivery, and the PR opens automatically — the pull request is the human gate, the same trust model as the self-healer's mode=pr. --ref-issue=N makes the PR body carry Refs #N (not Closes — the audit issue closes itself once no majors remain).

Safety properties specific to headless:

  • Composer lifecycle scripts can never run. Enabling them requires an interactive approval, and the headless interaction policy is permanently non-interactive — there is no flag that overrides this.
  • Explicit targets only: headless refuses to pick packages itself.
  • Budget (--budget, per package) and step ceiling (--max-steps) abort the run with distinct exit codes: 0 ok, 1 error, 2 budget, 4 max steps.
  • --output=json emits one JSON document per package on stdout (progress goes to stderr): outcome, steps, diff stat, token usage, and pr_url.

A label-triggered GitHub Actions workflow closes the loop — the scheduler maintains the issue, a maintainer applies the tackle-upgrade label, CI opens the PR. The ephemeral runner with a scoped GITHUB_TOKEN is stronger containment than any in-process guard:

name: tackle-upgrade
on:
  issues:
    types: [labeled]
permissions:
  contents: write
  pull-requests: write
  issues: read
concurrency: tackle-upgrade
jobs:
  upgrade:
    if: github.event.label.name == 'tackle-upgrade'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.4' }
      - run: composer install --no-interaction --no-scripts
      - run: |
          php artisan ai:upgrade pestphp/pest \
            --headless --output=json --no-worktree \
            --ref-issue=${{ github.event.issue.number }} \
            --budget=3
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPO: ${{ github.repository }}
          AI_CODE_GUARD_INJECTION: true

(--no-worktree because the checkout is already disposable. The label gate matters: issue bodies are untrusted input, so only maintainer action — a label they applied — may hand an agent write access. Enabling the injection classifier fences what the issue reader returns.)

Health check

php artisan tackle:health verifies that the package is correctly set up. Run it after installation or when something isn't working as expected.

php artisan tackle:health

It checks:

  • config/tackle.php and config/ai.php are published
  • An API key is configured for the active provider
  • The project is a git repository with at least one commit
  • .env.testing exists (warns if missing)
  • If healing is enabled: migration has been run, GitHub token is available

Pruning dangling worktrees

If a session is interrupted before cleanup (e.g. a crash or kill -9), worktrees may be left behind in /tmp. Use tackle:prune to remove them:

php artisan tackle:prune

# Preview without removing
php artisan tackle:prune --dry-run

Only directories matching the tackle-worktree-* pattern are touched — the command will never remove your main working tree.

Replay a healing attempt

php artisan tackle:replay re-dispatches a previous healing attempt — useful when you want to retry after adjusting config or fixing something manually.

# Replay the most recent healing attempt
php artisan tackle:replay

# Replay the last attempt for a specific job class
php artisan tackle:replay --class="App\Jobs\ProcessPayment"

# Replay a specific log entry by ID
php artisan tackle:replay --id=42

Limitations

Things Tackle cannot do in v1:

  • No internet access. The agent cannot fetch URLs, read documentation, or call external APIs. It works only with files in your workspace.
  • No binary files. ReadFile reads text. Images, compiled assets, and other binaries are not readable by the agent.
  • No auto-commit or push in standard mode. In a normal session all edits are left unstaged. In worktree mode the agent can commit and push to an existing PR branch using the CommitAndPush tool, but it will always call ConfirmAction first.
  • History persists as text. With the default memory=file, sessions resume across runs, but tool outputs and image attachments are not replayed — the agent re-reads what it needs. Set memory=none to start fresh every time.
  • Budget is estimated, not exact. The spend limit is calculated from token counts using approximate per-model pricing. Actual charges from your provider may differ slightly.
  • Tests need a working environment. RunTests runs your actual test suite. If tests require a database or other services, those must be running and configured before starting a session.

Customization

Generators

Tackle ships generator commands so you don't have to look up method signatures:

# Scaffold a tool at app/Ai/Tools/MyTool.php
php artisan tackle:tool MyTool

# Scaffold an agent that extends DefaultCodingAgent (most common)
php artisan tackle:agent MyAgent

# Scaffold a bare CodingAgent implementation
php artisan tackle:agent MyAgent --full

To customise the generated stubs, publish them first:

php artisan vendor:publish --tag="tackle-stubs"

This copies the stubs to stubs/tackle/ in your project root. Both commands check for published stubs before falling back to the package defaults.

Events

Tackle dispatches Laravel events around the agent lifecycle, so ordinary listeners can observe — or veto — what agents do. (For config-declared, ordered policy — including shell-command hooks and argument rewriting — see Hooks.)

Event When Payload
Tackle\Events\SessionStarted An ai:code / ai:run session begins command, provider, model
Tackle\Events\SessionEnded The session ends command, token counts, estimated cost
Tackle\Events\ToolCalling Before a tool executes — vetoable tool name, arguments
Tackle\Events\ToolCalled After a tool executes tool name, arguments, result, duration

A ToolCalling listener that returns false blocks the call (the agent receives a refusal and reroutes); returning a string uses it as the refusal message. Anything else observes without interfering:

use Tackle\Events\ToolCalling;
use Tackle\Events\ToolCalled;

Event::listen(ToolCalling::class, function (ToolCalling $event) {
    if ($event->tool === 'RunShell' && now()->isWeekend()) {
        return 'No shell commands on weekends.';
    }
});

Event::listen(ToolCalled::class, function (ToolCalled $event) {
    AgentAudit::record($event->tool, $event->arguments, $event->durationMs);
});

Events fire for the tools of DefaultCodingAgent (and subclasses) and the self-healer. This is an extension layer on top of the safety guards, not a replacement — PathGuard, shell modes, and allowlists still apply first. Requires a laravel/ai version with ToolNameResolver (0.10+); on older versions the events simply don't fire.

MCP client tools

Just as external clients can consume Tackle's tools over MCP, Tackle's agents can consume external MCP servers. Install laravel/mcp (requires laravel/ai >= 0.8), then return the client's tools from an agent's tools()laravel/ai wraps them automatically, prefixed mcp_tools_*:

use Laravel\Mcp\Facades\Mcp;

class MyCodingAgent extends DefaultCodingAgent
{
    public function tools(): iterable
    {
        return [
            ...parent::tools(),
            ...Mcp::client('playwright')->tools(),
        ];
    }
}

Now the agent can drive a browser to verify its own fix, query an external system, or use any other MCP server you trust — while Tackle's own tools keep their PathGuard and shell-policy enforcement.

Adding your own tools

Create a class that extends Tackle\Tools\AbstractTool, then extend DefaultCodingAgent to merge it into the tool list, and rebind the contract.

Step 1 — Generate the tool (or write it manually):

php artisan tackle:tool ReadDatabase

Step 2 — Implement the tool:

// app/Ai/Tools/ReadDatabase.php
namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\DB;
use Laravel\Ai\Tools\Request;
use Tackle\Tools\AbstractTool;

class ReadDatabase extends AbstractTool
{
    public function description(): string
    {
        return 'Run a read-only SQL query and return results as JSON. SELECT only.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'query' => $schema->string()
                ->description('The SELECT query to run.')
                ->required(),
        ];
    }

    public function handle(Request $request): string
    {
        $sql = $request->string('query', '');

        if (! str_starts_with(strtolower(ltrim($sql)), 'select')) {
            return 'Only SELECT queries are allowed.';
        }

        return json_encode(DB::select($sql), JSON_PRETTY_PRINT);
    }
}

Step 3 — Extend the agent (or generate it):

php artisan tackle:agent MyCodingAgent

Step 4 — Wire in your tool:

// app/Ai/MyCodingAgent.php
namespace App\Ai;

use App\Ai\Tools\ReadDatabase;
use Tackle\Agents\DefaultCodingAgent;

class MyCodingAgent extends DefaultCodingAgent
{
    public function __construct(
        private ReadDatabase $readDatabase,
        ...$args,
    ) {
        parent::__construct(...$args);
    }

    public function tools(): iterable
    {
        return [...parent::tools(), $this->readDatabase];
    }
}

Step 5 — Rebind in your service provider:

// app/Providers/AppServiceProvider.php
use App\Ai\MyCodingAgent;
use Tackle\Contracts\CodingAgent;

public function register(): void
{
    $this->app->bind(CodingAgent::class, MyCodingAgent::class);
}

The Laravel container resolves all constructor dependencies automatically, so your tool class can type-hint anything it needs (DB connections, services, etc.).

Tool contract

Every tool receives a Laravel\Ai\Tools\Request object in handle(). It behaves like a read-only request bag:

$request->string('key', 'default');   // string value
$request->boolean('key', false);      // boolean value
$request->integer('key', 0);          // integer value
$request->get('key', 'default');      // raw value
$request->all();                      // all arguments as array

When a tool should refuse an action, return a string explaining why rather than throwing an exception. The agent reads the refusal message and reroutes itself accordingly.

Swapping the agent entirely

If you need deeper control — different instructions, a different conversation strategy, or a completely different set of tools — implement the CodingAgent contract directly and rebind it:

// app/Ai/MyAgent.php
namespace App\Ai;

use Laravel\Ai\Promptable;
use Tackle\Contracts\CodingAgent;

class MyAgent implements CodingAgent
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a specialist in this project. Only touch the billing module.';
    }

    public function messages(): iterable
    {
        return [];
    }

    public function tools(): iterable
    {
        return [
            // your tools here
        ];
    }
}
$this->app->bind(\Tackle\Contracts\CodingAgent::class, MyAgent::class);

The CodingAgent contract extends Laravel\Ai\Contracts\Agent, HasTools, and Conversational, so laravel/ai's full streaming and tool-calling machinery works automatically as long as you use the Promptable trait.

Changing the model or provider

The quickest way is via .env:

AI_CODE_PROVIDER=openai
AI_CODE_MODEL=gpt-4o

The provider name must match a key in config/ai.php. Any provider supported by laravel/ai (Anthropic, OpenAI, Gemini, Groq, Ollama, etc.) works as long as it supports tool calling.

When switching models, also set AI_CODE_PRICE_INPUT / AI_CODE_PRICE_OUTPUT to the model's per-million-token rates — the budget cap is estimated from token counts, and the defaults assume Claude Sonnet pricing. For local models set both to 0.

Internally, Tackle injects provider and model values via two custom Laravel contextual attributes — #[AiProvider] and #[AiModel] — so any agent you write by extending DefaultCodingAgent inherits these config values automatically through the container.

Safety

  • Protected paths — Tackle's file tools cannot read or write .env, storage/, vendor/, or .git/ by default. Enforced in PathGuard, not via prompting — no wording in a prompt makes ReadFile return your .env. (This guards the tools, not the whole process — see What the guards do and don't stop.)
  • Unstaged edits — in standard mode all file changes are left unstaged. Review with git diff; discard with git checkout -- ..
  • Worktree isolation — in worktree mode all edits go to a temp copy of the repo. Live files are untouched until you open a PR. The worktree is cleaned up automatically when the session ends.
  • Environment-aware defaults — production defaults to shell: off and worktree: on. The artisan allowlist is narrowed per environment (e.g. test and migrate:* are only allowed locally). All of this is enforced in PHP, not via prompting.
  • Allowlist-gated commandsRunArtisan and RunTests check the allowlist for the current environment before executing. Commands not in the list are refused with a clear message. Destructive commands (e.g. migrate:fresh) require an interactive confirmation even when allowlisted.
  • Budget cap — the session aborts once estimated spend exceeds budget_usd.
  • Subprocess isolationRunArtisan, RunTests, RunPint, and RunShell all run as child processes. A broken generated file cannot crash the session.
  • Shell is gated — the default approve mode requires your confirmation before any shell command runs. Use --off for read-only exploration.

What the guards do and don't stop

Be clear-eyed about what these guarantees are worth, because it changes how you should deploy Tackle.

By default, the agent runs as you. Every subprocess it starts — RunTests, RunArtisan, RunPint, an allowlisted composer under RunShell — executes with your operating-system user's full privileges. Those are separate processes that Tackle's in-PHP guards do not sit inside.

The consequence: an agent that can write code and trigger its execution can reach anything your user can, PathGuard notwithstanding. It never needs to call ReadFile on .env — it can write a Pest test that reads env('APP_KEY') and surfaces it, then call RunTests; the subprocess reads the file your user can already read, and PathGuard is never in the path. Composer scripts are arbitrary PHP and are the same story. Worktree mode does not change this — the subprocess still runs as you and still reads the real .env.

This is not a bug in the implementation. It is the fundamental limit of running an agent at your own privilege level: in-process guards cannot contain in-process code execution. So:

  • What the guards do stop: honest mistakes, casual missteps, and the accidental blast radius of a confused agent — a stray rm, an edit to the wrong file, a migrate:fresh on the wrong database. This is the overwhelming majority of what actually goes wrong day to day, and it is real protection.
  • What they do not stop: a determined agent — or, more realistically, a prompt-injected one. ai:fix --sentry=, ai:respond, and issue-driven runs all ingest text an attacker can influence (a crafted exception message, a PR comment, an issue body). Treat that text as untrusted input that reaches an agent running as you.

How to actually bound it, in ascending order of real containment:

  1. Keep the production defaults. In production, shell is off, worktree is on, and the artisan allowlist narrows to route:list — the code-execution paths above are closed there. The exposure is real mainly on a developer's own machine and in staging.
  2. Run untrusted-input jobs (ai:fix, ai:respond, self-healing) in CI or a container with scoped, throwaway credentials — never with your production secrets in the environment. If the agent exfiltrates the env, there is nothing there worth taking.
  3. Isolate the process. True containment comes from an OS-level jail (a locked-down container, a disposable VM, throwaway credentials), not from in-language checks. If you need a structural guarantee rather than a best-effort one, that is the layer that provides it.

The short version: Tackle's guards make the agent safe to work with. They do not make an agent running on your machine safe to distrust. Deploy the untrusted-input paths accordingly.

Guard pack

Tackle ships optional first-party hooks that block the concrete paths above. Install the recommended registration with:

php artisan tackle:install guard

It prints three pre_tool hook entries to add under hooks.pre_tool in config/tackle.php:

  • SecretExfiltrationGuard (WriteFile, EditFile) — refuses writing code that reads secrets to surface them: env('…_KEY'), config('app.key'), reading .env directly. This closes the write-a-test-that-dumps-env path.
  • NetworkExfiltrationGuard (WriteFile, EditFile, RunShell) — flags the exfiltration transport: outbound HTTP in agent-authored code, curl … | sh, external curl/wget. Mode block (default), confirm, or off.
  • ComposerScriptGuard (RunShell) — blocks composer run-script/exec and lifecycle-script invocations, since composer scripts are arbitrary PHP.

Tune each via tackle.guard (AI_CODE_GUARD_SECRETS, …_NETWORK, …_COMPOSER); extend the secret patterns with tackle.guard.secret_patterns.

This is defense-in-depth — one more layer, not the wall. The guard pack raises the cost of the known exfiltration paths and catches mistakes and unsophisticated injection, but it runs in-process at the agent's privilege and a determined attacker who avoids the signatures is not stopped by it. It sits below mitigation #3 (OS-level isolation), never in place of it.

Injection shield (experimental)

The guards above defend the outbound paths — code the agent writes. The inbound threat is prompt injection through the untrusted text the agent reads: a crafted exception message, an issue body, a PR comment carrying instructions aimed at the agent. The injection shield screens the untrusted readers (ReadSentryIssue, ReadGitHubIssue, ReadPullRequest) with a cheap classifier model. Flagged content is returned fenced and labelled as untrusted data the agent must not obey — reframed, not blocked, so the reader still works. Enable it in config/tackle.php:

'guard' => [
    'injection_classifier' => [
        'enabled' => env('AI_CODE_GUARD_INJECTION', true),
        'model'   => 'claude-haiku-4-5-20251001',   // a small, fast model
    ],
],

It costs one cheap model call per untrusted read and fails open — a classifier error passes the content through unshielded rather than breaking the read. Same honest caveat, doubly so here: the classifier is itself an LLM and can be injected. It lowers the odds a crafted payload steers the main agent; it does not eliminate them. Defense-in-depth, still below OS isolation.

Troubleshooting

HTTP request returned status code 401

Your API key is missing or incorrect. Check that ANTHROPIC_API_KEY (or the key for your chosen provider) is set in .env and that config/ai.php has been published and contains the matching provider block.

Agent error: ... and the session continues

The agent caught an exception during a turn. The error is shown but the session stays alive — type your next task to continue. If the same error repeats, check the message for clues (auth issues, missing binaries, filesystem permissions).

Session aborted: estimated cost exceeds the budget limit

You've hit the budget_usd cap. Increase it in .env:

AI_CODE_BUDGET=5.00

Or pass a higher limit for a single session by editing the config temporarily. The default $1.00 limit is intentionally conservative.

ai:code requires an interactive TTY

ai:code is an interactive REPL and must be run in a real terminal — not piped, not in a CI job, not through a non-interactive shell, because its approval prompts need user input.

For pipes, CI jobs, and cron, use ai:run instead. It runs the same agent with the same tools and reports a structured result and an exit code.

Path '...' is outside the workspace root

The agent tried to access a file outside the configured workspace (defaults to base_path()). If you're working in a monorepo or non-standard layout, set workspace in config/tackle.php to the correct root path.

Path '...' matches protected pattern

The agent tried to read or write a protected file (.env, vendor/, etc.). This is intentional — protected paths are blocked in code, not via prompting. If you need to unblock a path (e.g. you're working on a package inside vendor/), remove or narrow the relevant pattern in protected_paths.

old_str not found / old_str appears N times

The agent is trying to edit a file but the string it wants to replace either doesn't exist or appears more than once. This usually means the agent needs to re-read the file to get the current content. Tell it: "read the file again before editing."

Pint is not installed

Install Pint as a dev dependency in the host app:

composer require laravel/pint --dev

Tests fail during a session

This is expected behaviour. When RunTests returns failures, the agent reads the output and attempts to fix the code. If it gets stuck, tell it what the failure means or paste the relevant stack trace as your next message.

Healer branch is pushed but no PR is opened

Tackle could not find a GitHub token. Check the resolution order:

  1. GITHUB_TOKEN in .env
  2. GitHub CLI: run gh auth status — if it shows "not logged in", run gh auth login
  3. tackle.healing.github_token in config/tackle.php

git worktree add failed

This means either:

  • The project is not inside a git repository. Run git init && git commit -am "initial".
  • The branch name already exists. Delete it: git branch -D tackle/heal-<id> and retry.

Healer ran but didn't fix the bug

The agent's fix will be in the PR (or logged). Review the PR description for the agent's reasoning. If the diagnosis was wrong, close the PR and fix it manually — the agent's attempt gives you a starting point and a working branch to build on.

Healer triggered a loop

HealJobFailure has $tries = 1, so a failing healer cannot create a loop with itself. If you see repeated healer jobs it means the original job keeps failing and threshold is set to 1. Raise the threshold or disable healing until the root cause is resolved:

AI_CODE_HEALING_ENABLED=false

Known Risks

laravel/ai is new and fast-moving. It reshapes its Agent contract on most 0.x minors, and an incompatible method signature is a compile-time fatal — the agent class cannot be declared at all, taking down every command with it. Tackle supports >=0.1 <0.11 and CI runs the suite against the oldest, middle, and newest of that range on PHP 8.3 and 8.4. A release beyond 0.10 is not covered until the matrix is extended.

This tool modifies your codebase and runs commands. Always run it inside a committed git working tree so you have a clear undo path (git checkout -- .).

Development

composer install
./vendor/bin/pest        # run tests
./vendor/bin/pint        # format code

License

MIT