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.
| Location | Role 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/** | 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. |
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”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”).
-
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-outor/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.mdhas been archived if its history matters (it’s append-only narrative for the operator, not load-bearing for any skill). -
Update
worktree_scope_guard.py. ChangeWORKTREES_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. -
Update
count_inflight_tasks.py. Replace the four.claude/worktreesreferences at lines 12, 65, 211, 367 (docstrings, JSON shape example, scan-target string,--project-rootargparse help) with.sdlc/worktrees. Verify the scan logic still works against the new path. -
Update
pr-check. Change the cursor directory inclassify_pr.py:39from<project-root>/.claude/pr-cursors/to<project-root>/.sdlc/pr-cursors/. Updatetest_classify_pr.py:17and:239plusplugin/skills/pr-check/invariants.yaml:46so the invariant phrase matches the new path. Sweeppr-check/SKILL.mdfor any prose reference. -
Update
orchestrate. Changeplugin/skills/orchestrate/SKILL.md(lines 10, 307, 431) andplugin/skills/orchestrate/invariants.yaml:50from.claude/orchestrator-log.mdto.sdlc/orchestrator-log.md. -
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. Updateplugin/skills/setup/tests/run_evals.pyfixtures and expectations (lines 67-96, 206) to match the new gitignore shape. -
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 thegit worktree addexample at line 74 and the--project-rootargument at line 91),cross-repo-task-pr/SKILL.md,project-cleanup/SKILL.md,import-planning/SKILL.md. -
Update conventions docs.
plugin/conventions/worktree-scope-guard.md(4 path references). Re-readplugin/conventions/project-local-skill-extension.mdto confirm its prose is already correct (no edits expected). -
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.gitignorenoting the line was removed in this task so future readers don’t add it back. -
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. -
Final grep. Run
grep -rn -E "\.claude/(worktrees|pr-cursors|orchestrator-log)" plugin/ .gitignoreand confirm zero hits. -
Refresh docs site. Run
/sdlc:dev-update-docsto surface any drift in the generated reference pages undersite/and resolve. -
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-dirThis 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. -
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-migratethen/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:setupwriter tomkdir .sdlc/after seeding the gitignore block — otherwise the first/sdlc:orchestratetick on a fresh project fails appending into a non-existent dir.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/scripts/worktree_scope_guard.py | modify | change WORKTREES_DIR_REL constant (line 58) and all docstrings/error messages (lines 12, 19, 100, 136, 160) |
plugin/scripts/count_inflight_tasks.py | modify | change scan-target dir and four references at lines 12, 65, 211, 367 |
plugin/skills/pr-check/classify_pr.py | modify | change cursor directory path at line 39 |
plugin/skills/pr-check/test_classify_pr.py | modify | update fixture paths and docstrings (lines 17, 239) |
plugin/skills/setup/setup_planning.py | modify | rework 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.py | modify | update gitignore fixtures (lines 67-96) and assertions (line 206) |
plugin/skills/task-work/SKILL.md | modify | .claude/worktrees/<basename> → .sdlc/worktrees/<basename> (7 refs) |
plugin/skills/task-close-out/SKILL.md | modify | same (3 refs) |
plugin/skills/milestones-from-file/SKILL.md | modify | same (2 refs) |
plugin/skills/entities-migrate/SKILL.md | modify | same (3 refs, includes git worktree add command + --project-root argument) |
plugin/skills/cross-repo-task-pr/SKILL.md | modify | same (3 refs to foreign-repo worktree path) |
plugin/skills/project-cleanup/SKILL.md | modify | same (1 ref) |
plugin/skills/import-planning/SKILL.md | modify | same |
plugin/skills/pr-check/SKILL.md | modify | .claude/pr-cursors/ → .sdlc/pr-cursors/ |
plugin/skills/orchestrate/SKILL.md | modify | .claude/orchestrator-log.md → .sdlc/orchestrator-log.md (3 refs) |
plugin/skills/pr-check/invariants.yaml | modify | phrase at line 46 becomes .sdlc/pr-cursors/ |
plugin/skills/orchestrate/invariants.yaml | modify | phrase at line 50 becomes .sdlc/orchestrator-log.md |
plugin/conventions/worktree-scope-guard.md | modify | replace 4 path references at lines 5, 20, 32, 36 |
plugin/conventions/project-local-skill-extension.md | modify | sanity-check only; expected to need no edits |
.gitignore | modify | drop 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/ | modify | any test fixtures referencing the old worktree path |
site/ | modify | generated reference pages (regenerate via /sdlc:dev-update-docs) |
Acceptance criteria
Section titled “Acceptance criteria”-
AC-1:
grep -rn -E "\.claude/(worktrees|pr-cursors|orchestrator-log)" plugin/ .gitignore --exclude-dir=migrate-runtime-state --exclude-dir=migratereturns zero matches. The two migration skills (
migrate-runtime-stateand themigrateumbrella) are the documented carve-outs — they legitimately reference the legacy paths as their migration source / documentation. -
AC-2:
worktree_scope_guard.pyenforces 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-checkwrites cursors to.sdlc/pr-cursors/<pr>.jsonand reads them back across invocations. The existingtest_classify_pr.pyround-trip test passes against the new path. -
AC-5:
/sdlc:orchestrateappends one line per tick to.sdlc/orchestrator-log.md;.claude/orchestrator-log.mdis not created or touched. -
AC-6: The
setupskill’s eval suite (plugin/skills/setup/tests/run_evals.py) passes against a fresh repo, producing a.gitignorethat (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.mdis 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-outcycle end-to-end with all runtime state landing under.sdlc/. -
AC-10:
/sdlc:setupcreates.sdlc/(the directory itself, not just the gitignore declaration) so the first/sdlc:orchestratetick on a fresh project can append to.sdlc/orchestrator-log.mdwithout ENOENT. Asserted by the setup eval suite (plugin/skills/setup/tests/run_evals.py). -
AC-11:
/sdlc:migrate-runtime-stateround-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 emitsNOTHING-TO-MIGRATE. Asserted byplugin/skills/migrate-runtime-state/tests/run_evals.py. -
AC-12:
/sdlc:migrate(the umbrella) invokes/sdlc:entities-migrateand/sdlc:migrate-runtime-statein order, forwards--dry-runto each, and supports--entities-only/--runtime-onlyfor one-at-a-time runs. Both subskills retain their existing contracts (entities-migrate opens its own PR; migrate-runtime-state mutates in place).
Out of scope
Section titled “Out of scope”- Migrating
.claude/settings.json,.claude/hooks/,.claude/hooks.json,.claude/commands/,.claude/agents/,.claude/skills/(project-local Claude Code skills likeproject-check/anddev-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 itsisolation: "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.
Dependencies
Section titled “Dependencies”- none (the drain-in-flight-state step happens during the task, not as an external precondition)
Discovery context
Section titled “Discovery context”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.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-22. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
grep -rn -E "\.claude/(worktrees|pr-cursors|orchestrator-log)" plugin/ .gitignorereturns zero matches (with--binary-files=without-matchto 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>reportsimplementing=1for.sdlc/worktrees/,0for.claude/worktrees/-only. - AC-4: auto —
python3 plugin/skills/pr-check/test_classify_pr.pyruns 18 tests against the new.sdlc/pr-cursors/default; all pass. - AC-5: agent-manual — verified by grep;
plugin/skills/orchestrate/SKILL.mdandinvariants.yamlreference.sdlc/orchestrator-log.mdexclusively, no.claude/orchestrator-logreferences survive anywhere inplugin/. - AC-6: auto —
plugin/skills/setup/tests/run_evals.pyruns 6 cases; all pass. Thepresent-with-hookschecker now asserts the.sdlc/*block + un-ignores + absence of the.claude/worktrees/carve-out line. - AC-7: auto —
plugin/scripts/lint_skill_prose.pywalks 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.mddo 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.
What worked
Section titled “What worked”- 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 moveworked 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.shcorrectly 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.
Friction and automation gaps
Section titled “Friction and automation gaps”- 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 movedoes not auto-create the destination’s parent directory; had tomkdir -p .sdlc/worktreesfirst. 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 underplugin/scripts/andplugin/skills/pr-check/got created when runningpython3 plugin/skills/pr-check/test_classify_pr.pyand 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 setsys.dont_write_bytecode = True. → T-1ZRX-gitignore-plugin-pycachesetup_planning.pypreviously didn’t manage the.sdlc/*block at all — the writer only seeded the Claude Code block, with the repo’s.gitignorecarrying the SDLC block by hand. AC-6 forced adding parallel_GITIGNORE_SDLC_BLOCKmanagement. 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-newcreates the task file but does not commit it./sdlc:task-workassumes 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-workagainst 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-bandgit add + commiton 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-newshould optionally commit the file, or/sdlc:task-workStep 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) addedreadiness_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 wasstatus: closed/donebut the worktree + branch had never been torn down./sdlc:task-close-outagainst an already-closed task emitsALREADY-CLOSEDand exits without doing teardown (by design — orphan recovery belongs to/sdlc:project-cleanup). This left manualgit worktree remove + branch -das the only way forward. Suggests: either/sdlc:task-close-outshould accept a--force-teardownflag 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
Bashinvocations where the first command had quoted arguments occasionally got the next line’sgitsubcommand glued into the previous command’s argv (e.g.git -C <path> add ...interpreted asgit 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-lineBashcall. Worth investigating whether this is afish-vs-bashshell-init quirk, a Bash tool parsing edge case, or some interaction with backslash line continuations. → T-TDES-investigate-bash-multiline-arg-glue
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-AVDD-ac-grep-allow-legacy-ref-marker — AC-grep checks honor
# allow-legacy-ref:opt-in marker for migration prose, created - T-132J-worktree-move-creates-parent-dir — document or auto-create destination parent dir for
git worktree move, created - T-1ZRX-gitignore-plugin-pycache — suppress
plugin/__pycache__from grep-based ACs, created - T-PKKV-setup-planning-drop-back-compat-helpers — drop back-compat gitignore helpers in
setup_planning.pyif no external callers, created - T-JZ07-task-new-commits-task-file —
task-newcommits the task file (ortask-workstages it) so the canonical flow doesn’t require an out-of-band commit, created - T-N2X3-start-task-preserves-readiness-stamp — already-existing in-progress task captures the remaining line-add rebase conflict gap; linked
- T-7NTU-task-close-out-force-teardown-flag —
task-close-outgrows--force-teardown(orALREADY-CLOSEDmessage points atproject-cleanup) for orphaned worktrees, created - T-TDES-investigate-bash-multiline-arg-glue — investigate intermittent Bash-tool multi-line argv-glue parsing oddity, created