Hangar

LunarCrush · Design and architecture, operator · Aug 17 to Sep 4, 2026 · Ongoing

The platform behind every LunarCrush coding agent: one runtime, agents defined as data, pipelines as dependency graphs, and a patrol that repairs its own failures. In 17 days it merged 121 tickets to staging.

LunarCrush correlates what the world is saying on social media with what is moving in the markets. I have led design there since January 2021, and since the 2026 rebuild I also hold governance of the front-end. Hangar is the piece of that governance that does not look like design: the platform that runs the autonomous coding agents which now deliver most of the product tickets to staging. This is the story of why I built it, how it is shaped, and what it took to make it run without me in the room.

A hangar is where the aircraft are kept, maintained and dispatched from. They fly out, do the work, and come back to the same building. Nobody builds a new hangar every time they need a new plane. That sentence is the whole architecture.

Why one building

By August 2026 we had two agents in production. One took engineering tickets from Linear, the other took design tickets. They had been written months apart, by me, from the same starting point, and each lived in its own repository on the same server. On paper that was two services. Measured against what was actually running, it was one agent copied twice, and the copies had started to drift in the parts that matter.

Ten of the shared modules were byte-identical. The worker, the dispatcher and the worktree manager differed by 235, 364 and 122 lines. The cost was not theoretical: the design agent carried a fix for Linear answering with a Cloudflare HTML error page instead of JSON, and the coding agent never got it, while the runbook stated that both parsed defensively. One agent was documented as protected and was not. That class of bug is what a shared core removes by construction.

There was a second pressure. Beatmap, my studio, needed the same kind of agent for a different product: a different Linear workspace, a different base branch, a staging that deploys by direct upload instead of git push. Forking the LunarCrush agent a third time would have made the drift permanent. So on Aug 17, 2026 I started Hangar with one rule written before any code: the engine knows nothing about any product.

The engine and the product

The repository is split in two halves, and the split is load-bearing. Everything under packages/ is true for every agent: the runtime, the pipeline engine, the router, the knowledge loader, the patrol, the inbox. Everything under projects/ belongs to one product: its repositories and their branches, its fleet of agents, its skills, its knowledge, its deploy. Adding a product is a folder, not a fork. LunarCrush and Beatmap live side by side today and agree on almost nothing.

An agent is a prompt, a list of skills and a set of grants. That is a file, not a service. The LunarCrush fleet is specialist-first: frontend reliability, product interface, web performance, product feature, API integration, technical SEO, brand imagery, plus the roles that run pipeline steps without owning tickets: spec writer, planner, plan reviewer, code reviewer, intake router, and the patrol itself. Fourteen definitions, each a short YAML, each versioned next to the code.

Skills compose in two layers. The engine ships the rules that are true of the craft rather than of a product: branch discipline, the verification bar, the escalation contract. Every project inherits them. A project skill with the same slug shadows the base one, so adapting a rule for one product never means editing the engine, and the override is reported rather than silent. Beatmap's own staging-is-direct-upload skill is the shape of this: a rule that is true there and false at LunarCrush, sitting next to five inherited ones.

Validation is the point of making all of this data. A skill an agent names but that does not exist, a typo that would silently drop a grant, a base branch left out of its own protected list, a template variable nothing defines: each fails at load, naming the file and the field. A prompt that reads "never touch " with nothing after it is worse than a startup error, because the agent reads it as no restriction at all.

The route a ticket takes

The operator submits an outcome, not an agent name. Intake reads the ticket and returns a structured, auditable decision: which specialist, which lane, what complexity, how confident, and which canonical knowledge to load. The model cannot invent an agent, a repo grant or a template; its answer is validated against the registry, and a heuristic router remains as the fallback when no model is available. I can force an agent or a lane, and Hangar keeps the override and reports the risk instead of ignoring it.

