Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decisions for the first runnable release

Fifty questions were put to the maintainer as a worksheet and answered in one pass on 18 September 2026. This file carries the reasoning behind each answer — why, not what, in the same shape as the decision log. Where an answer was close, the counter-argument is recorded too, because a decision whose alternative has been forgotten cannot be revisited honestly.

The first runnable release was 1.0, on 23 September 2026, and most of what is decided here is built. This file is kept as the record of what was decided before it and why; it is not updated as the code moves on. Later decisions, what of this is still unbuilt — worktree isolation, resident agents, parts of the Reserve — and the questions still open are in decisions.md. Known hazards are in risks.md; the design itself in architecture.md.


Starting a run

Why the flight body goes last in the payload. A run may be handed five things — instructions, memory, learnings, a handover, and the message that woke it. Models weight the start and end of a context differently, and whatever arrives last reads as the current instruction. The body is the instruction; everything above it is context for carrying it out. The handover sits immediately above the body because "you are continuing work that did not finish" only means something next to what the work is, and learnings sit above the handover so a recovery instruction is never buried under twenty-five lines of accumulated advice. Who sent the flight was added later, directly above the body, because it is part of the message rather than context for it. Implemented in payload.rs, with the order pinned by a test rather than left to whoever edits next.

Why memory is injected rather than fetched. The alternative — an agent calling layover_memory_read() when it wants its own notes — is cheaper and explicit, and it fails silently: an agent that forgets to call has no memory, and nothing anywhere reports that. Since fresh runs are what make memory deliberate in the first place, a memory system that quietly does not work would undo the decision it was built to serve. So the tail is always injected, capped, and says when it was cut; the whole file remains a tool call away. The tail rather than the head because the end of the file is the most recent thing written, and a memory that kept only its oldest entries would get less useful the longer an agent ran.

Why model reaches the CLI through a {model} placeholder. Each supported CLI spells the flag differently — --model, -m, a config key. A model_flag field on the runner would put half an invocation in one place and half in another; the placeholder keeps the runner command the single thing that knows how to invoke a given CLI, and lets the operator write --model x or --model=x as theirs requires. The failure it creates — an agent declaring a model whose runner cannot carry it — is caught by validation rather than discovered from a bill.

Why the Tower tells an agent its own name. Three words, and without them every prompt file hard-codes the name of the agent it belongs to, which drifts the first time one is renamed. It has to come from the Tower for the same reason everything else about identity does: a name an agent asserts is a name an agent can get wrong.

Why output is streamed to disk rather than collected at exit. One write path, a transcript that survives the Tower dying mid-run, and a live view for free. The cost is that parsing a runner's cost reporting becomes incremental rather than a single read, which is fiddlier for stream-json formats than it sounds. Idle detection — distinguishing a run that is thinking from one that is wedged — is deferred: it needs per-runner knowledge of what counts as output, and wall-clock timeout_sec is a coarse but honest stand-in until then.

Ending a run

Why Ground Stop signals before it kills. Killing immediately corrupts whatever a read-write agent was half-writing, and an operator burned by that once will hesitate before using the kill switch again. A kill switch people hesitate over is not a kill switch. So: block new spawns at once, signal, then kill after a grace period. Parked barriers survive, because a Ground Stop is a pause — you engage it to look at something — and losing parked work would mean the factory cannot be safely paused. Tokens are revoked, because otherwise a child mid-call can still send a flight after the factory has stopped, which would make the stop a lie.

Why a dead-end run is a success that poisons its barriers. An agent that exits without sending anything has not failed; not every agent forwards work, and marking it failed would make the dashboard cry wolf. But a barrier waiting on that agent can now never be satisfied, and the itinerary has to learn that immediately rather than waiting forever. Silent permanent stalling is the worst outcome in the system, so the run is recorded as fine and the barriers it stranded are marked unsatisfiable at once.

Why a timeout does not trigger recovery. Recovery exists for interruptions, where the cause has gone away — the machine restarted, the Tower died. A timeout means the work was too big or something is wedged, and both of those repeat identically, so retrying spends money to arrive in the same place. The run is killed, marked timed_out, and the sender is told, so a loop can decide what to do about it. An agent's recovery = "automatic" deliberately does not override this: a timeout and an interruption are different events and deserve different defaults.

Why a crash reuses the recovery policy. A crash and an interruption are the same shape of problem — a run that did not finish and left no verdict — and recovery already exists to say whether repeating a given agent's work is safe. A second, parallel policy would be two knobs for one question. The difference is that a crash leaves an exit code, so the handover can carry it: "the previous run exited with status 137" is a useful thing for its replacement to know.

Why an itinerary is done when it goes quiet. Quiescence — no run live, no barrier parked — needs no cooperation from any agent, which matters because agents are exactly the part that cannot be relied upon to report. An agent may still end a chain explicitly. This also forces an itineraries endpoint into existence, which is overdue: stalled is an itinerary state with nowhere to live, and the dashboard cannot currently distinguish a finished chain from a quiet one.

Concurrency

Why two simultaneous triggers make two itineraries. Deduplicating needs a notion of "the same request" that nothing supplies, and getting it wrong silently drops work somebody asked for. Two people triggering the same pipeline a second apart probably both meant it.

Why Hops remains the only bound on a loop. A per-route loop counter would be a third depth rail to explain, and the vocabulary is already the largest comprehension cost in the project. The real fix is arithmetic the factory author can do — the reference factory carries it as flights = 2N + 6 — so validation warns when a cycle exists and max_hops leaves room for fewer than two rework rounds. The counter-argument is real and was weighed: exhausting Hops mid-repair leaves half-finished work in a workspace, and "we warned you at load time" is thin comfort when it happens.

Why the Tower enforces barrier reset. A loop-back must re-dispatch the whole fan-out; if the developer sends only to the tester on a second pass, the reviewer's slot never fills and the chain stalls forever. Leaving that to prompt text made correctness depend on an agent remembering an instruction. The dispatch-wave tracking needed to enforce it already exists — it went in to stop join = "any" firing once per upstream — so the Tower resets a barrier that receives a flight from a new wave. Detection stays as the safety net: a barrier that becomes unsatisfiable marks the itinerary stalled immediately.

Why joins still wait for every declared upstream. Dispatch-aware barriers have a genuine race — a fast upstream can release the barrier before its sibling is dispatched at all — and closing it needs a dispatch window in the most correctness-sensitive part of the system, where a mistake produces exactly the silent stall everything else here is designed to prevent. The workaround costs one cheap run per skipped specialist. It stops being a good trade when a factory has several rarely-needed expensive helpers, and that is the signal to revisit.

Why a scheduled pipeline skips a tick it is still working on. Starting a second copy means paying twice for one result, and possibly two agents writing to one workspace. Skipping means being an hour late. For unattended spending those are not comparable. overlap = "allow" opts in, and a skip is logged — a schedule quietly skipping every tick because its job always overruns should be visible.

Restarts

Why run state is written before the process starts. A run that was live when the Tower died leaves no exit code, so the only way to know it existed is to have said so first. The record says what should be running and a liveness check confirms it is gone, which feeds the distinction Interruption already makes between "confirmed gone" and "unknown". The PID is stored with the process start time, because PIDs are reused and recovering into a world where something else now owns that number is worse than not recovering.

Why a handover carries memory and sent flights. Both are facts the Tower holds without interpreting anything. "You already sent a work item to the developer" is the single most useful thing a resumed run can know, because it is precisely what it would otherwise duplicate. A transcript summary would need a model, which Layover does not have; the last N lines of output are usually the middle of a thought.

Why a parked barrier is discarded on restart. Keeping partial state means waiting for upstreams whose runs no longer exist and will never arrive — a stall dressed as patience. Re-dispatching repeats work and costs money, but it terminates. The itinerary records that it was recovered, so the repeated work is explicable rather than mysterious.

Why a Layover cannot be booked past the retention horizon. Work that wakes up after its own history has been pruned wakes into an itinerary it cannot see the past of, which makes its handover a stub. Defaulting the limit to the horizon keeps "a resumed chain can read its own history" true by construction.

Money

Why an implausible cost is treated as unreported. Negative, NaN and infinite figures are already ignored and $0 alongside real tokens is already treated as silence, but a plausible-looking figure an order of magnitude too low is believed — and Fuel and the Reserve then under-count together. Marking it Unreported reuses machinery that already does the right thing: an unreported run downgrades the confidence of every total it appears in, so the dashboard says "this is a lower bound" instead of showing a precise-looking lie. Substituting a rate-card estimate would invent a different number with no better claim to truth; refusing the runner outright is too blunt for what might be one malformed line.

Why spawning gets a count-based cap as well as the Reserve. The Reserve is the right factory-wide rail conceptually and it depends on runners telling the truth about cost. A count of spawned itineraries is something the Tower knows for certain. When the honest rail and the cooperative rail disagree about how much is happening, the honest one needs to exist too. The cost is a fifth number bounding breadth, and a scanner that legitimately finds forty pull requests now reviews only the first N — which is its own kind of quiet wrong, and the reason the default is generous rather than tight.

Why Fuel gets a floor rather than a reservation. Metering after the fact lets an itinerary overshoot by one full invocation, and with fan-out by several at once. Proper two-phase reservation — estimate, reserve, reconcile — is the correct answer and makes the ledger substantially more complex, including a new question about what happens when the estimate was wrong. Refusing to start a run when remaining Fuel is below a floor is most of the benefit for a fraction of the work: it stops a chain with two cents left starting a four-dollar run. The real fix stays recorded as a risk rather than being quietly renamed as done.

Why a Reserve refusal has to be loud. "Nothing is running" and "nothing is allowed to run" must not look the same, or an operator spends an afternoon debugging a factory that is working exactly as configured. So: a banner naming the time the window rolls forward, a warning at 80%, an automatically raised help request — because that is where an operator already looks for "why is this stuck" and it puts the event in history — and a cap that can be raised by editing config and reloading, because hitting a ceiling at 2am should not mean bouncing the Tower and losing running work.

Why a dangerous factory is confirmed rather than forbidden. Every rail is configurable, so every rail can be configured to be useless. A compiled-in absolute ceiling would be wrong for somebody and would be worked around by exactly the people it was meant to protect. Showing the implied worst case in money and runs at the moment of the decision, and requiring an acknowledgement recorded in state, educates instead of forbidding. The counter-argument was weighed: for a tool whose failure mode is a five-figure bill, a hard ceiling with an explicit escape is defensible, and this is the entry to revisit if one ever gets close.

Trust

Why credentials are injected per run rather than inherited. Inheriting the Tower's environment means the telemetry agent holds the publishing credentials, the reviewer holds the cloud keys, and a single prompt injection reaches all of them. Injecting exactly what an agent's env_from names is the entire reason that field names variables instead of holding values, and it makes revocation meaningful: change one variable and the next run of one agent is affected.

Why the per-run token is the identity. A token that can be derived from the agent name is not a token. It is minted per run, random, passed in the environment, and the Tower looks up which agent and which itinerary it belongs to — so the child never asserts either, which is what §4.2 requires. It expires with the run, is revoked on Ground Stop, timeout, crash and recovery, and an unrecognised token is refused and raised as an incident rather than dropped: it means a bug or a child that outlived its run, and both are worth knowing about.

Why learnings are delimited, filtered and gated by impact. A learning applies to twenty runs with no human in the loop, and its text comes from an agent whose own input may include a work item or a pull request comment. That makes it a more durable foothold than any single prompt injection. Expiry already bounds the blast radius and an echo cannot confirm a learning; what remained was that the text itself was trusted. Delimiting it as untrusted costs nothing and helps most; refusing text that names tools, URLs or credentials or tries to override earlier instructions costs a few false negatives; confirming high-impact learnings before they apply costs a human press on the rare case that warrants one.

Why the prompt sandbox canonicalises. Lexical confinement handles .., absolute paths and UNC, and does not handle a symlink inside the prompt directory pointing anywhere at all. Today prompt files are reviewed repository content and anyone who can plant a symlink can also set runners.*.command, so it is not yet a boundary — but it becomes one the moment agents write their own prompts, which is a stated goal, and by then it is load-bearing. Doing it now is a small change with no downside; doing it later means doing it under pressure.

Why the local API is authenticated by default. Loopback is a real boundary and it stops being a sufficient one when the thing behind it can spend money — a shared machine, or a web page in a browser reaching localhost. Minting a token at startup and printing it in the dashboard URL is a proven pattern for exactly this situation and costs the operator one copy-paste. --no-auth exists for people who genuinely do not want it. The counter-argument: it is friction on the most-used command of a tool one person runs on their own laptop.

Why a factory definition is made visible rather than gated. A layover.toml names runner commands, so running someone else's is arbitrary code execution by design — it is a script that says which programs to run. A hard --trust gate would be annoying for the ninety-nine percent case of your own config, and annoyance is worked around with a flag people stop reading. So explain shows the runner commands prominently, validate warns when a config's hash differs from the last one seen and names what changed, and the documentation says plainly that a factory definition is code.

Platform

Why Windows is first-class. It is where the project is developed, and it is where the hard parts differ — killing a process tree is not sending a signal to a process group. "Best-effort" on the platform the author uses daily is a promise that quietly becomes false. CI runs there and a Windows failure blocks a release.

Why the Logbook stays one file. It is meant to be read by a human during an incident and edited by hand when an agent records something wrong; splitting it per agent to avoid a lock trades away the property that justified the format. Serializing every write through the Tower keeps both, and append-only removes the mode where two agents rewrite the same file and one silently loses.

Why structured logs come before traces. An itinerary is obviously a trace and a run obviously a span, and fan-out and joins are exactly what trace viewers are good at — but OpenTelemetry is a dependency, a collector and an operational story, and the thing has to run before it needs beautiful traces. Emitting tracing spans with the itinerary and run as fields from the start makes adding an OTel layer later a subscriber change rather than an instrumentation rewrite.

Shape

Why the whole aviation vocabulary stays. Reversed on 18 September 2026. The original answer kept the terms that name something plain English has no word for — Hops, Fuel, Ground Stop, Layover, Itinerary, Slots, Reserve — and dropped Tower, Hangar and Logbook as decoration over a supervisor, a state directory and shared memory. A design review had named the vocabulary the single biggest comprehension cost for a newcomer, and that reasoning still holds.

It was reversed on a different axis: cost of churn against capability. Renaming touches config, the API, type names and every page of prose, and buys a week in which nothing the project does gets better. Set against a supervisor that did not yet exist, that was the wrong week to spend — and the comprehension cost, while real, was paid by readers who did not exist yet either.

The consequence is accepted rather than argued away: the vocabulary is now load-bearing in the public API, so this stops being nearly free from here. Somebody will one day read Hangar and have to look it up.

Why entry = true stays. Reversed on 18 September 2026. The original answer removed it — two ways to say how work gets in is one too many, and a pipeline with no flags says what entry = true says. Removing it would also have removed the to field from POST /flights and a branch from flag resolution.

Same reasoning as the vocabulary: it is a simplification, not a capability, and it changes the config schema and the API for factories that already exist. The counter-argument recorded when it was first decided now carries the weight — entry is a permission and a pipeline is a named way in, and they are not quite the same statement.

Why a factory stays one file. One file means the whole factory is readable at once, which is what makes explain and the route map comprehensible. The reference factory is 259 lines and that is not yet a problem. Adding include later is easy and backward-compatible; removing it later is neither. The risk taken is that if factories routinely reach twenty agents, the pain arrives during this release's life rather than after it.

Why the MCP surface is fixed and enforced. Prompts and documentation named eleven tools, none of which existed, with names that disagreed between the prompts, the book and the code. The surface is now settled — send, peers, report, help and memory as the minimum; learn and logbook next; status and wait if they earn it; spawn never, because mode = "spawn" on a route already covers it and a tool would be a second permission model over the same graph. The part that matters beyond the list is that the registry lives in code and validate rejects a prompt naming a tool that does not exist, which is exactly the check that would have caught the drift.

Why the dashboard gets four write controls. Ground Stop, because a kill switch reachable only by creating a file by hand is not one anybody reaches for in a hurry. Cancelling a queued flight, because without it the trigger button is one-way. Confirming and rejecting a learning, because the documented justification for applying learnings without approval depends on revocation being one press, and shipping with that still untrue would repeat the exact mistake this project keeps finding in its own documentation. Resolving a help request, because otherwise the open count only grows. Steering a running itinerary and retrying a failed run wait.

Release

Why the crates are published and marked not-public-API. layover-cli was unclaimed, and the downside of squatting is somebody shipping malware under a name the documentation tells people to trust. Publishing claims it. Marking the library crates as not public API in their own documentation keeps the freedom to restructure internals without a major bump, which matters enormously while the supervisor is still being written — and it is what makes single Apache-2.0 licensing correct, since the Rust dual-licence convention exists to let GPLv2 projects consume libraries.

Why provenance and not signing. A checksum proves a download was not corrupted; it says nothing about who produced it, and it sits beside the file it describes. Build attestation turns "here is a hash" into "this was built from that commit by that workflow" and is free. Authenticode and Apple notarization cost money and key management and buy the removal of a scary dialog — worth it when there are enough users for the dialog to matter, not before.

Why the maintainer protects the branch against themselves. The objection to branch protection in a single-maintainer repository — there is nobody else to protect against — cuts the other way. The person most likely to tag a half-finished commit at 2am is the only person who can, and a guard rail set for yourself is the only kind available. Requiring CI to have passed, restricting who can create v* tags, and requiring a tagged commit to descend from main cost nothing during normal work.

Why the broken installers are documented rather than patched. The generated shell installer skips verification when sha256sum is missing — which is stock macOS — and the PowerShell one does not verify at all. Patching 1,598 lines of generated script creates a maintenance burden that rots silently the first time upstream restructures it, and a silently rotted checksum patch is worse than none because the documentation would again claim something untrue. So: report it upstream, say plainly what is and is not checked, and give a fail-closed manual path.

Why the on-disk layout is versioned. Without a version key, two releases silently disagreeing about the format is a data-loss bug waiting for its moment. A newer layout is refused rather than read hopefully; an older one is migrated forward once, and says so. There is no self-update: a tool that rewrites its own binary while potentially supervising running children is a category of bug nobody needs, and the installers already handle it. (Revisited on 15 October 2026: Layover may update itself, but only once nothing is running and no rendezvous holds work — the condition this paragraph names. The reasoning is in decisions.md, under "Why Layover looks for its own updates after all".)

What the first runnable release had to prove

Proven for 1.0. The soak ran 48.46 hours against a throwaway repository, one process and no intervention: 1,501 runs, all succeeded, and 12.6 MB of memory at the end. The figures are in the 1.0 notes. What it could not catch — the slow failures below — is still watched rather than proven.

Why "it ran for a week" rather than "it worked once". The reference factory completing end to end is the criterion the design was sized against, and it is not sufficient. This project claims lights-out, and every interesting failure in the system is a failure of the second day: a stalled barrier, a schedule overlapping itself, a run interrupted by a laptop sleeping, a Reserve window rolling over. None of those appear in one successful pass.

The bar is 48 hours unattended, revised down from seven days on 18 September 2026, with the operator only reading the dashboard. Any intervention resets the clock and becomes a bug.

Forty-eight hours is a compromise and worth naming as one. It catches the second-day failures — two nights, at least one of every schedule, a Reserve window rolling over more than once — and it does not catch the slow ones: a log or a journal growing without bound, a leak that takes a week to matter, a retention horizon being crossed. Those move from "proven" to "watched after release".

The soak runs against a throwaway sandbox repository with synthetic work items, not anything real. A first unattended run pointed at a repository somebody cares about would mean the first thing the factory ever does unsupervised is the thing hardest to undo.

Why a generic runner moves into scope. "We support these three CLIs" is the most likely reason somebody forks this instead of using it. A runner defined entirely by its command, its stdin behaviour and its cost parsing makes a new CLI a config entry rather than a code change, and it is mostly machinery that has to exist for the three supported ones anyway.