Skip to content

T-G834-move-plugin-runtime-state-to-sdlc-dir

Status: closed/done · Impact: high · Complexity: large

The sdlc plugin currently writes its runtime state under .claude/ (worktrees, pr-cursors, orchestrator-log), but .claude/ is conventionally Claude Code’s home for settings, hooks, and session state. The plugin’s own convention doc (plugin/conventions/project-local-skill-extension.md:41-46) already establishes .sdlc/ as the SDLC plugin’s project-local runtime directory; the runtime-state paths just haven’t followed. This task completes that separation so plugin state lives where the convention already says it should, and stops crowding Claude Code’s namespace with sdlc-plugin-owned files.

LocationRole today
**.claude/worktrees//**created by the plugin’s worktree-creating skills. Referenced in: plugin/skills/task-work/SKILL.md (lines 41, 101, 128, 135, 158, 218, 617), plugin/skills/milestones-from-file/SKILL.md (lines 46, 55), plugin/skills/entities-migrate/SKILL.md (lines 74, 91, 201), plugin/skills/cross-repo-task-pr/SKILL.md (lines 97, 104, 111), plugin/skills/task-close-out/SKILL.md (lines 5, 261, 264), plugin/skills/project-cleanup/SKILL.md (line 190), plugin/skills/import-planning/SKILL.md, plugin/scripts/worktree_scope_guard.py (constant WORKTREES_DIR_REL = ".claude/worktrees" at line 58 plus docstrings/error messages at lines 12, 19, 100, 136, 160), plugin/scripts/count_inflight_tasks.py (lines 12, 65, 211, 367), plugin/conventions/worktree-scope-guard.md (lines 5, 20, 32, 36), and plugin/skills/setup/setup_planning.py (lines 129, 175, 184).
**.claude/pr-cursors/.json**written by /sdlc:pr-check to detect new review comments between ticks. Referenced in: plugin/skills/pr-check/classify_pr.py:39, plugin/skills/pr-check/invariants.yaml:46, plugin/skills/pr-check/SKILL.md, and plugin/skills/pr-check/test_classify_pr.py (lines 17, 239).
**.claude/orchestrator-log.md**appended by /sdlc:orchestrate once per tick so the user can read what happened between check-ins. Referenced in plugin/skills/orchestrate/SKILL.md (lines 10, 307, 431) and plugin/skills/orchestrate/invariants.yaml:50.

All three pieces of sdlc-plugin runtime state live under .sdlc/ at the consuming project’s root:

  • .claude/worktrees/<basename>/.sdlc/worktrees/<basename>/
  • .claude/pr-cursors/<pr>.json.sdlc/pr-cursors/<pr>.json
  • .claude/orchestrator-log.md.sdlc/orchestrator-log.md

