Skip to content

Move Agent Bus to Laravel Zero - #20

Merged
jordanpartridge merged 1 commit into
masterfrom
claude/repo-code-review-4pr59y
Sep 12, 2026
Merged

jordanpartridge merged 1 commit into
masterfrom
claude/repo-code-review-4pr59y

Conversation

@jordanpartridge

Copy link
Copy Markdown
Contributor

Agent Bus was started with laravel new but is a console app that talks to a broker. It has no route, model, or view. Full spec in docs/laravel-zero-migration.md.

Why

The hot path already bypassed the framework. bin/agent-bus loaded Composer's autoloader and nothing else, so Laravel booted for exactly two commands while the repo carried Eloquent, an HTTP kernel, sessions, queues, Blade, Vite, Tailwind, a User model, and three migrations.

Measured on the pre-migration checkout:

Check Before
bin/agent-bus emit cold start 79 ms, zero Illuminate\ classes
php artisan boot 235 ms
Bare php -r 44 ms
Suite 59 tests, 45 passed, 14 skipped

Laravel Zero keeps what is actually used — container, config(), Process, Sleep, Pest's console helpers, and a home for the NATS service provider — and adds app:build.

One binary, two paths

bin/agent-bus dispatches on the first argument before booting anything.

Path Verbs Boots Cost
Hot emit send heartbeat session-end sessions hook opencode Composer autoload only 66 ms
Cold provision sidecar app:build Zero kernel 150 ms

This is now enforced rather than described. tests/Fixtures/hot-path-probe.php runs each hot verb through the real binary in a subprocess and fails if any Illuminate\, LaravelZero\, or Symfony\Component\Console\ class loads. The old test only grepped the file for bootstrap/app.php, a string the new binary legitimately contains.

Commands renamed: php artisan nats:provisionbin/agent-bus provision, php artisan agent-bus:sidecarbin/agent-bus sidecar. bin/agent-bus-sidecar is gone.

NATS_URL is the only broker setting

The hot path read NATS_URL while the framework path read nats_basis.connections.default.* — two ways to point at one broker. App\Bus\NatsUrl now parses nats://[user:pass@]host[:port] for both, and App\Providers\NatsServiceProvider projects it onto the package config. Issue #16's tailnet broker needs one variable. Connect timeout moves to AGENT_BUS_CONNECT_TIMEOUT, since 250 ms is right for loopback and wrong across a tailnet.

Bug fixed, found by running against a live broker

Laravel Zero does not register Illuminate's ArtisanServiceProvider, which is where Laravel calls Signals::resolveAvailabilityUsing(). Without it Command::trap() dereferences a null callable and fatals. The sidecar traps SIGTERM/SIGINT to leave the bus politely instead of waiting out the 90-second KV TTL, so this is the main path, not an edge. AppServiceProvider::register() installs the same resolver.

This is invisible without a broker — the command returns early on the reachability check. It is the concrete argument for the CI change.

CI runs a real broker

Every JetStream and KV test skipped in CI, and the workflow carried a comment telling you not to add one, so the core of the product was proven only on somebody's laptop.

CI now starts nats:2-alpine -js -m 8222 with the same flags as docker-compose.yml and waits on /healthz. A service container cannot pass -js, so it runs as a plain docker run step. Skipping stays correct on a laptop with no broker, but AGENT_BUS_REQUIRE_BROKER=1 turns it into a failure and CI sets it — a broker that quietly fails to start now fails the build instead of producing a green run that proved nothing. A third job compiles the binary and runs it so app:build cannot rot.

The PHAR, honestly

app:build produces one ~30 MB file that runs with no checkout and no composer install. That was the headline reason to move, and it does not work for hooks:

Entry Hot path
bin/agent-bus script 66 ms
Compiled binary, uncompressed 206 ms
Compiled binary, GZ 529 ms

PHAR stub and signature overhead adds ~140 ms to every tool call. GZ triples it, so box.json uses compression: NONE and accepts 30 MB over 109 MB. The binary is for the sidecar, which pays the cost once and stays up, and for running verbs by hand. Hooks keep pointing at bin/agent-bus in a checkout, and the README says so where hooks get configured.

This weakens the original case for the move. The rest — deleting a web framework from a CLI app, one broker setting, and CI that actually proves the bus — still holds.

Deleted

app/Http, app/Models, app/Console, database/, resources/, public/, routes/, nine config/ files, Vite, Tailwind, npm lockfiles, artisan, bin/agent-bus-sidecar, and the vendored .grok/skills tree of Laravel web-app guidance.

Laravel Boost goes with them — its tools (database-query, database-schema, browser-logs) have nothing to operate on here, and .mcp.json, boost.json, opencode.json, .grok/config.toml only configured it. CLAUDE.md and AGENTS.md were Boost-generated guidelines for a web app pointing at a .ai/rules directory that does not exist; they now describe this application.

Verification