Knowledge is also data. Fourteen entries declare authority, priority, routing signals, affected paths and enforceable rules: foundations, the design system, the tables standard, the webapp performance playbook, OAuth and reload safety, CI and tests, Stripe checkout. Foundational entries load for every ticket. The rest load only when the ticket or the paths it touches make them relevant. The selected packet is persisted on the run and prepended to every step prompt, with links to the canonical documents in the product repository rather than an untraceable copy.

A pipeline template is a dependency graph, not a long prompt. Every step names one agent, one required artifact, its dependencies, and whether a human must approve before downstream work starts. LunarCrush has two lanes. The fast lane is specialist, then reviewer, then me. The deep lane is spec writer, planner, plan reviewer, specialist, reviewer, me. Risk overrides apparent size: auth, payments, permissions, migrations, infrastructure, destructive data work and public contracts always go deep.

The engine persists each run with an atomic write, so a new process resumes the same run without reconstructing state from a chat log. It refuses dependency cycles, unknown agents, duplicate artifacts, premature completion and empty approvers. If an executor dies after claiming a step, the step returns to ready without erasing the attempt, and the next executor starts a new one instead of pretending no work ran.

Execution runs on subscription seats, not metered API keys. The host declares an account chain: Claude Code through its OAuth subscription, Codex Business and Codex Personal through isolated homes, Grok, and a free OpenCode seat at the end as the net that keeps the system moving when everything paid is out of quota. The scheduler filters by role, credentials, concurrency and cooldown, then walks priority order. A busy or exhausted seat fails over without changing the assigned agent or workflow. A genuine implementation failure remains a failure and is never hidden by asking another model.

What done means

The hardest part of the project was not making agents write code. It was defining done in a way a machine could check and a designer could trust.

The first definition was merged plus CI green. On Sep 2, 2026 that definition let two tickets with real type errors reach staging. The service ran with the production environment flag, under which the install step skips dev dependencies. The app's TypeScript 5 is a dev dependency, so the type check silently resolved to the 4.9 version hoisted at the monorepo root, which rejected the config with two diagnostics and read no source file. The base branch produced the same two lines, so "no new errors" passed every branch. The gate had been green for weeks and had checked nothing.

The rule that came out of it is the one I am proudest of: a verification step must prove it ran, not only that it found nothing. Delivery now runs the project's own compiler by path, refuses a compiler below the declared major, treats configuration diagnostics as "the check did not run", and parses any workflow file the branch touched before publishing. A gate whose clean result is indistinguishable from not executing is not a gate.

The second definition was proof of effect. On a review round of 33 tickets, 9 bounced, all for one reason: the pipeline verified code, and the tickets had promised effects. A cron that never registered because it lived on the wrong branch. A monitor whose own failure was swallowed. A CDN seeding step that shipped with zero objects seeded. The delivery gate now bounces document-only deliveries for implementation work, checks cron placement, and asks for machine-checkable assertions of the effect the ticket promised.

The patrol

An earlier version of Hangar needed me every time something new went wrong. The fallback rule for an unknown failure was literally "add a rule for it so the patrol handles it next time", which meant every new class of failure cost an interruption. I wrote the replacement rule on Aug 22, 2026 and it has held: technical problems are solved alone; product questions are decided, executed, and written down on the ticket with what was chosen, what was discarded and the fact that decided it. Only a missing credential or a tripped safety guard reaches me, because no reasoning substitutes for a secret and none overrides a guard.

The patrol is the component that makes this true. It runs on a timer, reads every flight, classifies each failure against a catalogue of causes, and acts. A stale worktree, an incomplete dependency snapshot, a launch that crashed before any step ran, a subscription window that closed, a transient install error: each has a retry disposition that costs no model time. A reviewer that blocks without saying what is missing, an implementer that answers the same finding with "no change needed", a task that exhausts its turn budget: each escalates once, with the cause and the next step, and never for a retry the patrol is about to make itself.

