change: always isolate a -d change in its own worktree, and add 'change list' #125
No reviewers
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!125
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/always-isolate"
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?
Closes allod/tools#116
Refs allod/tools#115
Implements
allod/strategydev-plans/allod-change-always-isolate.md, inheriting contracts C1–C5 fromdev-plans/concurrent-agent-workspace.md.What changed
begin -dcreates a worktree and anagent/<description>branch for every repo, protected or not, sited at~/changes/<slug>-<description>-XXXXXXinstead of/tmp(C1, C4). Aprotected-branchesentry still names the base for the repos that have one; every repo without one bases onorigin/HEAD. Protected repos keep their base selection unchanged but do pick up the two new preflights, since the plan applies them to every repo: anorigincheck, and a post-fetch verification thatorigin/<base>resolves — which turns a missing base from a raw 128 out ofworktree add, with a temp directory already created, into exit 1 before anything is created.beginwithout-dis unchanged: it prints the shared checkout path and creates nothing — no fetch, no branch, no directory. Protected repos still refuse it with exit 1 and a message naming-d. C4 removes protection as the isolation switch, not as a validation input, and an in-place commit is legitimate only on a repo's default branch, so if that branch is protected no in-place change in that checkout is legitimate at all.beginwrites the branch it created toallod-change-branchin the new worktree's private git dir (C3), for allod/tools#118's reader.worktree addand for the handoff write removes the directory and prunes the admin entry, then reports rather than guesses: it never deletesagent/<description>, because under concurrency another agent may have created it between the pre-check and the add, and it checks whether the entry is actually gone rather than trustingworktree prune, which reports a failed delete and still exits 0.allod change list [<repo-path>]prints one tab-separated row per linked worktree — repo, path, branch, state — enumerated from git rather than from a path, so it finds the/tmpworktrees that predate~/changes. It reports the strongest blocker only:prunable,locked,detached,submodule,dirty,unpushed,clean. The binding contract is thatcleanis reported if and only ifcleanupon that path would succeed, which means the negation of every one ofcleanup's refusal sources rather than the two it names — a detached or locked worktree looks clean by the dirty and unpushed checks alone.cleanuphas six refusal sources, not the five the plan enumerates.git worktree removealso refuses a worktree containing a populated submodule, so one would have been reportedcleanand then died 128. Found by the review pass below and reproduced.listreportssubmodulefor it; a gitlink whose directory holds no.gitis unpopulated and removes fine, which is the statebeginitself leaves behind, so both the fire and the no-fire case are asserted. No repo in the workspace uses submodules today, so this was latent.--force, no--all, and no age heuristic, because no local signal distinguishes a dead agent from a working one.cleanupis untouched and still refuses everything it refused before.README.mdgains "Making a change" and "Reclaiming worktrees" under Workflow;change --helpstates the-dcontract and the state words.Risk
R2 Medium, as triaged in the plan. One CLI, one repo, one rewritten function plus one new read-only one.
git worktree addnever moves the shared checkout's HEAD, index, or working tree, which is what makes the flip safe to run while another agent is mid-write in the same repo, and that is asserted directly.beginstill reaches the network only throughls-remoteandfetch, both reads; publication risk is unchanged. Rollback is a straight revert: worktrees already created under~/changesstay valid linked worktrees,recordandsubmitoperate on$PWDand work inside them unchanged, andcleanupstill reclaims them.Worth a human eye, in the plan's order: the
-d-absent branch ofchange_begin, which must stay byte-for-byte frictionless forallod/memoryandallod/strategy; the refusal to delete a branchbegincannot prove it created; and theliststate words againstcleanup's refusals,lockedanddetachedin particular.Validation
bash tests/allod-change.sh— 159 assertions, all passing, up from 79. New coverage:begin -don an unprotected repo (the flip, previously untested); the in-place flow proven to create no worktree, no branch and no~/changesat all, then to commit and push in place; both duplicate-description rails on the flipped path; the C3 handoff file and the linked-worktree git dir; both rollback paths through a mock git; theorigin,origin/HEADand post-fetch base preflights; one case perliststate checked by runningcleanupon the same worktree rather than by restating its refusals; and four precedence assertions, one per adjacent pair.Every new assertion was checked against a sabotaged implementation, since a guard that cannot be shown to fail does not count. Sixteen sabotages — reordered state precedence,
prunablecomputed from[[ -d ]]instead of git's annotation, droppedworktree prune, dropped handoff write, skipped base verification, restored unprotected passthrough, two-conditionclean, a rollback that deletes the branch, a rollback that keeps the directory, nooriginpreflight, alistthat prunes, alistthat skips directory-less worktrees,/tmpsiting, no submodule detection, submodule detection that fires on unpopulated gitlinks, and alistthat prints no rows at all — were each caught by the intended assertion../allod change listagainst the live workspace reports both/tmpworktrees still registered againstallod/tools, onecleanand onedirty, which is the direct demonstration that enumeration is git-derived rather than path-derived.Nothing here needs a rebuild to validate; the tests invoke the checkout directly and build their own repos under a temporary
HOME.begin -dwas never run against the live workspace.Correction to the plan
The plan and
concurrent-agent-workspace.mdboth recordworkspace_repo_default_branchas having a dead|| echo "master"fallback that returns empty whenorigin/HEADis unset, on the grounds that the||binds to a pipeline whose status issed's. That is true only withoutpipefail. All five consumers —allod,pull-all,work-diff,flake-status,flake-update-cascade— setset -euo pipefail, so the pipeline's status is git's and the fallback does fire. The helper therefore does not return empty; it silently guessesmaster.That inverts the hazard
begininherits: not a confusingorigin/error, but a repo quietly branched off the wrong base.lib/workspace.shis still untouched here, as the plan scopes it, andbegininstead asserts its own precondition —symbolic-ref refs/remotes/origin/HEADmust resolve — before falling through to the helper, which gives the contractedremote set-headrepair message. The empty-base guard stays as defence.The helper itself is now issue allod/tools#126, which also records how narrow the live exposure is: git 2.51.2 sets
refs/remotes/origin/HEADon clone and repairs it on any fetch, so the unresolvable state mostly survives in a repo that has never fetched.begin's assertion becomes redundant once allod/tools#126 lands and can be dropped then. Both dev plans carry the incorrect reading of the helper and should be corrected with it.Not in this PR
The
allod/memoryupdates (memory.md:49,git-workflow.md:9,:13,:28,allod.md:62, and alistentry) are a separate commit in a separate repo, gated on the human rebuild rather than on this merge:allodresolves to a nix store path, so until the rebuild the installed tool does not implement what the docs would describe.Read-only self-review pass over the diff. Two findings, both in the test harness, fixed in
339393a:init_repo_no_origin_headredirectedremote set-head origin -dto/dev/null 2>&1. That command is silent on success, so the redirect could only ever hide a failure — and underset -ethe run would then abort with no explanation, in the one fixture whose whole purpose is theorigin/HEADprecondition test.-dcase carried both a bare non-zero-exit check andassert_status 1. Redundant; the weaker one is gone.Still outstanding: the plan sets this PR's review depth at one pass by a different model, which this self-review is not. That pass has not been run.
Review pass by a different model, as the dev plan's R2 review depth requires. It ran read-only against the branch, built its own fixtures in a scratch copy, and sabotage-checked the suite independently. No severe defect; one real contract violation, one weak assertion, and some prose corrections. Fixed in
90d68df.The contract violation, confirmed and reproduced independently.
cleanuphas six refusal sources, not the five the dev plan enumerates:git worktree removealso refuses a worktree containing a populated submodule. Such a worktree passes every checklistwas making — emptystatus --porcelain, nothing unpushed, not locked, detached, or prunable — solistprintedcleanand advisedallod change cleanup <path>, which then died 128 withfatal: working trees containing submodules cannot be moved or removedand left the worktree in place. That is a direct counterexample to the binding contract, stated absolutely in the code comment, the README, and this PR body.Fixed by reporting rather than by weakening the claim.
listgains asubmodulestate, ranked with the other structural blockers abovedirty. The detection is deliberately narrow: a gitlink whose directory holds no.gitis unpopulated and removes fine, and unpopulated is exactly whatbeginitself leaves behind, so firing on any gitlink would falsely block every worktree of any repo that merely has submodules. Both the fire and the no-fire case are asserted, and sabotaging the detection in either direction — removing it, or making it fire on unpopulated gitlinks — is caught by the intended assertion.Latent rather than live: no repo under
~/workhas a.gitmodules, and the failure is loud and non-destructive when hit. Worth fixing anyway, because the contract is the part of this work that outlives the bash implementation.A weak assertion. "list prints one row per linked worktree" counted lines with
printf '%s\n' "$CAPTURE_OUTPUT" | wc -l, which returns 1 for empty output too, so it passed for alistthat printed nothing. The reviewer caught it by stubbingchange_list_repoto a no-op: the suite still bit, but one assertion later. Now counted withgrep -c ., and a zero-rowlistfails the assertion that claims to check it.Two prose corrections.
prunabledoes not mean the directory is gone — it means the link to it broke. A worktree whose directory survives but whose.gitfile was deleted is also reportedprunable, andgit worktree pruneclears the admin entry without deleting anything, so the README's old "the directory is gone; only the admin entry is left" could have invited anrm -rfover uncommitted work. Reworded, with the caution stated.This PR body claimed protected-repo behaviour was "bit-identical". Not quite: protected repos keep their base selection unchanged but do pick up the two new preflights, since the plan applies them to every repo. The code was right; the prose was loose. Corrected above.
Not changed, deliberately. The reviewer noted that after a failed handoff write,
beginleaves behind a branch it can prove it created —worktree add -bsucceeding is itself proof no other agent owns it — while reporting it with the hedged "may have been left behind" wording the lost-race path needs. That is exactly what the plan mandates ("failure handling for step 8 or 9 ... The branch is not deleted"), so it stays; tightening it to delete on the step-9 path only is a fair follow-up against an amended plan, not a change to make silently here.Also reported correct, for coverage: contract steps 1–10 in the plan's order; the
-d-absent path verified byte-identical againstorigin/masteron both protected and unprotected fixtures with zero new filesystem entries; pairwiselist/cleanupagreement for every other state including the unlock→clean→cleanup flip; the rollback's post-condition check exercised in both directions; end-to-end behaviour under a$HOMEcontaining spaces; and theworkspace_repo_default_branchdeviation independently reproduced, confirming the plan's record is what is wrong, not the code.