Skip to content

chore: enforce isolated worktrees for feature work - #980

Merged
castrojo merged 2 commits into
testingfrom
chore/worktree-workflow
Aug 7, 2026
Merged

chore: enforce isolated worktrees for feature work#980
castrojo merged 2 commits into
testingfrom
chore/worktree-workflow

Conversation

@castrojo

@castrojo castrojo commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Problem

The main checkout kept accumulating in-flight work from concurrent sessions. Three defects made that unavoidable:

  • .worktrees/ was never gitignored — even though validate-docs.py already excluded it from scanning. A nested .git directory stages as a gitlink and silently corrupts history, which is exactly what AGENTS.md warns about.
  • install-hooks.sh resolved hooks via --git-dir — inside a worktree that points at the per-worktree directory, which has no hooks/. The script failed outright.
  • A global core.hooksPath silently overrode the repo-local hook — so the origin push guard has never actually run for anyone who sets one.

Changes

.github/scripts/worktree.sh — create, list, retire, and prune worktrees:

bash .github/scripts/worktree.sh new fix/my-thing   # branches from a fresh projectbluefin/testing
bash .github/scripts/worktree.sh list               # worktrees + PR state
bash .github/scripts/worktree.sh done fix/my-thing  # retire one
bash .github/scripts/worktree.sh prune              # retire all merged/closed

Branch retirement is resolved through the forge via gh, because squash merges leave no ancestry for git branch --merged to find.

install-hooks.sh now:

  • installs into the common git dir, so it works from any worktree;
  • adds a guard refusing feature-branch pushes from the main checkout (SKIP_WORKTREE_GUARD=1 to override);
  • when core.hooksPath is overridden, redirects it repo-locally and shims every unmanaged global hook so they keep running. The original path is saved to bluefin.chainedHooksPath so re-runs stay idempotent.

Verification

just check, pre-commit run --all-files, and validate-docs.py all pass. Behavior tested end-to-end:

Case Result
Feature branch pushed from main checkout blocked
Same push from a worktree allowed
SKIP_WORKTREE_GUARD=1 from main checkout allowed
Push to origin blocked
newdone round-trip worktree and branch removed
install-hooks.sh run twice idempotent, chain preserved

A --show-toplevel bug was caught during testing: it returns the current worktree's root, so it misidentified the main checkout. Now derived from --git-common-dir.

Notes

  • Docs land in the same PR per the self-improvement mandate: new docs/skills/worktrees/SKILL.md, plus index and AGENTS.md entries.
  • Touches .github/scripts/, not .github/workflows/, so no CODEOWNERS gate.
  • Heads up: the global ~/.git-hooks/pre-push on at least one maintainer machine is mode 644, so it is silently skipped. The new hook now warns about this instead of failing quietly.

Assisted-by: Claude Opus 5 via GitHub Copilot CLI

github-actions Bot and others added 2 commits August 6, 2026 23:12
The main checkout kept accumulating in-flight work from concurrent
sessions. Three defects made that unavoidable:

- `.worktrees/` was never gitignored, even though validate-docs.py
  already excluded it. A nested .git directory stages as a gitlink and
  silently corrupts history.
- `install-hooks.sh` resolved hooks via `--git-dir`, which points at the
  per-worktree directory with no `hooks/`, so it failed outright inside
  a worktree.
- A global `core.hooksPath` silently overrode the repo-local hook, so
  the origin guard never actually ran.

Add `worktree.sh` to create, list, retire, and prune worktrees branched
from a freshly fetched projectbluefin/testing. Branch retirement is
resolved through the forge because squash merges leave no ancestry for
`git branch --merged` to find.

Extend the pre-push hook to refuse feature-branch pushes from the main
checkout, overridable with SKIP_WORKTREE_GUARD=1. Install hooks into the
common git dir and, when core.hooksPath is overridden, redirect it
locally while shimming every unmanaged global hook so it still runs.

Assisted-by: Claude Opus 5 via GitHub Copilot CLI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Blocking the remote named `origin` rejected every push in a clone made
directly from projectbluefin/bluefin, where `origin` is the correct
target. Match on the push URL instead, which enforces the documented
"no writes outside the projectbluefin org" boundary in both setups.

Also:
- restore the executable bit on install-hooks.sh, lost in the previous commit
- parse `git worktree list --porcelain` paths with substr instead of a
  whitespace-split field, so paths containing spaces are not truncated
- fold the worktree mandate into AGENTS.md "Change flow" so it no longer
  contradicts the plain `git checkout -b` instructions added by #979
- narrow the worktree boundary rule to another task's worktree

Tested: shellcheck on both scripts and the generated hooks; installer run
twice against a scratch clone with an existing hooksPath, with no
hooksPath, and with a nonexistent hooksPath; pre-push exercised for
feature branch from main checkout, from a linked worktree, on testing,
with SKIP_WORKTREE_GUARD=1, and against ublue-os/fork/projectbluefin
URLs; worktree.sh new/list/done/prune end to end.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant