---
name: "delete_story should do a full cleanup (CRDT op + DB row + filesystem shadow + worktree + pending timers)"
---

# Story 514: delete_story should do a full cleanup (CRDT op + DB row + filesystem shadow + worktree + pending timers)

## User Story

**Depends on story 520** (typed pipeline state machine). With 520 in place, `delete_story` becomes a single typed transition (`* → Archived(Abandoned)` or a hard-delete CRDT op) followed by event subscribers that handle the worktree, timers, and filesystem cleanup. This story should be re-shaped as the consumer migration once 520 lands.

---

**Note:** content stuffed into user_story per bug 509 workaround.

## Context

The MCP `delete_story` tool currently only **removes the filesystem markdown** from `.huskies/work/N_stage/`. It does NOT:

- Remove the row from `pipeline_items`
- Write a CRDT delete op to `crdt_ops`
- Tear down the in-memory CRDT entry
- Remove the `.huskies/worktrees/N_…/` worktree
- Cancel any pending rate-limit retry timers in `.huskies/timers.json`

So after `delete_story`, the story keeps appearing in `get_pipeline_status` (because the in-memory CRDT still has it), the timer fires and re-spawns an agent, the agent runs in the still-existing worktree, and the user has no idea why the "deleted" story keeps coming back.

## Real user story

As a user calling `delete_story` (via MCP, web UI, or chat command), I want a complete tear-down of all state associated with that story across every layer, so the story is actually gone — no in-memory cache entries, no pending agents, no timers, no worktree, no shadow files, no future spawns.

## Observed 2026-04-09

Repeatedly throughout the session. The most concrete example was around 17:20: I called `delete_story 478_…`, the tool returned success, the markdown file at `.huskies/work/1_backlog/478_…md` was removed, but at 17:25:17 the rate-limit retry timer fired and **re-spawned a coder-1 on the deleted story** because the worktree still existed, the pipeline_items row still existed, and the timer entry still existed in `.huskies/timers.json`. We then had to do sqlite surgery + manual worktree removal + manual timers.json edit to actually kill 478.

## Implementation note

The current `delete_story` is on the legacy filesystem path. The fix needs to wrap it in a transaction that touches every layer:

1. Cancel any pending timers for this story_id (read timers.json, filter, write back)
2. Stop any running/pending agents for this story_id (call `agent_pool.stop_agent` for each)
3. Remove the worktree if it exists (`git worktree remove`)
4. Write a CRDT delete op (`apply_and_persist` with a delete op)
5. Wait for the persist task to confirm
6. Delete the row from `pipeline_items` directly (or trust the materialiser to drop it)
7. Remove the filesystem shadow

Each step should be best-effort with logging — partial failures should be visible, not silent.

## Context

The MCP `delete_story` tool currently only **removes the filesystem markdown** from `.huskies/work/N_stage/`. It does NOT:

- Remove the row from `pipeline_items`
- Write a CRDT delete op to `crdt_ops`
- Tear down the in-memory CRDT entry
- Remove the `.huskies/worktrees/N_…/` worktree
- Cancel any pending rate-limit retry timers in `.huskies/timers.json`

So after `delete_story`, the story keeps appearing in `get_pipeline_status` (because the in-memory CRDT still has it), the timer fires and re-spawns an agent, the agent runs in the still-existing worktree, and the user has no idea why the "deleted" story keeps coming back.

## Real user story

As a user calling `delete_story` (via MCP, web UI, or chat command), I want a complete tear-down of all state associated with that story across every layer, so the story is actually gone — no in-memory cache entries, no pending agents, no timers, no worktree, no shadow files, no future spawns.

## Observed 2026-04-09

Repeatedly throughout the session. The most concrete example was around 17:20: I called `delete_story 478_…`, the tool returned success, the markdown file at `.huskies/work/1_backlog/478_…md` was removed, but at 17:25:17 the rate-limit retry timer fired and **re-spawned a coder-1 on the deleted story** because the worktree still existed, the pipeline_items row still existed, and the timer entry still existed in `.huskies/timers.json`. We then had to do sqlite surgery + manual worktree removal + manual timers.json edit to actually kill 478.

## Implementation note

The current `delete_story` is on the legacy filesystem path. The fix needs to wrap it in a transaction that touches every layer:

1. Cancel any pending timers for this story_id (read timers.json, filter, write back)
2. Stop any running/pending agents for this story_id (call `agent_pool.stop_agent` for each)
3. Remove the worktree if it exists (`git worktree remove`)
4. Write a CRDT delete op (`apply_and_persist` with a delete op)
5. Wait for the persist task to confirm
6. Delete the row from `pipeline_items` directly (or trust the materialiser to drop it)
7. Remove the filesystem shadow

Each step should be best-effort with logging — partial failures should be visible, not silent.

## Acceptance Criteria

- [ ] delete_story returns success only when ALL of the following are true: no row in pipeline_items, no op in crdt_ops referencing the story_id (or a delete op present), no in-memory CRDT entry, no worktree directory, no timer entries, no filesystem shadow
- [ ] Each tear-down step has its own log line so partial failures are diagnosable
- [ ] If any tear-down step fails, the tool returns an error with which step failed and what was already torn down (so the user can finish the cleanup manually)
- [ ] After delete_story, the story does NOT appear in get_pipeline_status, the web UI, or list_agents
- [ ] After delete_story, no rate-limit retry timer can re-spawn an agent on the deleted story
- [ ] Regression test using the 2026-04-09 repro: schedule a rate-limit timer for the story, call delete_story, fast-forward 5 minutes, assert no agent spawned

## Out of Scope

- TBD
