Conversation
The pool preflight reads every worktree in the pool, which is a git status per slot, and it now runs ahead of every spawn. Left unbounded it would be a new way for a launch to hang with nothing to say, which is the failure this work exists to remove. Bound it with fm_run_timed; hitting the bound is an unsettled question, not a refusal, so the spawn proceeds and the ordinary wait stays the backstop. Read the pane's failure marker with grep -o rather than a line anchor, so a prompt prefix or a carriage return in the captured pane cannot hide a get that already failed. Regression: an unreadable pool bounds its read instead of hanging the spawn. It fails on revert, taking 242s instead of the bounded path.
treehouse reads its config through a symlink, so the preflight must too: a linked lower cap is now still refused at that cap, and a linked higher cap no longer invents a full-pool refusal at treehouse's default.
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.
Summary
A crewmate spawn used to type
treehouse getinto the task pane and then watch the pane's working directory for sixty seconds. When the treehouse pool had nothing to hand out, the only thing the captain saw was a timeout naming the directory the shell was still sitting in. The real answer, whichtreehouseitself prints in one line ("all 16 worktrees are in use or dirty"), never reachedbin/fm-spawn.sh. One such outage cost six failed launches and several wrong repairs, including a duplicatemax_treeskey that made treehouse refuse its own config.This branch adds
bin/fm-treehouse-pool-lib.sh, which readstreehouse status --jsonbefore any endpoint, worktree, or record is created for the task, and refuses the spawn on the two causes it can prove: a pool at itsmax_treescap with every slot in use or dirty, and atreehouse.tomlthat treehouse will not load. The refusal carries the slot breakdown, and for each blocked slot either the count of unlanded commits (left alone, no clearing command offered) or the exact commands that first preserve and then clear its leftovers, plus the exact line to edit to raise the cap. Nothing is cleared automatically. Inside the pane, thetreehouse getcommand now prints an exit-status marker on failure, so a get that fails outright is reported immediately with the pool diagnosis or treehouse's own words, instead of being waited out. An inspection that cannot settle the question launches exactly as before, with the timeout as backstop.✅ What this PR does
treehouse.tomlwill not load, before anything for the task exists (bin/fm-spawn.sh#L2389-L2404).treehouse return --forcecommand pair (bin/fm-treehouse-pool-lib.sh).max_treesline and warning that a second key breaks the file, which is the failure mode hit during the incident.treehouse getat once with its exit status, by having the pane print a marker the spawn polls for during the wait, and falls back to the pane's last lines when the pool inspection is inconclusive.FM_TREEHOUSE_POOL_TIMEOUT(default 45s) so a slow or wedged repository cannot turn the preflight into a new hang; a timed-out read is treated as unknown and the spawn proceeds.FM_SPAWN_WORKTREE_WAIT(default 60s, positive integer enforced).max_treesthrough a symlinkedtreehouse.toml, so an alternate config location does not make the preflight invent or miss a full-pool refusal.docs/scripts.mdand points spawn regressions at the new test indocs/architecture.md.TREEHOUSE_DIRstill does not move the pool root, andtreehouse.tomlis still read only from the repository root. Both are treehouse behaviours the report now states, but they remain traps for anyone repairing by hand.max_treesdetection depends on treehouse's default of 16 being unchanged upstream; a drift there can delay a refusal, never create one.🧪 Regression evidence
tests/fm-spawn-pool-preflight.test.sh(permalink) runs against a faketreehouseand a real two-slot pool of worktrees. All eight cases pass on this branch:treehouse.tomltreehouse getin paneThe tests were run in this worktree only. No live spawn against the original 16-slot pool was reproduced here.
🤖 Generated with Claude Code
Risk Assessment
✅ Low: The fix round is a one-token predicate change that makes the cap read follow symlinks exactly as treehouse does, and both added regression tests exercise the behaviour through a real spawn in both directions and would fail if the -L test were restored.
Testing
Ran the targeted preflight test file (8 cases, ~30s) which drives the real fm-spawn.sh with a fake tmux and a fake treehouse status --json; all pass. Confirmed fail-on-revert twice: against the base commit's spawn script the test reproduces the incident's blind 60s timeout verbatim, and against the pre-symlink-fix pool lib the symlinked-lower-cap case fails. Captured a CLI transcript of the end-user refusal for both incident failure modes (full pool with leftover-only slots, duplicate max_trees key). No visual surface involved, so no screenshot.
Evidence: End-user refusal transcript: full pool and unloadable config
Source: End-user refusal transcript: full pool and unloadable config
Evidence: Base commit reproduces the incident's 60s timeout (test fails on revert)
Source: Base commit reproduces the incident's 60s timeout (test fails on revert)
Evidence: Symlink regression test fails with a6bb6ff reverted
Source: Symlink regression test fails with a6bb6ff reverted
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 3 issues found → auto-fixed ✅
bin/fm-spawn.sh:2397- The intent requires "find a way to never have issues with booting crewmates anymore ever", and the change converts the blind 60s timeout into a fast refusal that names the cause and the remedy. It does not touch why the pool filled: 11 of 16 slots were left dirty by tasks that had already finished, meaning the teardown or return path released those tasks withouttreehouse return --forcelanding, or panes were closed outside teardown. That failure remains fully reachable: the next time slots accumulate leftovers, every spawn for that project refuses again until the captain runs the printed commands by hand. Refusing is the correct call given AGENTS.md hard rule 3 (uncommitted changes are never landed), so auto-clearing is not the fix. The durable boundary is the one that finishes a task: bin/fm-teardown.sh already ownstreehouse return --force; the question is why those 11 slots escaped it. Remedy extends scope beyond this change (teardown investigation, possibly a slot-reclaim step gated on a captain decision), so this needs the author's decision, not a silent fix.bin/fm-treehouse-pool-lib.sh:67-fm_treehouse_pool_read_capreturns the default cap of 16 when treehouse.toml is a symlink ([ ! -L "$toml" ]). treehouse itself reads the file through the symlink (Go os.ReadFile follows links). Concrete wrong refusal: a repo whose treehouse.toml is a dotfiles symlink settingmax_trees = 32, pool holding 20 worktrees all in-use or dirty, zero available. treehouse get would create slot 21; the preflight computes total=20 >= cap=16 and free=0, sets verdictfull, and refuses the spawn with a report claiming "max_trees = 16" and telling the captain to add a max_trees line to a file that already has one. The header comment claims a drift in the default can "delay a refusal, never invent one"; this path invents one. The inverse (symlinked cap of 2, two dirty slots) silently downgrades to the old timeout. Fix: drop the-Ltest and read the file as treehouse does.bin/fm-treehouse-pool-lib.sh:54-fm_treehouse_pool_json_objectssplits the array on},{, which also splits inside each slot'sprocessesarray. On this host's real output an in-use slot with 7 processes yields 8 fragments; it works only becausename,path, andstatusprecedeprocessesand the process fragments carry nopathkey. The greedy.*"name"regex also returns the last process name (e.g.2.1.272) as the slot name for in-use slots; harmless today because the name is only printed for dirty slots, which have no processes. A treehouse update that reorders keys or adds apathto process objects degrades to verdictunknown, which is fail-open, so the risk is losing the preflight silently, not a wrong refusal. treehouse v2.3.0 is already advertised on this host. Noting the tradeoff; ajq-free parser was clearly the intent.🔧 Fix: read max_trees through symlinked treehouse.toml, add regression tests
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-spawn-pool-preflight.test.shon target commit: all 8 cases pass (full pool, unlanded slot, unloadable config, failing get reported verbatim, hanging pane backstop, bounded pool read, symlinked lower cap, symlinked higher cap)Same test file withbin/fm-spawn.shswapped to base commit 616049a: first case fails with the incident's original 60s timeout message, proving the test fails on revertSame test file withbin/fm-treehouse-pool-lib.shswapped to ce3486c (pre-symlink-fix):test_symlinked_lower_cap_is_readfails, the other 6 prior cases passManual CLI transcript: spawn against a 2-of-2 dirty pool (one slot holding a.gitignoreline, one an untracked scratch dir) and against a treehouse.toml with a duplicate max_trees key, captured to the evidence dirgit status --shortafter each swap: worktree restored clean✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.