Skip to content

fix: PR-badge refresh, cross-worktree sibling collaboration, retention sweep + start-script fixes - #179

Open
kovtcharov wants to merge 12 commits into
fix/reconnect-input-deliveryfrom
feature/fleet-task-triage
Open

fix: PR-badge refresh, cross-worktree sibling collaboration, retention sweep + start-script fixes#179
kovtcharov wants to merge 12 commits into
fix/reconnect-input-deliveryfrom
feature/fleet-task-triage

Conversation

@kovtcharov

Copy link
Copy Markdown
Contributor

Stacks on #155 (fix/reconnect-input-delivery) — set that as the base; retarget to main once #155 merges.

What's here

Fleet task-management + a couple of standalone bug fixes accumulated on this branch.

Bug fixes

  • PR/CI badge refresh for idle tasks — the periodic refresh only re-polled workspaces with an active task, so a task that pushed, opened a PR, and went idle while CI ran had its badge frozen at push-time state forever. Refresh is now driven by whether the PR can still change (CI running, or PR open/draft), via a pure unit-tested selectWorkspacesToRefresh. Bounded to concurrency 4 so one slow gh call can't stall the pass; terminal (merged/closed) PRs are skipped.
  • Cross-worktree sibling-task collaboration — the inter-task MCP tools already work across a workspace's worktrees, but their descriptions framed them as self-only, so coordinator agents never discovered siblings. Documents the discover→read→message loop across list_tasks/get_task_output/send_input/continue_task, fixes a self-contradicting list_tasks suffix, and restores the "never send to a busy task" state precondition.
  • Workspace reset now archives worktree tasks and removes their worktrees.
  • start.sh / start.ps1 — LISTEN-state-only port checks (no more false positives), safe stale-process cleanup, and no longer dying under set -e.
  • Whole workspace header is draggable for reordering.

Features

  • Archived-worktree retention sweep — implements the 2026-07-18 retention spec: archived tasks past a configurable age (default 30d) are deleted along with their worktree folders; active tasks are never touched.

Docs

  • Fleet-task-triage plan + archived-worktree retention design.

Testing

npx tsc --noEmit clean; npx vitest run green (incl. new pr-refresh tests). Badge fix verified against live data (4 previously-frozen workspaces detected).

…trees

Two gaps in workspace:reset:
1. The task filter was an exact workspaceId match, silently skipping
   every task running in the workspace's .claudia-worktrees/* children
   (fleet tasks — the majority). Reset now archives across the whole
   workspace family (root + worktree children).
2. Worktrees were never removed at all. Reset now force-removes each
   child worktree after its tasks are archived (primary-workspace guard
   kept), deletes the workspace record, and broadcasts the removal —
   matching the removal recipe in the archived-worktree retention spec.

resetResult payload and the frontend toast report worktreesRemoved /
worktreesFailed so partial failures are visible instead of silent.
The port check used lsof -ti:port, which matches ANY socket touching the
port — a browser's CLOSED client connection to a dead backend produced
false 'port in use' failures. Now checks LISTEN state only.

When a port IS held, the script identifies the listener and:
- claudia-owned (command line contains this checkout's path): offers to
  kill it (y/N prompt; --force skips; non-interactive never prompts).
  Backend gets plain SIGTERM with a 10s graceful-exit wait — its
  children are live Claude Code sessions and are NEVER tree-killed;
  only vite gets kill_tree (esbuild children).
- anything else (Claude Code sessions, other projects): reported and
  refused — the script never touches non-claudia processes.
The new LISTEN-only check ran under the script's set -e: lsof exits 1
when a port has no listener, so checking a FREE port killed the script
silently right after 'Checking ports...'. Guarded all failure-prone
statements (lsof/ps substitutions, read, both kill sites) with || true
or if-form. Verified: full check block runs green under set -e both
with free ports and with a stale claudia listener present.
Only the 14px grip icon accepted drags; grabbing the workspace name —
the natural gesture — did nothing, so reordering appeared broken.
The full header is now the drag surface; interactive children
(buttons/inputs/menus) and name-editing opt out; click still toggles
expand. User-verified.
…tention spec)

