From fb3906302728e922bce08d7202b882d0a5929eaf Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Tue, 30 Jun 2026 22:56:43 +0200 Subject: [PATCH 1/2] Add cdd-worktree-resume for multi-machine worktree pickup (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recreate a worktree on a second machine that has only a clone: fetch origin, attach the existing remote branch (no -b new branch, no handoff required), and launch plain Claude Code so the user can run a resume-side command (/cdd-process-pr, /cdd-merge-base, /cdd-pre-pr). With no argument, list remote feature branches not already checked out and prompt for one. Scope is worktree + branch recreation only: the handoff (.md) and state record (.state.json) stay local per machine and are not synced. That is safe because the resume-side commands read PR/branch state from git and gh, not the handoff, and cdd-state set no-ops when absent. Cross-machine sync of those artifacts is recorded as remaining Phase-13 work (the refs/cdd/ design sketch in the roadmap). - tools/cdd-worktree.sh: new cdd-worktree-resume + "Provides" header entry - scripts/worktree-resume-assert.sh: local-bare-repo-as-origin smoke with a stubbed claude; wired into template-smoke.yml - process doc §2.8 (four commands + multi-machine note) and §2.13 refinement - roadmap Phase 13: split #22 into landed worktree-resume + deferred sync - README + both CLAUDE.md workflow sections; CLAUDE.md build/test list Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/template-smoke.yml | 3 + CLAUDE.md | 5 + README.md | 4 +- .../claude-driven-development.md | 9 +- doc/knowledge_base/roadmap.md | 3 +- scripts/worktree-resume-assert.sh | 137 ++++++++++++++++++ template/CLAUDE.md | 1 + tools/cdd-worktree.sh | 119 +++++++++++++++ 8 files changed, 275 insertions(+), 6 deletions(-) create mode 100755 scripts/worktree-resume-assert.sh diff --git a/.github/workflows/template-smoke.yml b/.github/workflows/template-smoke.yml index daea853..9b5d64d 100644 --- a/.github/workflows/template-smoke.yml +++ b/.github/workflows/template-smoke.yml @@ -31,6 +31,9 @@ jobs: - name: Worktree-helper install smoke (against a throwaway HOME) run: ./scripts/install-smoke-assert.sh + - name: Worktree-resume smoke (local bare repo as origin) + run: ./scripts/worktree-resume-assert.sh + - name: Configure git identity for the scaffold commit run: | git config --global user.email "ci@example.com" diff --git a/CLAUDE.md b/CLAUDE.md index 94cf3aa..2b07db2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,10 @@ bash -n demo/setup.sh demo/teardown.sh demo/lib.sh # Worktree-helper install: run `cdd-worktree.sh install` against a throwaway HOME. ./scripts/install-smoke-assert.sh +# Worktree-resume: recreate a worktree on an existing remote branch, using a +# local bare repo as `origin` and a stubbed `claude` (no real session launched). +./scripts/worktree-resume-assert.sh + # End-to-end smoke: bootstrap into a tmpdir and run the assertion script. rm -rf /tmp/cdd-smoke && mkdir -p /tmp/cdd-smoke ./tools/bootstrap-cdd-project.sh --name "Demo Project" \ @@ -99,6 +103,7 @@ See `doc/knowledge_base/claude-driven-development.md` for the full picture. This project uses CDD on itself. Every CDD session is a fresh context doing exactly one job (see process doc section 3 for the session taxonomy). - **To start a new task** (handoff session): run `/cdd-next-step` from the main worktree to produce a handoff, then run `cdd-worktree ` to spin up the implementation worktree (implementation session, opens in plan mode). `/cdd-next-step` has three front-ends: no argument picks the next roadmap item; a task prompt starts off-roadmap work (intent-driven); and `#NN` / a bare integer / the `issue`/`issues` keyword sources the task from a GitHub issue (issue-driven), naming the branch `gh_issue_NN_`. +- **To pick up a task started on another machine** (resume): run `cdd-worktree-resume []` from the main worktree. It recreates the worktree on the existing remote branch (no handoff needed) and launches Claude Code so you can run `/cdd-process-pr`, `/cdd-merge-base`, or `/cdd-pre-pr`; with no argument it lists resumable remote branches. - **When main has advanced under a feature branch** (merge session): run `/cdd-merge-base` in a fresh context on the feature branch. - **Before opening a PR** (pre-PR session): run `/cdd-pre-pr` in a fresh context to verify the process doc and template are consistent and the roadmap reflects what landed; it auto-commits its own reconciliation edits (local, no push) and ends with an opt-in step to open the PR (adding `Closes #NN` when the branch carries the `gh_issue_NN` token). - **When a PR review leaves comments** (PR-review session): run `/cdd-process-pr` in a fresh context on the feature branch. diff --git a/README.md b/README.md index 8a328d6..4902ce7 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ CDD ships seven slash commands, all prefixed `cdd-` so they autocomplete as a gr | `/cdd‑retrofit` | Install or upgrade CDD in an existing project. | | `/cdd‑quick‑create` | Produce a one-off self-contained deliverable (script + README), no project substrate. | -`cdd-worktree` (and its companion `cdd-worktree-done`) is a **shell helper**, not a slash command. It's a single project-independent script — a machine-global toolchain dependency, like `git` or `gh` — that you install once and that then works in every CDD project. From a CDD repo checkout: `tools/cdd-worktree.sh install`. On a fresh machine with only a downstream project (no CDD repo), one command fetches and installs it: +`cdd-worktree` (and its companions `cdd-worktree-done`, `cdd-worktree-list`, and `cdd-worktree-resume`) is a **shell helper**, not a slash command. It's a single project-independent script — a machine-global toolchain dependency, like `git` or `gh` — that you install once and that then works in every CDD project. From a CDD repo checkout: `tools/cdd-worktree.sh install`. On a fresh machine with only a downstream project (no CDD repo), one command fetches and installs it: ```bash curl -fsSL https://raw.githubusercontent.com/drabaioli/cdd/main/tools/cdd-worktree.sh \ @@ -72,7 +72,7 @@ curl -fsSL https://raw.githubusercontent.com/drabaioli/cdd/main/tools/cdd-worktr && bash ~/.cdd/tools/cdd-worktree.sh install ``` -Either form wires `~/.bashrc` and `~/.zshrc` (idempotent); open a new shell afterwards. It spins up and tears down the per-task git worktree that an implementation session runs in. +Either form wires `~/.bashrc` and `~/.zshrc` (idempotent); open a new shell afterwards. It spins up and tears down the per-task git worktree that an implementation session runs in, and `cdd-worktree-resume []` recreates that worktree on a second machine — tracking the existing remote branch, no handoff needed — so a task started elsewhere can be picked up to run `/cdd-process-pr`, `/cdd-merge-base`, or `/cdd-pre-pr`. ## Questions? diff --git a/doc/knowledge_base/claude-driven-development.md b/doc/knowledge_base/claude-driven-development.md index 19abac0..95a5e77 100644 --- a/doc/knowledge_base/claude-driven-development.md +++ b/doc/knowledge_base/claude-driven-development.md @@ -129,11 +129,14 @@ Slash commands are declarative: they describe what to do, not how to orchestrate ### 2.8 The worktree shell helper (`cdd-worktree`) -A single, project-independent bash helper provides three commands. It is the same script for every CDD project: the functions derive the repository name, default branch, and handoff directory at runtime, so there is no per-project copy. +A single, project-independent bash helper provides four commands. It is the same script for every CDD project: the functions derive the repository name, default branch, and handoff directory at runtime, so there is no per-project copy. - `cdd-worktree `, creates a worktree for `` 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 []`, picks up a task started on another machine. It recreates a worktree tracking an **existing remote branch** (no handoff required) and launches plain Claude Code so you can 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. See the multi-machine note below. + +**Multi-machine pickup (`cdd-worktree-resume`).** A task can start on one machine — worktree and branch created, branch pushed, PR opened — and need to continue on a second machine that has only a clone of the repo: no local branch, no worktree, no handoff. `cdd-worktree-resume` fetches `origin`, recreates the worktree on the existing remote branch, and drops you into Claude Code to run the resume-side command of your choice. Its scope is deliberately **worktree and branch recreation only**: the handoff (§2.6) and the state record (§2.13) are local-per-machine artifacts and are *not* transferred. That is sound, because the resume-side commands read PR and branch state from git and `gh`, not from the handoff, and `cdd-state set` no-ops when the record is absent. Cross-machine *sync* of the handoff and state record is separate, still-future work (issue #22); the chosen 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), and migrates any handoffs from the legacy `~/.claude-handoffs/` location. After installing, the commands work in every CDD project — including ones bootstrapped later — without any further per-project setup. @@ -145,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/drabaioli/cdd/main/tools/cdd-worktr && bash ~/.cdd/tools/cdd-worktree.sh install ``` -The helper is a machine-global toolchain dependency, like `git` or `gh`: installed once per machine, newest wins, install idempotent. Install from latest `main`, never pinned per project — pinning would reintroduce the very conflict a single shared helper avoids. Its contract with projects is deliberately tiny and frozen: the three command names above plus the `~/.cdd/handoffs//.md` layout; everything project-specific is derived at runtime, so one copy stays compatible with every project and there is by construction no per-project helper to conflict. When that state must evolve, the change ships as a one-shot migration inside `install` (the `~/.claude-handoffs/` → `~/.cdd/handoffs/` move is the example), re-homing every project at once. +The helper is a machine-global toolchain dependency, like `git` or `gh`: installed once per machine, newest wins, install idempotent. Install from latest `main`, never pinned per project — pinning would reintroduce the very conflict a single shared helper avoids. Its contract with projects is deliberately tiny: the command names above plus the `~/.cdd/handoffs//.md` layout; everything project-specific is derived at runtime, so one copy stays compatible with every project and there is by construction no per-project helper to conflict. The contract grows only additively — `cdd-worktree-resume` was added without changing the layout or any existing command (it reads remote git state, not the handoff), so a newer helper stays backward-compatible with every project version. When that state must evolve, the change ships as a one-shot migration inside `install` (the `~/.claude-handoffs/` → `~/.cdd/handoffs/` move is the example), re-homing every project at once. The helper derives the repository's default branch from `origin`'s HEAD (falling back to `main`) and assumes the remote is named `origin`; the remote-name assumption is documented in `template/BOOTSTRAP.md`. @@ -209,7 +212,7 @@ A practice moves from **expected** to **enforced** in the same change that lands A small JSON file recording where a task sits in its lifecycle and which Claude Code sessions have worked it. It is an **additive sibling of the handoff** (§2.6): same per-repo directory, same `` basename, same branch-scoped/ephemeral lifecycle. The primary payoff is the **session chain** — given a branch, you can find and resume the session(s) that worked it (`claude --resume `) without grepping shell history. A dashboard (`cdd-dash`) is one downstream consumer, not the justification. -The record is **advisory**: a consumer that finds it missing or stale falls back to inferring state from handoffs, branches, and `gh`; a writer that finds it missing does not fabricate one (only `/cdd-next-step` creates it). It is a **local cache** of work on *this* machine — not a cross-machine transfer mechanism and not an event history; multi-machine resume is separate future work (issue #22). It fits the frozen worktree-helper contract (§2.8) without enlarging it: `cdd-worktree-done` only deletes the record, alongside the handoff. +The record is **advisory**: a consumer that finds it missing or stale falls back to inferring state from handoffs, branches, and `gh`; a writer that finds it missing does not fabricate one (only `/cdd-next-step` creates it). It is a **local cache** of work on *this* machine — not a cross-machine transfer mechanism and not an event history. Recreating a worktree on another machine is handled by `cdd-worktree-resume` (§2.8), which rebuilds the worktree from the remote branch without this record; *syncing* the record (and the handoff) across machines is separate future work (issue #22). It fits the frozen worktree-helper contract (§2.8) without enlarging it: `cdd-worktree-done` only deletes the record, alongside the handoff. Writes go through a small **`cdd-state` helper** (`tools/cdd-state.sh`), dual-mode and self-installing like the worktree helper: `cdd-state seed ` creates the record, `cdd-state set [--pr N]` advances it. Routing every write through one helper makes them atomic (temp-file + rename) and well-formed, so the malformed-JSON / wrong-field failure mode of hand-editing is gone; the model still decides *when* to call it. The helper is advisory end-to-end — absent `jq` or an absent record, it no-ops rather than failing the workflow. diff --git a/doc/knowledge_base/roadmap.md b/doc/knowledge_base/roadmap.md index 59de7fe..1cb7a7f 100644 --- a/doc/knowledge_base/roadmap.md +++ b/doc/knowledge_base/roadmap.md @@ -174,6 +174,7 @@ Give each task a machine-readable record of where it sits in its lifecycle and w - [x] Per-task state record + `cdd-state` helper: a `.state.json` sibling of the handoff, advanced through the lifecycle by the slash commands via `tools/cdd-state.sh` (atomic `seed`/`set`, self-installing). Advisory, local-only, append-only `{id, stage}` session chain. Full design and schema in process doc §2.13. — §2.13 + §2.6/§2.8/§3.3, all four command copies (repo + template), both `settings.json`, `tools/cdd-state.sh` (new) and `tools/cdd-worktree.sh` (deletion), architecture/feature docs, BOOTSTRAP.md. - [ ] Harden the one outcome transition a tool call owns: a `PostToolUse` hook on `gh pr create` that parses the PR number and writes `pr_open`/`pr=NN` mechanically (`cdd-state` as the hook target), removing the model-remembering dependency. (A `UserPromptSubmit` hook fires deterministically on every `/cdd-*` call, but only at invocation — it can stamp "stage started", not outcomes like `checks_passed` or the PR number, which stay model-driven via `cdd-state set`.) - [ ] Consume the record: teach the `cdd-dash` dashboard to read `stage`/`sessions` instead of inferring task state. (`cdd-worktree-list` already infers worktree/branch/PR status fine and does not need the record — fold in only if a concrete need appears.) -- [ ] Multi-machine resume: regenerate this state from a remote branch so a task can be picked up on another machine (issue #22). Needs a sync mechanism (git notes/refs) — explicitly out of scope for the local cache above. +- [x] Multi-machine resume — worktree + branch (issue #22): `cdd-worktree-resume []` recreates a worktree on an **existing remote branch** on a second machine (no handoff required) and launches Claude Code for a resume-side command (`/cdd-process-pr`, `/cdd-merge-base`, `/cdd-pre-pr`). Discovery mode lists remote feature branches not already checked out. — `tools/cdd-worktree.sh` (new command + header), process doc §2.8/§2.13, `scripts/worktree-resume-assert.sh` (new, local-bare-repo-as-`origin` test) + CI step, README, both `CLAUDE.md` workflow sections. +- [ ] Multi-machine resume — handoff + state sync (issue #22): the resume above deliberately does **not** transfer the handoff (`.md`) or state record (`.state.json`); they stay local per machine. To carry them across, the chosen direction is a per-task ref namespace `refs/cdd/` holding the handoff + state JSON as a blob/tree, pushed by the helper/commands at lifecycle transitions and fetched + materialized by `cdd-worktree-resume` before launch, degrading gracefully when no ref exists. git notes (`refs/notes/cdd`) is the standard alternative but anchors metadata to a commit, so it must chase the moving branch tip — the branch-keyed ref avoids that. Committing these files into the work tree is rejected: they are deliberately out-of-tree, per-user artifacts. **Milestone:** a task's lifecycle stage and its working sessions are recorded as data and surfaced by CDD tooling, not reconstructed by inference. diff --git a/scripts/worktree-resume-assert.sh b/scripts/worktree-resume-assert.sh new file mode 100755 index 0000000..11ff100 --- /dev/null +++ b/scripts/worktree-resume-assert.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# 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 +# branches, clones a fresh "machine B" working copy that has NO local feature +# branch / worktree, then sources the helper and asserts: +# - `cdd-worktree-resume ` creates a sibling worktree tracking +# origin/, and launches `claude` (stubbed on PATH) +# - a second `cdd-worktree-resume ` detects the existing worktree, +# returns 0, and does NOT relaunch claude +# - `cdd-worktree-resume` with no argument lists resumable remote branches and +# creates the selected one (fed a numbered choice on stdin) +# +# Usage: scripts/worktree-resume-assert.sh +# Takes no arguments; it provisions and tears down its own temp tree. A stubbed +# `claude` keeps the helper from launching a real session. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HELPER="$REPO_ROOT/tools/cdd-worktree.sh" + +fail() { echo "FAIL: $*" >&2; exit 1; } +pass() { echo "ok: $*"; } + +[[ -f "$HELPER" ]] || fail "helper not found: $HELPER" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# Isolate from the caller's git identity / signing config; keep runs deterministic. +export GIT_CONFIG_SYSTEM=/dev/null +export GIT_CONFIG_GLOBAL="$WORK/gitconfig" +cat > "$GIT_CONFIG_GLOBAL" <<'EOF' +[user] + name = CDD Smoke + email = smoke@example.com +[init] + defaultBranch = main +[commit] + gpgsign = false +EOF + +DEFAULT_BRANCH="main" +FEATURE_A="gh_issue_99_demo" +FEATURE_B="gh_issue_100_other" + +# Stub `claude` on PATH so the helper runs to completion without a real session. +mkdir -p "$WORK/bin" +export CLAUDE_STUB_LOG="$WORK/claude.log" +cat > "$WORK/bin/claude" <<'EOF' +#!/usr/bin/env bash +echo "claude $*" >> "$CLAUDE_STUB_LOG" +exit 0 +EOF +chmod +x "$WORK/bin/claude" + +# 1. Bare repo standing in for origin, seeded with a default + two feature branches. +git init --bare -q "$WORK/origin.git" +git clone -q "$WORK/origin.git" "$WORK/seed" 2>/dev/null # empty-repo warning is expected +( + cd "$WORK/seed" + echo "# seed" > README.md + git add README.md + git commit -q -m "seed" + git push -q -u origin "$DEFAULT_BRANCH" + git switch -q -c "$FEATURE_A" + echo "a" > a.txt; git add a.txt; git commit -q -m "feature a" + git push -q -u origin "$FEATURE_A" + 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" +) + +# Run the helper in a subshell so its `cd` and `set` don't leak into the test. +# $1 = clone dir, $2 = branch arg (may be empty), $3 = stdin for discovery prompt. +run_resume() { + ( + cd "$1" + export PATH="$WORK/bin:$PATH" + # shellcheck source=/dev/null + source "$HELPER" + if [[ -z "$2" ]]; then + cdd-worktree-resume <<<"$3" + else + cdd-worktree-resume "$2" + fi + ) +} + +# 2. Explicit-branch resume on a fresh clone (no local feature branch/worktree). +git clone -q "$WORK/origin.git" "$WORK/repoA" +: > "$CLAUDE_STUB_LOG" +set +e +run_resume "$WORK/repoA" "$FEATURE_A" "" >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 0 ]] || fail "cdd-worktree-resume $FEATURE_A exited $rc" + +WT_A="$WORK/repoA-$FEATURE_A" +[[ -d "$WT_A" ]] || fail "worktree not created at $WT_A" +head="$(git -C "$WT_A" rev-parse --abbrev-ref HEAD)" +[[ "$head" == "$FEATURE_A" ]] || fail "worktree HEAD is '$head', expected '$FEATURE_A'" +upstream="$(git -C "$WT_A" rev-parse --abbrev-ref "$FEATURE_A@{upstream}" 2>/dev/null || true)" +[[ "$upstream" == "origin/$FEATURE_A" ]] \ + || fail "branch upstream is '$upstream', expected 'origin/$FEATURE_A'" +[[ -s "$CLAUDE_STUB_LOG" ]] || fail "claude was not launched after resume" +pass "explicit resume created a tracking worktree and launched claude" + +# 3. Re-running on the same branch detects the existing worktree (no relaunch). +: > "$CLAUDE_STUB_LOG" +set +e +run_resume "$WORK/repoA" "$FEATURE_A" "" >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 0 ]] || fail "second cdd-worktree-resume $FEATURE_A exited $rc (expected 0)" +[[ ! -s "$CLAUDE_STUB_LOG" ]] || fail "already-exists path should not relaunch claude" +pass "already-exists resume returns 0 without relaunching 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). +git clone -q "$WORK/origin.git" "$WORK/repoB" +: > "$CLAUDE_STUB_LOG" +set +e +run_resume "$WORK/repoB" "" "1" >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 0 ]] || fail "discovery cdd-worktree-resume exited $rc" +WT_B="$WORK/repoB-$FEATURE_B" +[[ -d "$WT_B" ]] || fail "discovery did not create worktree for first candidate ($FEATURE_B)" +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" + +echo "all worktree-resume smoke checks passed" diff --git a/template/CLAUDE.md b/template/CLAUDE.md index f2f0805..3256b72 100644 --- a/template/CLAUDE.md +++ b/template/CLAUDE.md @@ -60,6 +60,7 @@ See `doc/architecture/index.md` for the full picture. This project uses the Claude-Driven Development workflow. Every CDD session is a fresh context doing exactly one job. - **To start a new task** (handoff session): run `/cdd-next-step` from the main worktree to produce a handoff, then run `cdd-worktree ` to spin up the implementation worktree (implementation session, opens in plan mode). `/cdd-next-step` has three front-ends: no argument picks the next roadmap item; a task prompt starts off-roadmap work (intent-driven); and `#NN` / a bare integer / the `issue`/`issues` keyword sources the task from a GitHub issue (issue-driven), naming the branch `gh_issue_NN_`. +- **To pick up a task started on another machine** (resume): run `cdd-worktree-resume []` from the main worktree. It recreates the worktree on the existing remote branch (no handoff needed) and launches Claude Code so you can run `/cdd-process-pr`, `/cdd-merge-base`, or `/cdd-pre-pr`; with no argument it lists resumable remote branches. - **When main has advanced under a feature branch** (merge session): run `/cdd-merge-base` in a fresh context on the feature branch. - **Before opening a PR** (pre-PR session): run `/cdd-pre-pr` in a fresh context to verify CI gates pass and that architecture/feature docs and the roadmap reflect the change; it auto-commits its own reconciliation edits (local, no push) and ends with an opt-in step to open the PR (adding `Closes #NN` when the branch carries the `gh_issue_NN` token). - **When a PR review leaves comments** (PR-review session): run `/cdd-process-pr` in a fresh context on the feature branch. diff --git a/tools/cdd-worktree.sh b/tools/cdd-worktree.sh index 9ac8701..1af3b4a 100755 --- a/tools/cdd-worktree.sh +++ b/tools/cdd-worktree.sh @@ -53,6 +53,15 @@ # with worktree / branch / PR status. Highlights # stale entries (handoff with no branch and no # worktree) so they're obvious to clean up. +# +# cdd-worktree-resume [] +# Pick up a task started on another machine: recreate +# a worktree tracking an EXISTING remote branch +# (no handoff required), then launch plain `claude` +# so you can run /cdd-process-pr, /cdd-merge-base, +# or /cdd-pre-pr. With no argument, lists remote +# feature branches not already checked out and +# prompts for one. Run from the main worktree. # Resolve the repo's default branch from origin's HEAD, falling back to "main". # The remote is assumed to be named "origin" (see template/BOOTSTRAP.md). @@ -284,6 +293,116 @@ cdd-worktree-list() { done } +# Recreate a worktree on an EXISTING remote branch so a task started on another +# machine can be picked up here. Unlike cdd-worktree, this requires no handoff and +# tracks the remote branch rather than creating a new one. The handoff and state +# record are local-only and are NOT synced across machines (see process doc §2.8); +# the resume-side commands (/cdd-process-pr, /cdd-merge-base, /cdd-pre-pr) read +# PR/branch state from git and gh, not the handoff, so their absence is fine. +cdd-worktree-resume() { + local branch="${1:-}" + + # Same guard as cdd-worktree: the sibling worktree name is derived from $PWD, so + # insist on the main worktree to avoid nesting names. + local default_branch current_branch + default_branch="$(cdd-worktree-default-branch)" + current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || return 1 + if [[ "$current_branch" != "$default_branch" ]]; then + echo "Run this from the main worktree on '$default_branch' (current: '$current_branch')." >&2 + return 1 + fi + + if ! git fetch origin; then + echo "git fetch origin failed, aborting." >&2 + return 1 + fi + + # Snapshot worktree branches once (reused for discovery and already-exists). + local worktree_branches + worktree_branches="$(git worktree list --porcelain 2>/dev/null \ + | awk '$1 == "branch" { sub("refs/heads/", "", $2); print $2 }')" + + if [[ -z "$branch" ]]; then + # Discovery: remote feature branches (exclude default + HEAD) not already + # checked out as a local worktree. + local have_gh=0 + if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then + have_gh=1 + fi + + local candidates=() rb + while IFS= read -r rb; do + rb="${rb#origin/}" + [[ "$rb" == "HEAD" || "$rb" == "$default_branch" ]] && continue + grep -qx "$rb" <<<"$worktree_branches" && continue + candidates+=("$rb") + done < <(git for-each-ref --format='%(refname:short)' refs/remotes/origin 2>/dev/null) + + if (( ${#candidates[@]} == 0 )); then + echo "No remote feature branches to resume (all are local worktrees or none exist)." >&2 + return 1 + fi + + echo "Remote branches available to resume:" + local i pr_line + for i in "${!candidates[@]}"; do + pr_line="" + if (( have_gh )); then + pr_line="$(gh pr list --head "${candidates[$i]}" --state all \ + --json number,state \ + --jq '.[0] | select(.) | " (PR #\(.number) \(.state))"' 2>/dev/null)" + fi + printf ' %2d) %s%s\n' "$(( i + 1 ))" "${candidates[$i]}" "$pr_line" + done + + local choice + read -r -p "Select a branch [1-${#candidates[@]}]: " choice + if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#candidates[@]} )); then + echo "Invalid selection: '$choice'." >&2 + return 1 + fi + 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 "Use 'cdd-worktree-resume' with no argument to list resumable branches." >&2 + return 1 + fi + fi + + # Already checked out as a worktree? Point the user at it and stop. + if grep -qx "$branch" <<<"$worktree_branches"; then + local existing + existing="$(git worktree list --porcelain 2>/dev/null | awk -v ref="refs/heads/$branch" ' + $1 == "worktree" { path = $2 } + $1 == "branch" && $2 == ref { print path; exit } + ')" + echo "Branch '$branch' is already checked out at: ${existing:-}" >&2 + return 0 + fi + + local repo_dir + repo_dir="$(basename "$PWD")" + local worktree_path="../${repo_dir}-${branch}" + + if git show-ref --verify --quiet "refs/heads/$branch"; then + # Local branch already exists (no worktree yet): attach it. + git worktree add "$worktree_path" "$branch" || return 1 + else + # Create a local branch tracking the existing remote branch. + git worktree add --track -b "$branch" "$worktree_path" "origin/$branch" || return 1 + fi + cd "$worktree_path" || return 1 + + echo + echo "Resumed worktree for '$branch' on origin/$branch." + echo "Handoff/state were NOT transferred (they're local to the originating machine)." + echo "Resume-side commands read PR/branch state from git and gh, so this is fine." + echo "Next: run /cdd-process-pr, /cdd-merge-base, or /cdd-pre-pr." + echo + claude +} + # Install this helper to its stable home and wire it into the user's shells. # Run directly (`tools/cdd-worktree.sh install`), never sourced. Idempotent. cdd-worktree-install() { From f089b9b4f9fc0aa611c5fb70976562f1f2a992b7 Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Tue, 30 Jun 2026 23:11:22 +0200 Subject: [PATCH 2/2] Fix cdd-worktree-resume discovery on newer git (origin/HEAD symref) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovery built candidates from `%(refname:short)` and stripped an `origin/` prefix to drop the origin/HEAD symref. That short form is "origin/HEAD" on git 2.34 (strips to "HEAD", excluded) but just "origin" on newer git (CI), which slipped past the HEAD check and became a bogus first candidate — `git worktree add --track -b origin origin/origin` then failed, so discovery mode exited 1 in CI. Iterate full refnames (`%(refname)`) and strip the full `refs/remotes/origin/` prefix instead; refs/remotes/origin/HEAD is stable across git versions, so the symref is excluded everywhere. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/cdd-worktree.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/cdd-worktree.sh b/tools/cdd-worktree.sh index 1af3b4a..723607e 100755 --- a/tools/cdd-worktree.sh +++ b/tools/cdd-worktree.sh @@ -330,13 +330,17 @@ cdd-worktree-resume() { have_gh=1 fi + # Iterate full refnames and strip the full prefix: the short form of the + # origin/HEAD symref is "origin/HEAD" on older git but just "origin" on + # newer git, which would slip past a "$rb" == HEAD check and become a bogus + # candidate. The full refname (refs/remotes/origin/HEAD) is stable. local candidates=() rb while IFS= read -r rb; do - rb="${rb#origin/}" + rb="${rb#refs/remotes/origin/}" [[ "$rb" == "HEAD" || "$rb" == "$default_branch" ]] && continue grep -qx "$rb" <<<"$worktree_branches" && continue candidates+=("$rb") - done < <(git for-each-ref --format='%(refname:short)' refs/remotes/origin 2>/dev/null) + done < <(git for-each-ref --format='%(refname)' refs/remotes/origin 2>/dev/null) if (( ${#candidates[@]} == 0 )); then echo "No remote feature branches to resume (all are local worktrees or none exist)." >&2