Files
Matthias 7186c4b2ae feat(daemon): place run worktrees per repository via worktree_roots
* feat(daemon): place run worktrees per repository

Pipeline run worktrees are created under <NM_HOME>/worktrees/<repo>/<run>,
outside every checkout. Directory-scoped toolchain configuration - mise,
direnv - resolves by path ancestry, so none of it ever reaches a run
worktree: an operator whose repositories are configured that way gets a
run environment that does not match the one they develop in, and there is
no way to move the worktree without moving NM_HOME for everything.

Add a worktree_roots map to the global config: keys are registered
checkout paths, values are absolute directories. A repository with an
entry gets its run worktrees at <value>/<runID>.

Placement is decided in exactly one place, internal/worktrees, and every
consumer of a worktree path goes through it: run creation, parked-run
resume, the fix-review step diff, startup cleanup, the run-cleanup and
startup process sweeps, gate context classification, and eject. That
package also owns the single path canonicalization the feature compares
with, so a spelling that matches in one consumer matches in all of them.

A configured root is the operator's own directory - it holds the
mise.local.toml or .envrc that motivated pointing runs at it - so what
may be touched there is bounded twice over. Cleanup and the process sweep
consider only directories named like a run ID (uppercase ULIDs, as this
daemon mints them); eject goes further and removes only the directories
this repository's own run rows name, so a leftover from anything else
survives. Validation makes the config incapable of expressing the case
that would defeat both: two checkouts sharing a root, two spellings of
one checkout, a non-absolute key or value, or a root equal to its
checkout are all rejected at load time. `init --worktree-root` refuses a
root inside the repository or inside <NM_HOME>/worktrees, and daemon
startup reports a key that matches no registered checkout, which would
otherwise silently place nothing.

With no worktree_roots entries the resolved paths, the directories
walked, and the processes matched are exactly what they were before.

* no-mistakes(review): reject unusable worktree roots and record run placement

* no-mistakes(review): sweep recorded run worktrees instead of configured roots

* no-mistakes(review): bound startup sweep set and tighten placement validation

* no-mistakes(review): resolve recorded run worktrees without consulting config

* no-mistakes(review): sweep resumed run worktrees and refuse in-checkout roots

* no-mistakes(review): align init placement checks and scope run validation

* no-mistakes(review): detect worktree_roots key in document, narrow classifier

* no-mistakes(review): tell operators to replace a re-pointed worktree_roots entry

* no-mistakes(review): sweep before every worktree removal, compare paths by identity

* no-mistakes(review): sweep at eject and tolerate unmigrated preflight schema

* no-mistakes(review): sweep eject placements from one bounded process snapshot

* no-mistakes(review): sweep startup cleanup from one process snapshot

* no-mistakes(review): guide worktree_roots edits by document shape

* no-mistakes(review): match the worktree_roots block's own indentation

* no-mistakes(document): document configurable run worktree placement in stale doc owners

* no-mistakes(review): refuse init registering configured roots; sweep default tree

* no-mistakes(review): never rewrite a worktree_roots key nobody could read

* no-mistakes(review): read block indent from unparseable config; refuse unloadable init

* no-mistakes(review): drop unreachable unparsed-config guidance path

* no-mistakes(document): document every-init worktree_roots refusals and root cleanup

* no-mistakes: apply CI fixes

* fix(daemon): fail closed when the registered-checkout list is unreadable

The placement guard validated against an empty protected set when
RepoWorkingPaths errored, so a configured root inside an unlisted
registered checkout passed startup and run-creation validation and the
run worktree dirtied that checkout. A guard that cannot see the set it
protects must refuse: startup now reports the read failure instead of
starting, and run creation fails that run with the cause recorded. The
database is already open and migrated by the time either caller runs,
so the failure is never routine.

Raised by Greptile on the pull request; addressed as a follow-up commit
per run custody.

* no-mistakes(document): document unusable worktree_roots startup refusal in troubleshooting
2026-08-20 09:49:22 -07:00
..