Ports and leases
Ports are derived from a hash and detached ones are written down, while a lease lasts exactly as long as the connection that asked for it.
Ports are derived, then remembered
A port comes from a hash of the context and the entry’s name, landing inside 20000 to 20999. That’s below Linux’s ephemeral range, so nothing else on the machine picks one of them by accident. Nothing has to be allocated in advance: a fresh checkout is addressable the moment it exists.
When two entries collide on one port, grove walks to the next free one. Every detached port is written down, in ports.json beside the CA. Without that record, colliding entries would re-resolve in whatever order they ask after a restart, and an entry could be handed the port another worktree’s stack is still listening on. A database URL pointing at someone else’s data is why the file exists. It covers the daemon coming back empty too, after a reboot that restarted your containers but not grove: a port the record gives another entry is left to that entry, so stopping a stack for the evening doesn’t cost it its port. A record outlives the worktree that earned it, so deleting a checkout and making it again hands the same ports back. With a thousand ports to a few dozen entries, the walk prunes nothing in normal use: a record whose port has gone quiet is reclaimed only if the range ever fills, which is a backstop rather than housekeeping. grove forget is the deliberate way to drop one, and doctor names the contexts whose worktree is no longer there. An attached port doesn’t need any of this, since its command is gone when the daemon is.
Lease lifetime is the point
Leases live in the daemon’s memory. An attached lease lasts exactly as long as the grove exec connection that asked for it: the daemon holds that connection open, so the kernel reports the client exiting, crashing, or being killed, and the lease ends with it. There’s nothing to time out and nothing to garbage collect.
A detached lease outlives its command, because something like supabase start returns in seconds and holds its ports for hours. grove hold re-asserts a context’s detached entries by reading the project again, which is what makes it safe for a worktree this process was never in. grove release ends them.
Either way, stop the daemon and every lease is gone. That’s why grove stop refuses while anything is answering on a port grove leased, and why --force exists.
Across a restart
grove restart covers for the daemon forgetting. Before it stops anything, it notes which worktrees hold detached leases. Once the new daemon is up, it runs the equivalent of grove hold in each of them. The ports come out the same as before, because they’re derived from the context, so a running stack never notices.
Attached leases aren’t restored. The dev server holding one keeps running on its port, but its hostname routes nowhere until you start it again under grove exec. Restart tells you which routes that cost.
One route, one command
A route belongs to one command at a time, and a second grove exec on a bound route is refused. A command that only reads the ports runs with --no-bind, which takes no lease.
What ls reads
grove ls shows idle for an entry nothing holds, claimed for a port grove handed out that nothing answers on, and running once something does. The answering check is a real connection to the port, so claimed is what a stopped stack looks like and running is something really there.