Every plugin source artifact (SKILL.md prose, Python scripts, invariants YAML, conventions docs) references the new paths. The /sdlc:setup gitignore writer no longer emits the special-case .claude/worktrees/ ignore line — it’s redundant once worktrees move under .sdlc/, which the existing .sdlc/* block already ignores. What stays under .claude/: settings.json, hooks/, hooks.json, commands/, agents/, project-local Claude Code skills (skills/project-check/, skills/dev-update-docs/), and scripts/ — these are genuinely Claude Code’s territory, not sdlc plugin runtime state, and are out of scope.

This is a hard cut — in-flight worktrees and orchestrator state must drain first, then the refactor lands as one PR. No backward-compat read-from-both shim, no automatic migration script for consuming projects (the convention is “upgrade plugin = drain your state first”).

  1. Drain in-flight state in this repo before opening the PR. Confirm .claude/worktrees/ is empty (all in-flight task PRs merged or aborted via /sdlc:task-close-out or /sdlc:project-cleanup), .claude/pr-cursors/ is empty or its contents are acceptable to discard (cursors regenerate on the next pr-check tick), and .claude/orchestrator-log.md has been archived if its history matters (it’s append-only narrative for the operator, not load-bearing for any skill).

  2. Update worktree_scope_guard.py. Change WORKTREES_DIR_REL = ".claude/worktrees" to ".sdlc/worktrees" at line 58. Update the docstrings at lines 12, 19, 100, 136 and the error-message paths in _format_violation (line 160) to match.

  3. Update count_inflight_tasks.py. Replace the four .claude/worktrees references at lines 12, 65, 211, 367 (docstrings, JSON shape example, scan-target string, --project-root argparse help) with .sdlc/worktrees. Verify the scan logic still works against the new path.

  4. Update pr-check. Change the cursor directory in classify_pr.py:39 from <project-root>/.claude/pr-cursors/ to <project-root>/.sdlc/pr-cursors/. Update test_classify_pr.py:17 and :239 plus plugin/skills/pr-check/invariants.yaml:46 so the invariant phrase matches the new path. Sweep pr-check/SKILL.md for any prose reference.

  5. Update orchestrate. Change plugin/skills/orchestrate/SKILL.md (lines 10, 307, 431) and plugin/skills/orchestrate/invariants.yaml:50 from .claude/orchestrator-log.md to .sdlc/orchestrator-log.md.

  6. Update setup_planning.py (the gitignore writer). Remove the .claude/worktrees/ ignore line at line 129 and the trailing-line handling at lines 175 and 184 — they’re moot once worktrees move under .sdlc/, which .sdlc/* already ignores. Update the explanatory comments at lines 113-126. Update plugin/skills/setup/tests/run_evals.py fixtures and expectations (lines 67-96, 206) to match the new gitignore shape.

  7. Sweep every SKILL.md prose reference. Replace .claude/worktrees/ with .sdlc/worktrees/ across: task-work/SKILL.md, task-close-out/SKILL.md, milestones-from-file/SKILL.md, entities-migrate/SKILL.md (including the git worktree add example at line 74 and the --project-root argument at line 91), cross-repo-task-pr/SKILL.md, project-cleanup/SKILL.md, import-planning/SKILL.md.

  8. Update conventions docs. plugin/conventions/worktree-scope-guard.md (4 path references). Re-read plugin/conventions/project-local-skill-extension.md to confirm its prose is already correct (no edits expected).

  9. Update this repo’s .gitignore. Drop the .claude/worktrees/ carve-out line (no longer needed once worktrees move). The existing .sdlc/* block already ignores the new worktree location. Leave a one-line comment in .gitignore noting the line was removed in this task so future readers don’t add it back.

  10. Run the full test suite. plugin/skills/pr-check/test_classify_pr.py, plugin/skills/setup/tests/run_evals.py, plugin/skills/entities-migrate/tests/, plus any plugin-wide invariants runner. Every test that pinned the old path now points at the new one.

  11. Final grep. Run grep -rn -E "\.claude/(worktrees|pr-cursors|orchestrator-log)" plugin/ .gitignore and confirm zero hits.

  12. Refresh docs site. Run /sdlc:dev-update-docs to surface any drift in the generated reference pages under site/ and resolve.

  13. Move the in-flight worktree itself. After all path edits are committed AND quality checks pass, but BEFORE the sync/push that opens the PR, relocate the worktree this task is being implemented in so the PR opens against a fully-drained .claude/worktrees/ (AC-8). From the main repo:

    git -C <main-repo> worktree move \
    .claude/worktrees/2026-05-22-move-plugin-runtime-state-to-sdlc-dir \
    .sdlc/worktrees/2026-05-22-move-plugin-runtime-state-to-sdlc-dir

    This step is exempt from AC-8’s “drain before PR opens” constraint because it satisfies AC-8 — the in-flight worktree itself moves to the new layout, so the PR opens against .claude/worktrees/ being empty.

  14. Ship the per-project migration tooling. Build two new skills under plugin/skills/: /sdlc:migrate-runtime-state (one-shot mover for the three runtime-state buckets: worktrees, pr-cursors, orchestrator-log) and /sdlc:migrate (umbrella that sequences /sdlc:entities-migrate then /sdlc:migrate-runtime-state). The runtime-state mover operates against the main repo directly (no self-worktree, because the things being moved ARE worktrees) and emits a deterministic stdout marker (MIGRATED ... / NOTHING-TO-MIGRATE / DRY-RUN ... / ERROR ...). Also patch the /sdlc:setup writer to mkdir .sdlc/ after seeding the gitignore block — otherwise the first /sdlc:orchestrate tick on a fresh project fails appending into a non-existent dir.

LocationKindChange
plugin/scripts/worktree_scope_guard.pymodifychange WORKTREES_DIR_REL constant (line 58) and all docstrings/error messages (lines 12, 19, 100, 136, 160)
plugin/scripts/count_inflight_tasks.pymodifychange scan-target dir and four references at lines 12, 65, 211, 367
plugin/skills/pr-check/classify_pr.pymodifychange cursor directory path at line 39
plugin/skills/pr-check/test_classify_pr.pymodifyupdate fixture paths and docstrings (lines 17, 239)
plugin/skills/setup/setup_planning.pymodifyrework gitignore writer to drop the .claude/worktrees/ carve-out (lines 129, 175, 184; update explanatory comments at 113-126)
plugin/skills/setup/tests/run_evals.pymodifyupdate gitignore fixtures (lines 67-96) and assertions (line 206)
plugin/skills/task-work/SKILL.mdmodify.claude/worktrees/<basename>.sdlc/worktrees/<basename> (7 refs)
plugin/skills/task-close-out/SKILL.mdmodifysame (3 refs)
plugin/skills/milestones-from-file/SKILL.mdmodifysame (2 refs)
plugin/skills/entities-migrate/SKILL.mdmodifysame (3 refs, includes git worktree add command + --project-root argument)
plugin/skills/cross-repo-task-pr/SKILL.mdmodifysame (3 refs to foreign-repo worktree path)
plugin/skills/project-cleanup/SKILL.mdmodifysame (1 ref)
plugin/skills/import-planning/SKILL.mdmodifysame
plugin/skills/pr-check/SKILL.mdmodify.claude/pr-cursors/.sdlc/pr-cursors/
plugin/skills/orchestrate/SKILL.mdmodify.claude/orchestrator-log.md.sdlc/orchestrator-log.md (3 refs)
plugin/skills/pr-check/invariants.yamlmodifyphrase at line 46 becomes .sdlc/pr-cursors/
plugin/skills/orchestrate/invariants.yamlmodifyphrase at line 50 becomes .sdlc/orchestrator-log.md
plugin/conventions/worktree-scope-guard.mdmodifyreplace 4 path references at lines 5, 20, 32, 36
plugin/conventions/project-local-skill-extension.mdmodifysanity-check only; expected to need no edits
.gitignoremodifydrop the .claude/worktrees/ carve-out line and its # Worktrees produced by /sdlc:work-task — never committed comment; leave a brief note that the line was removed in this task
plugin/skills/entities-migrate/tests/modifyany test fixtures referencing the old worktree path
site/modifygenerated reference pages (regenerate via /sdlc:dev-update-docs)
  • AC-1:

    grep -rn -E "\.claude/(worktrees|pr-cursors|orchestrator-log)" plugin/ .gitignore --exclude-dir=migrate-runtime-state --exclude-dir=migrate

    returns zero matches. The two migration skills (migrate-runtime-state and the migrate umbrella) are the documented carve-outs — they legitimately reference the legacy paths as their migration source / documentation.

  • AC-2: worktree_scope_guard.py enforces edits inside .sdlc/worktrees/<basename>/. Verified by running the guard against a synthetic worktree at the new path and confirming it accepts edits there; and against an edit outside the worktree and confirming it blocks.

  • AC-3: count_inflight_tasks.py --project-root <root> reports the correct count when worktrees live under .sdlc/worktrees/ and reports zero when only .claude/worktrees/ is populated (proving it no longer reads the old path).

  • AC-4: /sdlc:pr-check writes cursors to .sdlc/pr-cursors/<pr>.json and reads them back across invocations. The existing test_classify_pr.py round-trip test passes against the new path.

  • AC-5: /sdlc:orchestrate appends one line per tick to .sdlc/orchestrator-log.md; .claude/orchestrator-log.md is not created or touched.

  • AC-6: The setup skill’s eval suite (plugin/skills/setup/tests/run_evals.py) passes against a fresh repo, producing a .gitignore that (a) ignores .sdlc/*, (b) un-ignores .sdlc/skill-ext/, and (c) does NOT contain a .claude/worktrees/ carve-out line.

  • AC-7: All invariants files (plugin/skills/pr-check/invariants.yaml, plugin/skills/orchestrate/invariants.yaml) reference the new .sdlc/ paths, and the per-skill invariants runner passes for both skills.

  • AC-8: Before the PR opens, this repo’s .claude/worktrees/ is empty, .claude/pr-cursors/ is empty (or its contents are explicitly discarded), and .claude/orchestrator-log.md is absent (or its history was archived elsewhere) — drain verification.

  • AC-9: A consuming project upgrading the plugin and following the documented drain procedure can perform a full /sdlc:orchestrate/sdlc:task-work/sdlc:task-close-out cycle end-to-end with all runtime state landing under .sdlc/.

  • AC-10: /sdlc:setup creates .sdlc/ (the directory itself, not just the gitignore declaration) so the first /sdlc:orchestrate tick on a fresh project can append to .sdlc/orchestrator-log.md without ENOENT. Asserted by the setup eval suite (plugin/skills/setup/tests/run_evals.py).

  • AC-11: /sdlc:migrate-runtime-state round-trips a fixture project that has populated .claude/worktrees/<basename>/ (a real git worktree on a feat branch), .claude/pr-cursors/<pr>.json, and .claude/orchestrator-log.md — every legacy path is gone and every destination path under .sdlc/ is populated. Idempotent: a second run emits NOTHING-TO-MIGRATE. Asserted by plugin/skills/migrate-runtime-state/tests/run_evals.py.

  • AC-12: /sdlc:migrate (the umbrella) invokes /sdlc:entities-migrate and /sdlc:migrate-runtime-state in order, forwards --dry-run to each, and supports --entities-only / --runtime-only for one-at-a-time runs. Both subskills retain their existing contracts (entities-migrate opens its own PR; migrate-runtime-state mutates in place).

  • Migrating .claude/settings.json, .claude/hooks/, .claude/hooks.json, .claude/commands/, .claude/agents/, .claude/skills/ (project-local Claude Code skills like project-check/ and dev-update-docs/), or .claude/scripts/ — these are Claude Code’s own surface, not sdlc plugin runtime state.
  • Refactoring the Claude Code harness’s own use of .claude/worktrees/ (for its isolation: "worktree" agent option) — that’s Anthropic’s harness, not the sdlc plugin. After the migration the sdlc plugin’s .sdlc/worktrees/ and Anthropic’s .claude/worktrees/ are fully decoupled and may happily coexist as long as basenames don’t collide.
  • Backward-compat shims that read from both old and new paths — a hard cut was the explicit architectural decision; in-flight state drains before the PR lands.
  • Renaming the gitignore comment block header (# --- SDLC plugin project-local ---) or restructuring the .sdlc/ un-ignore policy beyond removing the now-redundant .claude/worktrees/ line.
  • none (the drain-in-flight-state step happens during the task, not as an external precondition)

Surfaced in a 2026-05-21 architecture discussion: .claude/ is conventionally Claude Code’s home (settings, hooks, session state), and plugin/conventions/project-local-skill-extension.md:41-46 already establishes .sdlc/ as the sdlc plugin’s project-local runtime directory — but runtime state (worktrees, pr-cursors, orchestrator-log) hadn’t followed. The convention was half-codified; this task closes the gap.

Captured by /sdlc:task-work on 2026-05-22. PR: pending.

  • AC-1: auto — grep -rn -E "\.claude/(worktrees|pr-cursors|orchestrator-log)" plugin/ .gitignore returns zero matches (with --binary-files=without-match to skip stale __pycache__).
  • AC-2: agent-manual — synthetic worktree under /tmp/wg-test*; guard correctly flags collisions, points at .sdlc/worktrees/... in error messages, and ignores legacy .claude/worktrees/ paths.
  • AC-3: agent-manual — synthetic project at /tmp/cif-test; count_inflight_tasks.py --project-root <root> reports implementing=1 for .sdlc/worktrees/, 0 for .claude/worktrees/-only.
  • AC-4: auto — python3 plugin/skills/pr-check/test_classify_pr.py runs 18 tests against the new .sdlc/pr-cursors/ default; all pass.
  • AC-5: agent-manual — verified by grep; plugin/skills/orchestrate/SKILL.md and invariants.yaml reference .sdlc/orchestrator-log.md exclusively, no .claude/orchestrator-log references survive anywhere in plugin/.
  • AC-6: auto — plugin/skills/setup/tests/run_evals.py runs 6 cases; all pass. The present-with-hooks checker now asserts the .sdlc/* block + un-ignores + absence of the .claude/worktrees/ carve-out line.
  • AC-7: auto — plugin/scripts/lint_skill_prose.py walks 21 SKILL.md files (13 with invariants); no violations.
  • AC-8: agent-manual — after git worktree move .claude/worktrees/<basename> .sdlc/worktrees/<basename>, .claude/worktrees/ is empty; .claude/pr-cursors/ and .claude/orchestrator-log.md do not exist.
  • AC-9: deferred-user — end-to-end live cycle (/sdlc:orchestrate/sdlc:task-work/sdlc:task-close-out) on a consuming project is post-merge work. Building blocks AC-2 through AC-7 cover every constituent step.
  • 12-step Approach decomposed cleanly into 9 logical commits; each commit was self-contained and reviewable in isolation.
  • The setup skill’s existing eval suite (run_evals.py) made AC-6 verification a one-command operation — the test infrastructure was already structured around the gitignore contract this task changed.
  • git worktree move worked atomically; the sub-agent continued operating in the new location without disruption to file state.
  • The hook at .sdlc/skill-ext/task-work/step1-post.sh correctly flagged the in-flight SKILL.md modifications, explicitly noting the runtime-loaded-from-${CLAUDE_PLUGIN_ROOT} carve-out so there was no risk of self-modification mid-run.
  • AC-1 grep matched legitimate comments documenting the migration as an anti-pattern (in setup_planning.py, run_evals.py) and one test constant (FORBIDDEN_CLAUDE_WORKTREES_LINE) — required rephrasing comments and assembling the test constant from string fragments (".claude" + "/" + "worktrees/"). Awkward; suggests path-grep ACs ought to allow an opt-in # allow-legacy-ref: ... marker for prose that documents migration history without weakening the AC. → T-AVDD-ac-grep-allow-legacy-ref-marker
  • git worktree move does not auto-create the destination’s parent directory; had to mkdir -p .sdlc/worktrees first. Worth documenting in a worktree-move helper or task-work’s prose if this becomes a recurring pattern. → T-132J-worktree-move-creates-parent-dir
  • __pycache__ directories under plugin/scripts/ and plugin/skills/pr-check/ got created when running python3 plugin/skills/pr-check/test_classify_pr.py and the compiled bytecode’s embedded source path strings tripped AC-1 grep. Required cleanup + --binary-files=without-match. Suggests: plugin/**/__pycache__/ should be explicitly gitignored at the plugin level, or test runners should set sys.dont_write_bytecode = True. → T-1ZRX-gitignore-plugin-pycache
  • setup_planning.py previously didn’t manage the .sdlc/* block at all — the writer only seeded the Claude Code block, with the repo’s .gitignore carrying the SDLC block by hand. AC-6 forced adding parallel _GITIGNORE_SDLC_BLOCK management. The original helpers (_find_claude_block_span, _block_contains_hooks_lines, _patch_block_with_hooks) are now thin back-compat wrappers; if no external caller imports them, they can be deleted in a future tidy. → T-PKKV-setup-planning-drop-back-compat-helpers
  • /sdlc:task-new creates the task file but does not commit it. /sdlc:task-work assumes the file is already tracked on main when it starts (the start_task.py edits and commits it on main, then rebases the feat branch). When a user runs /sdlc:task-work against a freshly-created untracked task file, the file is absent from the worktree’s HEAD and ensure-ready cannot stamp it cleanly in the worktree. This session required an out-of-band git add + commit on main, then a manual rebase of the feat branch to bring the file into the worktree — a 3-command detour that the canonical flow doesn’t acknowledge. Suggests: either /sdlc:task-new should optionally commit the file, or /sdlc:task-work Step 3 should detect an untracked task file and stage it as a precondition. → T-JZ07-task-new-commits-task-file
  • The rebase inside start_task.py (Step 5b) conflicted on the task file’s frontmatter because both the verify-stamp commit (ensure-ready’s output on feat) and the start-commit (start_task.py’s edit on main) added readiness_verified_at:. The recent “preserve readiness_verified_at across start_task rebase” fix (PR #86) preserves the stamp at the file-tree level but doesn’t avoid the line-add conflict — the verify commit becomes empty after the conflict resolution and gets dropped, but the conflict still requires manual resolution. Suggests: start_task.py could detect the case where (a) the feat branch already carries a stamp and (b) main is gaining the same stamp via the start-commit, and either skip-replay the verify commit explicitly or auto-resolve the trivial frontmatter conflict. → T-N2X3-start-task-preserves-readiness-stamp
  • A leftover worktree from a previously-merged task (2026-05-19-task-ensure-ready-accepts-in-progress) was discovered during pre-flight. Its task file was status: closed/done but the worktree + branch had never been torn down. /sdlc:task-close-out against an already-closed task emits ALREADY-CLOSED and exits without doing teardown (by design — orphan recovery belongs to /sdlc:project-cleanup). This left manual git worktree remove + branch -d as the only way forward. Suggests: either /sdlc:task-close-out should accept a --force-teardown flag for closed-but-orphaned tasks, or the ALREADY-CLOSED early-exit prose should explicitly point at /sdlc:project-cleanup. → T-7NTU-task-close-out-force-teardown-flag
  • Recurring shell-parsing oddity in this session: multi-line Bash invocations where the first command had quoted arguments occasionally got the next line’s git subcommand glued into the previous command’s argv (e.g. git -C <path> add ... interpreted as git add -C ...). Could not consistently reproduce, but it occurred 2-3 times and was always resolved by re-running each command in its own single-line Bash call. Worth investigating whether this is a fish-vs-bash shell-init quirk, a Bash tool parsing edge case, or some interaction with backslash line continuations. → T-TDES-investigate-bash-multiline-arg-glue

← Back to Tasks