feat(fleet): find work that is stranded, not work that is merely dirty - #39
Merged
Merged
Conversation
Every login ran `zellij -l projects`, which loaded a hand-maintained layout naming nine project tabs. That list is a copy of state that already lives in ~/dev, so it drifted: two of its nine tabs pointed at directories that no longer exist (openclaw, cockpit), and none pointed at the repos actually being worked on. Startup reliably produced tabs nobody wanted. The layout's only other content was a default_tab_template that is byte-for-byte zellij's compiled-in default (tab-bar / pane / status-bar, verified via `zellij setup --dump-layout default`). So the file contributed nothing but the stale list, and deleting it is the whole fix — a bare `zellij` yields one tab whose cwd is inherited from the launching terminal rather than hardcoded anywhere. Fixes the class, not the instance: there is no longer a project list to go stale. Open project tabs on demand instead.
This repo already had a fleet health check. It did not prevent the thing it looks like it prevents: on 2026-08-24 orangecat held 118 uncommitted files and fleetcrown 88, both untouched for ten days — a finished EntityType rename and a finished publishing workstream, each existing on exactly one disk. `git-health` had been reporting them accurately that entire time. The design error is worth naming, because it is easy to repeat: DIRTY IS NOT A DEFECT. Every tree is dirty while someone works in it, so a count that is equally true at minute five and at day ten carries no signal. It gets read as noise, and then it stops being read. Age is what separates work in progress from work that is going nowhere. So stranded-work.sh reports only what has aged past a threshold (default 3d) and prints NOTHING when the fleet is healthy. The silence is the mechanism, not manners: a check that speaks on every shell gets muted, and a muted check is an absent one. It covers both uncommitted files and commits that were made but never pushed — the second is how four of fleetcrown's commits hid, since a tree can be clean and still be holding everything back. It cannot run in CI: the work it looks for has never left the machine, so GitHub cannot see it by construction. It runs from .bashrc instead, printing the last scan from cache and refreshing in the background, so it never costs a prompt. `fleet-stranded` runs it on demand. The deciding half takes TSV on stdin and touches no git, no filesystem and no clock, so the tests drive the shipped rule rather than a restatement of it — the drift the existing ratchet test comments already worry about. Both failure modes are tested, because either one kills it: it must go red on aged work, and it must stay silent on a fresh tree. Writing the tests first paid immediately — the rename case failed, because the porcelain prefix was being stripped after the "old -> new" arrow was resolved, which ate three characters of every renamed path. orangecat's rename touched 18 deletions and a rename; the guard would have misread it. 17 tests, wired into CI.
_claude_autoworktree_enter creates .claude/worktrees/<name> per session, so every concurrent session leaves a permanently-untracked directory behind. That is scratch, not source, and it makes `git status` read dirty forever — the noise that let 206 genuinely stranded files hide across this fleet for ten days. Scoped to worktrees/ rather than all of .claude/, so tracked settings and CLAUDE.md are unaffected. Matches what threadkit already does.
`.playwright-mcp/` is written by the Playwright MCP server whenever an agent drives a browser. It had appeared in 9 repos and left 2 permanently dirty. The first fix was a .gitignore line in all 18 — 18 diffs, 18 reviews, and a 19th repo that gets missed next month. But no contributor without this setup ever sees the path, so it was never the project's business. One line in the global ignore covers every repo, including ones not created yet. Matters because a permanently dirty tree is the noise that lets genuinely stranded work hide — the thing scripts/fleet/stranded-work.sh exists to catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Shipped this morning to find work that never leaves the machine. It scanned
$FLEET_ROOT/*/ only — so it missed the place this fleet does most of its work.
Every agent session runs in a `git worktree` under .claude/worktrees/, and
those hold real branches. Measured tonight, hours after the guard went in:
26 unpushed commits across 25 locations, oldest 25 days
...while `fleet-stranded` reported two dirty files in dotfiles and nothing
else. fleetcrown alone held 9 branches, none of them pushed to origin. The
guard was not merely incomplete; it was certifying a fleet that had almost a
month of work sitting on one disk.
That is the failure mode named in this file's own header — a check that gets
read as "all clear" when it means "I did not look there" — and I built it into
the fix for it. A blind spot shaped like the dominant workflow is worse than no
guard at all.
Worktrees now report as `repo/worktree-dir` with their OWN branch, and the main
checkout is not double-counted as one of its own worktrees. Three tests pin it,
including that last one; all three go red with the scan removed.
Found while auditing fleetcrown for code debt — the audit's biggest finding
turned out to be in the auditor.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five commits from a day of fleet work. The through-line: a check that cannot go red is not a check, and two of these existed in that state.
The zellij tabs (the original ask)
Every terminal opened with nine hardcoded project tabs, two pointing at directories that no longer exist.
projects.kdl'sdefault_tab_templatewas byte-for-byte identical tozellij setup --dump-layout default, so the file contributed nothing except the stale list — deleted, and.bashrcnow runs barezellij, inheriting cwd from the launching terminal. Zero hardcoded paths.fleet-stranded— the guardgit-healthalready reported "118 dirty" every day for ten days and changed nothing, because dirty is not a defect. Every tree is dirty while someone works in it; a number equally true at minute five and day ten reads as noise and then isn't read at all.Age is the signal. Dirty for an hour is work; dirty for a week is stranded. So this reports only what has aged past a threshold and says nothing when the fleet is healthy — silence-when-clean is the mechanism, not politeness. It also can't be a CI job: the work it looks for has never left the machine, so GitHub cannot see it by definition.
At the time it went in, the fleet held 206 finished files uncommitted for ten days, and fleetcrown had been red on committed code since 14 Aug.
The bug in the guard, found hours later
It scanned
$FLEET_ROOT/*/only — so it never looked inside.claude/worktrees/, which is where every agent session actually works. Fleet-wide that hid:...while the guard printed "clean". That is the exact failure mode named in its own header — read as "all clear" when it means "I did not look there" — built straight into the thing meant to prevent it. A blind spot shaped like the dominant workflow is worse than no guard at all.
Worktrees now report as
repo/worktree-dirwith their own branch, and the main checkout is not double-counted as one of its own worktrees.Noise removal
.claude/worktrees/was untracked in 18 of 25 repos, leaving every tree permanently dirty — precisely the noise that lets real stranded work hide..playwright-mcp/went into the global ignore rather than 18.gitignores: no contributor without this setup ever sees that path, so it was never the projects' business, and one line covers repos that don't exist yet.Tests
20, in CI. Both directions are pinned, because both are failure modes: it must go red on aged work, and it must stay silent on a fresh dirty tree — the second is the likelier death, since a check that fires on healthy work gets muted, and a muted check is absent. Writing them first caught a real bug in the rename parsing (
moved.txtwas being read ased.txt). The three worktree tests all go red with the scan removed.