os-sdlc: workspace teardown deletes uncommitted/untracked worktree deliverables #263

Closed
opened 2026-08-04 16:40:56 +00:00 by jared · 2 comments
Owner

Teardown railguard: refuse destructive teardown on unsaved work [ticket]

Slice

os-sdlc-workspace teardown refuses to destroy a worktree/branch holding unsaved work — dirty/untracked files outside .sdlc/, or commits unmerged into the main checkout's current branch — unless --force is passed; the refusal enumerates the at-risk paths/commits.

Acceptance criteria

  • Given a worktree with modified or untracked files outside .sdlc/, when teardown <n> runs without --force, then it exits nonzero, destroys nothing (worktree, branch, and files intact), and lists each at-risk path.
  • Given a ticket branch with commits not reachable from the main checkout's current branch, when teardown <n> runs without --force, then it exits nonzero, destroys nothing, and lists the unmerged commits.
  • Given a clean worktree outside .sdlc/ and a fully merged branch, when teardown <n> runs, then teardown proceeds exactly as today (archive, worktree remove, branch delete).
  • Given a blocking condition, when teardown <n> --force runs, then teardown proceeds with today's behavior and takes no snapshot.
  • Given dirty state only under .sdlc/, when teardown <n> runs without --force, then teardown is not blocked (the ADR-0054 archiver owns that content).

Non-goals

  • No snapshot of dirty state under --force — discarded as speculative hardening; a real force-then-regret incident reopens it.
  • No os-sdlc-workspace merge subcommand (code-owned merge-back) — captured separately as #266.
  • No recording of the setup-time base branch; the unmerged check compares against the main checkout's current branch at teardown time (spurious refusal is answered by --force).

