workspace: show a repo's linked worktrees in work-diff #123

Merged
vnprc merged 1 commit from agent/worktree-aware-collector into master 2026-07-28 01:31:47 +01:00
Member

work-diff is the cross-repo view of uncommitted work, and it was blind to agent worktrees: three live /tmp/allod-change-* worktrees exist on this VM today and none of them appeared. This adds workspace_collect_worktrees to lib/workspace.sh and renders each repo's linked worktrees under it in work-diff, attributed to their branch.

Contracts implemented, both unchanged by this PR:

  • C2 — enumeration is git -C <repo> worktree list --porcelain, never a filesystem glob; the first record is the main checkout and is skipped positionally. Nothing depends on where worktrees are sited, so the /tmp to ~/changes move in allod/tools#116 needs no change here.
  • C5workspace_collect_repos is untouched, so pull-all, flake-status, and flake-update-cascade keep seeing exactly the registry checkouts.

Also included: show_tree_state (extracted from show_repo) now returns 0 explicitly. Its last statement was a trailing [[ -n "$unstaged" ]] && printf ..., so under set -e any repo with untracked-only changes made the function return 1 and aborted the whole run — work-diff exited 1 with the report truncated at that repo. This was pre-existing and reproducible on master; the worktree loop would have hit it constantly, since an agent worktree with only untracked files is the common case.

Risk

R1. Additive library function plus a read-only renderer; no tool that mutates a repo consumes the new entry point.

Residual: a linked worktree sited under $WORK_DIR would render twice — once as a repo, because workspace_is_repo_root accepts it, and once nested under its owner. C1 forbids that siting and allod/tools#116 owns it, so this PR does not filter for it. Filtering in work-diff was considered and rejected: when the owning repo is outside $WORK_DIR, dropping the top-level entry would hide the work entirely, which is the opposite of what this tool is for.

Output volume grows by one line per worktree even when clean. That is deliberate — a lingering worktree awaiting allod change cleanup is worth seeing.

Validation

bash tests/workspace/work-diff.sh   # 37 passed (was 20)
bash tests/workspace/pull-all.sh    # 30 passed (was 29)