Pint                        passed
Pest, no broker             54 passed, 14 skipped
Pest, live broker           68 passed, 0 skipped, 0 failed
Guard, broker down          4 failed (correct - proves the guard bites)
app:build                   compiled, binary runs both paths
Hot path                    66 ms, 0 framework classes, all 6 verbs
Cold path                   150 ms

Run against nats-server v2.14.6 with JetStream on loopback.

Not in scope

Packaging only. Tracked separately: stream retention limits on AGENT_BUS, a from field in the envelope, moving the OpenCode allowlist into the plugin so it stops spawning PHP per event, the non-deterministic fallback session id, broker auth (#16), MCP tools (#9).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FyZJ58TB6pGudJb4nreZUh


Generated by Claude Code

Agent Bus was started with `laravel new` but is a console app that talks to a
broker. It has no route, model, or view. The hot path already bypassed the
framework: `bin/agent-bus` loaded Composer's autoloader and nothing else, so
Laravel booted for exactly two commands while the repo carried Eloquent, an
HTTP kernel, sessions, queues, Blade, Vite, Tailwind, a User model, and three
migrations.

Laravel Zero keeps what is used — container, config, Process, Sleep, Pest's
console helpers, and a home for the NATS service provider — and adds app:build.

One binary, two paths. `bin/agent-bus` dispatches on the first argument before
booting anything. Hook verbs (Cli::HOT_VERBS) fire on every tool call and run
on Composer's autoloader alone; provision and sidecar boot the Zero kernel.
This is now enforced rather than described: a probe fixture runs each hot verb
in a subprocess and fails if any Illuminate, LaravelZero, or Symfony Console
class loads. The old test only grepped the file for a string the new binary
legitimately contains.

NATS_URL becomes the only broker setting. NatsUrl parses it for both paths and
NatsServiceProvider projects it onto the package config, so a remote broker
needs one variable instead of two mechanisms. Connect timeout moves to
AGENT_BUS_CONNECT_TIMEOUT; 250ms is right for loopback and wrong for a tailnet.

Fix a crash found by running the suite against a live broker: Laravel Zero does
not register Illuminate's ArtisanServiceProvider, where Laravel installs the
Signals availability resolver, so Command::trap() dereferences a null callable
and fatals. The sidecar traps SIGTERM/SIGINT to leave the bus politely instead
of waiting out the 90s KV TTL, so this is the main path. AppServiceProvider now
installs the same resolver.

CI runs a real broker. Every JetStream and KV test used to skip there, so the
core of the product was proven only on a laptop. CI starts nats:2-alpine with
the same flags as docker-compose.yml, and AGENT_BUS_REQUIRE_BROKER=1 turns a
skip into a failure so a broker that fails to start cannot look green. A third
job compiles the binary and runs it.

The PHAR is not for hooks. It adds ~140ms to every tool call (206ms against
66ms for the script; GZ compression made it 529ms, so box.json uses none and
accepts 30MB over 109MB). It is for the sidecar, which pays once and stays up,
and for running verbs by hand. README says so where hooks get configured.

Delete app/Http, app/Models, app/Console, database, resources, public, routes,
nine config files, Vite, Tailwind, npm lockfiles, artisan, bin/agent-bus-sidecar,
and the vendored .grok/skills tree of Laravel web-app guidance. Laravel Boost
goes with them: its tools query databases and browsers this app does not have.
CLAUDE.md and AGENTS.md were Boost-generated guidelines for a web app, pointing
at a .ai/rules directory that does not exist; they now describe this app.

Verified: Pint passes. 68 tests pass with zero skips against nats-server 2.14.6.
The guard fails correctly when the broker is down. app:build compiles and the
binary runs both paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyZJ58TB6pGudJb4nreZUh

@lexi-chief-of-staff lexi-chief-of-staff Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ship it.

  • One-variable broker config and strict hot-path enforcement are the right call for this console app.
  • CI now actually exercises JetStream/KV instead of skipping; the AGENT_BUS_REQUIRE_BROKER guard is clean.
  • The Command::trap null-callable fix via AppServiceProvider is the concrete justification for the Laravel Zero move.
  • One visible typo in .env.example (HERDR); everything else is mechanical scaffolding removal.

No blocking issues.

Copy link
Copy Markdown
Contributor Author

On the flagged HERDR typo in .env.example — checked, and it is correct as written, so no change.

herdr is the tool's actual name, not a misspelling of "herder". The sidecar shells out to herdr agent list and herdr agent prompt (app/Bus/Herdr.php), and AGENT_BUS_HERDR is the exact variable config/agent_bus.php:19 reads:

'herdr' => [
    'binary' => env('AGENT_BUS_HERDR', 'herdr'),
],

Renaming it would break the config lookup. There is no "herder" spelling anywhere in the repo.


Generated by Claude Code

@jordanpartridge
jordanpartridge merged commit 220e6db into master Sep 12, 2026
3 checks passed
@jordanpartridge
jordanpartridge deleted the claude/repo-code-review-4pr59y branch September 12, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants