Skip to content

plan_steps_todo

CSV plan-steps ↔ Claude TodoWrite projection. The canonical plan-steps CSV remains the audit-bearing plan-of-record; this module projects it into TodoWrite-shaped dicts for runtime visibility in Claude. Status writeback is append-only and mutates only the status column — every other column's values and the column order are preserved (the CSV is re-serialized with minimal quoting) via atomic write.

Opt-in (for the bridge skill artifact) via --target-host-features bridge:copilot-vscode-to-claude:todo-projection. The module is importable regardless of the host-feature flag; the flag only gates emission of the .claude/skills/todo-from-plan/SKILL.md skill file.

Disambiguation. This module's read_steps/PlanStep read the strict 11-column todo-projection schema (columns listed below) into dataclasses. The dict-row reader for .steps.csv audit artifacts is plan_steps; the tolerant 7-column runtime wave reader is parallel_plan.

Required CSV Columns

phase_id, step_id, phase_name, step_title, description, deliverable, dependencies, priority, effort, notes, status

Status Projection

CSV status TodoWrite status Legal for update_status()?
pending pending Yes
in_progress in_progress Yes
completed completed Yes
done completed (legacy synonym) Yes
blocked pending Yes
(anything else — e.g. legacy/typo strings like in-progress, wip, or blank) pending (falls through via .get(status, "pending")) No — update_status() raises ValueError; only the five statuses above are valid

Public Surface

@dataclass
class PlanStep:
    phase_id: str
    step_id: str
    phase_name: str
    step_title: str
    description: str
    deliverable: str
    dependencies: str
    priority: str
    effort: str
    notes: str
    status: str
    def to_todo(self) -> dict[str, str]: ...

read_steps(csv_path: Path) -> list[PlanStep]
Read a plan-steps CSV. Raises FileNotFoundError if missing or ValueError if required columns are absent.

project_to_todos(csv_path: Path) -> list[dict[str, str]]
Convenience: read CSV and return TodoWrite-shaped dicts with content, activeForm, status keys.

update_status(csv_path: Path, step_id: str, new_status: str) -> bool
Mutate the status column for a single step, preserving header order and all other column values. The file is re-serialized with minimal quoting, so it is not necessarily byte-identical to the original. Atomic via os.replace. Returns True if updated; False if step_id not found. Raises ValueError for an unknown new_status. Since 2026-08-13 it also delegates the write to atomic_rewrite_csv_rows (agentteams/atomicio.py), which raises ValueError of its own if the write fails round-trip verification (a rendered row or header doesn't parse back exactly as written).

detect_divergence(
    csv_path: Path,
    todo_items: list[dict[str, Any]],
) -> dict[str, list[str]]
Compare a TodoWrite snapshot against the canonical CSV. Returns {missing_in_todo, extra_in_todo, status_mismatch}. The orchestrator uses this to decide whether to refresh the projection.

render_skill() -> str
Return the contents of the .claude/skills/todo-from-plan/SKILL.md skill file. Pure function — no filesystem access.

Contract

  • The CSV is canonical. Structural changes (adding/removing/renaming steps) must go through the CSV; the projection picks them up on the next read.
  • The TodoWrite projection is read-mostly. The only writeback is update_status. All other TodoWrite fields are recomputed on every projection.
  • Column order and all non-status values in the CSV are preserved across writebacks — update_status re-serializes the header and every other row with minimal quoting (values and column order preserved, not necessarily byte-identical).

See also

  • Section fencing guide — how managed regions are delineated in generated artifacts (the projection's SKILL.md output participates in the same fencing model).