allod change record: guard against a moved checkout and a swept concurrent edit #118
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#118
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?
allod change recordcan commit into a checkout that another agent moved, and can sweep another agent's edits into your commit, in both cases without saying anything. Add the two refusals that make a concurrent collision loud.Part of allod/tools#115. The automatic-rebase half of this issue's original scope moved to allod/tools#124.
Primary goals:
beginhanded out,recordexits with a message naming the expected and actual branch instead of committing wherever it landed.git add -ustays the default there.allod/memorygit-workflow.mdalready instructs.recordleaves the tree exactly as it found it.Current state
recordresolves the current branch and checks it againstprotected-branches(allod:345) but has no notion of which branch the caller was given, so an agent whose checkout was switched out from under it commits to whatever HEAD now points at. That is precisely what happened in the incident in allod/tools#115: one agent's HEAD was moved by another agent betweenbeginand the commit.Staging defaults to
git add -u(allod:350), which stages every tracked modification in the tree. Untracked files are safely skipped, so a concurrent agent's new files cannot ride along, but a concurrent agent's edits to an already-tracked file will. For the shared memory and planning-doc flow, where two agents may both be appending to the same tracked file, that is the realistic collision and it is silent.Why the sweep guard is worktree-aware rather than blanket
The second goal's original wording asked staging to skip modifications the caller "did not name and did not make". The second half is not implementable. In the in-place default-branch flow
beginis never called, so there is no handoff, no baseline tree, and no signal that attributes an edit to an agent. The rule therefore collapses to a positional one — named versus unnamed — which is friction on exactly the flow the third goal protects.Scoping it by checkout type resolves that, because the hazard is not uniform:
recordrunsgit add -u, unchangedWorktree detection is
git rev-parse --path-format=absolute --git-dirdiffering from--git-common-dir, already implemented asmain_repo_dir_for_dir(allod:111).Promoting the file-naming rule from prose into the tool is architecture principle 14 working as designed, and the override stays standard git —
git add -u && git commit— rather than a bypass flag, per principle 4.Suggested shape
For the moved-checkout guard,
beginrecords the branch it created at$(git rev-parse --path-format=absolute --git-dir)/allod-change-branchinside the new worktree, andrecordcompares against it and exits non-zero on mismatch. In a linked worktree that path is the private<main>/.git/worktrees/<name>directory, so the file is per-worktree rather than per-repo andgit worktree removedeletes it, which means it needs no cleanup path of its own. A missing file meansbeginwas never called andrecordproceeds unchanged, which is what keeps the in-place flow unaffected. This is contract C3 inallod/strategydev-plans/concurrent-agent-workspace.mdand depends on allod/tools#116 writing it.For the sweep guard, refuse when no
-fwas given andgit diff --name-onlyis non-empty in a main checkout, listing the paths and pointing at-f. Note that the existing-fpath already stages only what it is given (allod:348), so the sweep is confined to the bare invocation.Scope
In:
allod change record's staging step and its branch check,allod change begin's handoff of the branch name, and tests covering a moved checkout, an unnamed concurrent modification in a main checkout, an unnamed modification inside a worktree that must still be allowed, and a refusal leaving the tree untouched. Out: automatic push resolution (allod/tools#124), worktree creation policy (allod/tools#116), and hook-level enforcement (allod/tools#119).Rescope analysis. This issue carries three mechanisms with three different blast radii, three different rollback stories, and — the part that was not visible before — three different dependencies. Two of them are not blocked by anything and one is.
The conflict is between goals 2 and 4, not between goals 2 and 3
Goal 2 says staging must not include modifications to tracked files the caller "did not name and did not make". The second half is not implementable: in the in-place default-branch flow
beginis never called, so there is no handoff, no baseline tree, and no principled way to attribute an edit to an agent. The rule therefore collapses to a positional one — named versus unnamed — which is friction on exactly the flow goal 4 promises not to burden. That tension is internal to goals 2 and 4 and survives deleting goal 3 entirely.It also dissolves cleanly once the guard is made worktree-aware. The sweep hazard exists only in a shared checkout; inside a linked worktree the tree belongs to one agent by construction and
git add -uis safe there. So:recordrunsgit add -uunchangedThat keeps goal 4 literally true for every isolated flow and reduces the in-place flow's cost to one flag per file — which
allod/memorygit-workflow.mdalready instructs agents to pass. Promoting that prose rule into the tool is architecture principle 14 working as intended, and the override stays standard git (git add -u && git commit) per principle 4, not a bypass flag.Goal 4 should be restated honestly as part of this rescope: it adds no additional command or ceremony, rather than no required argument.
The auto-rebase is safer than previously assessed, and separable
Tested against a two-clone fixture rather than reasoned about:
git pull --rebaserefuses outright (cannot pull with rebase: You have unstaged changes), touches nothing, and leaves the other agent's edit intact. It fails closed. My earlier assessment in allod/tools#115 that this reintroduces the arc's own failure class was too strong.git rebase --abortrestores the branch, the working tree, and the local commit completely.Two implementation facts follow, both easy to get wrong. The fail-closed property depends on
rebase.autoStashbeing false; with autostash on, git stashes the other agent's uncommitted work, rebases, and unstashes, which is the genuinely dangerous path. This VM setspull.rebase trueand leaves autostash unset, so the property holds today by default and would silently stop holding if that config changed — the implementation must pass-c rebase.autoStash=falseexplicitly rather than inherit. And the conflict path must--abortand report, per principle 11's "touches nothing on failure".So it is implementable safely. Whether it is worth implementing is a separate question: it saves one manual
git pull --rebase && git pushin a flow where the failure is already loud (allodrunsset -euo pipefail, so the rejected push already exits non-zero) and the recovery is already documented.Recommendation
Split along the safety/convenience seam, which is also the dependency seam:
The dependency argument is the one that actually changes the schedule. The moved-checkout guard reads the C3 branch handoff and cannot land before allod/tools#116 writes it. The sweep guard depends on nothing in the arc — C2 worktree detection already exists as
main_repo_dir_for_dir(allod:111). If the sweep guard should land immediately rather than waiting on allod/tools#116, split three ways instead of two and let it go first; the cost is a third small PR against the same function.Principles consulted: 15 (decompose so each step has its own blast radius and rollback; minimalism first), 11 (fail loud, preflight, touch nothing on failure), 14 (lessons become mechanism; do not elaborate gates that cannot show their worth), 4 (enforce below prose, ship no bypass flag).