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
3 changes: 3 additions & 0 deletions .github/workflows/template-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`; a stubbed `claude` guards that it is never 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" \
Expand Down Expand Up @@ -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 <branch>` 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_<slug>`.
- **To pick up a task started on another machine** (resume): run `cdd-worktree-resume [<branch>]` from the main worktree. It recreates the worktree on the existing remote branch (no handoff needed) and `cd`s into it, ready for you to 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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ 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 \
--create-dirs -o ~/.cdd/tools/cdd-worktree.sh \
&& 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 [<branch>]` 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?

Expand Down
7 changes: 4 additions & 3 deletions doc/knowledge_base/claude-driven-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,12 @@ 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 <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.

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.

Expand All @@ -145,7 +146,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/<repo>/<branch>.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 and grows only additively (`cdd-worktree-resume` was added without touching the layout or any existing command): the command names above plus the `~/.cdd/handoffs/<repo>/<branch>.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 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`.

Expand Down Expand Up @@ -209,7 +210,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 `<branch>` 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 <id>`) 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 <branch>` creates the record, `cdd-state set <stage> [--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.

Expand Down
3 changes: 2 additions & 1 deletion doc/knowledge_base/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<branch>.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 [<branch>]` recreates a worktree on an existing remote branch (no handoff required, discovery mode when no branch given), ready for a resume-side command. — `tools/cdd-worktree.sh`, process doc §2.8/§2.13, `scripts/worktree-resume-assert.sh` (new) + 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 (`<branch>.md`) or state record (`<branch>.state.json`); they stay local per machine. To carry them across, the chosen direction is a per-task ref namespace `refs/cdd/<branch>` 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.
139 changes: 139 additions & 0 deletions scripts/worktree-resume-assert.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/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 <branch>` creates a sibling worktree tracking
# origin/<branch>, and does NOT launch `claude`
# - a second `cdd-worktree-resume <branch>` detects the existing worktree and
# returns 0
# - `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` on PATH guards the "never launched" assertion: the helper must leave
# the log empty.

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 as a negative guard: the helper must never invoke it, so
# any output here (a non-empty log) is a regression.
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 "resume must not launch claude"
pass "explicit resume created a tracking worktree without launching claude"

# 3. Re-running on the same branch detects the existing worktree.
: > "$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 must not launch claude"
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).
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"
Loading
Loading