It also watches the product's own health. Four smoke workflows run against production and staging every six hours and after every deploy. A red check is rerun once, silently. If it stays red, the patrol opens a ticket with the failed steps and both run links, delegates it to Hangar, and tells me once. When the fix reaches staging I get two buttons on the phone.

One inbox, one channel

Until Sep 3, 2026 I was being reached through three doors built at different times: approvals on Telegram, red checks from GitHub posting to a Slack DM with their own token and wording, and patrol escalations as a second DM bolted on the day before. Three shapes, two tokens, and a question I had to ask every morning: where do the decisions arrive, and how do I answer each one.

The rule now is that the inbox is the only record of what the operator must see, and it has exactly one outbound channel. A question is written locally before any notification is attempted, so if Telegram is offline the question stays open and the failed delivery is recorded. No component talks to the operator directly. Adding a check means one line in the project config. Adding a channel means one adapter, never a second call to a chat API inside a package.

Every escalation carries three buttons. Accept moves the ticket to product review with the reviewer's findings. Request changes takes a text reply on the same message, posts it as a comment and sends the ticket back so Hangar reworks with it. Relaunch starts over on a fresh worktree. I answer most of them between other things, from the phone, in one line.

What it cost to learn

Hangar was built in the open, on a product that could not stop. The operating rules say that the running agents are never restarted to deploy a Hangar change, and that production is read-only until a change is proven. So every design mistake showed up as an incident, and every incident is written into the tree as a rule.

The chain of Aug 27 uncovered 17 failures sharing one root: the system assumed all work is LunarCrush, implemented as code, merged to staging. Anything else fell through a silent hole. The control plane once accepted delegations for 11 hours without executing or logging anything, because a notification call to Linear ran before the seat was taken and had no timeout. Approving a report nearly published uncommitted code, because delivery staged the whole worktree. The lessons I wrote that night have outlived the fixes: a silent failure is worse than a loud one; a broken resource declared available is worse than none; a guard that cannot distinguish causes blames the innocent; state in memory is state that will be lost.

The chain of Sep 2 was different. Forty-four patrol actions in one day, four hours of my time, and not one of the underlying defects was in a ticket's own code. Half the actions were "unclassified" launch crashes the patrol retried through a full model cycle because it had no rule for a stack trace. Four deliveries were escalated as credential failures because the rule matched the word "Authorization" in a checkout log. Good work was sent back to implementation because a broken linter on someone else's commit read as "the staging build failed". The standard I wrote afterwards is the one the system is now measured against: every gate proves it executed, every failure the host can cause has a retry that costs no model time, a red base branch holds deliveries instead of bouncing them, and the operator is paged once per problem.

The numbers

Between Aug 19 and Sep 4, 2026, Hangar merged 121 distinct tickets to the LunarCrush staging branch, in 599 commits under its own git identity. The platform itself is 377 commits and 21.973 lines of TypeScript across 14 packages, with 14 agent definitions and 14 knowledge entries for the LunarCrush project and a second project, Beatmap, proving the engine is portable. Production promotion stays manual by rule; the button exists for an emergency read on the phone.

Where the designer is in this

The people who run this product are designers. That sentence is in the operating rules, and it decided more of the architecture than any technical constraint. An engineering failure goes back to an engineer with the failure written on the ticket. Only a missing credential, a tripped guard or a product decision is worth interrupting a designer for. The inbox, the three buttons, the once-per-problem paging, the rule that a step never fails for the shape of its report: all of it is interface design for an operator whose attention is the scarcest resource in the system.

I did not write most of Hangar's code by hand. I designed the operating constraints, the data model for agents and pipelines, the definition of done, and the incident reviews, and I directed Claude Code and Codex through the implementation, reading every diff that touched a rule. The design work is in what the system refuses to do.

Closing

The agents are the part that gets attention. The building they come back to is the part that holds.

Latest updates

Nicolás Bañados