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
6 changes: 6 additions & 0 deletions .github/workflows/template-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
- name: Worktree-resume smoke (local bare repo as origin)
run: ./scripts/worktree-resume-assert.sh

- name: Task-ref sync smoke (refs/cdd/<branch> round-trip, local bare origin)
run: ./scripts/ref-sync-assert.sh

- name: Worktree GC smoke (reap merged tasks, keep scoped ones; stubbed gh)
run: ./scripts/gc-assert.sh

- name: Configure git identity for the scaffold commit
run: |
git config --global user.email "ci@example.com"
Expand Down
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ bash -n demo/setup.sh demo/teardown.sh demo/lib.sh
# local bare repo as `origin`; a stubbed `claude` guards that it is never launched.
./scripts/worktree-resume-assert.sh

# Task-ref sync: seed/set on "machine A" push refs/cdd/<branch> to a local bare
# `origin`; resume on "machine B" materializes the handoff + state (most-advanced
# wins); the no-ref path still resumes cleanly.
./scripts/ref-sync-assert.sh

# Worktree GC: with a stubbed `gh`, cdd-worktree-gc reaps a merged task's local
# handoff/state + remote refs/cdd/<branch>, but keeps a scoped-but-unstarted one.
./scripts/gc-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 All @@ -69,7 +78,7 @@ rm -rf /tmp/cdd-demo-smoke
demo/setup.sh mdr_demo_99 --base /tmp/cdd-demo-smoke --local-only
```

The `template-smoke` GitHub Actions workflow runs the same checks on every PR: shellcheck, the command-set drift check, the prompt-seam check, the worktree-helper install smoke, the end-to-end smoke, and the demo seed-overlay step.
The `template-smoke` GitHub Actions workflow runs the same checks on every PR: shellcheck, the command-set drift check, the prompt-seam check, the worktree-helper install smoke, the task-ref sync smoke, the worktree GC smoke, the end-to-end smoke, and the demo seed-overlay step.

When `/cdd-pre-pr` runs in this repo, the "build / format / lint / test" gates collapse into the checks above plus a doc reconciliation pass.

Expand Down Expand Up @@ -108,4 +117,5 @@ This project uses CDD on itself. Every CDD session is a fresh context doing exac
- **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.
- **To clean up finished tasks' artifacts** (maintenance, not a session): run `cdd-worktree-gc` from the main worktree. It reaps the handoff, state record, and synced `refs/cdd/<branch>` of any task whose PR has merged — the backstop for `cdd-worktree-done` never running or its ref cleanup failing across machines. Dry-run by default; `--force` to delete.
- Keep the process doc, template, and roadmap consistent as part of every change. Process-first, then template.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,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 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:
`cdd-worktree` (and its companions `cdd-worktree-done`, `cdd-worktree-list`, `cdd-worktree-resume`, and `cdd-worktree-gc`) 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, 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`.
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 — so a task started elsewhere can be picked up to run `/cdd-process-pr`, `/cdd-merge-base`, or `/cdd-pre-pr`. The task's handoff and state record ride along too, synced through a per-task git ref (advisory — resume still works without them). `cdd-worktree-gc` periodically reaps the handoff, state record, and synced ref of tasks whose PR has merged (dry-run unless `--force`), so those artifacts don't accumulate across machines.

## Questions?

Expand Down
23 changes: 23 additions & 0 deletions doc/architecture/shell-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,26 @@ The implementation session has no command file, so its two `cdd-state set` calls
## Resume discovery (`cdd-worktree-resume`)

The no-argument discovery mode fetches with `--prune`, so remote-tracking refs for branches deleted on the remote (as GitHub does when a PR merges) drop out before the listing. What remains — the default branch plus the feature branches still live on the remote, minus those already checked out locally — is exactly the resumable set, whether or not a branch has a PR yet.

## Task-ref sync (`refs/cdd/<branch>`)

The handoff (`<branch>.md`) and state record (`<branch>.state.json`) are out-of-tree, per-user files that would otherwise stay on the machine that created them. To carry them to a machine that resumes the task, `cdd-state` pushes them to a per-task ref and `cdd-worktree-resume` fetches and materializes them. The whole path is advisory: every step is best-effort and any failure (no `origin`, offline, missing `jq`, no ref on the remote) warns and continues — a resume with no ref behaves exactly as it did before the sync existed.