Also run green: tests/allod-change.sh, tests/allod-patch.sh, tests/flake/*, tests/git-hooks/protected-refs-policy.sh, tests/forge/*.

New coverage: a worktree with staged, unstaged, and untracked changes rendered under its owning repo with its branch; a detached worktree labelled; a prunable worktree skipped; ordering proving the worktree is nested under its own repo rather than appended after the next one; workspace_collect_worktrees skipping the main checkout; workspace_collect_repos returning exactly the registry checkouts for a workspace whose repos have worktrees; pull-all never invoking worktree at all against its mock git; and the untracked-only truncation regression.

Per architecture.md principle 11, every new assertion was verified to fail on sabotaged input rather than merely to pass. Eight mutations, each checked to have actually applied and to still parse, and the assertion each one broke:

Mutation Assertion that failed
show_tree_state loses its return 0 renders every repo without aborting on an untracked-only working tree
main checkout no longer skipped (record > 0) workspace_collect_worktrees skips the main checkout
prunable [[ -d "$path" ]] guard removed skips a worktree whose directory no longer exists
branch attribution dropped shows a linked worktree attributed to its branch
show_worktrees call removed shows a linked worktree attributed to its branch
worktrees rendered after all repos instead of nested does not defer worktree output past the next repository
${branch:-detached} reduced to $branch labels a worktree with a detached HEAD
workspace_collect_repos emits worktrees too workspace_collect_repos returns exactly the registry checkouts, and pull-all: never enumerates worktrees

Manual check against the real workspace: work-diff now lists all three live agent/* worktrees under allod/tools, including this change's own.

Closes allod/tools#117
Refs allod/tools#115

`work-diff` is the cross-repo view of uncommitted work, and it was blind to agent worktrees: three live `/tmp/allod-change-*` worktrees exist on this VM today and none of them appeared. This adds `workspace_collect_worktrees` to `lib/workspace.sh` and renders each repo's linked worktrees under it in `work-diff`, attributed to their branch. Contracts implemented, both unchanged by this PR: - **C2** — enumeration is `git -C <repo> worktree list --porcelain`, never a filesystem glob; the first record is the main checkout and is skipped positionally. Nothing depends on where worktrees are sited, so the `/tmp` to `~/changes` move in allod/tools#116 needs no change here. - **C5** — `workspace_collect_repos` is untouched, so `pull-all`, `flake-status`, and `flake-update-cascade` keep seeing exactly the registry checkouts. Also included: `show_tree_state` (extracted from `show_repo`) now returns 0 explicitly. Its last statement was a trailing `[[ -n "$unstaged" ]] && printf ...`, so under `set -e` any repo with untracked-only changes made the function return 1 and aborted the whole run — `work-diff` exited 1 with the report truncated at that repo. This was pre-existing and reproducible on master; the worktree loop would have hit it constantly, since an agent worktree with only untracked files is the common case. ## Risk R1. Additive library function plus a read-only renderer; no tool that mutates a repo consumes the new entry point. Residual: a linked worktree sited under `$WORK_DIR` would render twice — once as a repo, because `workspace_is_repo_root` accepts it, and once nested under its owner. C1 forbids that siting and allod/tools#116 owns it, so this PR does not filter for it. Filtering in `work-diff` was considered and rejected: when the owning repo is outside `$WORK_DIR`, dropping the top-level entry would hide the work entirely, which is the opposite of what this tool is for. Output volume grows by one line per worktree even when clean. That is deliberate — a lingering worktree awaiting `allod change cleanup` is worth seeing. ## Validation ``` bash tests/workspace/work-diff.sh # 37 passed (was 20) bash tests/workspace/pull-all.sh # 30 passed (was 29) ``` Also run green: `tests/allod-change.sh`, `tests/allod-patch.sh`, `tests/flake/*`, `tests/git-hooks/protected-refs-policy.sh`, `tests/forge/*`. New coverage: a worktree with staged, unstaged, and untracked changes rendered under its owning repo with its branch; a detached worktree labelled; a prunable worktree skipped; ordering proving the worktree is nested under its own repo rather than appended after the next one; `workspace_collect_worktrees` skipping the main checkout; `workspace_collect_repos` returning exactly the registry checkouts for a workspace whose repos have worktrees; `pull-all` never invoking `worktree` at all against its mock git; and the untracked-only truncation regression. Per `architecture.md` principle 11, every new assertion was verified to fail on sabotaged input rather than merely to pass. Eight mutations, each checked to have actually applied and to still parse, and the assertion each one broke: | Mutation | Assertion that failed | |---|---| | `show_tree_state` loses its `return 0` | renders every repo without aborting on an untracked-only working tree | | main checkout no longer skipped (`record > 0`) | `workspace_collect_worktrees` skips the main checkout | | prunable `[[ -d "$path" ]]` guard removed | skips a worktree whose directory no longer exists | | branch attribution dropped | shows a linked worktree attributed to its branch | | `show_worktrees` call removed | shows a linked worktree attributed to its branch | | worktrees rendered after all repos instead of nested | does not defer worktree output past the next repository | | `${branch:-detached}` reduced to `$branch` | labels a worktree with a detached HEAD | | `workspace_collect_repos` emits worktrees too | `workspace_collect_repos` returns exactly the registry checkouts, and `pull-all`: never enumerates worktrees | Manual check against the real workspace: `work-diff` now lists all three live `agent/*` worktrees under `allod/tools`, including this change's own. Closes allod/tools#117 Refs allod/tools#115
Author
Member

Independent verification pass, run from a clean checkout of the branch rather than from the authoring worktree.

C5 holds. The lib/workspace.sh diff is purely additive — workspace_collect_worktrees is new and workspace_collect_repos has no changed lines, so pull-all, flake-status, and flake-update-cascade see an identical set.

The set -e truncation bug is real and was pre-existing. Reproduced against 7a16f26 with a two-repo fixture where the first repo has only untracked files: work-diff exits 1, renders the first repo, and never renders the second. Same fixture on this branch exits 0 and renders both. The report was silently truncating, which is the failure mode worth naming in review — it looked like a short workspace rather than an aborted run.

Suites pass from the clean checkout: tests/workspace/work-diff.sh, tests/workspace/pull-all.sh, tests/allod-change.sh, and tests/git-hooks/protected-refs-policy.sh all green.

Sabotage spot-check reproduced independently, per architecture principle 11. Changing the main-checkout skip from record > 1 to record > 0 applied cleanly, still parsed, and made the suite fail — so that assertion is load-bearing rather than vacuous. I checked one of the eight rather than all of them.

Two things for human judgement, neither a defect:

  1. The bug fix widens the PR beyond allod/tools#117's stated scope. The argument for including it is sound — a worktree with only untracked files is the common case, so the new rendering would have tripped the bug immediately — but it is a behavior change to existing output that a reviewer should see deliberately rather than discover in the diff.
  2. No defensive guard against a worktree sited under $WORK_DIR. allod/tools#117's goal "a worktree is never treated as a repo to pull or switch" is satisfied by construction once allod/tools#116 lands C1, not by anything in this PR. That is the right call under C5 — an exclusion filter inside workspace_collect_repos is exactly what C5 forbids — but it means the guarantee rests on siting discipline alone. If a defensive guard is wanted it is a separate change with its own blast radius.
Independent verification pass, run from a clean checkout of the branch rather than from the authoring worktree. **C5 holds.** The `lib/workspace.sh` diff is purely additive — `workspace_collect_worktrees` is new and `workspace_collect_repos` has no changed lines, so `pull-all`, `flake-status`, and `flake-update-cascade` see an identical set. **The `set -e` truncation bug is real and was pre-existing.** Reproduced against `7a16f26` with a two-repo fixture where the first repo has only untracked files: `work-diff` exits 1, renders the first repo, and never renders the second. Same fixture on this branch exits 0 and renders both. The report was silently truncating, which is the failure mode worth naming in review — it looked like a short workspace rather than an aborted run. **Suites pass** from the clean checkout: `tests/workspace/work-diff.sh`, `tests/workspace/pull-all.sh`, `tests/allod-change.sh`, and `tests/git-hooks/protected-refs-policy.sh` all green. **Sabotage spot-check reproduced independently**, per architecture principle 11. Changing the main-checkout skip from `record > 1` to `record > 0` applied cleanly, still parsed, and made the suite fail — so that assertion is load-bearing rather than vacuous. I checked one of the eight rather than all of them. Two things for human judgement, neither a defect: 1. **The bug fix widens the PR beyond allod/tools#117's stated scope.** The argument for including it is sound — a worktree with only untracked files is the common case, so the new rendering would have tripped the bug immediately — but it is a behavior change to existing output that a reviewer should see deliberately rather than discover in the diff. 2. **No defensive guard against a worktree sited under `$WORK_DIR`.** allod/tools#117's goal "a worktree is never treated as a repo to pull or switch" is satisfied by construction once allod/tools#116 lands C1, not by anything in this PR. That is the right call under C5 — an exclusion filter inside `workspace_collect_repos` is exactly what C5 forbids — but it means the guarantee rests on siting discipline alone. If a defensive guard is wanted it is a separate change with its own blast radius.
vnprc approved these changes 2026-07-28 01:31:40 +01:00
vnprc merged commit 64f6d8274b into master 2026-07-28 01:31:47 +01:00
vnprc deleted branch agent/worktree-aware-collector 2026-07-28 01:31:47 +01:00
Sign in to join this conversation.
No description provided.