<p> <em>On the difference between knowing what to run and knowing how to run it — and why Grove stopped trying to do both.</em></p><p>There's a particular kind of bug that only shows up when you're being helpful.</p><p>Grove has always run your dev processes for you — a Vite server for HMR, a queue worker if you've got a real queue driver. Two processes, hardcoded, started when you type <code>grove dev start</code>. It worked, and it saved you an open terminal.</p><p>Then Laravel 13.16 shipped <code>php artisan dev</code>, and Grove's helpfulness quietly became a limitation.</p><h2>Why this needed to change</h2><p>Laravel's new dev command does something Grove couldn't: it lets your application declare what a dev session needs. The configuration moved out of <code>composer.json</code> and into your code, through a class called <code>DevCommands</code>:</p><pre><code class="language-php">use Illuminate\Foundation\Console\DevCommands;

DevCommands::artisan('reverb:start', 'reverb')-&gt;orange();
DevCommands::register('stripe listen --forward-to '.config('app.url'))-&gt;green();
</code></pre><p>Suddenly Grove was the least informed party in the room. Your app knew it needed Reverb. It knew about the Stripe webhook forwarder. Grove knew about <code>npm run dev</code> and <code>queue:work</code>, because that's what someone typed into a Rust file months ago.</p><p>It got worse in the details. Grove assumed npm. If your project used pnpm or bun, tough luck. And Grove ran <code>queue:work</code>, which doesn't pick up code changes without a restart — while Laravel's own default is <code>queue:listen</code>, which does. For a dev session, Laravel was simply right.</p><p>So 1.3.0 stops guessing. Grove now asks.</p><h2>How it works</h2><p>Laravel exposes the list as JSON. Here's a stock 13.16 app:</p><pre><code class="language-text">$ php artisan dev:list --json
[
  {"command":"php artisan serve --host=localhost",             "name":"server", "priority":0},
  {"command":"php artisan queue:listen --tries=1 --timeout=0", "name":"queue",  "priority":0},
  {"command":"php artisan pail --timeout=0",                   "name":"logs",   "priority":0},
  {"command":"npm run dev",                                    "name":"vite",   "priority":0}
]
</code></pre><p>Grove reads exactly this, then throws two entries away.</p><p><code>server</code> goes, because Grove is the server. Your site is already being served over FastCGI at <code>https://myapp.test</code> with a trusted certificate. Starting <code>artisan serve --host=localhost</code> alongside it isn't just redundant — it's actively misleading, a second URL that behaves differently from the real one.</p><p><code>logs</code> goes, because <code>grove logs</code> already tails your application log, and the GUI has a panel for it.</p><p>What's left gets supervised. So on the app above, you get:</p><pre><code class="language-text">$ grove dev start
✓ dev started for myapp: queue, vite
</code></pre><p>And with Reverb registered in a service provider:</p><pre><code class="language-text">$ grove dev start
✓ dev started for myapp: queue, vite, reverb
</code></pre><p>That third process is the whole point. Grove never knew about it before.</p><h3>Your runtimes, not your PATH</h3><p>Laravel hands over a full shell string — <code>"php artisan queue:listen --tries=1 --timeout=0"</code>, <code>"bun run dev"</code>. Grove doesn't just run it. It rewrites <code>php</code> to the CLI binary your site pins, and resolves Node package managers through its own bundled Node:</p><pre><code class="language-text">$ grove logs myapp-queue
started queue:listen via ~/Library/Application Support/Grove/runtimes/cli/8.5/php
</code></pre><p>This has a nice side effect. Because Laravel's <code>DevCommands::node()</code> uses its own lockfile detection, a bun project declares <code>bun run dev</code> — and Grove just runs it. The pnpm/yarn/bun gap closed itself, without a line of package-manager code in Grove.</p><p>Each process gets its own log, named after the process:</p><pre><code class="language-text">$ grove logs
service    grove · dev-myapp-queue.log
service    grove · dev-myapp-reverb.log
service    grove · dev-myapp-vite.log
</code></pre><h3>Vendor packages don't get a vote</h3><p>Grove passes <code>--except-vendor</code>. If a Composer package registers a dev process, Grove ignores it. A dependency you installed for something unrelated shouldn't be able to start a long-running process inside a background daemon that runs as root. Laravel's own defaults and your own code count; <code>vendor/</code> doesn't.</p><h3>When your app can't answer</h3><p>Not every site is Laravel, and not every Laravel is 13.16. If <code>dev:list</code> doesn't exist or the app can't boot, Grove falls back to exactly what it did before — Vite plus a queue worker. Nothing regresses; you just don't get the new powers.</p><h2>The bug we found by testing</h2><p>Here's the part I didn't expect.</p><p>While verifying the new code, we started three processes and stopped them. Two died. One didn't:</p><pre><code class="language-text">$ ps -o pid,ppid,command -p 565
  PID  PPID COMMAND
  565     1 node -e ...        ← orphaned
</code></pre><p>PPID 1. Adopted by init.</p><p>The reason is mundane and had been sitting in Grove since the feature shipped: <code>npm run dev</code> doesn't become Vite, it spawns Vite. Kill the npm process and the grandchild survives — still holding port 5173. Which means your next <code>grove dev start</code> gets a Vite on 5174, your asset URLs point at the wrong port, and you spend twenty minutes doubting your <code>vite.config.js</code>.</p><p>This was never new. It was a pre-existing bug that the old two-process design just happened to hide well. Reading the app's declaration made it worse, because now there are more processes and more of them have children.</p><p>The fix: every dev process gets its own process group, and teardown signals the whole group — SIGTERM first so Vite and your queue worker can exit cleanly, SIGKILL for anything stubborn.</p><p>There's a guard in there worth mentioning, because it protects against something genuinely nasty. If setting up the process group ever failed, the child would inherit Grove's own group — and signalling that group would kill the daemon. So before signalling, Grove verifies the child really does lead its own group. We tested this under root, with privileges being dropped, and watched the daemon survive its own teardown.</p><h2>Don't run both</h2><p><code>grove dev</code> and <code>php artisan dev</code> supervise the same processes. Running both gives you two Vite servers competing for one port and two queue workers competing for the same jobs — subtly, with no error message.</p><p>Grove now notices:</p><pre><code class="language-text">$ grove dev start
✓ dev started for myapp: queue, vite
  ! `php artisan dev` is already running — if it belongs to this site, stop it.
    Grove supervises the same processes, so they are now running twice.
</code></pre><p>It's a warning, not an error, and deliberately so: the check can see that a <code>php artisan dev</code> is running, but not which project it belongs to. Better to tell you and let you judge.</p><p>Getting that detection right took two attempts, both instructive. The first version matched <code>artisan dev</code> anywhere in a command line — and immediately fired on our own test script, which merely contained the string. The second version anchored on token position — and broke on Grove's own PHP, because it lives under <code>~/Library/Application Support/…</code>, and <code>ps</code> reports command lines unquoted, so the path shatters into three tokens. The version that shipped scans for the PHP binary itself, and knows that <code>php8.5</code> is one but <code>php.ini</code> and <code>phpstan</code> aren't.</p><h2>Small things that were annoying</h2><p><code>grove dev start</code> no longer needs a site name. It defaults to the site in your current directory, resolved from <code>grove.toml</code>'s <code>name</code> or the directory name — the way <code>grove link</code>, <code>grove secure</code> and <code>grove up</code> always have:</p><pre><code class="language-text">~/Code/myapp $ grove dev start
✓ dev started for myapp: queue, vite
</code></pre><p><code>grove path install</code> now installs <code>grove</code> itself. This one was embarrassing. You'd download the macOS app, run <code>grove path install</code> to get Grove's PHP and Node on your PATH, and still have no <code>grove</code> command — because the binary only ever existed inside the <code>.app</code> bundle. It now sits alongside the toolchain shims where you'd expect.</p><h2>Upgrading</h2><p>The macOS app auto-updates. Because the dev-process logic lives in the daemon rather than the CLI, you'll want to reinstall the service so the background daemon picks it up:</p><pre><code class="language-bash">sudo grove install
</code></pre><p>Then delete the <code>dev</code> script from your <code>composer.json</code> if you like, and use <code>grove dev</code> instead.</p><p>One honest note: on Laravel 13.16+, <code>grove dev start</code> boots your application once to read <code>dev:list</code>. That's a beat of startup latency, and your service providers do run. If the app can't boot, Grove falls back rather than failing — but it's a real change in what happens when you press the button.</p><p>Two more things to know. <code>queue:listen</code> replaces <code>queue:work</code> for declared processes, which means your queue now picks up code changes without a restart — better, but different. And prebuilt macOS releases are Apple Silicon only; notarizing the Intel build on CI hangs more often than it succeeds, so Intel Macs should build from source with <code>cargo build --release</code>.</p><h2>The shape of it</h2><p>The nice thing about this release isn't the feature list. It's the division of labour it settles.</p><p>Laravel knows <em>what</em> your dev session needs. It has the service container, the config, the packages, and now a first-class way to declare all of it. Grove knows <em>how</em> to run things: without an open terminal, with per-process logs, with pinned runtimes, starting at boot, stopping cleanly.</p><p>Those were always two different jobs. For a while Grove tried to do both, and did the first one badly. Now it does one of them properly and asks about the other.</p>