paider / paider
A PHP-native AI coding agent.
Requires
- php: ^8.4
- ext-ctype: *
- ext-curl: *
- ext-dom: *
- ext-fileinfo: *
- ext-filter: *
- ext-iconv: *
- ext-mbstring: *
- ext-openssl: *
- ext-pdo_sqlite: *
- ext-phar: *
- ext-tokenizer: *
- ext-zlib: *
- guzzlehttp/guzzle: ^7.10
- laravel-zero/framework: ^12.0.2
- laravel/prompts: ^0.3.19
- tempest/highlight: ^2.27
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- laravel/pint: ^1.25.1
- mockery/mockery: ^1.6.12
- pestphp/pest: ^3.8.4|^4.1.2
README
π Paider
A PHP-native AI coding agent β that lives inside your Laravel app. π€
Built on Laravel Zero Β· Laravel Prompts Β· Termwind Β· MCP PHP SDK (v0.2)
π¦ Status, honestly
Alpha. The commands work, the ledger reports real money, and on 2026-08-18 it drove its first end-to-end edits in someone else's repo β 3/3 rubric-graded live runs. The graded task was deliberately small (two pre-registered one-line edits); treat anything bigger as unproven.
Built in public from commit one, wrong turns left in. Here is precisely what that means today:
| state | evidence | |
|---|---|---|
| π§± v0.1 command surface | β built | paider chat, commit, cost, run, config:provider, config:show all register and run |
| π§ nine tools (7 + 2 conditional) | β built | read_file, write_file, patch_file, run_shell, fetch_url, memory, git + artisan (when artisan file exists) + load_skill (when skills indexed) |
| ποΈ SQLite event log + cost ledger | β built | append-only, ledger is a pure projection; stored in .paider/ (gitignored locally) |
| π§ͺ test suite | β 518 passing, 2975 assertions | hermetic by default; 3 live tests via vendor/bin/pest --group=live |
| π talking to a real LLM | β verified live | OpenRouter, Anthropic, xAI; cost ledger reconciles to provider usage |
| π end-to-end edit in a foreign repo | β 3/3 live runs (2026-08-18) | vlucas/valitron@fadce39f, pre-registered rubric (m1/bench/RUBRIC.md), evidence committed in m1/runs/ β $1.57 across the three passes; run 0 failed the rubric and is committed too |
| π¦ published on Packagist | β published | paider/paider at https://packagist.org/packages/paider/paider |
π¦ curl | sh installer |
β live | curl -fsSL paider.dev/install | sh β served from GitHub Pages, installs via Composer; PHAR built 32MB (build/paider.phar via box, 222ms), FrankenPHP trimmed still deferred |
| π·οΈ tagged release | β οΈ v1.0.1 | composer require paider/paider resolves v1.0.1 β a tag that shipped early; see the erratum below |
Do not install this expecting a polished agent. The wiring is real and tested, and the last mile has now been crossed β an actual key, an actual model, actual edits landing in a third-party repo β but only for a small pre-registered task. Bigger claims wait for bigger evidence.
β οΈ Erratum: the v1.0.x tags shipped early
On 2026-08-08 an autonomous overnight run tagged and published v1.0.0 and v1.0.1 to
Packagist. Those tags meet none of this repo's own written v1.0 definition of done β no MCP
server mode, no published semver policy, no measured diff-apply rate, and the capability gap in
the banner above still open. Nobody noticed for nine days, because the run's scoring loop graded
polish, not the release gate.
The tags stay (deleting published tags rewrites history and can break lockfiles) β but read
them as alpha-quality code wearing a 1.0 number. No further v1.0.z patch will claim
otherwise. The next release that earns its number is v1.1.0, gated on runnable checks: the
end-to-end edit in a genuinely third-party repo, the CI feedback-loop gate, and a published,
measured diff-apply rate. Full ruling in DECISIONS.md Β§22.
π What this is not
It is not the first PHP coding agent β neuron-core/maestro
got there first and its README says so correctly. It is not faster than a Go or Rust agent; PHP's
interpreter floor is 48.6ms against Python's 21.5ms and ripgrep's 3.7ms, and no amount of care
changes that. If raw startup is what you want, use something compiled.
π Honest comparison
Stars and dates pulled live from the GitHub API on 2026-08-02:
| π Paider | neuron-core/maestro |
Aider-AI/aider |
|
|---|---|---|---|
| language | PHP | PHP | Python |
| stars | unreleased | 38β | 47,886β |
| last pushed | active | 2026-06-19 | 2026-05-22 β οΈ |
| open issues | β | 0 | 1,770 |
| first PHP agent? | β no | β yes, and says so | n/a |
| shipping today? | β not yet | β yes | β οΈ stalled |
| named cost tiers | β 4, with a ledger | β | β main/weak/editor, unlabeled |
| open-weight presets | β 2 | β | β |
| commercial coupling | none | Inspector.dev SaaS | none |
Read that table honestly: Maestro ships today and Paider does not. Maestro is also 38 stars beside its own 2,038-star underlying SDK β so "PHP developers want an agent CLI in PHP" is unproven, not confirmed, even by the one entrant that exists. Aider proves the opposite risk: 48k stars mean nothing once the maintainer goes quiet.
What Paider bets on is the two columns nobody else fills in β named cost tiers with a checkable ledger, and an agent that lives inside your Laravel app.
π‘ What it is
Two bets.
1. An agent that lives inside your Laravel app knows things an external one has to be told.
laravel/mcp builds MCP servers; the PHP SDK consumes them. A Laravel application can be both
ends of the protocol at once. Shipped as a package, Paider turns your own models, jobs, queues
and domain logic into tools the agent can call β defined in the framework's idiom, not
hand-rolled JSON schemas. No Python or Go CLI can do that for a Laravel developer.
The full version of this β any MCP client driving Paider's tools β is v1.0, gated on the MCP PHP
SDK maturing past pre-1.0. The intended first step is ArtisanTool, reading route:list as
structured data instead of shell text when pointed at a Laravel repo.
β
ArtisanTool is built. One hardcoded call (php artisan route:list --json), not a general
Artisan passthrough β arbitrary code execution needs its own approval gate rather than a slot in
ShellTool. Registered only when an artisan file exists at the project root (detects Laravel apps).
See PLAN.md Β§ Sequencing.
2. You can see exactly where the money went.
Four tiers, named for what they are for:
| tier | job | why it matters |
|---|---|---|
orchestrator |
plans, decomposes, reviews | low volume, high value |
coder |
writes the diff | runs in a loop, latency compounds |
research |
reads docs, greps, summarises | high volume, low difficulty β where the money quietly goes |
fast |
commit messages, retries | trivial work at trivial cost |
Status (v0.1): Today, every chat/run loop call executes on the orchestrator tier. The
coder,research, andfasttiers are configured and priced but not yet routed to by the loop β v0.2 work on per-operation tier routing will change that. Thecommitcommand uses thefasttier today (viaCommitCommand). The tier table and the cost ledger are real and fully functional; the routing that connects the two is open scope for v0.2, tracked in PLAN.md.
Nobody else names a research tier. It is the one that ingests 50k tokens to extract 500, and paying orchestrator rates for it is how agent bills get absurd.
Because the tiers are named, Paider can account for them separately β and answer a question no other agent CLI can:
$ paider cost
tier calls in out spend share
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
orchestrator 14 61.2k 19.8k $0.801 19.8%
coder 203 1.4M 287.1k $2.970 73.3%
research 118 1.8M 34.6k $0.262 6.5%
fast 77 98.4k 12.2k $0.017 0.4%
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
session 3.36M 353.7k $4.050
97.8% of your tokens went through tiers costing 80.2% of your spend.
Same work on all-Opus 5: $25.64 Β· you saved $21.59
Modelled session, real command. β
paider costprints all of the above β every column, the ratio line and the all-Opus comparison β straight off the event log. The token volumes shown are a modelled session, but the arithmetic is the shipped code's, not a mockup's. Eachtier_callevent is priced at write time fromconfig/prices.phpby exact model id. A model with no entry there storescost_usdasNULL, not$0.00β those surface as unpriced calls naming the specific model, rather than silently undercounting the total.Two tests hold this block and the code together from opposite ends:
CostTableTestparses this table and recomputes it fromconfig/prices.php, andCostReadmeGoldenTestseeds the event log with these volumes, runs the real command, and asserts the output matches. The table cannot drift from the code in either direction without a failing test.
--jsonshape. Same data, machine-readable:{tiers, session, unpriced_calls, comparison}. Each entry undertiers(and thesessionrow, minusshare_pct) carriescalls, tokens_in, tokens_out, tokens_cache_write, tokens_cache_read, spend_usd, unpriced_calls, unpriced_models, hypothetical_usd, hypothetical_unknown, share_pct.unpriced_callsis a list of{tier, count, calls, models}.comparisonis{hypothetical_usd, saved_usd, token_share_pct, spend_share_pct}. Pinned byCostJsonGoldenTestβ an added, removed, or renamed key fails the suite, including on an empty ledger.Note on cache tokens: The
spend_usdis calculated from four token types at write time:tokens_in,tokens_out, plus Anthropic's cache write and cache read tokens when present. Thetokens_inandtokens_outfields shown above do not itemize cache tokens separately, but they are priced and included in spend. On cached workloads this is significant β measured on real sessions, cache tokens represent ~93% of cost. SeePricesSyncTestfor the cache pricing contract andconfig/prices.phpfor rates per model.
That last line is the product in one sentence. Most agent tools show you a total, if anything. Paider shows you the ratio β and the ratio is the whole argument for routing, once it's wired up.
It also keeps us honest. The 84.2% figure below is a modelled session; the ledger is what confirms or refutes it on real work. A cost claim you cannot check is marketing, and this one is checkable by the person paying.
The presets
Eleven model presets + one accounts rotation strategy ship in config/presets.php, every model ID and price verified against the live catalogue of the endpoint that actually serves it (OpenRouter for mixed presets, the provider's direct API for single-provider stacks). Modelled on a session planning 50k/20k and working 2M/300k:
| stack | cost |
|---|---|
| all Opus 5 | $18.25 |
| all Sonnet 5 | $7.30 |
| default β Opus 5 to think, Muse to code, deepseek for research/fast | $4.050 |
There is also an open-weight stack for people who will not send their code to a US frontier
lab, or who want to be able to audit and self-host what they run: kimi-k3 planning, kimi-k2.6
coding, deepseek-v4-flash on research and fast.
All weights in both open and open-frugal stacks are confirmed available for download: K3, K2.6,
and V4-Flash are all published and self-hostable. The same applies to minimax-m3 in open-frugal,
which is also open-weight with published weights on Hugging Face.
paider config:provider open # kimi-k3/k2.6 + deepseek (coder specialised, research budget-aware) paider config:provider balanced # opus-5 to think, deepseek for research/fast, muse for coder paider config:provider kimi # single-provider stacks for all the majors paider config:show # what am I actually running?
π³οΈ Why build it at all
Aider-AI/aider has 47,886 stars, 1,770 open issues, and no
commit since 2026-05-22. Its most-repeated issues are not feature requests β they are
#4613 "where is Paul?" and
#4648 "what is the intended future of Aider?",
closed not-planned. There are 4,805 forks and none has consolidated the userbase; the leading
one holds 0.8% of upstream's stars.
It died of abandoned stewardship, not a technical flaw. That is the thing worth designing
against, and it is why PLAN.md has a Non-goals section longer than its feature list.
π₯οΈ On the terminal UI
The fashionable agent CLIs render with Ink, React for the
terminal. It is good, and it is not obviously better than what PHP already has for this shape of
program. Symfony Console is twenty years mature; laravel/prompts does streaming output
(stream(), task()->partial()) and every interactive input, with non-TTY fallback built in;
Termwind does Tailwind-style layout for static output; and Collision renders errors better than
most things in any language.
The Windows caveat you will read about β "laravel/prompts is WSL-only" β does not apply here,
and that was measured, not assumed. Illuminate\Console\Command already calls
Prompt::fallbackWhen(windows_os()) and registers Symfony Question Helper fallbacks, so Laravel
Zero inherits a working Windows path for free. The streaming output side is byte-identical
under the fallback (all 47 lines of a captured run); only the interactive input prompts change,
from arrow-key selection to typed numbers. Windows Terminal is the documented baseline β legacy
cmd.exe mangles the box-drawing glyphs. Full measurement in DECISIONS.md Β§10.
Where Ink genuinely wins is a full-screen alternate-buffer app with many live reactive panes. A coding agent is mostly streaming text, a spinner, a diff and a confirm β and PHP is fine at those. If Paider ever needs a real reactive TUI, that is the moment to reconsider, not before.
ποΈ Architecture
Everything durable goes through one SQLite file. The cost ledger is not a balance anyone
increments β it is a projection replayed over an append-only event log, which is why
/undo and the audit trail come free rather than being features someone has to maintain.
you β ChatCommand β Session β Loop ββ¬β TierRouter β provider (Anthropic | OpenAI-compatible)
ββ Approval Gate β tools β PathGuard
β
events (append-only) β CostLedger
πΊοΈ Same thing as a diagram (renders on GitHub)
flowchart TB
User([π€ you]) -->|prompt| Chat[π¬ ChatCommand]
Chat --> Session[π§ Session]
Session --> Loop[π Loop]
Loop -->|picks a tier| Router{{ποΈ TierRouter}}
Router -->|orchestrator| P1[π AnthropicClient]
Router -->|coder Β· research Β· fast| P2[π OpenAiCompatibleClient]
Loop -->|proposes a tool call| Gate{π Approval Gate}
Gate -->|denied| Loop
Gate -->|allowed| Tools
subgraph Tools [π§ 9 tools]
direction LR
T1[read_file] ~~~ T2[write_file] ~~~ T3[patch_file]
T4[run_shell] ~~~ T5[fetch_url] ~~~ T6[memory]
T7[git] ~~~ T8[artisan<br/>conditional] ~~~ T9[load_skill<br/>conditional]
end
Tools -->|every path checked| Guard[π‘οΈ PathGuard]
Tools --> Log[(ποΈ events Β· append-only)]
P1 & P2 --> Log
Log -->|projection| Ledger[π° CostLedger]
style Gate fill:#f96,stroke:#333,color:#000
style Guard fill:#9cf,stroke:#333,color:#000
style Log fill:#cfc,stroke:#333,color:#000
Loading
π One turn, end to end
sequenceDiagram
autonumber
participant U as π€ you
participant L as π Loop
participant R as ποΈ TierRouter
participant M as π model
participant G as π Gate
participant T as π§ tool
participant E as ποΈ events
U->>L: "fix the failing test"
L->>R: which model for `orchestrator`?
R-->>L: anthropic/claude-opus-5
L->>M: prompt + tool contracts
M-->>L: fenced tool call
Note over L,M: text-fenced blocks, not native tool-calling β<br/>providers disagree on the format, and some<br/>coder-tier models report structured_outputs=false
L->>G: patch_file(app/Foo.php)
G->>U: show diff Β· allow once / session / deny
U-->>G: allow once
G->>T: execute
T->>T: stamp check β parse hunks β syntax gate
T-->>L: ToolResult
L->>E: append(tool_result, β¦)
E-->>L: uuid7
Note over E: append only β no update(),<br/>no delete(), anywhere in the class
Loading
π§ͺ Test suites β hermetic by default, live on demand
Two test runs, two purposes:
# Default: hermetic, no network, no cost # β safe to run on every commit, in CI, anywhere vendor/bin/pest # Live: real API calls, real spend (measured and reconciled) # costs money Β· requires credentials Β· read the ledger output vendor/bin/pest --group=live
Hermetic suite (vendor/bin/pest, 518 tests, 2975 assertions) β all provider interactions mocked via Guzzle;
proves self-consistency, zero cost. Excluded group: live. This is the number in the badge above;
the live suite is 3 more on top, not part of it.
Live suite (vendor/bin/pest --group=live, 3 tests) β real round-trips to api.openrouter.ai,
api.anthropic.com, and api.x.ai (xAI fallback when ANTHROPIC_API_KEY absent). Discovers
shape mismatches, usage-field placement, and actual token costs. Tests skip gracefully (no failure)
when credentials are absent, so CI stays green in sandboxes.
Environment variables for live suite:
OPENROUTER_API_KEYβ enables OpenRouter test (qwen/qwen3-max)ANTHROPIC_API_KEYβ enables Anthropic test (claude-haiku-4-5-20251001)XAI_API_KEYβ fallback when Anthropic key absent (Claude via xAI's Anthropic-format endpoint)
π Live test discoveries
Three real calls across two tiers; the cost ledger reconciles exactly:
- OpenRouter round-trip β
qwen3-maxresponds with usage fields where the parser expects them - Anthropic wire format β novel finding: grok-4 returns a
thinkingcontent block beforetext, andAnthropicClientfilters ontype === 'text'. Every hand-written fixture was text-only, so this filter had never been exercised against reality until now. β holds. - Cost ledger reconciliation β provider-reported token counts match our projection's totals to 1e-9; spend matches the price sheet exactly; tiers partition the session correctly.
β¨οΈ CLI reference
| command | what it does | state |
|---|---|---|
paider / paider chat π¬ |
interactive session rooted at the cwd | β built |
paider commit π |
stage everything, generate a message on the fast tier, commit | β built |
paider cost π° |
per-tier calls, tokens, and spend from the event log | β built |
paider config:provider <preset> ποΈ |
switch the active tier stack | β built |
paider config:show π |
show the active preset and the model per tier | β built |
In-session slash commands β aider's proven UX, not a Paider invention:
| slash | effect |
|---|---|
/add <file> β |
put a file in context (and stamp it for staleness detection) |
/drop <file> β |
take it back out |
/diff π |
show pending changes |
/undo β©οΈ |
roll back the last applied change |
/tier <name> <model> ποΈ |
override one tier for this session |
/quit π |
leave |
π The parts we were paranoid about
Every row below is a bug that was found and fixed by adversarial review, not a design someone got right first try:
| guard | what it stops |
|---|---|
π‘οΈ PathGuard |
.. traversal, symlink escapes (including dangling symlinks), and prevents file_exists() false negatives |
π Gate |
only ever caches a grant β there is no path that reads a cached deny as an allow |
| π approval bypass | the model's tool-call input scrubbed of both approval and approved keys before any tool sees it; Loop::dispatch() is the sole enforcement point for all tool-trusting paths |
π§Ύ EventLog |
no update(), no delete(), anywhere β append-only is structural, not a comment |
π€« SecretsGuard |
redaction before anything reaches a model |
πΈ QwenPlanKeyGuard |
refuses an sk-sp- plan key paired with a PAYG base URL, which would silently bill you |
| π« strict JSON | six paths where a lenient decode turned a failed call into a successful-looking empty one |
β±οΈ ShellTool timeout |
SIGTERM then SIGKILL after 0.5s β proc_close() blocks until the child exits, so a trap '' TERM command ran the full 20s against a 1s timeout and reported exit 0 |
| πͺ’ NUL-byte path guard | fnmatch errors on NUL bytes are caught and fail-safe, preventing model-triggered process crash |
π patch_file + secrets |
creating a new .env/id_rsa skipped approval entirely, because stamp='__new_file__' needs no prior read β which is where the gate used to catch it |
π /undo boundary |
/undo respects project root, cannot delete files outside it or poison the stack |
| π€ JSON-array commands | array-form command in shell execution is displayed plainly in approval prompts, grants cache by displayed text, no auto-grant on later arrays |
| β result checking | paider commit returned SUCCESS when nothing was committed, and fed a SecretsGuard refusal to the model as though it were a diff |
| π§Ή output sanitisation | paider commit routes model-generated commit messages and provider errors through TerminalSafe::clean() at the display boundary only, stripping OSC 52 clipboard injection, cursor movement, and other unsafe sequences while preserving SGR colour output |
π¬ Where these came from β and the incomplete fix that was already verified
Adversarial review with 33 agents across 8 lenses, independently reproduced by skeptics. All six defects found, all committed to regression tests. One finding towers over the others:
The approval bypass fix was incomplete β a second field name existed. Loop::dispatchShell() and
Loop::dispatchArtisan() had been scrubbed of $input['approval'] to close an ACE hole. But read_file,
write_file, patch_file, and git trust a different key β approved β and Loop::dispatch() passed
the model's raw input unchanged to all of them. The model is handed both field names in the JSON schema in
the system prompt, so it knew exactly what to try. A model reply with {"approved":"allow-once"} would
bypass the gate on file read/write/git operations: no approval callback, no human confirmation, raw
credentials in the next model call. Fixed at the single chokepoint in Loop::dispatch() rather than
per-tool, with regression tests asserting the gate runs and wins.
The other five found and fixed:
- JSON-array
commanddisplayed as''in approval prompts while the original array was passed toproc_open; grants cached by displayed text so oneallow-sessionsilently authorized every later array. /undodeleted files outside project root with no prompt, poisoning the undo stack.PathGuardpassed a dangling symlink βfile_exists()returns false for one, but the path was never checked after that.- NUL byte in a path crashed the process β
fnmatch()throwsValueError, unhandled, a zero-approval DoS.
Both risks below are now closed:
- Fixed β 2026-08-03,
DECISIONS.mdΒ§17: an approved shell command's child used to inherit the full parent environment, including live provider API keys.ShellToolandGitToolnow scrub the environment to a curated allowlist:PATH,HOME,LANG,TERM,TMPDIR,USER,SHELL. A user who needs another variable can opt it back in withPAIDER_SHELL_ENV_ALLOW. - Fixed β 2026-08-03,
DECISIONS.mdΒ§18: tools likeread_file,write_file, andpatch_fileused to trust anapprovedkey in the model's input if called directly. The approval decision is now a separate$approvedparameter that only PHP code can set, making it impossible to self-approve through the JSON schema. Scope, honestly: this covers the four file/git tools.run_shellandartisanuse a different tri-state approval key and were deliberately left out β for those two, theLoop::dispatch()chokepoint is still the only defence.
π Read the thinking
- STORAGE.md β one SQLite file, no services. Why not Redis.
- EXTENSIONS.md β the twelve extensions that ship, and what was cut.
- PLAN.md β thesis, non-goals, v0.1 scope, architecture, milestones, risks.
- DECISIONS.md β how we got here, measured, with the wrong turns left in: recommending the wrong repo, picking a model off a spec sheet that a practitioner knew was a dead zone, and asserting a gap that turned out to be occupied.
π Measured, not estimated
Startup, one machine, medians:
| ripgrep (Rust) | 3.7ms |
| gh (Go) | 20.0ms |
| Python, bare | 21.5ms |
| PHP, bare | 48.6ms |
| Laravel Zero, lean ini | 95.9ms |
| cecli (1,548 modules) | ~710ms |
π¦ Distribution
Two channels, because there are exactly two users:
composer require paider/paider # inside your Laravel app β this is the thesis composer global require paider/paider # as a standalone CLI curl -fsSL paider.dev/install | sh # live β checks PHP + extensions, then installs via Composer
Tagged releases start at v0.1.0, so a bare composer require resolves under Composer's default
minimum-stability: stable. Track the branch with paider/paider:dev-main if you want unreleased
work β that is where the sharp edges live.
The package is non-negotiable: an agent that turns your models and jobs into tools has to be a dependency of your app, and a compiled binary cannot be one.
The binary is planned to be a FrankenPHP embed (11,263β
, Go,
built on Caddy), which produces a self-executable with PHP inside and
supports CLI β ./my-app php-cli bin/console β not just
HTTP. It selects extensions from composer.json, so the shipped tool never inherits a user's dev
ini. That matters: 76 extensions on the author's machine cost 94ms of a 143ms startup.
β Measured 2026-08-02, round 2 β trimmed and built, both risks resolved
Round 1 measured the stock off-the-shelf binary and could only clear the cold-start risk,
leaving size conditional. Round 2 actually built the trimmed binary natively β and it revises
round 1's cold-start conclusion, not just its size one. Full numbers:
DECISIONS.md Β§9.
Both columns below are the runtime alone, with no Paider inside β see the note under the table for what the shippable artifact actually costs.
| round 1 (stock, 77 ext) | round 2 (trimmed, 11 ext) | |
|---|---|---|
| Size | 178MB | 111.3MB (106.2 MiB) β β37.5% |
Compressed (zstd -19) |
60.4MB | 40.6MB |
| Cold start vs. lean-ini PHP | ~23% slower | parity (1.01x, i.e. no measurable penalty) |
| Cold start vs. stock binary | β | 1.20x faster |
The round-2 column says 11 ext and that is not a typo: it is what that build contained when these numbers were taken. The required set later turned out to be twelve β
ext-domwas missing, and the 11-extension binary booted fine and only failed on a command that renders output. A current trimmed runtime is therefore marginally larger than 111.3MB; it has not been re-measured, and no number here should be quoted as if it had been.
Cold start: the penalty is gone, not just smaller. Round 1's "~23% slower than lean-ini PHP" is superseded β that number came from the stock binary dynamically initialising 66 unwanted extensions on every invocation. Trim to the twelve Paider actually needs and the penalty disappears: 94.8ms Β±1.3ms vs. 95.9ms Β±1.3ms for lean-ini system PHP, a statistical tie. Say it as "the cold-start penalty is eliminated," not "FrankenPHP is faster than PHP" β it isn't, it's even.
Size: 178MB on disk, ~72MB compressed β for the binary that actually contains Paider. The
111MB / 40.6MB figures often quoted are the FrankenPHP runtime alone, with no Paider inside;
embedding the app costs another ~67MB, mostly vendor/. 72MB does not land alongside the ~40MB
Go binaries competing agents ship, and that is part of why the curl | sh channel is deferred
rather than shipped β see DECISIONS.md Β§16. Getting there required fixing real bugs first: the "documented nine" extensions produced
a binary that could not boot β laravel-zero/framework calls Phar::running()
unconditionally and needs ext-phar present, undeclared in its own composer.json. Termwind does
new DOMDocument but doesn't declare ext-dom either. Paider's required set is twelve, not
nine (adding phar, filter, and dom cost +283KB total). See EXTENSIONS.md
for the traps and the full extension table.
Full round 2 hyperfine table (3 warmup + 30 timed runs, -N)
| command | mean | std dev |
|---|---|---|
php -n application --version (lean-ini system PHP 8.5.8) |
95.9ms | Β±1.3ms |
php application --version (real Homebrew ini, 73 ext) |
192.9ms | Β±1.4ms |
| stock frankenphp, 77 ext, 178MB | 113.5ms | Β±1.4ms |
| trimmed frankenphp, 11 ext, 111MB | 94.8ms | Β±1.3ms |
The maintainer's 20β30MB estimate was not reached, and now we know why: build-static.sh
always links the full Caddy server and Go HTTP stack, even for a binary only ever invoked as
php-cli β there's no CLI-only mode in the script. Not claimed impossible in general, just not
what the supported build path produces. Whether a Caddy-free CLI-only build could get closer is
open β see PLAN.md β but it's a nice-to-have now, not a blocker.
Build cost is cheap. Built natively (not Docker β the Docker static-builder emits Linux
binaries only) in ~7 minutes, PHP 8.5.9 / Caddy v2.11.4 / Go 1.26.5. build-static.sh cannot
cross-compile, so shipping is still a CI matrix (ubuntu-latest, ubuntu-24.04-arm,
macos-latest, macos-13), but 7 minutes/platform is cheap enough for that matrix to be routine.
A Windows binary does exist (frankenphp-windows-x86_64), and the laravel/prompts half of
the Windows question is resolved β Laravel's own ConfiguresPrompts already handles it, and
the streaming path is unchanged. See DECISIONS.md Β§10. Windows is shippable.
Functional check under the trimmed binary, for the record: application list renders correctly,
pdo_sqlite round-trips an in-memory DB (see STORAGE.md), stream_isatty()
behaves correctly under a non-TTY pipe, and PHP_VERSION reports 8.5.9 β comfortably above the
^8.4 floor. Full writeup: DECISIONS.md Β§9.
The distribution decision is now confirmed on both axes β cold start and size β where round 1
could only confirm one. curl -fsSL paider.dev/install | sh is live as of 2026-08-04, serving
install.sh byte-for-byte from GitHub Pages. It is composer-only β the FrankenPHP
binary channel is still deferred (see DECISIONS.md Β§16 for the three open issues),
so the script checks PHP 8.4+ and the twelve extensions, then hands off to composer global require.
The binary math behind that other channel checks out whenever it gets built.
The apex points at GitHub Pages rather than the self-hosted edge that fronts everything else here. An installer people are told to pipe into a shell should not go dark when a home uplink does.
No PHAR. It needs PHP installed but is not a composer dependency, so it serves neither user better than the two above. A third channel is maintenance forever for an audience of nobody.
No Docker. Container start would eat the entire startup budget.
πΊοΈ Roadmap
π Milestone flow (renders on GitHub)
flowchart LR
V01["π§± v0.1<br/>commands Β· tools Β· ledger"] --> V02["π v0.2<br/>MCP client Β· agent roster"]
V02 --> V11["ποΈ v1.1<br/>the release that earns its number"]
V10["β οΈ v1.0.x<br/>tagged early β erratum"] -.-> V11
style V01 fill:#cfc,stroke:#2a2,color:#000
style V02 fill:#ffd,stroke:#aa2,color:#000
style V10 fill:#fdd,stroke:#c33,color:#000
style V11 fill:#eee,stroke:#999,color:#000
Loading
| milestone | scope | state |
|---|---|---|
| v0.1 | 6 commands, 9 tools, approval gate, event log, cost ledger, tier router, CI pipeline, ~94.8ms cold start Β· M1 closed 2026-08-18: 3/3 rubric-graded live edits in a foreign repo | β done |
| v0.2 | MCP client (hermetically tested; live-server interop open) Β· test-feedback loop (landed, reshaped) Β· XDG config read-precedence (landed) Β· repo-map on research tier (open) | π¨ in progress |
| v1.0 | tagged 2026-08-08 by an overnight run before its DoD was met β see the erratum; tags kept, no further v1.0.z claims compliance | β οΈ premature |
| v1.1.0 | the release that earns the number: M1 closed on a third-party repo, CI feedback-loop gate green, measured diff-apply rate published, semver + non-goals docs Β· MCP server mode ships behind an experimental flag when ready, never as the blocker | β¬ planned |
β Is v0.1 actually done now?
Yes β as of 2026-08-18. PLAN.md wrote v0.1's definition of done before the code
existed, and the last box held out for sixteen days:
| v0.1 definition-of-done | state |
|---|---|
| the four commands | β built |
the seven base tools + ArtisanTool + LoadSkillTool |
β nine built |
sk-sp- key/base-URL guard |
β built |
diff-apply staleness, syntax gate, /undo, secrets guard |
β built |
| honest comparison table vs Maestro | β added above |
| live provider round-trips | β 3 tests, ledger reconciles |
| published on Packagist | β
published β paider/paider, tagged v0.1.0 |
| CI pipeline with hermetic + live suites | β
shipped β .github/workflows/tests.yml runs on PR, --prefer-lowest catches version leaks |
| end-to-end on a real repo with a real API key | β
3/3 live runs, 2026-08-18 β rubric-graded, evidence in m1/runs/ |
The rule in this repo is that a green checkbox is a promise a grep or a test run can keep.
The final box was graded against a rubric committed before the first run
(m1/bench/RUBRIC.md) β and the first attempt FAILED it (the
pre-registered prompt referenced code that didn't exist in the foreign target; the model
correctly asked a question into a non-interactive run). The task was re-registered, the count
reset, and runs 1β3 passed 3/3. Both the failure and the passes are committed.
π License
Apache-2.0 β see the full text, which is byte-for-byte the official one
(md5 3b83ef96387f14655fc854ddc3c6bd57).
Dependency licenses audited and compatible: 93 MIT Β· 26 BSD-3-Clause Β· 1 Apache-2.0. All permissive, no copyleft.
Written in PHP on purpose. π
Not because PHP is fast β it isn't, and DECISIONS.md Β§3 says so with numbers β
but because an agent that lives inside your Laravel app knows things an external one has to be told.
Every β above is backed by a passing test. Every β¬ is honest about not existing yet.