From 8d36a56fe8661af412b09096f78f3b8593774915 Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Sun, 19 Jul 2026 20:23:00 +0200 Subject: [PATCH 1/3] Record the handoff session and per-session dir in seeded state records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cdd-state seed previously wrote sessions: [], so the /cdd-next-step handoff session (which seeds the record on the main worktree) was never recorded and could not be resumed; only the implementation session's first `set` landed an id. Seed now records the current session as the first {id, stage: scoped, dir} entry when CLAUDE_CODE_SESSION_ID is set (empty list otherwise — older Claude Code, don't guess), and every session entry in both seed and set carries a `dir` = the worktree root (git rev-parse --show-toplevel), the natural cd target for `claude --resume`. `dir` is additive and optional: no CDD_STATE_SCHEMA_VERSION bump, so old and new records interoperate (readers ignore unknown fields; the reader half is cdd-dash PR #10, which falls back to the row's worktree path when dir is absent). Closes the writer half of issue #51. Docs updated to match: the helper header block, process doc §2.13 (schema example, append-only paragraph, scoped table row), and a checked Phase 13 roadmap item. install-smoke-assert.sh gains a jq-guarded assertion that seed records the handoff session with its dir, and omits the entry when no session id is set. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../claude-driven-development.md | 6 ++-- doc/knowledge_base/roadmap.md | 1 + scripts/install-smoke-assert.sh | 34 ++++++++++++++++++ tools/cdd-state.sh | 35 +++++++++++++++---- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/doc/knowledge_base/claude-driven-development.md b/doc/knowledge_base/claude-driven-development.md index 9430164..b306ccc 100644 --- a/doc/knowledge_base/claude-driven-development.md +++ b/doc/knowledge_base/claude-driven-development.md @@ -222,17 +222,17 @@ Schema (`schema_version` lets consumers version their parser): "branch": "task_state_tracking", "stage": "plan_approved", "pr": null, - "sessions": [ { "id": "", "stage": "plan_approved" } ] + "sessions": [ { "id": "", "stage": "plan_approved", "dir": "" } ] } ``` -`pr` is the integer PR number once a PR exists, else `null`. `sessions` is **append-only**: each in-worktree session that advances the task appends a `{id, stage}` entry, so the chain is preserved and the last element is the most recent session. The helper takes the id from `CLAUDE_CODE_SESSION_ID` and appends only when it is non-empty and differs from the last entry's `id` (dedups repeated writes within one session); if the variable is unset — older Claude Code — it omits the entry rather than guessing. A consumer derives the resume command as `claude --resume `. +`pr` is the integer PR number once a PR exists, else `null`. `sessions` is **append-only**: each session that works the task appends a `{id, stage, dir}` entry, so the chain is preserved and the last element is the most recent session. `dir` is the worktree root the session ran in (`git rev-parse --show-toplevel`) — the natural `cd` target before `claude --resume`; it is the handoff session's main worktree for the seeded entry and each in-worktree session's own worktree thereafter. `dir` is additive and optional: it is not versioned by `schema_version`, and a consumer that finds it absent falls back to the branch's known worktree path. Seeding records the **handoff session** as the first entry — so the `/cdd-next-step` session that scoped the task is resumable too — rather than starting from an empty list; the append that follows begins with the implementation session. The helper takes the id from `CLAUDE_CODE_SESSION_ID` and records/appends only when it is non-empty and differs from the last entry's `id` (dedups repeated writes within one session); if the variable is unset — older Claude Code — it omits the entry rather than guessing (seed then leaves `sessions: []`). A consumer derives the resume command as `claude --resume `. `stage` is a single enum (no separate status); each transition and its writer: | `stage` | written by | | --------------------- | --------------------------------------------------- | -| `scoped` | `/cdd-next-step` — seeds the record (`sessions: []`; it runs on a different session, on the default branch) | +| `scoped` | `/cdd-next-step` — seeds the record and records itself as the first session `{id, stage: scoped, dir}` (empty `sessions` only when no session id is available); it runs on a different session, on the default branch | | `plan_approved` | implementation session — on plan approval, before any code | | `implementation_done` | implementation session — after its local commit | | `merged` | `/cdd-merge-base` — after a successful merge | diff --git a/doc/knowledge_base/roadmap.md b/doc/knowledge_base/roadmap.md index 780a74e..e509468 100644 --- a/doc/knowledge_base/roadmap.md +++ b/doc/knowledge_base/roadmap.md @@ -175,6 +175,7 @@ Prepare CDD to be open-sourced publicly: license it, rewrite the README to expla Give each task a machine-readable record of where it sits in its lifecycle and which Claude Code sessions have worked it, so tooling can show task state instead of inferring it from handoffs, branches, and `gh`. - [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. +- [x] Record the handoff session (and per-session working dir) in seeded records (issue #51, writer half; reader half is cdd-dash PR #10): `cdd-state seed` now records the `/cdd-next-step` handoff session as the first `sessions[]` entry so it is resumable, and every session entry carries a `dir` (the worktree root, the `cd` target for `claude --resume`). Additive/optional — no `schema_version` bump; old and new records interoperate. — `tools/cdd-state.sh` (seed + set), process doc §2.13, `scripts/install-smoke-assert.sh`. - [ ] 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.) - [x] Multi-machine resume — worktree + branch (issue #22): `cdd-worktree-resume []` 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. diff --git a/scripts/install-smoke-assert.sh b/scripts/install-smoke-assert.sh index f670513..3fbe60a 100755 --- a/scripts/install-smoke-assert.sh +++ b/scripts/install-smoke-assert.sh @@ -156,4 +156,38 @@ markers=$(grep -cF "CDD state helper (managed by cdd-state.sh install) BEGIN" "$ [[ "$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)" +# `cdd-state seed` must record the handoff session (issue #51): with a session id +# in the environment, the seeded record's first `sessions[]` entry is the current +# session at stage `scoped`, carrying `dir` = the worktree root. Run it from this +# repo (a real git repo) against FAKE_HOME so the record lands under the temp tree. +# Guarded on jq, like the helper itself. +if command -v jq >/dev/null 2>&1; then + SEED_BRANCH="issue51_seed_probe" + # The record path uses the repo name derived from git's common-dir (the main + # worktree), not this checkout's basename — mirror the helper's derivation. + REPO_NAME="$(cd "$REPO_ROOT" && basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")")" + SEED_FILE="$FAKE_HOME/.cdd/handoffs/$REPO_NAME/$SEED_BRANCH.state.json" + EXPECT_DIR="$(cd "$REPO_ROOT" && git rev-parse --show-toplevel)" + # The helper is dual-mode: executed directly it only installs, so source it and + # call the function (the same path the PATH shim takes) to reach `seed`. + ( cd "$REPO_ROOT" \ + && HOME="$FAKE_HOME" CLAUDE_CODE_SESSION_ID=seed-probe-123 \ + bash -c 'source "$1"; cdd-state seed "$2"' _ "$STATE_HELPER" "$SEED_BRANCH" >/dev/null ) + [[ -f "$SEED_FILE" ]] || fail "seed did not write $SEED_FILE" + got="$(jq -r '.sessions[0] | "\(.id)|\(.stage)|\(.dir)"' "$SEED_FILE")" + [[ "$got" == "seed-probe-123|scoped|$EXPECT_DIR" ]] \ + || fail "seed session entry = '$got', expected 'seed-probe-123|scoped|$EXPECT_DIR'" + pass "cdd-state seed records the handoff session {id, stage: scoped, dir}" + + # Without a session id (older Claude Code), seed keeps sessions empty — no guessing. + ( cd "$REPO_ROOT" \ + && HOME="$FAKE_HOME" CLAUDE_CODE_SESSION_ID='' \ + bash -c 'source "$1"; cdd-state seed "$2"' _ "$STATE_HELPER" "$SEED_BRANCH" >/dev/null ) + count="$(jq -r '.sessions | length' "$SEED_FILE")" + [[ "$count" -eq 0 ]] || fail "seed with no session id left $count session(s), expected 0" + pass "cdd-state seed omits the session entry when no session id is set" +else + echo "skip: jq not found; seed assertions skipped (advisory)" +fi + echo "all install smoke checks passed" diff --git a/tools/cdd-state.sh b/tools/cdd-state.sh index 3989b58..f8feb4d 100755 --- a/tools/cdd-state.sh +++ b/tools/cdd-state.sh @@ -25,16 +25,23 @@ # # Provides (when sourced): # cdd-state seed Create the record beside the handoff, at stage -# `scoped` with an empty `sessions`. Used by -# /cdd-next-step on the default branch. +# `scoped`. Used by /cdd-next-step on the +# default branch. Records the current (handoff) +# session as a {id, stage, dir} entry when +# $CLAUDE_CODE_SESSION_ID is set, so the handoff +# session is resumable too; otherwise seeds an +# empty `sessions` (older Claude Code — no id). # cdd-state set [--pr N] Advance an existing record to (and set # the PR number with --pr). Derives repo/branch # from the current worktree. Skips silently if # the record is absent (writers never fabricate -# one). Appends a {id, stage} entry for +# one). Appends a {id, stage, dir} entry for # $CLAUDE_CODE_SESSION_ID unless it is empty or # already the last entry's id. # +# `dir` on a session entry is the worktree root the session ran in (from +# `git rev-parse --show-toplevel`): the natural `cd` target for `claude --resume`. +# # Stages (a single enum; the record carries no separate status): # scoped plan_approved implementation_done merged checks_passed pr_open addressed @@ -78,11 +85,22 @@ cdd-state() { repo_name="$(basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")")" || return 1 dir="$HOME/.cdd/handoffs/${repo_name}" mkdir -p "$dir" + # Record the handoff session (this /cdd-next-step session, on the main + # worktree) so it is resumable too — guarded exactly like `set`'s append: + # only when CLAUDE_CODE_SESSION_ID is set (older Claude Code → empty list, + # don't guess). `dir` is the worktree root, the `cd` target for --resume. + local sid="${CLAUDE_CODE_SESSION_ID:-}" toplevel sessions='[]' + toplevel="$(git rev-parse --show-toplevel 2>/dev/null)" + if [[ -n "$sid" ]]; then + sessions="$(jq -n --arg id "$sid" --arg dir "$toplevel" \ + '[{id: $id, stage: "scoped", dir: $dir}]')" || return 1 + fi local content content="$(jq -n \ --argjson v "$CDD_STATE_SCHEMA_VERSION" \ --arg branch "$branch" \ - '{schema_version: $v, branch: $branch, stage: "scoped", pr: null, sessions: []}')" || return 1 + --argjson sessions "$sessions" \ + '{schema_version: $v, branch: $branch, stage: "scoped", pr: null, sessions: $sessions}')" || return 1 cdd-state-write "${dir}/${branch}.state.json" "$content" \ && echo "Seeded state: ${dir}/${branch}.state.json" ;; @@ -112,16 +130,19 @@ cdd-state() { local filter='.stage = $stage' [[ -n "$pr" ]] && filter="$filter | .pr = (\$pr | tonumber)" # Append this session unless CLAUDE_CODE_SESSION_ID is empty or already the - # last entry's id (dedups repeated writes within one session). - local sid="${CLAUDE_CODE_SESSION_ID:-}" + # last entry's id (dedups repeated writes within one session). `dir` is the + # worktree root this session ran in — the `cd` target for `claude --resume`. + local sid="${CLAUDE_CODE_SESSION_ID:-}" toplevel + toplevel="$(git rev-parse --show-toplevel 2>/dev/null)" if [[ -n "$sid" ]]; then - filter="$filter | if (.sessions[-1].id // \"\") == \$sid then . else .sessions += [{id: \$sid, stage: \$stage}] end" + filter="$filter | if (.sessions[-1].id // \"\") == \$sid then . else .sessions += [{id: \$sid, stage: \$stage, dir: \$dir}] end" fi local content content="$(jq \ --arg stage "$stage" \ --arg pr "$pr" \ --arg sid "$sid" \ + --arg dir "$toplevel" \ "$filter" "$file")" || { echo "cdd-state: failed to update $file" >&2; return 1; } cdd-state-write "$file" "$content" \ && echo "State: $(basename "$file") -> $stage${pr:+ (pr #$pr)}" From 412b3400f5736d9270c6f5ef8fdf2338eac4b71e Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Sun, 19 Jul 2026 21:19:53 +0200 Subject: [PATCH 2/3] adopt: port #51 dir/handoff-session detail into relocated shell-helpers.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge with origin/main (#50) relocated the state-record schema and stage-to-writer table from process-doc §2.13 into doc/architecture/shell-helpers.md. The §2.13 conflict was resolved by taking #50's condensed version; this ports the issue #51 semantics into their new home: the `dir` field on session entries, the schema note that `dir` is additive/optional, that seeding records the handoff session as the first entry, and the updated `scoped` writer row. Co-Authored-By: Claude Opus 4.8 (1M context) --- doc/architecture/shell-helpers.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/architecture/shell-helpers.md b/doc/architecture/shell-helpers.md index dc799c2..11fb706 100644 --- a/doc/architecture/shell-helpers.md +++ b/doc/architecture/shell-helpers.md @@ -31,7 +31,7 @@ Nothing project-specific is configured or copied per project: the repo name (the ## State-record writes (`cdd-state`) -Every write is atomic — rendered to a temp file in the destination directory, then `mv`'d into place — so a crashed or concurrent write cannot leave a truncated record. The session chain's ids come from `CLAUDE_CODE_SESSION_ID`: an entry is appended only when the variable is non-empty and differs from the last entry's id (deduping repeated writes within one session); when it is unset (older Claude Code), the entry is omitted rather than guessed. The helper is advisory end-to-end: absent `jq`, or an absent record, it no-ops rather than failing the workflow (writers never fabricate a record; only `/cdd-next-step` seeds one). +Every write is atomic — rendered to a temp file in the destination directory, then `mv`'d into place — so a crashed or concurrent write cannot leave a truncated record. The session chain's ids come from `CLAUDE_CODE_SESSION_ID`: an entry is appended only when the variable is non-empty and differs from the last entry's id (deduping repeated writes within one session); when it is unset (older Claude Code), the entry is omitted rather than guessed. Each entry also carries `dir`, the worktree root the session ran in (`git rev-parse --show-toplevel`) — the natural `cd` target before `claude --resume`. Seeding records the handoff session (`/cdd-next-step`, on the main worktree) as the first entry so it is resumable too, then `set` appends each in-worktree session thereafter. The helper is advisory end-to-end: absent `jq`, or an absent record, it no-ops rather than failing the workflow (writers never fabricate a record; only `/cdd-next-step` seeds one). ### Schema @@ -43,11 +43,11 @@ Every write is atomic — rendered to a temp file in the destination directory, "branch": "task_state_tracking", "stage": "plan_approved", "pr": null, - "sessions": [ { "id": "", "stage": "plan_approved" } ] + "sessions": [ { "id": "", "stage": "plan_approved", "dir": "" } ] } ``` -`pr` is the integer PR number once a PR exists, else `null`. `sessions` is append-only; the last element is the most recent session, and a consumer derives the resume command as `claude --resume `. +`pr` is the integer PR number once a PR exists, else `null`. `sessions` is append-only; the last element is the most recent session, and a consumer derives the resume command as `claude --resume `, run from `dir`. `dir` is additive and optional — not versioned by `schema_version`, so old and new records interoperate; a consumer that finds it absent falls back to the branch's known worktree path. ### Stages and writers @@ -55,7 +55,7 @@ Every write is atomic — rendered to a temp file in the destination directory, | `stage` | written by | | --------------------- | --------------------------------------------------- | -| `scoped` | `/cdd-next-step` — seeds the record (`sessions: []`; it runs on a different session, on the default branch) | +| `scoped` | `/cdd-next-step` — seeds the record and records itself as the first session `{id, stage: scoped, dir}` (empty `sessions` only when no session id is available); it runs on a different session, on the default branch | | `plan_approved` | implementation session — on plan approval, before any code | | `implementation_done` | implementation session — after its local commit | | `merged` | `/cdd-merge-base` — after a successful merge | From 1c9dbd89e3cd50b1af75aca3d798738c4e45e77e Mon Sep 17 00:00:00 2001 From: Diego Andres Rabaioli Date: Sun, 19 Jul 2026 21:32:41 +0200 Subject: [PATCH 3/3] reconcile: point state-record roadmap items at shell-helpers.md schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #50 relocated the state-record schema out of process doc §2.13 into doc/architecture/shell-helpers.md. Update the two completed roadmap items (#51 writer half, and the earlier per-task-record item) so their file trails point at where the schema actually lives. Co-Authored-By: Claude Opus 4.8 (1M context) --- doc/knowledge_base/roadmap.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/knowledge_base/roadmap.md b/doc/knowledge_base/roadmap.md index e509468..070d8d4 100644 --- a/doc/knowledge_base/roadmap.md +++ b/doc/knowledge_base/roadmap.md @@ -174,8 +174,8 @@ Prepare CDD to be open-sourced publicly: license it, rewrite the README to expla Give each task a machine-readable record of where it sits in its lifecycle and which Claude Code sessions have worked it, so tooling can show task state instead of inferring it from handoffs, branches, and `gh`. -- [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. -- [x] Record the handoff session (and per-session working dir) in seeded records (issue #51, writer half; reader half is cdd-dash PR #10): `cdd-state seed` now records the `/cdd-next-step` handoff session as the first `sessions[]` entry so it is resumable, and every session entry carries a `dir` (the worktree root, the `cd` target for `claude --resume`). Additive/optional — no `schema_version` bump; old and new records interoperate. — `tools/cdd-state.sh` (seed + set), process doc §2.13, `scripts/install-smoke-assert.sh`. +- [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 in process doc §2.13; schema in `doc/architecture/shell-helpers.md`. — §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. +- [x] Record the handoff session (and per-session working dir) in seeded records (issue #51, writer half; reader half is cdd-dash PR #10): `cdd-state seed` now records the `/cdd-next-step` handoff session as the first `sessions[]` entry so it is resumable, and every session entry carries a `dir` (the worktree root, the `cd` target for `claude --resume`). Additive/optional — no `schema_version` bump; old and new records interoperate. — `tools/cdd-state.sh` (seed + set), `doc/architecture/shell-helpers.md`, `scripts/install-smoke-assert.sh`. - [ ] 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.) - [x] Multi-machine resume — worktree + branch (issue #22): `cdd-worktree-resume []` 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.