AI Agent Orchestrator for Laravel Applications
Sage is a local development dashboard that orchestrates AI coding agents across Git worktrees, providing instant preview URLs for each feature branch — like GitHub PR previews, but on your machine.
- Agent: Claude Code (requires
claudeCLI in system PATH andANTHROPIC_API_KEYin environment) - Server: Laravel Artisan Serve (built-in development server with auto port assignment)
- Platform: macOS, Linux, Windows (via WSL2)
- Environment agnostic — Works on macOS, Linux, and Windows (via WSL)
- Container optional — Run natively or in Docker, as long as project folders are accessible
- Zero config server management — Sage uses Laravel's built-in
artisan servefor instant preview URLs - Single binary distribution — Download and run. No PHP install required (FrankenPHP)
- Create/delete worktrees from the dashboard
- Each worktree gets an instant preview URL (e.g.,
feature-auth.myapp.local) - Visual status of all active worktrees
- Artisan Serve driver — Uses Laravel's built-in development server
- Each worktree gets its own port automatically assigned
- Zero configuration required - works out of the box
- Spawn Claude Code agent on specific worktrees
- Real-time terminal output streaming
- Model selection (claude-sonnet-4-20250514, claude-opus-4-20250514, etc.)
- Agent switching (future: OpenCode, Cursor, Aider, etc.)
- Kanban — Drag tasks through stages (idea → in-progress → review → done)
- Terminal — Interact with main repo or any worktree
- CLAUDE.md Editor — Manage agent instructions per project/worktree
- Spec Generator — AI-assisted feature specification from ideas
- Env Manager — View/edit
.envfiles across worktrees
| Layer | Tech | Notes |
|---|---|---|
| Backend | Laravel 12, Octane, Reverb | |
| Frontend | React 19, shadcn/ui, Tailwind v4, Inertia.js | |
| Database | SQLite (single file for data + queues) | |
| Runtime | FrankenPHP (static binary) | |
| Process | Laravel Queues (database driver), Symfony Process | |
| Testing | Pest + Pest Browser | Full coverage: unit, feature, and E2E browser tests |
| Agents | Claude Code | Uses system ANTHROPIC_API_KEY from environment |
| Server | Artisan Serve | Built-in Laravel development server |
- Git clone —
git clone https://github.com/Prvious/sage && cd sage && ./sage serve - Binary download — Single executable for each platform, built with FrankenPHP static builds
| Issue | Solution |
|---|---|
| APP_URL per worktree | Each worktree needs its own .env with correct APP_URL. Sage should auto-generate/patch this on worktree creation |
| Database isolation | Worktrees sharing a DB will collide. Options: separate SQLite per worktree, or DB prefix, or user chooses |
| Port conflicts | Artisan serve automatically assigns available ports per worktree to avoid conflicts |
| Windows path hell | WSL2 file access from Windows is slow. Recommend keeping projects inside WSL filesystem |
| Reverb + Octane | Both need to run. Sage binary needs to boot both (Reverb on separate port) |
| Pest Browser + FrankenPHP | Need to verify Dusk/Pest Browser works when app runs via Octane. May need php artisan serve fallback for tests |
| Agent PATH | Binary requires claude in system PATH. Sage reads ANTHROPIC_API_KEY from system environment, not config |
| Feature | Notes |
|---|---|
| Tunnel support | Expose preview URLs publicly via Cloudflare Tunnel or ngrok for mobile testing |
| Webhook on merge | Auto-cleanup worktree when branch is merged on GitHub |
| Cost tracking | Track API token usage per task/agent |
| Diff viewer | Show what agent changed before merge |
| Snapshot/restore | Save worktree state before risky agent operations |
| Multi-project | Manage multiple Laravel apps from one Sage instance |
- Sage runs with access to your codebase + can execute commands. It should never be exposed publicly
- Consider adding optional auth even for local (PIN code, etc.)
- Agent API keys stored in Sage's
.env— make sure it's gitignored
Project
├── id, name, path, server_driver, base_url
│
├── Worktree (hasMany)
│ ├── id, branch_name, path, preview_url, status
│ └── env_overrides (JSON)
│
├── Task (hasMany)
│ ├── id, title, description, status, worktree_id
│ ├── prompt, agent_output, model, agent_type
│ └── commits (hasMany → Commit)
│
└── Spec (hasMany)
├── id, title, content (markdown)
└── generated_from_idea
sage serve # Start dashboard
sage worktree:create feature-x # Create worktree + preview
sage worktree:list # Show all worktrees
sage task:run "add dark mode" # Create task + spawn agent
sage preview:open feature-x # Open preview URL in browserUse FrankenPHP to build a single static binary of the application 'Sage'.
The binary will start the webserver (FrankenPHP), start the queue (queue:work), and the scheduler (schedule:work).
Worktrees use Laravel's artisan serve for individual preview servers.
use Illuminate\Support\Manager;
class Agent extends Manager
{
public function createClaudeDriver(): ClaudeDriver
{
return $this->container->make(ClaudeDriver::class);
}
public function getDefaultDriver(){
return config('sage.agents.default', default: 'claude');
}
}
// Implementations
ClaudeDriver::class // Uses system 'claude' binary and ANTHROPIC_API_KEY from environment
FakeAgentDriver::class // Fake agent for testing/mocksClaude Code is CLI-based, so the abstraction is straightforward — spawn a process with system environment, stream output, track status. There must be a FakeAgentDriver::class for testing.
Important: The agent relies on the user's system environment:
claudebinary must be in PATHANTHROPIC_API_KEYmust be set in system environment (not in Sage's config)- All Process calls use
->env(getenv())to access system environment
Config would look like:
// config/sage.php
'agents' => [
'claude' => [
'driver' => ClaudeDriver::class,
'binary' => 'claude', // Resolved from system PATH
'default_model' => 'claude-sonnet-4-20250514',
],
],
'default' => 'claude',// Unit: isolated logic
it('generates correct preview url from branch name', function () {
expect(PreviewUrl::fromBranch('feature/auth-system'))
->toBe('feature-auth-system.myapp.local');
});
// Feature: full Laravel stack
it('creates a worktree with preview server', function () {
$project = Project::factory()->create(['server_driver' => 'artisan']);
post('/api/worktrees', [
'branch' => 'feature-payments',
'project_id' => $project->id,
])->assertCreated();
$worktree = Worktree::where('branch_name', 'feature-payments')->first();
expect($worktree)->not->toBeNull();
expect($worktree->preview_url)->toContain('http://127.0.0.1:');
});// Browser: real user flows
it('can create a task from the kanban board', function () {
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard')
->click('@new-task-button')
->type('@task-prompt', 'Add user authentication with Laravel Breeze')
->select('@agent-select', 'claude')
->press('Create Task')
->waitForText('Task created')
->assertSee('Add user authentication');
});
});
it('streams agent output in real-time', function () {
$this->browse(function (Browser $browser) {
$browser->visit('/tasks/1')
->waitFor('@terminal-output')
->waitForText('Creating files...', 30) // websocket streaming
->assertPresent('@agent-running-indicator');
});
});| Challenge | Solution |
|---|---|
| Agent processes | Mock Process facade or use a FakeAgentDriver |
| Reverb websockets | Pest Browser can wait for DOM changes triggered by WS |