Repository navigation
ISS-14028, ISS-14024: Bind fleet chats to their programs, safe migrate - #428
Merged
Merged
Conversation
Each orchestrator chat is bound to its own fleet program when it starts or resumes one, so every vibe hook and script in that chat resolves that program without FLEET_HOME. A program has one chat, claimed atomically; another chat is refused with HELD unless it passes --takeover. FLEET_HOME stays an explicit override, and a home's current program keeps serving anything with no binding. fleet-home.mjs launcher prints or installs fleet-claude, which starts Claude Code with the compaction window from context-guard.json. fleet-home.mjs migrate refuses every path collision before moving anything, merges append-only files into an existing home instead of overwriting them, and rolls back a run that fails partway. vibe-core 1.0.6, vibe 1.1.83.
Handoff review fixes. A binding records only the chat's own id, since the other harness id is inherited from another conversation. Each bind runs under a per-program lock file linked into place with its owner's pid and token, reclaimed only from a dead or long-abandoned owner. A chat whose program was taken over is refused with TAKEN-OVER by scripts given the home path while the taker holds it. migrate binds as the last journaled step. Removed an unused variable and a duplicate import.
Stale-lock reclaim runs only under an exclusive reclaim guard and re-reads the lock first; a guard left by a dead reclaimer is cleared after 30 seconds. Claims a chat leaves, or that a taken-over chat still holds, are released best effort under their own locks, never the program the chat is bound to; a claim with no bound chat is stale. A migrate rollback restores the chat's earlier binding, claims and any chat it took over.
Reclaim a stale program lock through a tombstone linked for the exact lock read, never by renaming a guard, and chain past dead reclaimers. Restore a binding on rollback under the program locks without giving a program to a chat that does not hold it. Order binding and claim writes so a crash leaves a stale claim, never two holders. Key homes by their real path on disk and programs by their directory's name, comparing claim files. Close re-reads its binding under the lock.
shafty023
added a commit
that referenced
this pull request
Nov 12, 2026
ISS-14028 and ISS-14024 (#428) took vibe 1.1.84 and vibe-core 1.0.7. The unit-status guard keeps the bound provider's rule and gains the chat's own program board; its tests keep both cases. The fleet-resume header covers both changes, the bundles are rebuilt, the ticket slugs the merge brought into runtime comments are dropped, the launcher mark written into installed launchers stays as an allowlisted compatibility line, and the word count is remeasured against the new main.
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
ISS-14028 and ISS-14024 in one PR; each keeps its own criteria.
ISS-14028: two orchestrator chats on one machine keep their state apart without launch variables. Operator: "I'd rather wait till we fix this properly than do a workaround" and "I know you're not expecting people to have to know to add those env variables before launching claude".
plugins/vibe-core/lib/fleet-orchestrator/scripts/chat-binding.mjsrecords each chat's home and program under~/.vibe-hook-scope/fleet-binding/. The chat is the harness conversation id:CLAUDE_CODE_SESSION_IDorCODEX_THREAD_IDthroughharness-identity.mjs, or the hook event'ssession_id.fleet-home.mjs start,resumeandmigratewrite it.HELD.HELDnames the holder, when it bound, and whether its transcript looks stale. Only--takeovermoves a program, and it printsTAKEOVER <chat>. The chat taken over is then refused withTAKEN-OVERby scripts given the home path, for as long as the taker holds the program.--home, thenFLEET_HOME(still an explicit override), then the chat's bound home, then~/fleet-orchestrator. In a home, the chat's own program wins; otherwisecurrent, while no other chat holds it.fleet-home.mjs, the context guard (flush prompt and window notice), the PreCompact instructions, the SessionStart reading list, the unit status guard, andwork-dir.mjs resolveWorkDir, which feeds board.mjs, fleet.sh, the watcher and snapshot.currentbelongs to another chat, getsUNBOUNDwith how to bind, never the other chat's paths. A script given the home path refuses such a caller withAMBIGUOUS-HOMEonce another program in that home is bound, so workers are briefed with the program dir.currentmeans now: the program of anything with no binding. One-orchestrator homes behave as before. A chat started before this release claims its unheldcurrenton its next tool call, a plainresumebinds to an unheldcurrent,startis refused while that program is open, andcloseclears it.fleet-home.mjs launcher [--install] [--bin <dir>]prints, or installs asfleet-claude, a launcher that setsCLAUDE_CODE_AUTO_COMPACT_WINDOWfromcontext-guard.jsonand passes every argument toclaude. The context guard's notice names it. The first-run setup offers to install it and installs it only on a yes.ISS-14024:
fleet-home.mjs migratenever half-moves a tree or overwrites a home's files. The migration moved tofleet-migrate.mjs.programs/<slug>in another home, or a program another chat holds. The dry run and the real run both print each one and refuse (exit 1) with nothing moved.lessons.mdandstanding-rulings.mdare appended to (MERGE) when every entry is one line. An identical file isSAME. Any other differing file is aCOLLISIONwith the fix.current). If any step fails, it undoes the completed ones (ROLLED-BACK).currentis never written over another program's.Versions: vibe-core 1.0.7, vibe 1.1.84, past the numbers #427 (ISS-14039) used and #426 has reserved;
VIBE_CORE_MINis 1.0.7, and the bundles were rebuilt. When #426 lands, main is merged again with no second renumber.Follow-ups (filed by the lead): binding pruning and
fleet-claudekeeping the home it was installed for (ISS-14036); program claims lost when a bound home is moved and replaced by a symlink (ISS-14064). The cloud validate defects are on ISS-14023.Prevention (ISS-14024, prevent-recurrence)
plugins/vibe-core/lib/fleet-orchestrator/scripts/fleet-migrate.mjs. No other instance: the otherrenameSynccallers (fleet-remote-evidence.mjs,frozen-render.mjs,control-evidence.mjs,recover.mjs) each do a single move with an existence check or their own rollback.grep -rn "renameSync(" plugins/vibe plugins/vibe-core --include='*.mjs', excluding tests, dist and temp-file renames.fleet-home.test.mjsmigrate cases), plus a plan-first design where collisions refuse before any move.Handoff reviews. These ran as vibe launcher turns: guardrails, adversarial, Storybook decomposer and verify. The receipts are bound to the pushed tree.
current.BUSY, and a dead holder's tombstone is chained past;P/pon a case-insensitive volume, shares one claim and lock.Handoff gate. The four launcher review receipts all bind the tree of head 2ee6ade (git tree a414afcb, source tree hash 7a59da3e) and report no blocking findings. Head c7043df adds only the merge of main 8fe42f5 and the renumber on top of that tree:
handoff-gate.mjspassed the receipt and merge checks, then sat at full CPU in the pull-request inventory. Anode --profrun of that inventory on this head put 94% of samples incheckGuardedConstants(comment-rules.mjs:113), reached throughcheckCommentRulesanddiff-rules.mjs evaluateDiff. That is the quadratic scan over generated dist bundles filed as ISS-14051. This diff rebuilt three vibe-workers dist bundles.Vibe gate override: ISS-14051: comment-rules checkGuardedConstants is quadratic over generated dist bundles; the pull-request inventory never finishes
Breaking changes
None for one orchestrator per home. With two programs bound in one home, a script given the home path by a caller with no binding is refused with
AMBIGUOUS-HOME, and that caller passes the program dir instead. A program another chat holds now needs--takeover.Test plan
code_tests_for_changesover the 8 changed sources (index at main 39b9262): 121 tests. That is 98 node:test files, 16 vibe-workers vitest files and 7 vibe-bench files; 101 of them come throughplugins/vibe/shared/vibe-core.mjs(theVIBE_CORE_MINbump). The two new modules are not in the trunk index;fleet-home.test.mjs, which is in the set, covers them. All 121 ran through the heavy slot on tree aeafe052 (head f5d1e97): node 1299 of 1303, vitest 556 of 558, vibe-bench all pass. The review fixes in da0b190 and baab220 change chat-binding.mjs, work-dir.mjs, fleet-migrate.mjs, context-guard.mjs, the docs and the tests. The review fixes in 2ee6ade change chat-binding.mjs, fleet-home.mjs, work-dir.mjs, fleet-migrate.mjs, the tests and CHANGELOG.md. The four files that cover them directly (fleet-home.test.mjs and the context-guard, fleet-resume and unit-status-guard hook tests) pass, 82 of 82, on the tree of 2ee6ade. The seven other test files importing the changed modules (vibe-core, fleet, board, fleet-remote-transaction, finding-brief, snapshot and plugin-version-bootstrap tests) pass, 100 of 100, through the heavy slot on that tree before the last two-line unbind change. Four case-spelling tests need a case-insensitive volume: they ran on macOS and skip on Linux CI. CI runs the full suite.docker-disk-admission.test.mjsand twoe2e-lock.test.mjsCompose cases need a Docker daemon; they fail on clean main on this laptop too. Therecover.test.mjssupervisor case timed out under load and passed alone. Twomain-sync.test.tsCLI cases timed out at 15s under load. A cloud rerun of those 4 files passed on f5d1e97 (routine trig_01D2tbFBGasCthQKYawWtaYw) and on main (trig_01S6wY6FUzg1iqCrwEymk5WF).fleet-home.test.mjs,unit-status-guard.test.mjsandcontext-guard.test.mjscover:TAKEN-OVER, and the takeover note lapsing after the taker closes;HELDby default and--takeover;FLEET_HOME, and theFLEET_HOMEoverride;AMBIGUOUS-HOMEand binding aliases;BUSY, a dead one passes to exactly one next reclaimer, and an empty or unreadable lock's chain;Ptoprename,start Pover dirp, close through a symlinked home, and a close waiting on the lock while a takeover removes its binding.Mutation checks fail each.
reply-checker.test.tsand the two Python version-pin tests pass. The rest of Vibe Eval T0 was skipped: this diff changes only the version pin an eval reads.flamingo-fleet, and a chat bound tovibe-bugsresolves its second home with noFLEET_HOME. Neither home was written.