Skip to content

fix(vibe): acknowledge and watch mid-run scope changes - #347

Merged
shafty023 merged 4 commits into
mainfrom
fix/vibe-scope-change-ack
Oct 26, 2026
Merged

shafty023 merged 4 commits into
mainfrom
fix/vibe-scope-change-ack

Conversation

@shafty023

Copy link
Copy Markdown
Collaborator

Purpose

Ships vibe 1.0.83. A Claude writer that is in the middle of one long turn now cannot silently miss a mid-run answer or scope change. The orchestrator records each change on the fleet board, the writer acknowledges it by id, and the watcher wakes the orchestrator when an acknowledgement is late or the writer's worktree has not moved.

Version: 1.0.82 is taken by the open fix/vibe-one-hook-per-event branch, so this takes 1.0.83. Whichever merges second rebases. The one expected conflict is test_vibe_1081_lessons_contract.py, where both branches drop the 1.0.81 version pin.

The incident (ISS-13096, 2026-10-26)

What the transcripts show:

  • The orchestrator spawned fix-13096, an iTerm2 pane teammate, at 14:49Z with an ad hoc brief. It did not use the fleet worker preamble, so the brief had no "end the turn while long commands run" line and no ack ids.
  • The writer ran one turn from 14:49Z to past 17:06Z, about 300 tool rounds. It used 10 to 15 minute foreground timeouts and sleep 240/280/500 poll loops on background runs.
  • None of the orchestrator's 5 messages reached it during that turn: an answer at 14:55, item 4 at 14:56, a heads-up at 15:14, item 5 at 15:15 and a recap at 15:57. Every SendMessage reported success. The writer's own transcript holds no delivered teammate message after its spawn prompt. Background task notifications did reach it mid-turn.
  • It committed items 1 to 3 at 15:47Z, 52 minutes after the answer. At 15:57Z it reported "building it OFF until you say otherwise". It never re-asked after an answer; it never received one.
  • The orchestrator never ran board.mjs deliver or question for this writer, so nothing on the board could flag it. The operator found the problem at 16:52Z by asking for status.

SendMessage's tool text says messages "drain at the receiver's next tool round". This incident shows a pane teammate drains them only when its turn ends, which matches what the fleet skill already said.

Changes

Where Change
fleet-orchestrator/scripts/board.mjs writer --worktree <path>. deliver --kind scope --text <change> (or --reference) refuses a recipient with no worktree and records that worktree's HEAD. ack <writer> --id <id> now acknowledges a scope change as well as an answer, and answered --text stores the answer. pending <writer> lists unacknowledged scope changes and answers and exits 1 while any remain: this is the writer's inbox, read by pull, so it works whatever the mailbox does. status adds UNACKED-SCOPE (told 15 minutes ago, no ack) and SCOPE-NOT-STARTED (30 minutes after it was told, the worktree is still clean at the recorded HEAD). watch-lines prints those and the late-question flags without minutes. board.md lists the open scope changes.
codex-fleet-monitor/scripts/snapshot.sh Adds the watch-lines output to the snapshot when the work dir has a board.json, so the armed watcher wakes once when a flag appears and again when it clears. Before this, QUESTION-UNANSWERED and UNACKED for Claude writers showed only when the orchestrator ran board.mjs status itself.
Worker preamble (references/templates.md) Messages arrive only when the turn ends. No foreground wait and no sleep poll; run the command in the background and end the turn. Before each commit and each verification phase, run board.mjs pending <you>, apply and ack each item, and name what was applied in the next status. This replaces "Check the worker inbox between steps", which named no action: a Claude agent has no inbox to check.
Fleet SKILL.md Send every mid-run change through deliver --kind scope, then the message, then told; add --worktree to writer rows. On UNACKED-SCOPE or SCOPE-NOT-STARTED, interrupt at once or TaskStop and relaunch with the change in the prompt, because a resend never reaches a writer mid-turn.
judgment.md A withdrawn instruction ("your version stands") is sent as a scope change too. This is the same gap in a second place.
codex-fleet-monitor/SKILL.md One table row for the board ... lines.

