Skip to content

ISS-14028, ISS-14024: Bind fleet chats to their programs, safe migrate - #428

Merged
shafty023 merged 5 commits into
mainfrom
vibe/iss-14028-fleet-home
Nov 12, 2026
Merged

shafty023 merged 5 commits into
mainfrom
vibe/iss-14028-fleet-home

Conversation

@shafty023

@shafty023 shafty023 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

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".

  • New plugins/vibe-core/lib/fleet-orchestrator/scripts/chat-binding.mjs records each chat's home and program under ~/.vibe-hook-scope/fleet-binding/. The chat is the harness conversation id: CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID through harness-identity.mjs, or the hook event's session_id. fleet-home.mjs start, resume and migrate write it.
  • A program has one chat, held by a claim file. Each bind runs its check and every write under a per-program lock file linked into place already holding its owner's pid and token. Of chats claiming one program at the same moment, exactly one wins and the rest get HELD. HELD names the holder, when it bound, and whether its transcript looks stale. Only --takeover moves a program, and it prints TAKEOVER <chat>. The chat taken over is then refused with TAKEN-OVER by scripts given the home path, for as long as the taker holds the program.
  • Only the chat's own id is recorded. When both harness ids are set, the other one was inherited from another conversation (ISS-13834).
  • Resolution order: --home, then FLEET_HOME (still an explicit override), then the chat's bound home, then ~/fleet-orchestrator. In a home, the chat's own program wins; otherwise current, while no other chat holds it.
  • These all use it: fleet-home.mjs, the context guard (flush prompt and window notice), the PreCompact instructions, the SessionStart reading list, the unit status guard, and work-dir.mjs resolveWorkDir, which feeds board.mjs, fleet.sh, the watcher and snapshot.
  • A chat with no program of its own, whose home's current belongs to another chat, gets UNBOUND with how to bind, never the other chat's paths. A script given the home path refuses such a caller with AMBIGUOUS-HOME once another program in that home is bound, so workers are briefed with the program dir.
  • What current means now: the program of anything with no binding. One-orchestrator homes behave as before. A chat started before this release claims its unheld current on its next tool call, a plain resume binds to an unheld current, start is refused while that program is open, and close clears it.
  • Compaction window: fleet-home.mjs launcher [--install] [--bin <dir>] prints, or installs as fleet-claude, a launcher that sets CLAUDE_CODE_AUTO_COMPACT_WINDOW from context-guard.json and passes every argument to claude. 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 migrate never half-moves a tree or overwrites a home's files. The migration moved to fleet-migrate.mjs.

  • Collisions are computed before any move: two sources for one target, a target that already exists, an existing 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.
  • In an existing home, lessons.md and standing-rulings.md are appended to (MERGE) when every entry is one line. An identical file is SAME. Any other differing file is a COLLISION with the fix.
  • The real run journals every step (moves, merges, config rewrite, relinks, current). If any step fails, it undoes the completed ones (ROLLED-BACK). current is 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_MIN is 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-claude keeping 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)

  • Class: a multi-step file move renamed over targets without checking for existing or duplicate targets, and without a rollback, leaving a half-moved tree or clobbered files.
  • Instances: fixed plugins/vibe-core/lib/fleet-orchestrator/scripts/fleet-migrate.mjs. No other instance: the other renameSync callers (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.
  • Search: grep -rn "renameSync(" plugins/vibe plugins/vibe-core --include='*.mjs', excluding tests, dist and temp-file renames.
  • Rung: guard test (fleet-home.test.mjs migrate cases), plus a plan-first design where collisions refuse before any move.
  • Proof: removing the collision refusal or the undo loop fails the collision, merge and rollback tests.
  • Why not higher: no static check can see target collisions; they depend on the runtime tree.
  • Follow-up: none. Event recorded: in the lead's report.

Handoff reviews. These ran as vibe launcher turns: guardrails, adversarial, Storybook decomposer and verify. The receipts are bound to the pushed tree.

  • Round 1, on f5d1e97, adversarial: an inherited-id alias (blocking), a claim race while a chat moved off a program, and taken-over chats still resolving current.
  • Round 2: the new lock could be read half written, and the release of the left program, a stale takeover note and migrate binding outside its journal were wrong. Fixed in da0b190.
  • Round 3: clean, with three residuals. By orchestrator ruling all three were fixed in this PR:
    • a three-way race on a dead owner's lock, fixed with an exclusive reclaim guard;
    • the release of the left program failing after the bind committed, now best effort;
    • migrate's rollback dropping the earlier binding, now restored.
  • Rounds 4 to 6 fixed the low findings each raised:
    • a dead reclaimer's guard caused a busy spin;
    • leftover claims revived after unbind;
    • a missing lock was read as an empty one;
    • a takeover could release a chat's live claim;
    • a guard's age and contents were read separately.
  • Round 6 is clean in all four roles, on head baab220.
  • Rounds 7 to 10 were Fable hunts for two holders of one program, each followed by the four launcher roles. Bar: no interleaving may end with two holders; a refusal, or a chat with no program, is fine. Fixed in 2ee6ade:
    • the reclaim guard was renamed aside, which let two chats hold one lock; a stale lock is now reclaimed through a tombstone linked for the exact lock contents read. A live holder keeps it BUSY, and a dead holder's tombstone is chained past;
    • the rollback restore gave a program back to a chat that no longer held it; it now runs under the program locks and only removes or restores records that are still that chat's;
    • write order: a takeover removes the old holder's binding before the claim moves, and unbind and rollback remove the binding before the claim, so a crash leaves a stale claim, never two holders;
    • spelling: a home is keyed by its real path as the disk spells it and a program by its dir's name, and every program comparison compares claim files, so a symlinked or differently cased home, or P/p on a case-insensitive volume, shares one claim and lock.
  • Round 11 (scoped to the spelling fixes) and round 12 (scoped to close re-reading its binding under the lock) are clean in all four roles and in a Fable check.
  • Earlier plain-agent reviews (a general one and the Fable ones) are extra evidence, not receipts.

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:

  • vibe-guardrails-reviewer, session 445f7315-6d9a-4f5f-b891-036308f9b817, request handoff-guardrails-reviewer-round12b
  • vibe-adversarial-reviewer, session b3325fa1-6c0a-426c-a036-cbf6c64edc1e, request handoff-adversarial-reviewer-round12
  • vibe-storybook-decomposer, session 775b0e6e-b1d4-43a7-8429-50aeba930e7f, request handoff-storybook-decomposer-round12
  • vibe-verify-worker, session 378bc8ea-f342-4815-988c-747f64a29246, request handoff-verify-worker-round12; its pr-evidence file passes pr-evidence-schema.mjs.

handoff-gate.mjs passed the receipt and merge checks, then sat at full CPU in the pull-request inventory. A node --prof run of that inventory on this head put 94% of samples in checkGuardedConstants (comment-rules.mjs:113), reached through checkCommentRules and diff-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

  • Tests chosen with closedloop-graph code_tests_for_changes over 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 through plugins/vibe/shared/vibe-core.mjs (the VIBE_CORE_MIN bump). 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.
  • The 4 misses are environmental, in files this diff does not change. docker-disk-admission.test.mjs and two e2e-lock.test.mjs Compose cases need a Docker daemon; they fail on clean main on this laptop too. The recover.test.mjs supervisor case timed out under load and passed alone. Two main-sync.test.ts CLI 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).
  • New tests in fleet-home.test.mjs, unit-status-guard.test.mjs and context-guard.test.mjs cover:
    • two chats bound to two programs in one home, with each chat's hooks resolving its own program;
    • a Codex thread with an inherited Claude id;
    • 8 concurrent claimers per round, and racers moving off a program they hold;
    • lock reclaim: an empty fresh lock gets BUSY, while an old lock and a dead owner's lock are reclaimed;
    • TAKEN-OVER, and the takeover note lapsing after the taker closes;
    • a failing final bind rolling migrate back, and a rollback after a committed bind restoring the earlier binding;
    • six chats meeting a dead owner's lock with exactly one winner;
    • the reclaim guard, including a dead reclaimer's guard;
    • leftover claims released after unbind, but never the program the chat is bound to;
    • an unbound chat told how to bind;
    • HELD by default and --takeover;
    • a 12-round two-process race with exactly one winner;
    • a second home with no FLEET_HOME, and the FLEET_HOME override;
    • legacy single-orchestrator behavior, AMBIGUOUS-HOME and binding aliases;
    • the launcher;
    • both reported migrate cases, the multi-line merge refusal, a held program, and rollback;
    • tombstone reclaim: a live reclaimer's tombstone keeps BUSY, a dead one passes to exactly one next reclaimer, and an empty or unreadable lock's chain;
    • rollbacks racing a takeover or another claimer, and a takeover, unbind and rollback each cut short between writes, all ending with at most one holder;
    • two spellings of one program and of one home (symlink and case), a takeover after a P to p rename, start P over dir p, close through a symlinked home, and a close waiting on the lock while a takeover removes its binding.
      Mutation checks fail each.
  • Targeted npm checks: Plugin Manifest Versions passes; the vibe-workers rebuild matches the committed bundles; vibe-bench reply-checker.test.ts and 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.
  • Full-suite cloud runs were inconclusive because of validate defects, now on ISS-14023: trig_019qVeeDLYjTnqJTTARimNGB (main), trig_01Dc4shzDhGtCxyVi1Af3oWz (pre-race-fix tree) and trig_01XoGWsvFKyvueWFExNE85jC (no result). CI runs the full suite.
  • Read-only live check against the two real homes on this laptop: an unbound chat resolves flamingo-fleet, and a chat bound to vibe-bugs resolves its second home with no FLEET_HOME. Neither home was written.

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.
Merge origin/main (ISS-14039, #427) and take vibe 1.1.84 and vibe-core 1.0.7, past the numbers #427 used and #426 has reserved. VIBE_CORE_MIN, the README version line, the release-pin tests and the CHANGELOG entries follow, and the vibe-workers bundles are rebuilt.
@shafty023
shafty023 merged commit 7c34119 into main Nov 12, 2026
12 checks passed
@shafty023
shafty023 deleted the vibe/iss-14028-fleet-home branch November 12, 2026 16:51
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.
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