Notes for implementer

  • New TeardownPreflight in plugins/os-sdlc/lib/os_sdlc/, mirroring WorkspacePreflight (setup's guard); wire into Workspace#tear_down (workspace.rb:63–67) ahead of archive/remove/delete.
  • Checks: git status --porcelain in the worktree filtered to paths outside .sdlc/; unmerged commits via rev-list <main-checkout-current-branch>..sdlc/<n>.
  • Governing ADRs: ADR-0054 (archiver scope is .sdlc/tickets/ only), ADR-0075 (sequencing stays in SKILL.md — this guard is an invariant, not sequencing; do not build a stepper), ADR-0076 (Workspace-only isolation), ADR-0090 (Zeitwerk autoload — no manual requires).
  • Update skills/implement/SKILL.md step 15 with one line noting teardown may refuse and why (prose narrates, CLI enforces).
  • Origin incident: during ticket #256's run, teardown deleted uncommitted/untracked deliverables; nothing was recoverable.
# Teardown railguard: refuse destructive teardown on unsaved work [ticket] ## Slice `os-sdlc-workspace teardown` refuses to destroy a worktree/branch holding unsaved work — dirty/untracked files outside `.sdlc/`, or commits unmerged into the main checkout's current branch — unless `--force` is passed; the refusal enumerates the at-risk paths/commits. ## Acceptance criteria - [ ] Given a worktree with modified or untracked files outside `.sdlc/`, when `teardown <n>` runs without `--force`, then it exits nonzero, destroys nothing (worktree, branch, and files intact), and lists each at-risk path. - [ ] Given a ticket branch with commits not reachable from the main checkout's current branch, when `teardown <n>` runs without `--force`, then it exits nonzero, destroys nothing, and lists the unmerged commits. - [ ] Given a clean worktree outside `.sdlc/` and a fully merged branch, when `teardown <n>` runs, then teardown proceeds exactly as today (archive, worktree remove, branch delete). - [ ] Given a blocking condition, when `teardown <n> --force` runs, then teardown proceeds with today's behavior and takes no snapshot. - [ ] Given dirty state only under `.sdlc/`, when `teardown <n>` runs without `--force`, then teardown is not blocked (the ADR-0054 archiver owns that content). ## Non-goals - No snapshot of dirty state under `--force` — discarded as speculative hardening; a real force-then-regret incident reopens it. - No `os-sdlc-workspace merge` subcommand (code-owned merge-back) — captured separately as #266. - No recording of the setup-time base branch; the unmerged check compares against the main checkout's current branch at teardown time (spurious refusal is answered by `--force`). ## Notes for implementer - New `TeardownPreflight` in `plugins/os-sdlc/lib/os_sdlc/`, mirroring `WorkspacePreflight` (setup's guard); wire into `Workspace#tear_down` (workspace.rb:63–67) ahead of archive/remove/delete. - Checks: `git status --porcelain` in the worktree filtered to paths outside `.sdlc/`; unmerged commits via `rev-list <main-checkout-current-branch>..sdlc/<n>`. - Governing ADRs: ADR-0054 (archiver scope is `.sdlc/tickets/` only), ADR-0075 (sequencing stays in SKILL.md — this guard is an invariant, not sequencing; do not build a stepper), ADR-0076 (Workspace-only isolation), ADR-0090 (Zeitwerk autoload — no manual requires). - Update `skills/implement/SKILL.md` step 15 with one line noting teardown may refuse and why (prose narrates, CLI enforces). - Origin incident: during ticket #256's run, teardown deleted uncommitted/untracked deliverables; nothing was recoverable.
Author
Owner

This was generated by AI during triage.

Refine notes

Refined to ticket tier after a grilling session with the maintainer. Decisions made (all resolved in-session — no decision children needed):

  • Scope: teardown railguard only. The os-sdlc-workspace merge subcommand (code-owned merge-back, closing the remaining freeform tail of implement steps 13–15) is captured separately as #266.
  • Blocking conditions: both dirty-outside-.sdlc/ and unmerged branch commits (the #256 incident was the dirty case; the unmerged case is the same loss one commit later).
  • --force: plain informed override, no snapshot — snapshot judged speculative hardening; a real force-then-regret incident reopens it.
  • Unmerged base: the main checkout's current branch at teardown time; setup records no base branch (worktree add -b from current HEAD, worktree.rb:10) and recording one is out of slice.

Verified in code: Workspace#tear_down (workspace.rb:63–67) has no preflight counterpart to setup's WorkspacePreflight — teardown is unconditionally destructive.

Governing ADRs: ADR-0054, ADR-0075, ADR-0076, ADR-0090 (linked in body).

> *This was generated by AI during triage.* ## Refine notes Refined to ticket tier after a grilling session with the maintainer. Decisions made (all resolved in-session — no decision children needed): - **Scope:** teardown railguard only. The `os-sdlc-workspace merge` subcommand (code-owned merge-back, closing the remaining freeform tail of implement steps 13–15) is captured separately as #266. - **Blocking conditions:** both dirty-outside-`.sdlc/` **and** unmerged branch commits (the #256 incident was the dirty case; the unmerged case is the same loss one commit later). - **`--force`:** plain informed override, no snapshot — snapshot judged speculative hardening; a real force-then-regret incident reopens it. - **Unmerged base:** the main checkout's current branch at teardown time; setup records no base branch (`worktree add -b` from current HEAD, worktree.rb:10) and recording one is out of slice. Verified in code: `Workspace#tear_down` (workspace.rb:63–67) has no preflight counterpart to setup's `WorkspacePreflight` — teardown is unconditionally destructive. Governing ADRs: ADR-0054, ADR-0075, ADR-0076, ADR-0090 (linked in body).
Author
Owner

Resolution

Done: TeardownPreflight railguard implemented via os-sdlc pipeline: teardown refuses on dirty/untracked files outside .sdlc/ or unmerged commits, fails closed on git errors; --force routes to tear_down_without_check. All five ACs test-covered.

Evidence: Merged to main in 9e29717 (commit c992e27, 7 files, +327/-29); green-assert 609 runs 0 failures, rubocop and ac-lint clean; opus reviewer APPROVE after one REQUEST-CHANGES round (fail-open unmerged check fixed to fail closed); railguard passed on its own teardown of worktree sdlc-263.

Follow-ups: #268 (WorkspaceHostSetup refactor); #266 pre-exists for the merge subcommand

## Resolution **Done:** TeardownPreflight railguard implemented via os-sdlc pipeline: teardown refuses on dirty/untracked files outside .sdlc/ or unmerged commits, fails closed on git errors; --force routes to tear_down_without_check. All five ACs test-covered. **Evidence:** Merged to main in 9e29717 (commit c992e27, 7 files, +327/-29); green-assert 609 runs 0 failures, rubocop and ac-lint clean; opus reviewer APPROVE after one REQUEST-CHANGES round (fail-open unmerged check fixed to fail closed); railguard passed on its own teardown of worktree sdlc-263. **Follow-ups:** #268 (WorkspaceHostSetup refactor); #266 pre-exists for the merge subcommand
jared closed this issue 2026-08-04 19:11:44 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#263
No description provided.