To keep instruction length flat, the fleet skill drops three lines other places already state: the transient-recovery line (the monitor's table covers recovery), the "run every lane" line (the preamble, the resume prompt and pr-facts.mjs cover it), and the "writers message each other" line (the preamble covers it). The plugin's instruction text is 2 words shorter than 1.0.81 (instruction-words.mjs --base origin/main --changelog CHANGELOG.md --version 1.0.83).

Other places checked

  • vibe's own persistent writer gets new requests through writer-state.mjs's serial queue and a resume per request, so they always land on a turn boundary. Not the same gap.
  • Codex writers get changes through fleet.sh session steer, which adds to the active turn. Not the same gap.
  • QA guides end their turn ("ready and waiting") before a checkpoint is delivered, so they are idle when it arrives. Not the same gap.
  • The withdrawn-instruction case in judgment.md is the same gap; fixed above.

Prevention

Prevention
- Class: The orchestrator sent a mid-run answer or scope change to a Claude writer and took a successful send as delivery, while the writer stayed in one long turn that never drained its messages, so the writer committed and verified the old scope and nobody noticed until the operator asked.
- Instances: fixed fleet-orchestrator SKILL.md, references/templates.md (worker preamble), references/judgment.md (withdrawn instruction) | not instances: vibe writer queue, Codex steer, QA guide checkpoints
- Search: grep -rn -e SendMessage -e "next tool round" -e "turn ends" -e inbox -e acknowledg plugins/vibe/agents plugins/vibe/skills (Markdown only); the incident transcripts for the orchestrator and fix-13096
- Rung: 3 (tooling check) via board.mjs status, pending and watch-lines, surfaced by codex-fleet-monitor's snapshot; 4 for the preamble and skill rules
- Proof: the two new board.test.mjs cases fail against main's board.mjs (deliver --kind scope is refused); replacing the head check with `false` fails the ISS-13096 case; the new snapshot.test.mjs case fails against main's snapshot.sh
- Why not higher: the checks only fire when the orchestrator records the change. Making that unskippable needs an Agent guard (refuse a background Claude writer brief without the preamble's pending line while a fleet program is active) and a SendMessage guard (remind to `deliver --kind scope` when messaging a board writer). Both are PreToolUse hooks, and hooks are being consolidated into one dispatcher on fix/vibe-one-hook-per-event right now.
- Follow-up: after that branch merges, add `brief-guard` on the Agent route and `scope-guard` on the SendMessage route of hooks/pre-tool-use.mjs; also make foreground-guard refuse `sleep N` (N of 60 or more) inside a vibe worktree
- Event recorded: fleet home lessons.md (one-line ledger format, tagged repeat of the 1.0.77 "answers sent three times" lesson)

Commands run

The suites ran on the second laptop at the final head, because the first laptop was at load 118 on 10 cores. A local run there timed out the zsh snapshot tests at 50 seconds each.

  • node --test over every plugins/**/*.test.mjs, as CI runs it: 791 of 791 passed. An earlier run caught generic.test.mjs rejecting a ticket id and an email address in the new board code and tests; they are now named by date and use a plain git identity.
  • uv run pytest plugins/: 2,493 passed, 3 skipped. Locally, plugins/code/tools/python/test_install_script.py fails 7 of 9 on main as well as on this branch; on the second laptop it passes.
  • uv run ruff check .: clean. uv run pyright: 0 errors, 0 warnings.
  • node plugins/vibe/skills/fleet-orchestrator/scripts/instruction-words.mjs --base origin/main --changelog CHANGELOG.md --version 1.0.83: added 404 removed 406 net -2, exit 0.

Test changes

  • test_vibe_1074_lessons_contract.py pinned "Check the worker inbox between steps". That line is replaced on purpose (it named no action), so the pin now checks for board.mjs <orchDir> pending <you>.
  • test_vibe_1081_lessons_contract.py loses its 1.0.81 version pin; the new test_vibe_1083_scope_change_ack_contract.py pins 1.0.83 and the new wiring.

- Record mid-run scope changes on the board with the writer's head
- Writers read board.mjs pending and ack each change by id
- Flag UNACKED-SCOPE and SCOPE-NOT-STARTED; the watcher wakes on them
- Preamble: no sleep polls; read pending before commits and checks
- Ship vibe 1.0.83 (1.0.82 is the open one-hook-per-event branch)

Testing: node --test board and snapshot suites, vibe pytest, ruff
Risks: deliver --kind scope refuses a writer row with no --worktree
- Name the incident by date in board and snapshot scripts and tests
- Use a plain git identity in the board test fixture

Testing: node --test generic and board suites
Risks: None identified
#345 took 1.0.83 on main. Retire its 1.0.83 version pin and pin this
release at 1.0.84.
@shafty023
shafty023 merged commit 02e3312 into main Oct 26, 2026
10 checks passed
@shafty023
shafty023 deleted the fix/vibe-scope-change-ack branch October 26, 2026 18:45
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