kintsugi / laravel
Repairs your broken app with gold: catches production exceptions, analyzes them with an AI model, and opens draft fix PRs on GitHub.
Requires
- php: ^8.2
- anthropic-ai/sdk: ~0.5 || ^1.0
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^10.0 || ^11.0 || ^12.0
- illuminate/contracts: ^10.0 || ^11.0 || ^12.0
- illuminate/queue: ^10.0 || ^11.0 || ^12.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- pestphp/pest: ^2.34 || ^3.0
- phpstan/phpstan: ^1.11 || ^2.0
This package is auto-updated.
Last update: 2026-08-22 14:19:00 UTC
README
金継ぎ — the Japanese art of repairing broken pottery with gold.
Your Laravel app breaks in production. Kintsugi catches the exception, waits until it has recurred enough to matter, has an AI model work out whether it's actually fixable in code, and — only when it is — opens a draft pull request on your GitHub repo with the smallest possible fix, a root-cause analysis, and a suggested regression test. The repair arrives as a visible, reviewable seam.
Everything runs on your infrastructure with your keys. No SaaS. The only things that ever leave your servers are the redacted error context sent to the AI provider you configure, and the PR sent to GitHub.
composer require kintsugi/laravel
How it works
exception reported ──► fingerprint ──► recurred ≥ N times in window? ── no ──► ignore (blip)
│ yes (once)
▼ (your queue)
rate limits ─► existing-PR check ─► source snippets (redacted)
│
▼
AI analysis: "is this fixable in code?" ── no ──► log & stand down
│ yes, confident
▼
patch applied against repo HEAD ── drift? ──► abort
│
▼
branch `kintsugi/fix-{fingerprint}` ─► draft PR 🎉
Three design decisions do the heavy lifting:
- Recurrence before spend. One-off blips (deploy races, transient nulls, a flaky upstream) never reach analysis: an error must occur
min_occurrencestimes insideoccurrence_window_minutesbefore Kintsugi spends a single AI token on it. - Verdict-first honesty. The AI must first answer whether the exception is fixable in application code. Most production exceptions aren't (infrastructure, configuration, third-party outages) — the prompt and response schema make "no" a first-class answer, and only
fixable_codeverdicts above your confidence threshold become PRs. - Patch against HEAD, abort on drift. Analysis reads the deployed files (what actually threw); the patch is applied to your repository's HEAD via the GitHub API. If they've diverged so the edit no longer matches, Kintsugi stands down instead of guessing.
Requirements
- PHP 8.2+ · Laravel 10, 11, or 12
- A real queue worker (Redis, database, SQS, …). Kintsugi refuses to analyze on the
syncdriver by default — an AI call inside a failing request is never acceptable. - An API key for one of the bundled AI drivers (Anthropic, OpenAI, or Gemini)
- A GitHub fine-grained PAT scoped to one repository with Contents: Read and write + Pull requests: Read and write
Setup
composer require kintsugi/laravel php artisan vendor:publish --tag=kintsugi-config
KINTSUGI_ENABLED=true # AI provider — pick one driver KINTSUGI_AI_DRIVER=anthropic # anthropic (default) | openai | gemini KINTSUGI_ANTHROPIC_KEY=sk-ant-... # falls back to ANTHROPIC_API_KEY # KINTSUGI_OPENAI_KEY=sk-... # falls back to OPENAI_API_KEY # KINTSUGI_GEMINI_KEY=AIza... # falls back to GOOGLE_API_KEY # GitHub KINTSUGI_GITHUB_TOKEN=github_pat_... KINTSUGI_GITHUB_REPO=your-org/your-app
Then verify the whole path end to end:
php artisan kintsugi:test # validates keys, pings GitHub, fires a synthetic exception php artisan kintsugi:test --sync # same, but runs the analysis inline so you see the result immediately
No changes to your exception handler are needed — Kintsugi hooks Laravel's reportable() automatically, on Laravel 10 handler classes and 11/12 bootstrap/app.php apps alike.
Configuration reference
Everything lives in config/kintsugi.php. The important knobs:
| Key | Default | What it does |
|---|---|---|
enabled |
false |
Master opt-in (KINTSUGI_ENABLED). |
disabled |
false |
Kill switch (KINTSUGI_DISABLED=1) — wins over everything, checked first. |
environments |
production, staging |
Only these app environments capture anything. |
ai.default |
anthropic |
Driver name; community drivers via AiManager::extend(). |
ai.confidence_threshold |
0.7 |
Below this confidence, an analysis never becomes a PR. |
ai.max_rounds |
2 |
Round 1 + up to one "show me another file" round. |
github.draft |
true |
Open PRs as drafts (recommended). |
github.base_branch |
null |
null = the repo's default branch. |
github.branch_prefix |
kintsugi/fix- |
Fix branches are {prefix}{fingerprint} — deterministic, so they double as dedupe. |
queue.connection / queue.queue |
app default | Where analysis jobs run. |
queue.allow_sync |
false |
Permit the sync driver (only sensible in local experiments). |
limits.min_occurrences |
3 |
An error must happen this many times before analysis. Set 1 to analyze on first sight. |
limits.occurrence_window_minutes |
15 |
The rolling window for the count above. |
limits.analyses_per_hour / per_day |
5 / 20 |
AI spend budget. |
limits.prs_per_day |
5 |
PR budget. |
limits.fingerprint_cooldown_hours |
72 |
One analysis per distinct error per cooldown. |
limits.max_files_changed / max_lines_changed |
4 / 120 |
Hard caps on fix size — bigger fixes are aborted, not trimmed. |
context.app_frames |
5 |
How many non-vendor frames get source snippets. |
context.snippet_lines |
20 |
Context lines around each frame. |
context.include_request_body |
false |
Request bodies are opt-in and always redacted. Headers are never captured. |
paths.deny / paths.allow |
see file | What the AI may read (round 2) and edit. .env* is denied unconditionally. |
ignore_exceptions |
[] |
Extra exception classes to never analyze (merged with sensible defaults: validation, auth, 404s, CSRF, all 4xx HTTP exceptions). |
storage.store |
null |
Cache store for dedupe/counters — use redis/memcached/database in production. |
log_channel |
null |
Every Kintsugi log line is tagged kintsugi: true. |
What context the AI sees
Kintsugi captures where the exception actually happened:
- HTTP requests — method, path, route name (body opt-in, headers never).
- Queued jobs — the job class, queue, and connection. Most production errors live here, and the job class is often the difference between a right and wrong verdict.
- Console / scheduled commands — the running artisan command name.
Plus: the redacted exception message, the stack trace (capped at 60 frames), and source snippets around the top app frames, read from the deployed files so the analysis matches what actually ran.
What data leaves your server
Only two destinations, both configured by you:
- Your AI provider receives: exception class + redacted message, redacted runtime context, the stack trace, redacted source snippets around the failing frames, and (round 2 only) redacted contents of files the model requested — after path-policy checks.
- GitHub receives: the fix branch, commit, and PR body (also redacted).
Redaction runs before any egress: well-known credential shapes (sk-ant-…, github_pat_…, ghp_…, AIza…, JWTs, Bearer …), your configured keys as exact values, and sensitive array keys (password, authorization, cookie, …). .env files are unreadable regardless of configuration.
Safety model
The guardrails are code, not prompt suggestions:
- Draft PRs by default — Kintsugi proposes, never merges.
- Recurrence threshold + budgets + cooldowns — no PR storms, no runaway bills.
- Path policy on reads and edits (
vendor/,config/,.github/,composer.json, … denied by default). - Diff caps — oversized fixes abort rather than shrink.
- Abort on drift — if the repo has moved past the deployed code, no guessing.
- Untrusted-input rule — the prompt instructs the model that exception messages and request data are runtime values, never instructions; and everything it returns is schema-validated and policy-checked anyway.
- Kill switch —
KINTSUGI_DISABLED=1stops everything, immediately.
AI drivers
anthropic (default), openai, and gemini ship in the box. Add your own exactly like a Laravel mail/cache driver:
use Kintsugi\Laravel\AiManager; app(AiManager::class)->extend('mistral', fn ($app) => new MyMistralDriver(...));
A driver is one method:
public function completeStructured(string $system, array $messages, array $schema, string $schemaName): array;
Force the JSON schema however your provider does it (structured outputs, forced tool call, response schema) and return the decoded object.
What it will not do
- Merge anything. Ever.
- Analyze one-off errors, 4xx client errors, validation/auth noise, or its own exceptions.
- Fix things it judges infrastructure-shaped (
not_code_fixable), too risky to automate (unsafe_to_automate— payments, security-sensitive logic), or that it isn't confident about. - Retry a failed analysis (paid API calls never loop).
- Open a second PR for an error that already has one open.
Troubleshooting
| Symptom | Cause |
|---|---|
| Nothing happens on errors | KINTSUGI_ENABLED unset, environment not in environments, or the error hasn't hit min_occurrences yet. |
Log warning about the sync queue |
Run a real queue worker, or (local only) set queue.allow_sync. |
Outcome drift in the logs |
Deployed code no longer matches the repo's HEAD — deploy, then let the error recur. |
Outcome rate_limited |
One of the budgets in limits is exhausted for the window. |
| Duplicate-looking errors not analyzed | Same fingerprint is under its 72h cooldown — that's the design. |
Every decision is logged with kintsugi: true context: verdicts, PR URLs, and stand-down reasons.
Testing
composer test # Pest: unit + feature + architecture composer analyse # PHPStan level 6
The architecture test enforces that Kintsugi\Core never imports Illuminate\* — the core is framework-agnostic by construction, so Symfony and other bridges can reuse it unchanged.
Roadmap
- Notifications (Slack/mail) when a PR opens
kintsugi:status+ database-backed audit trail- Per-verdict cooldowns, monthly cost budget
kintsugi:prunefor stale fix brancheskintsugi/php-coresplit + Symfony bundle · GitHub App auth · GitLab support
License
MIT. Issues and PRs welcome at AmineAlyate/kintsugi.