Make the workspace toolchain safe for concurrent agents in one VM #115
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/tools#115
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Several coding agents now run concurrently inside a single dev VM, and the workspace tooling was designed for one. This issue records that topology decision and tracks the work that makes
allod change, the git hooks, and the workspace collectors safe under it.Primary goals:
agent/*branch gets its own worktree; an in-place commit to a repo's default branch stays in the shared checkout with zero added friction.Incident that surfaced this
Two agents independently picked the same issue and worked in the shared
forgejo-configcheckout at the same time. Both created branches withgit switch -c(agent/repo-archive-cleanupandagent/vps-archive-cleanup), which is what the workflow prose instructs for unprotected repos. Because both branches pointed at the same commit, the second switch rewrote no files and git raised no objection, so neither agent saw anything wrong — HEAD simply moved out from under the first agent while it kept writing. Detection came from an agent harness's read-before-write staleness check on a shared file, not from git and not from any rail.Reflog timeline:
01:06:15first branch created,01:07:28second created and HEAD leaves the first,01:08–01:09both agents write files into the one tree,01:10:52manual recovery,01:12:39first agent commits.Root cause
Isolation is coupled to branch protection.
allod change beginreturns the shared repo path untouched for any repo absent fromprotected-branches(allod:267-272), which is 12 of 19 checkouts on a typical dev VM. The archived design confirms this was deliberate rather than an oversight:allod/strategyarchive/dev-plans/allod-change-dev-plan.md:27describesbeginas setting up "isolation if needed", where "needed" meant branch protection, and:32specifies the no-op passthrough for unprotected repos. Concurrency between agents was never a design input.The deeper point is where the failure sits on the enforcement ladder in
allod/memoryarchitecture.mdprinciple 4. Neither agent calledallod change beginat all; both ran bare git, because prose told them to. A fix that lives only in the tool is a tool-default fix for a prose-layer failure, so the hook layer has to carry part of the load.Children
allod change beginalways isolates a branch change in its own worktreeallod change recordrefuses a moved checkout and a swept concurrent editallod change recordresolves a concurrent push (deferred; split out of allod/tools#118, gated on a decision criterion)Sequencing, contracts, and per-issue review depth live in
allod/strategydev-plans/concurrent-agent-workspace.md. Only allod/tools#119 has a hard dependency, and only on allod/tools#112 — see the triage comment below for why allod/tools#112 is not an arc-wide prerequisite.Scope
In:
allod/tools—allod change,git-hooks/protected-refs-policy,lib/workspace.shand its consumers. Out: agent identity as a general feature, which is worth its own issue if lock ownership or attribution is wanted but does not address any rail's repo lookup; per-VM agent topology, whicharchitecture.mdprinciple 7 already covers and which is not the chosen configuration here.Two adjacent concerns were checked and need no work.
flake-update-cascadealready refuses a repo that is not on its default branch (flake/flake-update-cascade:204), so it cannot stomp an agent branch.pull-all --switchcan currently move a clean, fully-pushed agent branch back to the default branch, but allod/tools#48 already made that opt-in, and once agents stop holding branches in shared checkouts there is nothing left for it to move.Triage of the five children, plus the arc's shared contracts, now live at
allod/strategydev-plans/concurrent-agent-workspace.md. That document is this arc's home for anything more than one child depends on; allod/tools#116 and allod/tools#118 get their own dev plans that inherit from it.Review depth per child, set by the blast radius of each change rather than by the severity of the bug it fixes:
allod/tools#118 earns a convergence cycle because two of its three mechanisms carry unresolved tension. Its auto-rebase goal rewrites history and moves the working tree in the shared checkout, where by this arc's premise another agent may be mid-write, so it can reintroduce the exact failure class the arc exists to close. And because nothing can tell who edited a tracked file, the only implementable sweep guard is "refuse any modification not passed through
-f", which converts thegit add -udefault into explicit-files-required for every caller — in direct tension with that issue's fourth goal. Worth considering splitting the two refusals from the auto-rebase.Sequencing correction
allod/tools#112 is not an arc-wide prerequisite. It is a hard dependency for allod/tools#119 only, which needs the hook to resolve identity from inside a worktree. The argument that unconditional isolation would move every agent change into an unguarded zone does not hold in practice: inside a linked worktree on an
agent/*branch,allowed-external-remotesandactive-pr-branchesare remote-keyed and still fire, theagent/*force-push block is branch-name-keyed and still fires,signing-required-branchesis currently an empty file, and theprotected-branchescheck is unreachable because git refuses to check out the protected branch in a second worktree while the main checkout holds it. The gap is latent, activating the day a branch is added tosigning-required-branches. Details and the forge-side finding that drives this are in the plan's Risk Assessment.Recommended order: allod/tools#117 first, since
work-diffis already blind to the/tmpworktrees that exist today and the fix pays off before anything else lands. Then allod/tools#116, then allod/tools#118. allod/tools#112 and allod/tools#119 are a second wave that can run in parallel, since they share no files with the first.Contracts the children must not redecide
Five facts are fixed in the plan so the children cannot drift: worktree siting under
~/changes(C1); worktree detection and repo identity via--git-dirvs--git-common-dirplusworktree list --porcelain(C2); the branch handoff file at$(git rev-parse --git-dir)/allod-change-branch, which is per-worktree and dies withgit worktree remove(C3);-das the isolation switch (C4); andworkspace_collect_repos's output set staying frozen so the mutating consumers keep seeing registry checkouts only (C5). C2 and C3 are verified against real worktrees rather than assumed.Keeping it current: each child's PR description states which contracts it implements and whether any changed. A contract change lands in the plan in the same PR that changes the behavior, never after — that is the one update rule worth enforcing, since a stale contract here is worse than no contract at all. When the last child merges, the plan moves to
archive/dev-plans/.