paider/paider

A PHP-native AI coding agent.

Maintainers

Package info

github.com/shoemoney/paider

Homepage

pkg:composer/paider/paider

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 11

v1.0.1 2026-08-08 17:10 UTC

This package is auto-updated.

Last update: 2026-08-24 13:59:50 UTC


README

🐘 Paider

A PHP-native AI coding agent β€” that lives inside your Laravel app. πŸ€–

status php license packagist ci tests cold start

Built on Laravel Zero Β· Laravel Prompts Β· Termwind Β· MCP PHP SDK (v0.2)

Paider TUI β€” ./paider -y in YOLO mode, resuming a 39-message session

🚦 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, and fast tiers are configured and priced but not yet routed to by the loop β€” v0.2 work on per-operation tier routing will change that. The commit command uses the fast tier today (via CommitCommand). 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 cost prints 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. Each tier_call event is priced at write time from config/prices.php by exact model id. A model with no entry there stores cost_usd as NULL, 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: CostTableTest parses this table and recomputes it from config/prices.php, and CostReadmeGoldenTest seeds 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.

--json shape. Same data, machine-readable: {tiers, session, unpriced_calls, comparison}. Each entry under tiers (and the session row, minus share_pct) carries calls, tokens_in, tokens_out, tokens_cache_write, tokens_cache_read, spend_usd, unpriced_calls, unpriced_models, hypothetical_usd, hypothetical_unknown, share_pct. unpriced_calls is a list of {tier, count, calls, models}. comparison is {hypothetical_usd, saved_usd, token_share_pct, spend_share_pct}. Pinned by CostJsonGoldenTest β€” an added, removed, or renamed key fails the suite, including on an empty ledger.

Note on cache tokens: The spend_usd is calculated from four token types at write time: tokens_in, tokens_out, plus Anthropic's cache write and cache read tokens when present. The tokens_in and tokens_out fields 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. See PricesSyncTest for the cache pricing contract and config/prices.php for 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-max responds with usage fields where the parser expects them
  • Anthropic wire format β€” novel finding: grok-4 returns a thinking content block before text, and AnthropicClient filters on type === '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 command displayed as '' in approval prompts while the original array was passed to proc_open; grants cached by displayed text so one allow-session silently authorized every later array.
  • /undo deleted files outside project root with no prompt, poisoning the undo stack.
  • PathGuard passed 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() throws ValueError, unhandled, a zero-approval DoS.

Both risks below are now closed:

  1. 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. ShellTool and GitTool now 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 with PAIDER_SHELL_ENV_ALLOW.
  2. Fixed β€” 2026-08-03, DECISIONS.md Β§18: tools like read_file, write_file, and patch_file used to trust an approved key in the model's input if called directly. The approval decision is now a separate $approved parameter 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_shell and artisan use a different tri-state approval key and were deliberately left out β€” for those two, the Loop::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-dom was 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.