Worktrees whose owning tasks were archived lingered forever — invisible
in the sidebar (no tasks) yet counted by the workspace badge, with no
cleanup mechanism. Implements the spec:

- worktree-reaper.ts: pure classifyWorktree decision fn (unit-tested,
  8 cases) + force-remove with unlock retry ('--force --force')
- Hourly sweep + startup pass in server.ts: orphaned worktrees (no
  owning task anywhere) removed immediately; archived-owner worktrees
  removed after worktreeRetentionDays (default 30, 0 disables); any
  worktree referenced by a live/disconnected task is NEVER touched;
  every removal and skip logged with reason; archived-task metadata +
  history deleted via the existing path; workspace records deleted and
  broadcast so badges update live
- Settings: 'Archived Worktree Retention (days)' field; config read
  each sweep run, changes apply without restart
The periodic refresh only re-polled workspaces with an active task
(busy/starting/waiting_input) plus a one-time lazy fetch. A task that
pushed, opened a PR, and went idle while CI ran had its badge frozen at
the push-time state forever. Drive refresh by whether the PR can still
change (CI running, or PR open/draft) instead, via a pure, unit-tested
selectWorkspacesToRefresh. Also bound the pass to concurrency 4 so one
slow gh call can't stall the whole pass, and skip terminal PRs.
…escriptions

list_tasks already spans the workspace's worktree tree and the other
inter-task tools take a raw taskId, but their descriptions framed them as
self-only, so coordinator agents never discovered siblings. Describe the
discover->read->message loop across list_tasks/get_task_output/send_input/
continue_task via a shared SIBLING_TASKS constant, fix list_tasks' suffix
that contradicted the new text, and restore the state precondition
(never send to a busy task) on send_input/continue_task.
Built from a mined catalog of 16 bug classes across main's history and
all 56 PRs. Closes the three highest-severity zero-coverage gaps:

1. SESSION LIFECYCLE (the session-history-loss disaster class):
   session-lifecycle.test.ts mocks node-pty and drives real
   create/reconnect flows — asserts --resume pointer integrity,
   unambiguous-only orphan recovery (coordinator-trap declined),
   recovery-map one-shot retirement, and --system-prompt on reconnect.
   The systemPrompt test was written first and FAILED on the live bug
   (reconnect dropped the persisted prompt on every restart/sleep-wake);
   fix included: reconnect args now re-apply persisted.systemPrompt.

2. WS-HANDLER INTEGRATION: server-ws-integration.test.ts boots the real
   server via createApp(tmpDir) + shutdownForTests (new test-safe
   teardown), real temp git repo + worktree, seeded state, real WS
   client. Guards the workspace:reset family-archiving + worktree
   removal regressions and #100/#102 path validation.

3. TERMINAL INJECTION: terminal-injection.test.ts replays polluted
   history through @xterm/headless (new devDep) — raw history provably
   emits responses, stripped history emits ZERO bytes; split-boundary
   carry protocol proven end-to-end.

4. SCRIPTS: scripts/test-start-port-check.sh runs the REAL start.sh
   (check-only mode + env port overrides + overridable lock) — set -e
   survival, LISTEN-only matching, foreign-process refusal, --force
   cleanup. Wired into CI as a blocking step; shellcheck now BLOCKING
   (all findings fixed); drifted copy-paste dep-check removed.

Backend: 22 files / 611 tests. All green.
…counting

- fixtures/fake-claude.sh: PATH-injected fake CLI speaking the TUI
  contract (ready banner, input echo, session-file creation, argv/stdin
  logging, HUP/TERM traps) — makes the full task lifecycle testable
  without Anthropic: create → spawn → prompt delivery → session capture
  → follow-up input → archive kill, plus reconnect asserting --resume
  and --system-prompt at the true process boundary. 5 tests.
- vitest coverage: all:true + include src/** in BOTH packages. Without
  it, files never imported by tests were absent from reports — frontend
  claimed 85% while its real number is ~8% (TerminalView/useWebSocket/
  WorkspacePanel at 0% were invisible). Coverage now tells the truth.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant