0009 — Human gates park jobs; they never block workers¶
Status: accepted · Date: 2026-08-14
Context¶
Vibey inherits a hard rule from the *loop family: never block on a human.
Those runners enforce it by denying any tool call that would ask a question,
pushing the model to proceed on a stated assumption instead.
But vibey's Phase 1 and Phase 3 are defined as conversations with a human. The rule and the requirement appear to contradict.
Decision¶
They don't, once the rule is read precisely. The rule is never block a worker, not "never involve a human." Vibey inverts control:
- Vibey owns the conversation, not the engine. Engines are used for the autonomous parts of Phases 1 and 3 (research, synthesis, demo generation, triage). The interactive turn-taking is vibey's.
- A handler that needs a human returns
Park(gate). The worker writes ahuman_gaterow, sets the job toawaiting_human, and releases its lease and picks up the next job. No thread waits on stdin. - The developer answers via
vibey answer <gate_id>, the TUI, or a notification action. That writes the answer, flips the job toready, andNOTIFYwakes a worker.
Rationale¶
Parking rather than blocking means an unanswered Phase 1 question does not stall the three research jobs running in parallel, and an unanswered Phase 3 triage question does not stall the rest of the queue. The developer can walk away mid-interview and come back; work that does not depend on the answer keeps going.
It also preserves the property that makes Phase 2 safe: no code path anywhere in vibey waits on a human, so a worker pool cannot be deadlocked by an absent developer.
Gate anatomy¶
@dataclass(frozen=True, slots=True)
class HumanGate:
gate_id: UUID
job_id: UUID
kind: GateKind # question | approval | escalation | budget | handoff_failure
prompt: str
options: tuple[GateOption, ...] # structured choices wherever possible
default: str | None
timeout_at: datetime | None
Structured options wherever possible. A gate that offers three labeled choices is answerable from a phone notification; one that demands prose is not.
Timeout defaults. A gate with a default and a timeout_at auto-resolves,
which is what makes overnight runs viable for low-stakes decisions. Every
auto-resolution is recorded as an AssumptionStated event, so the decision is
visible, travels through every handoff, and is checked by gate rule R4. An
assumption taken at 3 a.m. because nobody answered is exactly the kind of thing
that must not vanish.
Consequences¶
Good. Interactive phases and a never-blocking worker pool coexist. Parallel work continues around an open question. Gates are durable — a crash does not lose one.
Bad. More states in the job machine (awaiting_human, awaiting_capacity) and
a UX surface to build for answering. A gate raised with no notification configured
can sit unnoticed; vibey status surfaces open gates prominently and the TUI
shows a count.