Force-clear / kill stuck board card from the UI #609

Closed
opened 2026-04-30 21:08:26 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator watching the planner board, I want a one-click "kill / force-clear" action on a stuck card, so that I can manually unblock workflow when the automatic dispatch chain has gotten wedged (e.g. a changes_requested review that never triggered an address-review dispatch, leaving the card idle with a successful prior task that /task/:id/redispatch refuses to re-run).

Background

Concrete situation that motivated this ticket — charles/claude-hooks#567 / PR #604 on 2026-04-30:

  • Dev's first dispatch on #567 finished status=success ~12 min before this ticket (only row in task_history for that issue).
  • Reviewer requested changes on PR #604; CI succeeded.
  • No follow-up address-review dispatch was enqueued — task_history still shows just the one success row, so the review event silently failed to fan out (separate bug — see companion ticket).
  • Board card sits in the IN REVIEW column with stall age climbing. Existing UI affordances are dead-ends:
    • Re-dispatch button is gated to failure / cancelled / interrupted (apps/server/src/main.ts:1266); a success task returns 409.
    • Cancel button is gated to card.status === "running" (apps/web/src/components/board/board-side-panel.tsx:222); idle cards do not show it.
    • Reroute does change assignees + posts an audit comment but is intended for "wrong agent type", not "kick the same agent again".

The current escape hatches are all off the dashboard: dismiss + re-request the review on Forgejo, or unassign+reassign on Forgejo, or hand-craft a POST /task. None are operator-friendly.

Acceptance criteria

Server-side endpoint

  • New POST /board/kick (or extend POST /task/:id/redispatch with a force=true flag — pick one, document the choice in the handler docstring).
  • Body: { repo: string, issue_number: number }. Resolves the assigned agent type from the latest task_history row (or current Forgejo assignee), rebuilds the same task prompt the regular issues.assigned flow would have built, and enqueues it through the normal pool scheduler.
  • Skips the status === "success" 409 gate and the running/queued duplicate gate (those gates exist for the redispatch path and are exactly what blocks the operator here).
  • Posts an audit comment on the issue: 🦵 Operator kicked the queue — re-running address-review.
  • Returns 202 { task_id } on success; clear error codes for unknown issue, no agent token available, etc.
  • Reuses session resume: if a prior session exists for <type>:<repo>:<issue>, the new task picks up from there (mirrors /task/:id/redispatch).

Skill selection

  • When the linked PR has changes_requested outstanding, dispatch the address-review skill (the same one event-handlers.ts:347 would have picked).
  • When there's no PR or no outstanding review, dispatch implement (matches the post-reroute behaviour).
  • Document the decision tree in the handler docstring.

UI

  • Always-visible "Kick" button on the side panel (apps/web/src/components/board/board-side-panel.tsx), regardless of status. Tone error, leadingIcon something like Zap or RotateCw from lucide-react per apps/web/CLAUDE.md.
  • Confirm dialog before firing (cheap to misclick): "Re-dispatch task for <repo>#<n>? A fresh task will be enqueued even if the previous one succeeded."
  • On success, toast Kicked → task <id8>, invalidate ["board"] + ["history"] query keys.
  • Disabled while a request is inflight (isPending).
  • Reachable via keyboard; respects the foundation <Button> primitive.

Tests

  • Server: unit test for the new endpoint covering: successful kick on success task (the case redispatch refuses), address-review selection when outstanding changes_requested review, implement selection otherwise, missing token / unknown issue error paths.
  • Web: a Playwright or react-testing-library smoke test for the side-panel button + confirm dialog + mutation success path.

Audit + observability

  • New row in task_history for the kicked dispatch (the regular dispatch path already does this; just verify the marker / agent_type is right).
  • Forgejo comment from item above provides the human-visible audit trail.

Out of scope

  • Auto-detecting and self-healing the underlying bug (address-review not firing on changes_requested) — covered in the companion ticket Investigate: address-review did not dispatch for PR #604 / issue #567.
  • A "remove from board" action that detaches a card without enqueuing work. The board mirrors Forgejo state — closing the issue / merging the PR is the right way to make a card disappear, and we should not introduce a phantom "dismissed" state.
  • Bulk kick of N cards at once.

References

  • apps/server/src/main.ts:1232handleTaskRedispatch (the gate that blocks the operator today)
  • apps/server/src/main.ts:1266 — the status === "success" 409 path
  • apps/server/src/domain/views/board.ts:1097handleBoardReroute (closest existing pattern; reuse the rate-limit + audit comment patterns)
  • apps/server/src/domain/workflow/event-handlers.ts:338-347 — pivot logic for implement → address-review when PR has outstanding changes_requested
  • apps/server/src/domain/workflow/review-loop.ts — review-loop cap (relevant for kick safety)
  • apps/web/src/components/board/board-side-panel.tsx:115-233 — current side-panel button gating
  • apps/web/src/routes/planner.board.tsx:486 — wiring of cancel/reroute mutations
  • apps/web/CLAUDE.md — foundation primitives + <Button> + Lucide icon rules
