Slice 08: the cleanup gets done — remove a worktree from the browser, and it stays gone #470
No reviewers
Labels
No labels
agent-ready
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
needs-human
needs-input
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli!470
Loading…
Reference in a new issue
No description provided.
Delete branch "dashboard-control-plane"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The card (frozen)
Goal — I remove the worktrees I am finished with from the browser, where I
already see them, and they stay gone.
Fit test — I clear out the worktrees I am finished with from the browser in
one sitting, without opening a terminal. It closes only on Michal's read:
nothing missing, nothing foreign, and "is it actually gone, or does the sweep
bring it back?" answered correctly.
Fit passed on his read.
The card's three invariants shaped every decision below:
a second, separate confirm with the changed files named first.
runs
./iw rmand re-implements none of its guards.What ships
The board already showed 70 registered worktrees and no page offered a way to
remove one. The only existing removal,
DELETE /api/v1/worktrees/:issueId,unregistered from server state alone — and slice 01's disk sweep re-registered
the worktree on the next
GET /, so an unregister-only remove undid itself../iw rmcan be driven without a terminal.--no-promptrefuses a dirtyworktree with exit
2instead of asking a question nobody can answer;--expect-dirty <id>forces only over the changes the caller already named,re-reading porcelain and refusing on a mismatch so nothing is destroyed
unnamed;
--path <dir>removes a worktree that follows no naming convention.DirtyStateis the one reading of what a removal would destroy, shared bythe CLI and the dashboard, so the two cannot disagree about it.
It confirms, names every uncommitted file before the second confirm, shows the
click landed, and takes the card away when the worktree is gone.
./iw rm, run as a subprocess in the project theregistration recorded. The dashboard reads the exit code and parses no prose.
Proven, not asserted
The fit test asks whether a removal sticks. Both halves are pinned:
WorktreeSweepItest— a sweep that demonstrably recovers a live worktreebrings back neither a removed-and-unregistered one nor a removed one whose
best-effort unregister never landed.
WorktreeRemovalItest— the real route drives a real worktree off diskthrough a
./iwthat first checks the dashboard asked the way./iw rmexpects to be asked.
workspace: directory gone, workspace closed, branch kept, off the board, off
the project page, detail 404 — still true after a real sweep and a cold
restart.
Found on the way, fixed at source
ProcessAdapter.runtrimmed every command's stdout, eating the leadingspace of
git status --porcelain— soM README.mdparsed as a stagedchange to
EADME.md. Phase-commit validation had been reading its first filethat way for as long as it has existed.
forever, indistinguishable from a pending fetch. Four duplicated tracker
dispatches are now one pure
IssueSource.Prompt.confirmthrew on a closed stdin wherePrompt.askhad alwaystaken the default.
iw rmdeletes the branch. It never has. The flags,the refusals and the exit codes are now in
docs/commands/rm.md.From the close review
Two rounds, seven skills. Two criticals, both fixed and pinned:
hx-valswas built byinterpolation; htmx reads that attribute back with
getAttribute, which undoesthe HTML escaping, then parses it as JSON. A registered key is checked only for
being non-empty, so a key holding a quote rendered a confirm whose button
posted a forced removal of a different worktree, past the dirty-file
list. Round 2 then found the same key still raw in a URL path (where
/and..read as dot segments the browser resolves away) and in a CSS selector(where a comma widens the swap target). Each context is now escaped for itself.
./iw rmexits 0 both when it removedthe worktree and when a human declined its prompt. The dashboard now checks
disk before reporting
Removed— andCommandResult.cancelled(4) makes theCLI's own contract say it, leaving
0to mean the worktree is gone.Also fixed in-loop: a failed dirty check was shown as "nothing to lose"; the
assembled removal path had no test; the
removablecard wiring was untested;confirm/confirmDirtywere duplicated.Full report:
project-management/efforts/dashboard-control-plane/slices/08-the-cleanup-gets-done/review-slice-20260826-141022.mdFollow-up (parked, not done here)
accepts any non-empty key with any absolute path — so whoever can register
can plant a card pointing at another worktree's directory. No escaping
fixes that; it is the registration boundary. →
the-board-says-who-may-look./iw rmprocesses over one directory.
./iw rm's output, including whatever a project cleanup hook prints, isrendered verbatim on failure.
are in its workspace".
still renders as the same skeleton a pending fetch renders. Carries with it
that
IssueSourcenow falls back to the sharedexample.complaceholderfor a GitHub project with no repository configured.
→
the-quiet-ones-fold-awayLessons
Eight entries triaged with Michal before this PR. Three filed against kanon:
#189 (an
iw kanon/lessoncommand that validates the entry shape),#190 (a
rendered control is not a verified control) and
#191
(escaping is per-context). Two landed as
.claude/skills/dashboard-htmx-controls/, one inCLAUDE.md, the rest atsource.
The dashboard matched linear, github and youtrack and fell through to `Left("Unknown tracker type: Forgejo")`. WorktreeCardService reads that Left as "the API failed, use the cache", the cache was never filled, and the skeleton card rendered — the same rendering a card shows while it is genuinely still loading. Every Forgejo worktree sat at "Loading…" permanently: 0 of 8 cached in the live state, against 32 of 44 for YouTrack. The dispatch existed four times over — two fetch overloads and two URL builders — and three of them were missing Forgejo, the fourth answering `https://example.com/issue/IW-426`. They are now one pure `IssueSource.resolve`, which decides which tracker, which repository and which instance without a network call, and carries the page a human opens. Fetching is left with only the call and its credentials. ForgejoClient.fetchIssue and Constants.EnvVars.ForgejoApiToken already existed; only the dispatch to them was missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfg3hyasiGoVrKpgaBjrqJ