Skip to content

T-TZQ7-sdlc-working-dir-configurable-via-sdlc-yaml

Status: closed/obsoleted · Impact: medium · Complexity: medium

The SDLC plugin currently writes its runtime state (PR cursors, orchestrator log, task worktrees) under .claude/, which is conceptually Claude Code’s own directory for settings, hooks, and session state. Conflating “Claude Code’s stuff” with “SDLC plugin’s stuff” hurts ownership clarity and forces narrow per-path permission allowlists in .claude/settings.json for files the plugin owns end-to-end. This task introduces an explicit configurable working directory for SDLC runtime artifacts so projects can adopt a cleaner default (.sdlc/) while preserving the option to point elsewhere.

Three kinds of SDLC plugin runtime state live under .claude/:

  • .claude/pr-cursors/<N>.json — per-PR “last seen comment at” cursors written by /sdlc:pr-check Step 4 (see plugin/skills/pr-check/SKILL.md:170-203).
  • .claude/orchestrator-log.md — append-only digest log written one line per tick by /sdlc:orchestrate Step 5 (see plugin/skills/orchestrate/SKILL.md ”## 5. Digest”).
  • .claude/worktrees/<basename>/ — git worktrees created by /sdlc:task-work Step 4 and torn down by /sdlc:task-close-out. Referenced by plugin/scripts/count_inflight_tasks.py and by plugin/skills/orchestrate/SKILL.md when dispatching resolution sub-agents.

Each of those paths is a literal string in the skill prose. There is no configuration knob, no shared “where does SDLC runtime state live” helper, and no provisioning step in /sdlc:setup. The .claude/* blanket gitignore covers them, but every write requires a narrow allowlist entry in .claude/settings.local.json because the harness defaults to cautious on .claude/ writes.

sdlc.yaml gains a top-level working_dir: key (exact key name TBD — see “Open questions” in Approach) whose value is a project-root-relative path under which all SDLC runtime artifacts live. Default value: .sdlc/ (the clean break from .claude/). Each skill that today writes to a .claude/<thing> path reads the configured working_dir and writes to <working_dir>/<thing> instead. /sdlc:setup provisions the directory on first run and adds it to .gitignore if not already covered. A migration step (one-shot script or part of /sdlc:setup) moves any existing .claude/pr-cursors/, .claude/orchestrator-log.md, and .claude/worktrees/ content to the new location, including git worktree move for active worktrees.

Open questions to resolve before implementation:

  • Key name. working_dir, runtime_dir, state_dir, sdlc_working_dir? Lean working_dir since it lives under the sdlc.yaml namespace already.
  • Default value. .sdlc/ (clean break, conceptually correct) vs .claude/ (backwards-compatible, no migration needed for existing projects). Recommend .sdlc/ — the migration is a one-shot cost, and the conceptual separation pays back every day after.
  • Migration scope. Active worktrees (git worktree move) are the riskiest piece. Do we migrate live, require a drained queue, or support a hybrid (new worktrees under new dir, old ones stay)?
  • Per-skill or per-project. Is this one global directory for all SDLC runtime, or per-skill subdirs configurable independently? Recommend one directory with conventional subpaths (<working_dir>/pr-cursors/, etc.) — simpler.

Implementation steps:

  1. Decide the open questions above (likely via a short /sdlc:discuss-phase-style interaction or PR-review feedback).

  2. Document the working_dir: key in

    plugin/conventions/sdlc-yaml.md with default, semantics, and the conventional subpaths.

  3. Add a shared loader helper (e.g.

    plugin/scripts/resolve_working_dir.py) that reads sdlc.yaml, applies the default, returns the absolute path. Callers shell out to it rather than re-parsing YAML.

  4. Update plugin/skills/pr-check/SKILL.md Step 4 to use

    <working_dir>/pr-cursors/ (default .sdlc/pr-cursors/).

  5. Update plugin/skills/orchestrate/SKILL.md Step 5 to use

    <working_dir>/orchestrator-log.md.

  6. Update plugin/skills/task-work/SKILL.md,

    plugin/skills/task-close-out/SKILL.md, and plugin/scripts/count_inflight_tasks.py to use <working_dir>/worktrees/.

  7. Update /sdlc:setup (plugin/scripts/setup_planning.py and the

    skill prose) to create the working_dir and add it to .gitignore.

  8. Write a migration script (plugin/scripts/migrate_working_dir.py?)

    that detects existing .claude/pr-cursors/, .claude/orchestrator-log.md, .claude/worktrees/ and moves them to the configured working_dir, using git worktree move for worktrees.

  9. Run the migration on this project (sdlc-plugin itself) and verify

    no in-flight task-work runs break.

  10. Add the new permission allowlist entries

    (Write(<working_dir>/pr-cursors/*), etc.) to the project’s .claude/settings.local.json and document the pattern for downstream consumers.

  • plugin/conventions/sdlc-yaml.md — document the working_dir: key, default, conventional subpaths.
  • plugin/scripts/resolve_working_dir.py (new) — shared loader that resolves <project-root>/sdlc.yaml’s working_dir to an absolute path.
  • plugin/skills/pr-check/SKILL.md — replace literal .claude/pr-cursors/ with <working_dir>/pr-cursors/.
  • plugin/skills/orchestrate/SKILL.md — replace literal .claude/orchestrator-log.md with <working_dir>/orchestrator-log.md.
  • plugin/skills/task-work/SKILL.md — replace literal .claude/worktrees/ with <working_dir>/worktrees/.
  • plugin/skills/task-close-out/SKILL.md — same replacement.
  • plugin/scripts/count_inflight_tasks.py — same replacement.
  • plugin/skills/project-cleanup/ (any helper that scans worktrees) — same replacement.
  • plugin/scripts/setup_planning.py — create <working_dir> and add to project’s .gitignore if absent.
  • plugin/scripts/migrate_working_dir.py (new) — one-shot migration for existing .claude/-based projects.
  • sdlc.yaml (this project) — add explicit working_dir: .sdlc entry.
  • .claude/settings.local.json — add allowlist entries for the new .sdlc/ write paths (and eventually drop the .claude/ ones).
  • .gitignore — ensure .sdlc/ is covered (or whatever the configured default is).
  • AC-1: sdlc.yaml accepts a working_dir: key; the documented default is applied when the key is absent or the file is missing.
  • AC-2: After running this task’s migration on a clean checkout of this project, no .claude/pr-cursors/, .claude/orchestrator-log.md, or .claude/worktrees/ paths remain; equivalents exist under the configured working_dir.
  • AC-3: /sdlc:orchestrate runs a full tick successfully against the new working_dir layout (writes the digest line, dispatches resolution sub-agents with worktree paths derived from the new config).
  • AC-4: /sdlc:setup on a fresh project creates <working_dir>/ and adds it to the project’s .gitignore.
  • AC-5: plugin/conventions/sdlc-yaml.md documents the new key including default, semantics, and conventional subpaths.

<Things adjacent to this task that are deliberately NOT being addressed here. Useful for keeping PR review focused and for future tasks to point back to. Always required: if scope is obvious and nothing is excluded, leave a single ”- none” bullet so the explicit signal is “scope considered, nothing to exclude.”>

  • none

<Other tasks, branches, infra changes, or external decisions this task waits on. For hard “B cannot start until A closes” dependencies on other tasks or epics, also record them in the frontmatter depends_on: array (strict wikilink shape, e.g. 2026-04-12-other-task or E0042) — the audit walks that graph for cycle detection. This prose section is the human-readable narrative; depends_on: is the machine-readable canonical list. Leave a single ”- none” bullet if there are none.>

  • <dependency or “none”>

Surfaced 2026-05-21 during a working session with the orchestrator: permission prompts for .claude/pr-cursors/ and .claude/orchestrator-log.md writes (the harness defaults to cautious on .claude/ writes) prompted a discussion about whether the SDLC plugin should own its own runtime directory. The immediate permission prompt issue was patched separately with narrow allowlist entries; this task captures the broader reorganization that makes those allowlist entries cleaner and the ownership distinction explicit.


← Back to Tasks