## User story As an operator watching the planner board, I want a one-click "kill / force-clear" action on a stuck card, so that I can manually unblock workflow when the automatic dispatch chain has gotten wedged (e.g. a `changes_requested` review that never triggered an address-review dispatch, leaving the card idle with a successful prior task that `/task/:id/redispatch` refuses to re-run). ## Background Concrete situation that motivated this ticket — `charles/claude-hooks#567` / PR #604 on 2026-04-30: - Dev's first dispatch on #567 finished `status=success` ~12 min before this ticket (only row in `task_history` for that issue). - Reviewer requested changes on PR #604; CI succeeded. - No follow-up `address-review` dispatch was enqueued — `task_history` still shows just the one `success` row, so the review event silently failed to fan out (separate bug — see companion ticket). - Board card sits in the `IN REVIEW` column with stall age climbing. Existing UI affordances are dead-ends: - **Re-dispatch** button is gated to `failure` / `cancelled` / `interrupted` (`apps/server/src/main.ts:1266`); a `success` task returns 409. - **Cancel** button is gated to `card.status === "running"` (`apps/web/src/components/board/board-side-panel.tsx:222`); idle cards do not show it. - **Reroute** does change assignees + posts an audit comment but is intended for "wrong agent type", not "kick the same agent again". The current escape hatches are all *off* the dashboard: dismiss + re-request the review on Forgejo, or unassign+reassign on Forgejo, or hand-craft a `POST /task`. None are operator-friendly. ## Acceptance criteria ### Server-side endpoint - [ ] New `POST /board/kick` (or extend `POST /task/:id/redispatch` with a `force=true` flag — pick one, document the choice in the handler docstring). - [ ] Body: `{ repo: string, issue_number: number }`. Resolves the assigned agent type from the latest `task_history` row (or current Forgejo assignee), rebuilds the same task prompt the regular `issues.assigned` flow would have built, and enqueues it through the normal pool scheduler. - [ ] Skips the `status === "success"` 409 gate and the running/queued duplicate gate (those gates exist for the redispatch path and are exactly what blocks the operator here). - [ ] Posts an audit comment on the issue: `🦵 Operator kicked the queue — re-running address-review`. - [ ] Returns `202 { task_id }` on success; clear error codes for unknown issue, no agent token available, etc. - [ ] Reuses session resume: if a prior session exists for `<type>:<repo>:<issue>`, the new task picks up from there (mirrors `/task/:id/redispatch`). ### Skill selection - [ ] When the linked PR has `changes_requested` outstanding, dispatch the `address-review` skill (the same one `event-handlers.ts:347` would have picked). - [ ] When there's no PR or no outstanding review, dispatch `implement` (matches the post-reroute behaviour). - [ ] Document the decision tree in the handler docstring. ### UI - [ ] Always-visible "Kick" button on the side panel (`apps/web/src/components/board/board-side-panel.tsx`), regardless of `status`. Tone `error`, leadingIcon something like `Zap` or `RotateCw` from `lucide-react` per `apps/web/CLAUDE.md`. - [ ] Confirm dialog before firing (cheap to misclick): "Re-dispatch task for `<repo>#<n>`? A fresh task will be enqueued even if the previous one succeeded." - [ ] On success, toast `Kicked → task <id8>`, invalidate `["board"]` + `["history"]` query keys. - [ ] Disabled while a request is inflight (`isPending`). - [ ] Reachable via keyboard; respects the foundation `<Button>` primitive. ### Tests - [ ] Server: unit test for the new endpoint covering: successful kick on `success` task (the case redispatch refuses), `address-review` selection when outstanding `changes_requested` review, `implement` selection otherwise, missing token / unknown issue error paths. - [ ] Web: a Playwright or react-testing-library smoke test for the side-panel button + confirm dialog + mutation success path. ### Audit + observability - [ ] New row in `task_history` for the kicked dispatch (the regular dispatch path already does this; just verify the marker / `agent_type` is right). - [ ] Forgejo comment from item above provides the human-visible audit trail. ## Out of scope - Auto-detecting and self-healing the underlying bug (address-review not firing on `changes_requested`) — covered in the companion ticket `Investigate: address-review did not dispatch for PR #604 / issue #567`. - A "remove from board" action that detaches a card without enqueuing work. The board mirrors Forgejo state — closing the issue / merging the PR is the right way to make a card disappear, and we should not introduce a phantom "dismissed" state. - Bulk kick of N cards at once. ## References - `apps/server/src/main.ts:1232` — `handleTaskRedispatch` (the gate that blocks the operator today) - `apps/server/src/main.ts:1266` — the `status === "success"` 409 path - `apps/server/src/domain/views/board.ts:1097` — `handleBoardReroute` (closest existing pattern; reuse the rate-limit + audit comment patterns) - `apps/server/src/domain/workflow/event-handlers.ts:338-347` — pivot logic for `implement → address-review` when PR has outstanding `changes_requested` - `apps/server/src/domain/workflow/review-loop.ts` — review-loop cap (relevant for kick safety) - `apps/web/src/components/board/board-side-panel.tsx:115-233` — current side-panel button gating - `apps/web/src/routes/planner.board.tsx:486` — wiring of cancel/reroute mutations - `apps/web/CLAUDE.md` — foundation primitives + `<Button>` + Lucide icon rules
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
charles/claude-hooks#609
No description provided.