Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/knowledge_base/claude-driven-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ A single, project-independent bash helper provides four commands. It is the same
- `cdd-worktree <branch>`, creates a worktree for `<branch>` and launches Claude Code in plan mode in it with the suggested first prompt already submitted. Requires a handoff file to exist.
- `cdd-worktree-done`, run from a feature worktree once the PR has landed or the branch is being abandoned. Returns to the default branch, pulls, removes the worktree, resolves the branch (safe-delete if merged, force-delete if squash-merged, prompt otherwise), and deletes the handoff — and its sibling state record (§2.13) — iff the branch was deleted.
- `cdd-worktree-list`, lists active handoffs with worktree/branch/PR status. Highlights stale entries.
- `cdd-worktree-resume [<branch>]`, picks up a task started on another machine that has only a clone of the repo (no local branch, worktree, or handoff). It fetches `origin` and recreates a worktree tracking an **existing remote branch**, then `cd`s into it, ready for you to run a resume-side command (`/cdd-process-pr`, `/cdd-merge-base`, `/cdd-pre-pr`). With no argument it lists remote feature branches not already checked out and prompts for one. Its scope is deliberately **worktree and branch recreation only**: the handoff (§2.6) and the state record (§2.13) stay local per machine and are *not* transferred — sound, because the resume-side commands read PR/branch state from git and `gh`, not the handoff, and `cdd-state set` no-ops when the record is absent. Cross-machine *sync* of those two artifacts is separate, still-future work (issue #22); the direction is sketched in the roadmap.
- `cdd-worktree-resume [<branch>]`, picks up a task started on another machine that has only a clone of the repo (no local branch, worktree, or handoff). It fetches `origin` and recreates a worktree tracking an **existing remote branch**, then `cd`s into it, ready for you to run a resume-side command (`/cdd-process-pr`, `/cdd-merge-base`, `/cdd-pre-pr`). With no argument it lists remote feature branches not already checked out and prompts for one; the fetch it runs first uses `--prune`, so branches deleted on the remote (as GitHub does when a PR merges) drop out of the list, leaving just the live branches — the default plus whatever feature branches still exist on the remote, whether or not they yet have a PR. Its scope is deliberately **worktree and branch recreation only**: the handoff (§2.6) and the state record (§2.13) stay local per machine and are *not* transferred — sound, because the resume-side commands read PR/branch state from git and `gh`, not the handoff, and `cdd-state set` no-ops when the record is absent. Cross-machine *sync* of those two artifacts is separate, still-future work (issue #22); the direction is sketched in the roadmap.

The helper installs itself to a stable home that does not depend on a live CDD checkout. Run `tools/cdd-worktree.sh install` once (the script is dual-mode: sourced it defines the functions; run directly with `install` it sets itself up): this copies the script to `~/.cdd/tools/cdd-worktree.sh`, appends a marker-guarded source line to `~/.bashrc` and `~/.zshrc` (idempotent), drops thin PATH shims into `~/.local/bin` (one per command), and migrates any handoffs from the legacy `~/.claude-handoffs/` location. The PATH shims matter because the rc `source` line only reaches **interactive** shells — a stock `~/.bashrc` returns early for non-interactive shells via its `case $- in *i*` guard — so a command invoked from a non-interactive shell (notably Claude Code's Bash tool) would otherwise be "command not found"; the shim sources the helper and dispatches, while interactive shells still prefer the sourced function (functions shadow PATH, which is what lets `cdd-worktree` `cd` the caller into the new worktree). After installing, the commands work in every CDD project — including ones bootstrapped later — without any further per-project setup.

Expand Down
46 changes: 44 additions & 2 deletions scripts/worktree-resume-assert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# End-to-end smoke for `cdd-worktree-resume` (issue #22) against a local bare repo.
#
# The real cross-machine flow is hard to exercise in CI, so this stands in a local
# `git init --bare` for `origin`: it pushes a default branch plus two feature
# `git init --bare` for `origin`: it pushes a default branch plus three feature
# branches, clones a fresh "machine B" working copy that has NO local feature
# branch / worktree, then sources the helper and asserts:
# - `cdd-worktree-resume <branch>` creates a sibling worktree tracking
Expand All @@ -11,6 +11,10 @@
# returns 0
# - `cdd-worktree-resume` with no argument lists resumable remote branches and
# creates the selected one (fed a numbered choice on stdin)
# - discovery's `git fetch --prune` drops a branch deleted on the remote (as
# GitHub does on merge), so it does not appear in the list
# - explicit `cdd-worktree-resume <branch>` for a remote-deleted branch is
# refused and creates no worktree
#
# Usage: scripts/worktree-resume-assert.sh
# Takes no arguments; it provisions and tears down its own temp tree. A stubbed
Expand Down Expand Up @@ -46,6 +50,10 @@ EOF
DEFAULT_BRANCH="main"
FEATURE_A="gh_issue_99_demo"
FEATURE_B="gh_issue_100_other"
# A branch that gets deleted on the remote (as GitHub does when a PR merges).
# Discovery's `git fetch --prune` must drop it, and explicit resume must refuse
# it. Sorts before A/B so it would be candidate 1 if pruning were missing.
FEATURE_C="gh_issue_50_gone"

# Stub `claude` on PATH as a negative guard: the helper must never invoke it, so
# any output here (a non-empty log) is a regression.
Expand Down Expand Up @@ -73,6 +81,9 @@ git clone -q "$WORK/origin.git" "$WORK/seed" 2>/dev/null # empty-repo warning i
git switch -q -c "$FEATURE_B" "$DEFAULT_BRANCH"
echo "b" > b.txt; git add b.txt; git commit -q -m "feature b"
git push -q -u origin "$FEATURE_B"
git switch -q -c "$FEATURE_C" "$DEFAULT_BRANCH"
echo "c" > c.txt; git add c.txt; git commit -q -m "feature c"
git push -q -u origin "$FEATURE_C"
)

# Run the helper in a subshell so its `cd` and `set` don't leak into the test.
Expand Down Expand Up @@ -122,7 +133,8 @@ pass "already-exists resume returns 0 without launching claude"

# 4. Discovery mode (no argument): pick the first listed branch via stdin.
# for-each-ref sorts refnames, so candidate 1 is the lexicographically first
# feature branch ($FEATURE_B = gh_issue_100_other sorts before $FEATURE_A).
# feature branch ($FEATURE_B = gh_issue_100_other sorts before $FEATURE_A;
# $FEATURE_C = gh_issue_50_gone sorts first but is not selected here).
git clone -q "$WORK/origin.git" "$WORK/repoB"
: > "$CLAUDE_STUB_LOG"
set +e
Expand All @@ -136,4 +148,34 @@ head="$(git -C "$WT_B" rev-parse --abbrev-ref HEAD)"
[[ "$head" == "$FEATURE_B" ]] || fail "discovery worktree HEAD is '$head', expected '$FEATURE_B'"
pass "discovery mode resumed the selected remote branch"

# 5. Discovery prunes branches deleted on the remote. Clone first (so the stale
# origin/$FEATURE_C ref exists locally), then delete the branch on origin, as
# GitHub does when a PR merges. Feed an invalid choice (0) so the helper prints
# the candidate list then bails without creating a worktree; inspect it.
git clone -q "$WORK/origin.git" "$WORK/repoC"
git -C "$WORK/seed" push -q origin --delete "$FEATURE_C"
set +e
list_out="$(run_resume "$WORK/repoC" "" "0" 2>/dev/null)"
set -e
grep -q "$FEATURE_A" <<<"$list_out" || fail "discovery should list live $FEATURE_A"
grep -q "$FEATURE_B" <<<"$list_out" || fail "discovery should list live $FEATURE_B"
if grep -q "$FEATURE_C" <<<"$list_out"; then
fail "discovery must prune $FEATURE_C after it was deleted on the remote"
fi
pass "discovery prunes branches deleted on the remote"

# 6. Explicit resume of a remote-deleted branch is refused, and no worktree is
# created for it. repoC still carries the stale ref; the fetch --prune inside
# resume drops it, so show-ref then fails.
: > "$CLAUDE_STUB_LOG"
set +e
run_resume "$WORK/repoC" "$FEATURE_C" "" >/dev/null 2>&1
rc=$?
set -e
[[ "$rc" -ne 0 ]] || fail "explicit resume of remote-deleted $FEATURE_C should exit non-zero"
[[ ! -d "$WORK/repoC-$FEATURE_C" ]] \
|| fail "explicit resume of remote-deleted $FEATURE_C must not create a worktree"
[[ ! -s "$CLAUDE_STUB_LOG" ]] || fail "refused resume must not launch claude"
pass "explicit resume of a remote-deleted branch is refused without a worktree"

echo "all worktree-resume smoke checks passed"
13 changes: 9 additions & 4 deletions tools/cdd-worktree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,11 @@ cdd-worktree-resume() {
return 1
fi

if ! git fetch origin; then
echo "git fetch origin failed, aborting." >&2
# --prune drops stale remote-tracking refs for branches deleted on the remote
# (GitHub deletes the head branch when a PR merges), so discovery lists exactly
# what still exists on origin — the default branch plus live feature branches.
if ! git fetch --prune origin; then
echo "git fetch --prune origin failed, aborting." >&2
return 1
fi

Expand All @@ -324,7 +327,8 @@ cdd-worktree-resume() {

if [[ -z "$branch" ]]; then
# Discovery: remote feature branches (exclude default + HEAD) not already
# checked out as a local worktree.
# checked out as a local worktree. The fetch above pruned merged-and-deleted
# branches, so what remains is the set of live branches shown on GitHub.
local have_gh=0
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
have_gh=1
Expand Down Expand Up @@ -368,7 +372,8 @@ cdd-worktree-resume() {
branch="${candidates[$(( choice - 1 ))]}"
else
if ! git show-ref --verify --quiet "refs/remotes/origin/$branch"; then
echo "No remote branch origin/$branch (after fetch)." >&2
echo "No remote branch origin/$branch (after fetch --prune)." >&2
echo "It may have been merged and deleted on the remote, or never pushed." >&2
echo "Use 'cdd-worktree-resume' with no argument to list resumable branches." >&2
return 1
fi
Expand Down
Loading