<p>Every developer knows the moment. Something that worked yesterday doesn't work today. A page that was fast is slow. A request that used to return 200 now returns 500. Your mind immediately fills with questions, and none of them are about writing code:</p><p>Which route got slower? When did it start? Which commit did it? If I replay the request, will it even fail the same way again? And can I look at my colleague's branch without wrecking my own setup to do it?</p><p>Grove has always been the web server for your local sites, so it sees every request. 1.10 is about using that position to answer those questions. Every one of these answers comes with a promise we kept throughout: your checkout and your database are never touched.</p><h2>Which route got slower?</h2><p><strong>The problem.</strong> A page feels slow, or a test run takes longer, but "slow" is a feeling. Some requests are slow anyway, for boring reasons. The database was starting up, OPcache was recompiling, or the machine was busy. If you chase every slow request you'll never get anything done, and if you ignore them you won't notice the N+1 query until production.</p><p>Grove times every request PHP answers and keeps the times per route. A route is the method plus the path, with ids, UUIDs, ULIDs and long hashes folded into placeholders, so <code>/orders/17</code> and <code>/orders/18</code> are the same route:</p><pre><code class="language-text">$ grove routes
SITE   ROUTE              SEEN  TYPICAL   RECENT
shop   GET /orders/{id}     26     21ms    222ms  slower 10.6× for 0s, e.g. request #66
shop   GET /                20     21ms     21ms
</code></pre><p>The design is all about not crying wolf.</p><ul><li><p><strong>Typical</strong> is the median of the route's last 50 requests. <strong>Recent</strong> is the median of its last five. Both are medians, so one slow request moves nothing. A cold database start or an OPcache recompile won't raise an alarm.</p></li><li><p>A route is flagged only when recent is at least twice typical and at least 50 ms more, and only once it has ten requests behind it.</p></li><li><p>While a route is flagged, its baseline stops taking samples. Otherwise a regression would quietly become the new normal after a few dozen requests, and the flag would clear itself without anything being fixed. The flag clears when the route is actually fast again. If the new speed is deliberate, <code>--reset</code> accepts it.</p></li><li><p>Only answers below 400 count, because how fast an error comes back tells you nothing about the route. Static files don't count at all.</p></li></ul><p>We tested it on a route that went from 21 ms to 222 ms. It was flagged on the third slow request, and the baseline stayed at 21 ms through fifty more.</p><p>And here's what it doesn't catch, stated as plainly as the changelog states it: a slowdown that creeps in a few percent at a time is absorbed into the baseline and never flagged. <code>grove routes</code> is for the route that jumps, the N+1 query or the dropped index. It isn't a profiler, and it doesn't pretend to be one.</p><h2>Which commit broke it?</h2><p><strong>The problem.</strong> A request that used to work now returns 500, and you have forty commits between "worked" and "doesn't". <code>git bisect</code> is the right tool, but running it on a web app by hand is miserable. You check out a commit, run <code>composer install</code>, migrate a database, reload the page, and mark good or bad. Then you do it again, while your own work is stashed and your database has been migrated back and forth through forty schemas.</p><p>Now you give Grove a commit where the request worked, plus one of the requests it recorded:</p><pre><code class="language-text">$ grove bisect --good 3f2a9c0 --request 1
COMMIT     STATUS       SUBJECT
59f8d721a3 500    bad   c8: notes
aba631d973 200    good  c4: more notes
fcb901b823 500    bad   c6: rename total to amount in the report
b540fc73ef 200    good  c5: report sums invoice totals

first bad commit: fcb901b823 c6: rename total to amount in the report
</code></pre><p>For each step, Grove checks the commit out beside your checkout, at <code>shop--bisect.test</code>. It gives that copy a fresh copy of your database migrated to that commit, runs <code>composer install</code> only if <code>composer.lock</code> changed, and replays the recorded request with its method, path, headers and body. <code>git bisect</code> does the narrowing. Your checkout never moves, and neither does your database.</p><p>Two small details show how carefully this was built. First, Grove checks that the request really does fail at the "bad" end before it starts, so you don't bisect towards nothing. Second, it waits three seconds per step, because at two seconds OPcache was still answering with the previous commit's code. The answer is only useful if every step tests the commit it says it tests.</p><h2>Replay the same request against the same data</h2><p><strong>The problem.</strong> Grove has been able to replay a recorded request since the beginning, and it's a wonderful debugging loop: replay, read the error, fix, replay. Except when the request writes. The second time you replay "create order", the order already exists. The third time you replay "sign up", the email is taken. By the fourth time, you're debugging the side effects of your own replays.</p><pre><code class="language-text">$ grove replay 1 --same-data
baseline taken of MySQL `shop`; later --same-data replays start from it
replayed → 200 in 1ms
$ grove replay 1 --same-data
MySQL `shop` put back to the baseline first
replayed → 200 in 1ms
</code></pre><p>The first <code>--same-data</code> replay takes a snapshot of the site's database, and every later one puts it back before sending the request. Now you can change the code and try the exact same request from the exact same starting point as many times as you need. On MySQL and SQLite, three replays of a POST that inserts a row left one row more than the baseline, not three.</p><p>One honest detail: the starting point is your data at that first <code>--same-data</code> replay, not at the moment the original request was made. For a failed request that rolled back, those are the same. If the original request did write something, undo it once before you start.</p><h2>Another branch, running beside yours</h2><p><strong>The problem.</strong> A colleague asks you to look at their branch. Reviewing it properly, meaning actually clicking through it, used to cost you: stash your work, check out their branch, <code>composer install</code>, <code>npm install</code>, migrate your database into their schema, look around, then undo all of it and hope your data survived.</p><pre><code class="language-text">$ grove try feature/invoices
</code></pre><p>Now their branch runs beside yours, at <code>myapp--feature-invoices.test</code>. It's in its own worktree, on its own copy of your database, with your <code>.env</code> copied and the hostname replaced everywhere it appears. <code>vendor</code>, <code>node_modules</code> and <code>public/build</code> are cloned copy-on-write, so it takes seconds and uses no extra disk. <code>composer install</code> only catches up with the differences in their lock file, and their migrations run on their copy.</p><p>You can click around their feature in one tab and keep working on yours in the next. Both have real data, and neither touches the other.</p><p>It's careful about cleanup too. A try whose setup fails part-way is undone completely, so there's no half-made worktree left over. <code>grove try … --done</code> refuses while the worktree holds uncommitted work, and names the files. (And if you wondered why the name has a double hyphen: every subdomain of a site already routes to that site, so <code>myapp-feature</code> could collide and <code>myapp--feature</code> can't.)</p><h2>Sandboxes for coding agents</h2><p>The same machinery that powers <code>try</code> powers something for AI agents. With <code>grove mcp --allow-write</code>, an agent can get a sandbox: a complete running copy of a site on a new branch, in its own worktree, with its own migrated copy of the database and its own HTTPS address. Every other Grove tool can then be pointed at that sandbox by name.</p><p>What this means in practice: you can let an agent run migrations, seed data and hit the app, and your checkout and your database are never involved. When it's done, closing the sandbox keeps the branch with the agent's commits, so you review them like any other branch. Closing refuses while anything is uncommitted, and names the files.</p><p>An agent also gets <code>grove_routes</code>, so it can check whether its own change made a route slower before telling you it's finished.</p><p>These tools only exist with <code>--allow-write</code>. A read-only <code>grove mcp</code> never offers them.</p><h2>And one small speed-up</h2><p>1.9 introduced on-demand databases, which run only while something is connected. The first query after a database had gone idle took about 0.35 s, because the server was starting behind it.</p><p>1.10 starts it earlier. A browser resolves <code>myapp.test</code> before it connects, often while you're still typing the address, and Grove answers that DNS lookup by starting the site's idle databases in the background. With a lookup 0.4 seconds ahead, the first request after MySQL had gone idle took 43 ms instead of 354. That's the same as if MySQL had been running all along. You get the memory saving without having to wait for it.</p><h2>Why these belong together</h2><p>Look at the features again:</p><ul><li><p><code>grove routes</code> tells you which route broke.</p></li><li><p><code>grove bisect</code> tells you which commit broke it.</p></li><li><p><code>--same-data</code> lets you retry the fix against the same starting point, as many times as it takes.</p></li><li><p><code>grove try</code> and agent sandboxes let you, or an agent, run code you haven't adopted yet, beside the code you have.</p></li></ul><p>They only work because Grove already owns the web server and the database. It sees every request, times every route, and can snapshot, copy, migrate and put back a database without asking you for a connection string. And each tool keeps the promise we care about most in a development tool: it works beside what you're doing, never on it. Your checkout and your database are never touched.</p><h2>Get it</h2><p>Grove 1.10.0 is at <a target="_blank" rel="noopener noreferrer nofollow" href="https://elyracode.com/grove">elyracode.com/grove</a>, for macOS on Apple silicon and Intel, and Linux as AppImage, deb and rpm. The daemon and the CLI use the same protocol as 1.9.0, so there's nothing to do but update.</p><p>Then try it on the route you've been meaning to look at:</p><pre><code class="language-bash">grove routes
grove bisect --good &lt;a commit from last week&gt; --request &lt;the one that fails&gt;
</code></pre><p>And the next time a colleague says "can you take a look at my branch?", say yes without stashing anything.</p>