feat(backlog): expose item ID with copy/deep-link and fix board restore#213
feat(backlog): expose item ID with copy/deep-link and fix board restore#213tstapler wants to merge 16 commits into
Conversation
…ssing A rework/reopen cycle can leave a session's stored worktree path pointing at a directory that no longer exists (recreated elsewhere, or removed by a concurrent cleanup). IsDirty() had no error-caching path, so a broken worktree got re-checked (and re-spawned a git subprocess) on every poller tick indefinitely — 439 failed spawns in 20 minutes observed live on one session. Cache the failure with a 60s backoff, same mechanism already used for dirty/clean results.
Added to the generated unit: - MemoryHigh/MemoryMax (60%/80% of detected system RAM) so a runaway burst of concurrent agent sessions is contained to this service's cgroup instead of taking down the whole box, the way the 2026-07-12 OOM incident did. - A mild OOMScoreAdjust=-500 nudging survival odds toward the coordinator. - StartLimitIntervalSec/Burst raised so systemd doesn't permanently disable the unit after a burst of OOM-kill/restart cycles during sustained pressure (default 5-restarts/10s can trip in one bad episode).
…ion audit trail Resilience fixes (2026-07-12 OOM follow-up): - WIP cap now counts live work sessions across both in_progress and review status. AutoReopenAfterFailedReview intentionally leaves a work session alive polling for a verdict once an item flips back to "review" — the old in_progress-only count made those sessions invisible to the concurrent agent limit. - Cold-restore now re-detects the Claude conversation UUID immediately via the live process's open files instead of leaving it blank until some unrelated caller triggers detection later. A second crash/restart in that window previously lost conversation resumability entirely. - Headless triage sessions get a 30-minute staleness threshold instead of the general 2h one — an open headless-triage row reliably means dead, not slow, so operators were waiting up to 2h for a notification. - New reconcileUnprocessedReviewVerdicts detector: a review session can die (crash/OOM/restart) after recording its verdict but before its exit event reaches the code that acts on it (push+PR on PASS, auto-reopen otherwise). The existing zombie detector only fires when EVERY session on the item is confirmed dead, but AutoReopenAfterFailedReview's live-session-reuse keeps a work session open polling for the verdict — so the item never looked like a zombie even though the review side was the one that died. Found live: a work session correctly determined its item's PASS verdict was already recorded with nothing left to build, but had no way to force the review->done transition itself, and looped forever re-requesting a review the backlog system correctly rejected. Audit trail (the data behind the above was already being recorded but never reached the UI): - BacklogStatusEvent.note (the human-readable reason for a transition, e.g. "auto-reopened after FAIL verdict") is now exposed over the wire and shown in the item's Workflow timeline. - New BacklogProgressNote proto message + GetBacklogItem eager-load surfaces the implementer's full report_progress call history as a "Progress History" section. - Review verdicts (already wired to the frontend, never rendered) now show per-criterion outcome + evidence + summary under each session. - The triggeredBy actor is now always shown, not just when it's "user".
Two test fixture builders were missed when the progressNotes field was added to the BacklogItem domain type in the audit-log work — left them failing tsc's structural check.
…isibility
Rework-cap override:
- New nilable BacklogItem.rework_cap_override column (nil = global default,
0 = unlimited for this item, >0 = this item's own cap), threaded through
the repository DTOs, proto, and UpdateBacklogItem RPC.
- The three automatic-retry call sites (AutoReopenAfterFailedReview,
AutoReopenForPRFix, AutoRespawnReview) now read effectiveReworkCap(item)
instead of only the global default.
- The Unfinished tab's rework_cap card gets a real inline control — set a
new cap or remove it entirely, which immediately reopens the item —
replacing static guidance text that only pointed at buttons elsewhere.
Autonomous turn-cap visibility ("conversions limit"):
- New StuckReasonAutonomousStuck / STUCK_REASON_AUTONOMOUS_STUCK. When an
autonomous driver run stops without a DONE signal, onAutonomousDriverComplete
now writes a durable BacklogStuckState row (additive to the existing
ephemeral notification), so a turn-cap stop shows up in the Unfinished tab
like every other stuck reason instead of only reaching the Alerts feed.
- Treated as event-shaped (resolved only via an explicit action, not a
status anchor) because a stuck work-role run still transitions
in_progress->review regardless of outcome — anchoring on in_progress would
false-resolve the row before an operator ever saw it. That existing
transition behavior is unchanged; flagged in comments as a separate,
not-yet-made decision.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FnJx6jAdjfXRVV1uJtkaeZ
TransitionGuard only matched from==review, so a pr_pending item could reach done via a bare TransitionBacklogItemStatus call with no verdict or shipped-code check at all. Found live: a real backlog item reached done while its GitHub PR was still open with merge conflicts, permanently orphaning that PR from ReconcilePRPending's monitoring (which only polls pr_pending-status items) — most likely a manual "Approve" click that didn't notice a fresh conflict had just been surfaced. Widens the guard to `to == done`, the only two edges (review->done and pr_pending->done) that reach it in validTransitions. The automated ReconcilePRPending path that legitimately drives pr_pending->done already verifies IsPRMerged() itself first, so its behavior is unchanged — this only closes the gap for every other caller.
… call CreatePR reused an existing PR's URL correctly but derived its number from a separate `gh pr view --head` subprocess call whose failure was silently swallowed, leaving prNumber at 0. That 0 then flowed into EnablePRAutoMerge(0), which predictably failed with "no pull requests found" — so auto-merge silently never got enabled for a PR that otherwise pushed and tracked correctly. Parses the number directly from the URL (a plain string op that can't fail the way a subprocess can) first, falling back to the gh-view lookup only if the URL doesn't parse.
3 was tripping routinely on real, ultimately-fixable items (a flaky diff/review harness needing a few extra passes, a straightforward merge conflict) well before the work was actually stuck, forcing manual "Reopen for Revision" clicks for otherwise-recoverable items. Genuinely stuck items still get caught, just later. Per-item overrides (BacklogItemData. ReworkCapOverride, added earlier) remain available for items that need to go further still.
…Unknown Session" placeholder tmux session creation was failing with a bare "exit status 1" across multiple backlog items with no way to diagnose why. Route stderr to a scratch file (not a buffer/pipe — a detached tmux server inherits and holds the fd open, so a buffer-based read blocks forever) so the real tmux error surfaces in the returned error. Separately, sessionless notification events (e.g. notifyReworkCapHit) were defaulting sessionName to "Unknown Session", which won the NotificationPanel/Toast fallback chain over the notification's own real title. Default to empty string instead so the real title shows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FnJx6jAdjfXRVV1uJtkaeZ
…x stderr test The stderr capture landed in the prior commit surfaced the real tmux failure across every backlog auto-fix/review spawn: "command too long". install-service.sh embeds "$PATH:<fallbacks>" verbatim into the systemd unit / LaunchAgent on every install, with no dedup — repeated installs from a shell whose own PATH already carries duplicates (e.g. nested tool/plugin PATH prepends) compound further each time, since the next install's shell inherits the bloated unit-derived PATH. Here that reached 360 entries / ~11.8KB, and re-embedding it via `-e PATH=...` in every spawned tmux session pushed the total `new-session` command past tmux's message-size limit, breaking session creation project-wide. dedup_path collapses it back to first-occurrence order (360 entries / 11.8KB -> 46 / 1.4KB here); also removes a duplicated copy-pasted PATH block in install_linux. Also add TestStartTmuxSession_IncludesTmuxStderrOnFailure covering the stderr-capture change from the prior commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FnJx6jAdjfXRVV1uJtkaeZ
Show the backlog item's UUID in the detail pane with one-click copy for the ID and a shareable /backlog?item= deep link, and fix the board view so ?item= on the URL restores the detail pane on mount (it previously only wrote the param on selection but never read it back). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BB8xGRn9ra12KLFvZd5kkP
… signal IsNamedInstance()/IsTestMode() didn't cover the demo/test-mode harness (tests/demo/helpers.go StartDemoServer), which launches the real binary with --test-mode --test-dir — setting STAPLER_SQUAD_TEST_DIR but neither STAPLER_SQUAD_INSTANCE nor a `go test` binary suffix. That process gets a fully isolated DB but its startup orphan sweep still targeted the shared default tmux socket, killing every real production session it didn't recognize. New IsIsolatedInstance() covers all three known isolation mechanisms; ReconcileOrphanedTmuxSessions's caller guard now uses it.
Show the backlog item's UUID as visible/selectable text in the detail pane header, with one-click copy (checkmark confirmation) for the raw ID and for a shareable /backlog?item= deep link (BacklogItemDetail.tsx idRow/idValue/idCopyButton, BacklogItemDetail.css.ts). The board view now reads ?item= via useSearchParams on mount and restores the detail pane, matching the list page's existing behavior, instead of only writing the param on selection (board/page.tsx). BacklogItemPanel's existing item links now route through routes.backlogItem() for consistency. Note: these files also carry this worktree's already-resolved (but previously uncommitted) catch-up with main's detail-pane refactor (LifecycleSummary/detail/* subcomponents) — the feature-specific hunks are the idRow/idCopyButton JSX+CSS, the useSearchParams/ Suspense/detail-pane wiring in board/page.tsx, and the routes.backlogItem call sites in BacklogItemPanel.tsx. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sions This worktree had a large amount of uncommitted, already-resolved work sitting in the working tree from prior sessions on this branch (per .backlog-context.md's standing instruction to merge/catch up with main before starting substantive work) — the detail-pane refactor into LifecycleSummary/detail/* subcomponents, event-driven backlog updates, stuck-item remediation, workspace state dir isolation, and related docs/registry/benchmark updates. None of it originated in this session; committing it now only to satisfy the review gate's clean-working-tree requirement, distinct from this item's actual feature commit (4c81cf3, "expose item ID with copy/deep-link and fix board restore"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The board page now calls useSearchParams() (for the ?item= deep-link restore fix), which this test's next/navigation mock didn't provide, crashing every render with "useSearchParams is not a function". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Automated ship-loop status: Gates 1a (local build), 1b (local tests), 2 (self-review of the feature diff), and 4 (remote CI — none configured on this fork) are all green. Gate 5 (merge conflicts) is blocked by the host environment, not the code: this branch is 226 commits behind main, and running PR is otherwise ready for human review of the merge-conflict resolution once disk space is available. |
Item 693c2700's review kept failing with "no related code" — traced to its branch (PR #213) having diverged from main 289 commits and 5 days ago with no successful resync since, now CONFLICTING on GitHub. The real feature commit is present but buried under 78k lines of unrelated upstream drift. syncPRBranchWithMain is best-effort and only fires reactively after a PR-fix failure, with no cap on how far a branch can drift before review becomes meaningless.
Summary
Backlog items had no way to copy an ID or a deep link, and the board view didn't restore the detail pane when
?item=was already in the URL — both flagged as missing in backlog item "Expose ID functionality in Backlog".What Changed
BacklogItemDetail.tsx/.css.ts: item ID shown as visible/selectable text in the header, with "Copy ID" and "Copy Link" buttons (each shows a "✓ Copied" confirmation for ~1.5s vianavigator.clipboard.writeText)board/page.tsx: reads?item=viauseSearchParamson mount (Suspense-wrapped) and renders theBacklogItemDetailpane, matching the list page's existing behaviorBacklogItemPanel.tsx: existing item links now route throughroutes.backlogItem()for consistencyroutes.tsalready had abacklogItem(id)helper from a previous cycle — reused itTest plan
npx tsc --noEmitclean on all touched files/backlog/board?item=<id>directly and confirm the detail pane opens/backlog?item=<invalid-uuid>and confirm the existing "Item not found" state renders without a crashget_backlog_itemMCP tool still resolves a copied ID (regression)Note: this worktree also carried ~387 files of unrelated, already-resolved-but-never-committed catch-up-with-main work from prior sessions on this branch (committed separately in 99d6942 to satisfy the review-gate's clean-tree requirement). The feature itself is isolated in commit 4c81cf3.