**Ref layout.** One ref per task at `refs/cdd/<branch>` on `origin`. The two files are bundled into a git *tree* under stable in-tree names — `handoff.md` and `state.json`, decoupled from the branch-named on-disk files — and the tree is wrapped in a **parentless (orphan) commit** that the ref points at. This is the side-ref-of-commits pattern git uses for `refs/stash` and `refs/notes/*`; a commit (rather than a bare tree/blob ref) is chosen because commits are push/fetch's native case and avoid cross-version transport surprises, and the commit can carry a message. `git notes` was rejected because it anchors metadata to a commit and so must chase the moving branch tip; the branch-keyed ref does not.

**Push (in `cdd-state`, on `seed` and `set`).** The state helper is the transition funnel, so folding the push in there means no slash command has to remember it and it cannot drift. It sits under `cdd-state`'s existing `jq` guard, so a machine without `jq` skips the whole thing. The ref is built with plumbing only — `git hash-object -w` each file that exists, `git mktree`, `git commit-tree` — so it never touches the index or working tree of the live worktree. The commit uses a fixed `cdd`/`cdd@local` author+committer identity, so it neither depends on nor fails from an unset user git identity. The push is a force-push (`git push --force origin <commit>:refs/cdd/<branch>`): the metadata is advisory, latest-wins, so whichever machine last advanced the task holds the truth. A fresh orphan commit per push is fine precisely because of the force-push — the commit SHA is never load-bearing (the round-trip smoke asserts on file contents, not hashes). `seed` is what first lands the immutable handoff `.md`; each `set` refreshes the `.json`.

**Fetch + materialize (in `cdd-worktree-resume`, before it finishes).** After the worktree is created, it fetches `refs/cdd/<branch>` (leaving the commit at `FETCH_HEAD`) and, if present, extracts the two blobs into `~/.cdd/handoffs/<repo>/`. Extraction streams `git show FETCH_HEAD:<name>` straight to a temp file then `mv`s it into place, so bytes are preserved exactly (no command-substitution newline mangling) and the write is atomic. The two files reconcile differently:

- **Handoff `.md`** is immutable after seed, so it is materialized only when absent locally — a present local handoff is never clobbered.
- **State `.json`** follows **most-advanced-stage wins**: the `.stage` of the ref's record and the local record are mapped to their index in the lifecycle enum (least → most advanced), and the ref overwrites local only when it is strictly further along; otherwise local is kept. Absent local → take the ref; absent ref blob → keep local; unparseable stage → treated as least advanced. When `jq` is unavailable the comparison can't run, so it falls back to write-only-if-absent (never clobber). The enum order is mirrored in `cdd-worktree.sh` (`cdd-worktree-stage-index`) with `cdd-state.sh`'s `cdd-state-stages` as the source of truth — a small, deliberate duplication because the two helpers are separate self-installing files.

**Cleanup (in `cdd-worktree-done`).** When `done` deletes the branch, it also best-effort deletes `refs/cdd/<branch>` on `origin` (`git push origin --delete`), so the namespace does not accumulate. Like everything else here, a failed delete warns and never blocks the teardown.

## Garbage collection (`cdd-worktree-gc`)

`cdd-worktree-done` is the primary reaper, but it only fires when a human runs it from the worktree, and its remote-ref delete is a single best-effort attempt. Three gaps leak artifacts: `done` is never run; its ref delete fails while offline and is never retried; or a task resumed on several machines leaves a materialized handoff/state on each, while `done` (run on one machine) cleans only that one. `cdd-worktree-gc` is the periodic sweep that closes them.

**Reap predicate — merged PR only.** The tempting signal, "the branch no longer exists on `origin`", is wrong: a freshly scoped task has a handoff and a `refs/cdd/<branch>` ref *before* `cdd-worktree` ever creates the branch (`cdd-state seed` pushes the ref; the branch reaches `origin` only at first push), so it is byte-for-byte indistinguishable from a merged-and-branch-deleted task by ref or branch presence alone. The only trustworthy "done" signal is a **merged PR** — the same signal `cdd-worktree-done` trusts (`gh pr list --head <branch>`). So GC reaps a task iff its PR state is `MERGED`; anything else (open PR, or no PR yet) is treated as in-flight or scoped and left untouched. This makes `gh` a hard dependency for GC: without an authenticated `gh` it cannot tell a finished task from a fresh one, so it reaps nothing and says so.

