From 9d5fa39c0456989b535fb9f48e359af3ffc8a40f Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Sat, 4 Jul 2026 14:52:39 +0200 Subject: [PATCH 1/3] Fix cdd-worktree-done stranding the caller via the PATH shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cdd-worktree-done (and cdd-worktree/-resume) `cd` the caller's shell, so they only work as sourced functions. The install shims dispatched into a subshell, whose `cd` can't reach the parent — when the command resolved to the shim (function not loaded, e.g. a commented-out rc block) the caller was silently left in the just-removed worktree while the shim printed success. - Ship a refuse-shim for the three cwd-changing commands: reaching it means the function isn't loaded, so it fails loudly with guidance instead of dispatching. cdd-worktree-list keeps its source-and-dispatch shim (it changes no cwd). - Make install self-repairing: key the idempotency guard on the active source line, not a bare marker match, so a present-but-disabled (commented-out) block is rewritten as an active one instead of skipped. Same fix in cdd-state.sh. - Fix the shim-loop comment that omitted cdd-worktree-done from the cwd-sensitive commands. - Extend install-smoke-assert.sh: disabled block is repaired on re-install (both helpers), and a cwd-changing shim fails loudly rather than silently no-op'ing. - Document the refuse-shim behavior and self-repair in process doc §2.8. Co-Authored-By: Claude Opus 4.8 --- .../claude-driven-development.md | 2 +- scripts/install-smoke-assert.sh | 62 ++++++++++++++- tools/cdd-state.sh | 30 ++++++-- tools/cdd-worktree.sh | 76 ++++++++++++++----- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/doc/knowledge_base/claude-driven-development.md b/doc/knowledge_base/claude-driven-development.md index d329c74..bd93c82 100644 --- a/doc/knowledge_base/claude-driven-development.md +++ b/doc/knowledge_base/claude-driven-development.md @@ -136,7 +136,7 @@ A single, project-independent bash helper provides four commands. It is the same - `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 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. +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 self-repairing** — see below), drops thin PATH shims into `~/.local/bin`, 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". A shim is only ever reached when the function is *not* loaded in the caller's shell (interactive shells prefer the sourced function, since functions shadow PATH). That distinction is load-bearing for the three commands that `cd` the **caller's** shell — `cdd-worktree` and `cdd-worktree-resume` (into the new worktree) and `cdd-worktree-done` (back to main): a shim runs in a subshell, which cannot change its parent's cwd, so dispatching from it would silently strand the caller in the wrong (or, for `-done`, the just-removed) directory. These three therefore ship a shim that **fails loudly** — it tells the user to source the helper (or open a new shell) and refuses — instead of dispatching; only `cdd-worktree-list`, which changes no cwd, keeps a real source-and-dispatch shim (as does `cdd-state`, §2.13). The self-repair completes the guarantee: because a bare marker match can't tell an active managed block from one a user disabled by commenting it out, `install` keys on the *active source line* — a present-but-disabled block is rewritten as an active one rather than skipped — so re-running `install` always restores a working interactive setup. After installing, the commands work in every CDD project — including ones bootstrapped later — without any further per-project setup. On a machine without the CDD repo checked out (a fresh clone of only a downstream project), the same one-time install is a single command — fetched to disk first, since `curl … | bash` can't work (the installer copies itself from its own file path): diff --git a/scripts/install-smoke-assert.sh b/scripts/install-smoke-assert.sh index 8253282..f670513 100755 --- a/scripts/install-smoke-assert.sh +++ b/scripts/install-smoke-assert.sh @@ -28,6 +28,19 @@ STATE_HELPER="$REPO_ROOT/tools/cdd-state.sh" fail() { echo "FAIL: $*" >&2; exit 1; } pass() { echo "ok: $*"; } +# Disable a managed rc block by prefixing every line from BEGIN to END with "# ", +# simulating a user (or tool) commenting it out — the state `install` must detect +# and self-repair. Matches markers by substring so it works on active blocks. +comment_block() { + local file="$1" begin="$2" end="$3" tmp + tmp="$(mktemp "${file}.XXXXXX")" + awk -v b="$begin" -v e="$end" ' + index($0, b) { skip = 1 } + skip { print "# " $0; if (index($0, e)) skip = 0; next } + { print } + ' "$file" > "$tmp" && mv -f "$tmp" "$file" +} + [[ -x "$HELPER" ]] || fail "helper not found/executable: $HELPER" [[ -x "$STATE_HELPER" ]] || fail "state helper not found/executable: $STATE_HELPER" @@ -68,11 +81,24 @@ pass "cdd-worktree* PATH shims written to ~/.local/bin and executable" # The shim must actually resolve and dispatch from a non-interactive shell with # only ~/.local/bin on PATH and no rc sourced — the exact case it exists for. -# `cdd-worktree-list` is side-effect-free, so use it as the probe. +# `cdd-worktree-list` is side-effect-free AND changes no cwd, so it keeps a real +# source+dispatch shim; use it as the probe. env -i HOME="$FAKE_HOME" PATH="$FAKE_HOME/.local/bin:/usr/bin:/bin" \ bash -c 'command -v cdd-worktree-list >/dev/null && cdd-worktree-list >/dev/null 2>&1' \ || fail "cdd-worktree-list shim did not resolve/dispatch in a non-interactive shell" -pass "cdd-worktree shim resolves and dispatches non-interactively" +pass "cdd-worktree-list shim resolves and dispatches non-interactively" + +# The cwd-changing commands (cdd-worktree, cdd-worktree-resume, cdd-worktree-done) +# must FAIL LOUDLY via the shim rather than dispatch into a subshell whose `cd` +# can't reach the caller — the regression that stranded the user in the removed +# worktree. Probe cdd-worktree-done (the regression subject): the shim exits +# before sourcing anything, so no git state is needed. +done_out="$(env -i HOME="$FAKE_HOME" PATH="$FAKE_HOME/.local/bin:/usr/bin:/bin" \ + bash -c 'cdd-worktree-done' 2>&1)" && \ + fail "cdd-worktree-done shim succeeded silently (should refuse when unsourced)" +grep -qF "must run as a sourced shell function" <<<"$done_out" \ + || fail "cdd-worktree-done shim did not print the sourced-function guidance; got: $done_out" +pass "cwd-changing shim (cdd-worktree-done) fails loudly instead of silently no-op'ing the cd" [[ -f "$FAKE_HOME/.cdd/handoffs/someproj/feature_x.md" ]] \ || fail "legacy handoff not migrated to ~/.cdd/handoffs/" @@ -86,6 +112,23 @@ markers=$(grep -cF "CDD worktree helper (managed by cdd-worktree.sh install) BEG [[ "$markers" -eq 1 ]] || fail "second install duplicated the marker block (found $markers)" pass "second install is idempotent (no duplicate marker block)" +# Self-repair: a managed block that is present but DISABLED (commented out) must be +# rewritten as an active source line on re-install — the case a bare marker grep +# could not see, which left a user whose block got commented unable to re-enable it. +WT_BEGIN="# --- CDD worktree helper (managed by cdd-worktree.sh install) BEGIN ---" +WT_END="# --- CDD worktree helper END ---" +# shellcheck disable=SC2016 +WT_ACTIVE='^[[:space:]]*\[\[ -f "\$HOME/\.cdd/tools/cdd-worktree\.sh" \]\] && source' +comment_block "$FAKE_HOME/.bashrc" "$WT_BEGIN" "$WT_END" +grep -qE "$WT_ACTIVE" "$FAKE_HOME/.bashrc" \ + && fail "precondition: block still active after comment_block" +HOME="$FAKE_HOME" "$HELPER" install >/dev/null +grep -qE "$WT_ACTIVE" "$FAKE_HOME/.bashrc" \ + || fail "install did not self-repair a disabled worktree block to an active source line" +markers=$(grep -cF "CDD worktree helper (managed by cdd-worktree.sh install) BEGIN" "$FAKE_HOME/.bashrc") +[[ "$markers" -eq 1 ]] || fail "self-repair left more than one worktree marker block (found $markers)" +pass "install self-repairs a disabled worktree block (active again, still single)" + # The task-state helper self-installs identically; assert its shim contract too. HOME="$FAKE_HOME" "$STATE_HELPER" install >/dev/null STATE_SHIM="$FAKE_HOME/.local/bin/cdd-state" @@ -98,4 +141,19 @@ resolved=$(env -i HOME="$FAKE_HOME" PATH="$FAKE_HOME/.local/bin:/usr/bin:/bin" \ [[ "$resolved" == "$STATE_SHIM" ]] || fail "cdd-state resolved to '$resolved', expected the shim $STATE_SHIM" pass "cdd-state PATH shim written and resolves non-interactively" +# The cdd-state installer shares the self-repair guard; assert it too. +ST_BEGIN="# --- CDD state helper (managed by cdd-state.sh install) BEGIN ---" +ST_END="# --- CDD state helper END ---" +# shellcheck disable=SC2016 +ST_ACTIVE='^[[:space:]]*\[\[ -f "\$HOME/\.cdd/tools/cdd-state\.sh" \]\] && source' +comment_block "$FAKE_HOME/.bashrc" "$ST_BEGIN" "$ST_END" +grep -qE "$ST_ACTIVE" "$FAKE_HOME/.bashrc" \ + && fail "precondition: state block still active after comment_block" +HOME="$FAKE_HOME" "$STATE_HELPER" install >/dev/null +grep -qE "$ST_ACTIVE" "$FAKE_HOME/.bashrc" \ + || fail "install did not self-repair a disabled state block to an active source line" +markers=$(grep -cF "CDD state helper (managed by cdd-state.sh install) BEGIN" "$FAKE_HOME/.bashrc") +[[ "$markers" -eq 1 ]] || fail "self-repair left more than one state marker block (found $markers)" +pass "install self-repairs a disabled state block (active again, still single)" + echo "all install smoke checks passed" diff --git a/tools/cdd-state.sh b/tools/cdd-state.sh index 7148c20..607b165 100755 --- a/tools/cdd-state.sh +++ b/tools/cdd-state.sh @@ -160,6 +160,13 @@ cdd-state-install() { local marker_begin="# --- CDD state helper (managed by cdd-state.sh install) BEGIN ---" local marker_end="# --- CDD state helper END ---" + # An ACTIVE block is one whose source line is executable (not commented out). + # Match it anchored to line start allowing only leading whitespace: a commented + # line begins with '#', so it can never match. This lets `install` self-repair a + # block a user (or tool) disabled by commenting, which a bare marker grep can't + # tell apart from an active one. + # shellcheck disable=SC2016 + local active_re='^[[:space:]]*\[\[ -f "\$HOME/\.cdd/tools/cdd-state\.sh" \]\] && source' local rc rcs=() [[ -f "$HOME/.bashrc" ]] && rcs+=("$HOME/.bashrc") [[ -f "$HOME/.zshrc" ]] && rcs+=("$HOME/.zshrc") @@ -168,17 +175,30 @@ cdd-state-install() { rcs+=("$HOME/.bashrc") fi for rc in "${rcs[@]}"; do - if grep -qF "$marker_begin" "$rc" 2>/dev/null; then + if grep -qE "$active_re" "$rc" 2>/dev/null; then echo "Already wired: $rc (skipped)" - else - cat >> "$rc" </dev/null; then + # Present but inactive (commented/mangled): strip it, then re-append below. + # index() matches the marker substring even when the line is commented. + local tmp + tmp="$(mktemp "${rc}.XXXXXX")" || return 1 + awk -v b="$marker_begin" -v e="$marker_end" ' + index($0, b) { skip = 1 } + skip && index($0, e) { skip = 0; next } + skip { next } + { print } + ' "$rc" > "$tmp" && mv -f "$tmp" "$rc" + echo "Repaired disabled CDD block in $rc" + fi + cat >> "$rc" </dev/null; then + if grep -qE "$active_re" "$rc" 2>/dev/null; then echo "Already wired: $rc (skipped)" - else - cat >> "$rc" </dev/null; then + # A managed block is present but inactive (commented out or otherwise + # mangled). Strip it entirely, then re-append a fresh active block below. + # index() matches the marker substring even when the line is commented + # (e.g. "## # --- … BEGIN ---" still contains "# --- … BEGIN ---"). + local tmp + tmp="$(mktemp "${rc}.XXXXXX")" || return 1 + awk -v b="$marker_begin" -v e="$marker_end" ' + index($0, b) { skip = 1 } + skip && index($0, e) { skip = 0; next } + skip { next } + { print } + ' "$rc" > "$tmp" && mv -f "$tmp" "$rc" + echo "Repaired disabled CDD block in $rc" + fi + cat >> "$rc" < "$bin_dir/$cmd" <&2 +echo "It changes your shell's working directory, which a subshell cannot do." >&2 +echo "Fix: open a new shell, or 'source ~/.cdd/tools/cdd-worktree.sh', then re-run." >&2 +echo "If your shell rc CDD block was disabled: bash ~/.cdd/tools/cdd-worktree.sh install" >&2 +exit 1 SHIM chmod +x "$bin_dir/$cmd" done - echo "Installed PATH shims in $bin_dir: cdd-worktree, cdd-worktree-resume, cdd-worktree-list, cdd-worktree-done" + cat > "$bin_dir/cdd-worktree-list" <<'SHIM' +#!/usr/bin/env bash +# Managed by cdd-worktree.sh install — PATH entry point so this command resolves +# in non-interactive shells too. Regenerated on each install; do not hand-edit. +source "$HOME/.cdd/tools/cdd-worktree.sh" +cdd-worktree-list "$@" +SHIM + chmod +x "$bin_dir/cdd-worktree-list" + echo "Installed PATH shims in $bin_dir: cdd-worktree-list (dispatch); cdd-worktree, cdd-worktree-resume, cdd-worktree-done (refuse-if-unsourced)" case ":$PATH:" in *":$bin_dir:"*) ;; *) echo "Note: $bin_dir is not on your PATH; add it so the cdd-worktree* commands resolve everywhere." >&2 ;; From 1b7607a84a47d2ecb00667e445a96915ab1ad0bb Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Sat, 4 Jul 2026 15:28:35 +0200 Subject: [PATCH 2/3] Record worktree-shim strand-caller fix + rc self-repair on the roadmap Adds a checked Phase 8 bug-fix entry for the cwd-changing shim regression and the installer self-repair, reconciling the roadmap with what landed in this branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- doc/knowledge_base/roadmap.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/knowledge_base/roadmap.md b/doc/knowledge_base/roadmap.md index 9325caf..1e56998 100644 --- a/doc/knowledge_base/roadmap.md +++ b/doc/knowledge_base/roadmap.md @@ -108,6 +108,7 @@ Make the per-task session loop nicer to drive once a project is already on CDD. - [x] Auto-commit at workflow gates: the implementation session and `/cdd-pre-pr` commit their own changes automatically (local, no push) in a non-disruptive way; `/cdd-process-pr` and `/cdd-merge-main` reviewed to fit one shared commit convention. — issue #20. Process doc §2.11 (new) + §3.3/§3.5/§3.7/§4 + both `cdd-pre-pr.md` and `cdd-next-step.md` copies + both CLAUDE.md `/cdd-pre-pr` bullets. - [x] Prefix every CDD slash command with `cdd-` (`/cdd-next-step`, `/cdd-pre-pr`, `/cdd-merge-main`, `/cdd-process-pr`, `/cdd-bootstrap`, `/cdd-retrofit`, `/cdd-quick-create`) so they autocomplete as a discoverable group. — issue #27. Renamed all 11 command files (7 repo + 4 template), swept every cross-reference across the process doc, template, demo, and docs, and updated `scripts/command-drift-whitelist.txt` + `scripts/command-drift-check.sh`. Scope was slash commands only; the worktree helper was unified separately (next item). - [x] Unify the worktree helper into a single, self-installing, project-independent `cdd-worktree` — issues #24 + #18. Deleted the per-project `template/tools/PROJECT-worktree.sh`; the canonical `tools/cdd-worktree.sh` is now dual-mode (sourced → defines the functions; `install` → copies itself to `~/.cdd/tools/`, wires `~/.bashrc` + `~/.zshrc` idempotently, migrates old handoffs). Handoffs moved `~/.claude-handoffs//` → `~/.cdd/handoffs//`. Collapsed the placeholder model three → two (`` and the bare `PROJECT` token removed; `--slug` dropped from the bootstrap script). Demo installs the shared helper once instead of per-instance rc blocks; drift/smoke checks no longer compare a rendered helper. — process doc §2.6/§2.8/§2.9 + bootstrap script + both settings.json + both `cdd-next-step.md` + `cdd-bootstrap.md`/`cdd-retrofit.md`/`cdd-pre-pr.md` + template/BOOTSTRAP.md + both CLAUDE.md + README + demo subsystem + scripts + CI. +- [x] **Worktree PATH shims stranded the caller for cwd-changing commands.** Fixed: the `~/.local/bin` shims for `cdd-worktree`, `cdd-worktree-resume`, and `cdd-worktree-done` now **refuse loudly** instead of `source`-and-dispatch — a shim runs in a subshell, so its `cd` can't reach the caller's shell, which left `cdd-worktree-done` silently stranding the user in the just-removed worktree while printing success. Only `cdd-worktree-list` (changes no cwd) keeps a real dispatch shim. Also made both installers **self-repair**: "already wired" now keys on the *active* source line (regex anchored to line-start, so a commented-out block can't match) rather than a bare marker grep, so re-running `install` re-enables a disabled rc block instead of skipping it. — `tools/cdd-worktree.sh` + `tools/cdd-state.sh` + process doc §2.8 + `scripts/install-smoke-assert.sh` (refuse-loudly + self-repair coverage). **Milestone:** starting an off-roadmap task — typed or sourced from a GitHub issue — is a first-class, structured `/cdd-next-step` flow. From b39c416dd9532eedca3b084c235b7604a6075299 Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Sat, 4 Jul 2026 17:02:01 +0200 Subject: [PATCH 3/3] Trim over-detailed roadmap entry, process-doc paragraph, and shell comments (PR #46 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on PR #46: - roadmap.md: collapse the verbose worktree-shim fix entry to a one-liner. - claude-driven-development.md §2.8: trim the worktree-helper paragraph back to a high-level description, dropping the shim internals this PR had added. - cdd-worktree.sh / cdd-state.sh: shorten the rc self-repair and PATH-shim comment blocks, keeping the load-bearing "why" without the repetition. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../claude-driven-development.md | 2 +- doc/knowledge_base/roadmap.md | 2 +- tools/cdd-state.sh | 8 ++-- tools/cdd-worktree.sh | 43 +++++++------------ 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/doc/knowledge_base/claude-driven-development.md b/doc/knowledge_base/claude-driven-development.md index bd93c82..da99003 100644 --- a/doc/knowledge_base/claude-driven-development.md +++ b/doc/knowledge_base/claude-driven-development.md @@ -136,7 +136,7 @@ A single, project-independent bash helper provides four commands. It is the same - `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 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 **and self-repairing** — see below), drops thin PATH shims into `~/.local/bin`, 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". A shim is only ever reached when the function is *not* loaded in the caller's shell (interactive shells prefer the sourced function, since functions shadow PATH). That distinction is load-bearing for the three commands that `cd` the **caller's** shell — `cdd-worktree` and `cdd-worktree-resume` (into the new worktree) and `cdd-worktree-done` (back to main): a shim runs in a subshell, which cannot change its parent's cwd, so dispatching from it would silently strand the caller in the wrong (or, for `-done`, the just-removed) directory. These three therefore ship a shim that **fails loudly** — it tells the user to source the helper (or open a new shell) and refuses — instead of dispatching; only `cdd-worktree-list`, which changes no cwd, keeps a real source-and-dispatch shim (as does `cdd-state`, §2.13). The self-repair completes the guarantee: because a bare marker match can't tell an active managed block from one a user disabled by commenting it out, `install` keys on the *active source line* — a present-but-disabled block is rewritten as an active one rather than skipped — so re-running `install` always restores a working interactive setup. After installing, the commands work in every CDD project — including ones bootstrapped later — without any further per-project setup. +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 self-repairing — re-running `install` re-enables a disabled block rather than skipping it), drops thin PATH shims into `~/.local/bin`, and migrates any handoffs from the legacy `~/.claude-handoffs/` location. The PATH shims exist because the rc `source` line only reaches interactive shells, so a command run from a non-interactive shell (notably Claude Code's Bash tool) would otherwise be "command not found". The three commands that `cd` the caller's shell (`cdd-worktree`, `cdd-worktree-resume`, `cdd-worktree-done`) ship a shim that fails loudly rather than dispatch, because a shim runs in a subshell and cannot change its parent's cwd; only `cdd-worktree-list` (and `cdd-state`, §2.13), which change no cwd, keep a dispatching shim. After installing, the commands work in every CDD project — including ones bootstrapped later — without any further per-project setup. On a machine without the CDD repo checked out (a fresh clone of only a downstream project), the same one-time install is a single command — fetched to disk first, since `curl … | bash` can't work (the installer copies itself from its own file path): diff --git a/doc/knowledge_base/roadmap.md b/doc/knowledge_base/roadmap.md index 1e56998..19b7246 100644 --- a/doc/knowledge_base/roadmap.md +++ b/doc/knowledge_base/roadmap.md @@ -108,7 +108,7 @@ Make the per-task session loop nicer to drive once a project is already on CDD. - [x] Auto-commit at workflow gates: the implementation session and `/cdd-pre-pr` commit their own changes automatically (local, no push) in a non-disruptive way; `/cdd-process-pr` and `/cdd-merge-main` reviewed to fit one shared commit convention. — issue #20. Process doc §2.11 (new) + §3.3/§3.5/§3.7/§4 + both `cdd-pre-pr.md` and `cdd-next-step.md` copies + both CLAUDE.md `/cdd-pre-pr` bullets. - [x] Prefix every CDD slash command with `cdd-` (`/cdd-next-step`, `/cdd-pre-pr`, `/cdd-merge-main`, `/cdd-process-pr`, `/cdd-bootstrap`, `/cdd-retrofit`, `/cdd-quick-create`) so they autocomplete as a discoverable group. — issue #27. Renamed all 11 command files (7 repo + 4 template), swept every cross-reference across the process doc, template, demo, and docs, and updated `scripts/command-drift-whitelist.txt` + `scripts/command-drift-check.sh`. Scope was slash commands only; the worktree helper was unified separately (next item). - [x] Unify the worktree helper into a single, self-installing, project-independent `cdd-worktree` — issues #24 + #18. Deleted the per-project `template/tools/PROJECT-worktree.sh`; the canonical `tools/cdd-worktree.sh` is now dual-mode (sourced → defines the functions; `install` → copies itself to `~/.cdd/tools/`, wires `~/.bashrc` + `~/.zshrc` idempotently, migrates old handoffs). Handoffs moved `~/.claude-handoffs//` → `~/.cdd/handoffs//`. Collapsed the placeholder model three → two (`` and the bare `PROJECT` token removed; `--slug` dropped from the bootstrap script). Demo installs the shared helper once instead of per-instance rc blocks; drift/smoke checks no longer compare a rendered helper. — process doc §2.6/§2.8/§2.9 + bootstrap script + both settings.json + both `cdd-next-step.md` + `cdd-bootstrap.md`/`cdd-retrofit.md`/`cdd-pre-pr.md` + template/BOOTSTRAP.md + both CLAUDE.md + README + demo subsystem + scripts + CI. -- [x] **Worktree PATH shims stranded the caller for cwd-changing commands.** Fixed: the `~/.local/bin` shims for `cdd-worktree`, `cdd-worktree-resume`, and `cdd-worktree-done` now **refuse loudly** instead of `source`-and-dispatch — a shim runs in a subshell, so its `cd` can't reach the caller's shell, which left `cdd-worktree-done` silently stranding the user in the just-removed worktree while printing success. Only `cdd-worktree-list` (changes no cwd) keeps a real dispatch shim. Also made both installers **self-repair**: "already wired" now keys on the *active* source line (regex anchored to line-start, so a commented-out block can't match) rather than a bare marker grep, so re-running `install` re-enables a disabled rc block instead of skipping it. — `tools/cdd-worktree.sh` + `tools/cdd-state.sh` + process doc §2.8 + `scripts/install-smoke-assert.sh` (refuse-loudly + self-repair coverage). +- [x] Fix the worktree PATH shims stranding the caller for cwd-changing commands, and make `install` self-repair a disabled rc block. — `tools/cdd-worktree.sh` + `tools/cdd-state.sh` + process doc §2.8 + `scripts/install-smoke-assert.sh`. **Milestone:** starting an off-roadmap task — typed or sourced from a GitHub issue — is a first-class, structured `/cdd-next-step` flow. diff --git a/tools/cdd-state.sh b/tools/cdd-state.sh index 607b165..3989b58 100755 --- a/tools/cdd-state.sh +++ b/tools/cdd-state.sh @@ -160,11 +160,9 @@ cdd-state-install() { local marker_begin="# --- CDD state helper (managed by cdd-state.sh install) BEGIN ---" local marker_end="# --- CDD state helper END ---" - # An ACTIVE block is one whose source line is executable (not commented out). - # Match it anchored to line start allowing only leading whitespace: a commented - # line begins with '#', so it can never match. This lets `install` self-repair a - # block a user (or tool) disabled by commenting, which a bare marker grep can't - # tell apart from an active one. + # Match the ACTIVE source line (anchored to line start, so a commented-out + # copy can't match) rather than the bare marker, so `install` can self-repair + # a block disabled by commenting. # shellcheck disable=SC2016 local active_re='^[[:space:]]*\[\[ -f "\$HOME/\.cdd/tools/cdd-state\.sh" \]\] && source' local rc rcs=() diff --git a/tools/cdd-worktree.sh b/tools/cdd-worktree.sh index 642076f..76c445d 100755 --- a/tools/cdd-worktree.sh +++ b/tools/cdd-worktree.sh @@ -437,11 +437,9 @@ cdd-worktree-install() { # is always at least one entry point. local marker_begin="# --- CDD worktree helper (managed by cdd-worktree.sh install) BEGIN ---" local marker_end="# --- CDD worktree helper END ---" - # An ACTIVE block is one whose source line is executable (not commented out). - # Match it anchored to line start allowing only leading whitespace: a - # commented line begins with '#', so it can never match. This distinguishes a - # live block from one a user (or tool) disabled by commenting — the case the - # old bare marker grep could not see, which left `install` unable to self-repair. + # Match the ACTIVE source line (anchored to line start, so a commented-out + # copy can't match) rather than the bare marker, so `install` can tell a live + # block from one disabled by commenting and re-enable the latter. # shellcheck disable=SC2016 local active_re='^[[:space:]]*\[\[ -f "\$HOME/\.cdd/tools/cdd-worktree\.sh" \]\] && source' local rc rcs=() @@ -457,10 +455,9 @@ cdd-worktree-install() { continue fi if grep -qF "$marker_begin" "$rc" 2>/dev/null; then - # A managed block is present but inactive (commented out or otherwise - # mangled). Strip it entirely, then re-append a fresh active block below. - # index() matches the marker substring even when the line is commented - # (e.g. "## # --- … BEGIN ---" still contains "# --- … BEGIN ---"). + # A managed block is present but inactive: strip it, then re-append a + # fresh active block below. index() matches the marker even when the + # line is commented ("## # --- … BEGIN ---" still contains the marker). local tmp tmp="$(mktemp "${rc}.XXXXXX")" || return 1 awk -v b="$marker_begin" -v e="$marker_end" ' @@ -480,31 +477,21 @@ RCBLOCK echo "Wired: $rc" done - # Also expose the cdd-worktree* commands as executables on PATH. The rc - # `source` line above only reaches INTERACTIVE shells (a stock ~/.bashrc - # returns early for non-interactive shells via its `case $- in *i*` guard), so - # without a PATH entry a command is "command not found" in a non-interactive - # shell (e.g. Claude Code's Bash tool). Interactive shells always prefer the - # sourced function (functions shadow PATH), so a shim is only ever reached when - # the function is NOT loaded in the caller's shell. - # - # Three of the commands — cdd-worktree, cdd-worktree-resume, cdd-worktree-done — - # `cd` the CALLER's shell (into the new worktree, or back to main). A shim runs - # in a subshell, which cannot change its parent's cwd, so dispatching from the - # shim would silently strand the caller in the wrong (or, for -done, the just- - # removed) directory while printing success. These three therefore ship a shim - # that FAILS LOUDLY instead of dispatching: reaching it means the function is - # unloaded, so the only correct action is to tell the user to load it. Only - # cdd-worktree-list changes no cwd, so it keeps a real source+dispatch shim. + # Expose the commands on PATH too: the rc `source` line only reaches + # interactive shells, so a non-interactive shell (e.g. Claude Code's Bash + # tool) would otherwise get "command not found". The three cwd-changing + # commands ship a shim that FAILS LOUDLY instead of dispatching — a shim runs + # in a subshell and can't change the caller's cwd, so dispatching would + # silently strand the caller. cdd-worktree-list changes no cwd, so it keeps a + # real source+dispatch shim. local bin_dir="$HOME/.local/bin" cmd mkdir -p "$bin_dir" for cmd in cdd-worktree cdd-worktree-resume cdd-worktree-done; do cat > "$bin_dir/$cmd" <&2 echo "It changes your shell's working directory, which a subshell cannot do." >&2 echo "Fix: open a new shell, or 'source ~/.cdd/tools/cdd-worktree.sh', then re-run." >&2