**What it enumerates and removes.** Candidates are the union of local handoff/state basenames in `~/.cdd/handoffs/<repo>/` and the `refs/cdd/*` names from `git ls-remote origin` — so a machine that never held a task's files can still reap that task's leaked remote ref, and a machine holding orphaned local copies can reap them even after the ref is already gone. For each merged candidate it removes whichever of the three artifacts are present (local `.md`, local `.state.json`, remote ref). It is **dry-run by default** — printing `reap … would remove …` / `keep …` lines — and only deletes under `--force`. Being conservative and idempotent, it is safe to run (or schedule) routinely.
5 changes: 3 additions & 2 deletions doc/knowledge_base/claude-driven-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ A single, project-independent bash helper provides four commands — the same sc
- `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, removes the worktree, resolves the branch, 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, highlighting stale entries.
- `cdd-worktree-resume [<branch>]`, picks up a task started on another machine that has only a clone of the repo: recreates a worktree tracking an **existing remote branch**, ready for a resume-side command (`/cdd-process-pr`, `/cdd-merge-base`, `/cdd-pre-pr`); with no argument it lists the resumable remote branches. Its scope is worktree and branch recreation only: the handoff (§2.6) and state record (§2.13) stay local per machine — cross-machine *sync* of those two artifacts is separate future work (issue #22).
- `cdd-worktree-gc [--force]`, reaps the artifacts of **finished** tasks — the local handoff, the state record (§2.13), and the synced per-task ref (§2.13) — for any task whose PR has merged. It is the backstop for `cdd-worktree-done` never running, its ref cleanup failing offline, or a task resumed on several machines leaving materialized copies behind on all but the one where `done` ran. Deliberately conservative: it reaps only merged tasks (a merged PR is the sole trustworthy "done" signal — a scoped-but-unstarted task's handoff and ref exist *before* its branch does, so ref/branch presence alone cannot tell the two apart), and it is dry-run unless `--force`.
- `cdd-worktree-resume [<branch>]`, picks up a task started on another machine that has only a clone of the repo: recreates a worktree tracking an **existing remote branch**, ready for a resume-side command (`/cdd-process-pr`, `/cdd-merge-base`, `/cdd-pre-pr`); with no argument it lists the resumable remote branches. If the originating machine synced them, it also fetches and materializes the handoff (§2.6) and state record (§2.13) from a per-task ref before it finishes — advisory and best-effort, so a task with no synced ref resumes exactly as before (the resume-side commands read PR/branch state from `git`/`gh`, not the handoff). The sync mechanics live in `doc/architecture/shell-helpers.md`.

The helper installs itself once per machine (`tools/cdd-worktree.sh install`) to a stable home that does not depend on a live CDD checkout; after that the commands work in every CDD project — including ones bootstrapped later — with no per-project setup. It is a machine-global toolchain dependency, like `git` or `gh`: newest wins, install idempotent, always from latest `main` — never pinned per project, since pinning would reintroduce the very conflict a single shared helper avoids. Its contract with projects is deliberately tiny and grows only additively: the command names above plus the `~/.cdd/handoffs/<repo>/<branch>.md` layout; when that shared state must evolve, the change ships as a one-shot migration inside `install`, re-homing every project at once. The install and dispatch mechanics live in `doc/architecture/shell-helpers.md` and the script's own comments.

Expand Down Expand Up @@ -150,7 +151,7 @@ A practice moves from expected to enforced in the same change that lands its mec

A small JSON sibling of the handoff (§2.6) — same directory, same `<branch>` basename, same branch-scoped ephemeral lifecycle — recording where a task sits in its lifecycle (`stage`), its PR number once one exists, and, append-only, the chain of Claude Code sessions that have worked it, so a session can be found and resumed (`claude --resume <id>`) without grepping shell history.

The record is **advisory** and **local**: 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` seeds it); and it is a cache of work on *this* machine, not a cross-machine transfer mechanism (recreating a worktree elsewhere is `cdd-worktree-resume`, §2.8; syncing the record itself is future work, issue #22). Writes go through the `cdd-state` helper (§2.8), which keeps them atomic and well-formed and no-ops rather than failing the workflow. The slash commands call it at their stage transitions; the implementation session's calls are driven by a standing instruction in the handoff (§3.3). The schema and the stage-to-writer mapping live in `doc/architecture/shell-helpers.md`.
The record is **advisory**: a consumer that finds it missing or stale falls back to inferring state from handoffs, branches, and `gh`, and a writer that finds it missing does not fabricate one (only `/cdd-next-step` seeds it). It syncs across machines: every write also pushes the handoff and record to a per-task ref, which `cdd-worktree-resume` (§2.8) materializes on the picking-up machine — best-effort, so it degrades to purely local when there is no remote to reach. Writes go through the `cdd-state` helper (§2.8), which keeps them atomic and well-formed and no-ops rather than failing the workflow. The slash commands call it at their stage transitions; the implementation session's calls are driven by a standing instruction in the handoff (§3.3). The schema and the stage-to-writer mapping live in `doc/architecture/shell-helpers.md`.

## 3. Lifecycle

Expand Down
Loading
Loading