AS
All work

Project 01 · August 2026

Veranda

An autonomous coding agent that turns tickets into pull requests

A model writing a patch is the easy part. The hard part is a two-hour, side-effectful job that has to survive a pod rollout, a revoked API key, a flaky integration test, a red CI run and a reviewer who asks for changes three days later. This is what I built around the model to make that work.

GoLLM AgentsRAGTemporalKubernetesDockerMCPPlaywrightCI/CDObservability
Multi-model
Claude · Codex · Gemini · Kimi · Grok · GLM · Composer
Workflow
~40 durable activities
Durability
Resumes mid-run after a pod rollout
Bounded
5 review rounds, 1 CI fix, then a human

What it is

Veranda is a coding agent that runs as a service rather than in a terminal. You assign it a Linear ticket or mention it in Slack; it clones the repository, implements the change, validates it with the repo's own build, lint and test commands, opens a pull request, reviews its own diff and pushes it through CI. Then it waits for human comments and revises, unwatched.

The model behind it is swappable, and that is the point. Claude, Codex, Gemini, Kimi, Grok, GLM and Cursor's Composer all run the same work; a ticket can pin one with a label, and if it is unavailable the run moves to another by itself. Nothing above the adapter layer knows which vendor ran.

Linear poller30s tick · label-gatedSlack socketoutbound · no ingressdedup gateDurable workflow · ~40 activities1. Setup1.6 red testsClone · Analyze · Inject prompt · Baseline coverage + tests2. Agent2.5 test agentRun the coding CLI · graceful deadline inside a hard timeout3. Validationearly shipBuild · Lint · Unit · Integration · Coverage, then draft PR4. Ship≤5 roundsSimplify · Review · Fix loop · Mark ready · Watch CI · Fix CI6. Feedback≤10 roundsWait for human comments · Apply · Re-review · PushPull requestdraft at phase 3ready at phase 4
One run, end to end: four phases plus a bounded feedback loop.

Picking the repository, and building the context

Two questions have to be answered from a sentence someone typed in a hurry: which repository is this about, and what does the agent need to know that is not in the code it is about to change.

The repo is inferred, not guessed

Detection scores four signals: an explicit declaration, a GitHub URL, a label, and last, a repository name in prose. The first two count as high confidence and prose as low, and on low confidence it asks in a comment instead of proceeding, because a well-tested pull request against the wrong service is worse than a question. A cheap-tier model pass then confirms or corrects that guess, and returns whether the work needs a proto, a migration, or visual verification.

Context arrives in four layers

None of it is prepended to a prompt. It goes into the clone as a file the CLI reads as project instructions. Each layer is narrower than the one above:

  • A generic prompt for any repository: security rules, commit conventions, when to split work across sub-agents.
  • Organisation runbooks from an architecture repo, narrowed to the ones the ticket touches: the how-we-do-things-here a model cannot read off the source.
  • Per-repo hints, for what only that repo knows. Regenerate the queries after a schema change; the integration tests need Docker.
  • Facts read off the repository: lint config, build tags, whether integration tests need containers.

They stay separate because they have different owners and go stale at different rates. Merged into one prompt, no one can tell which line is still true, so no one deletes anything. The filename itself depends on the backend, since each CLI reads its own, and it is kept out of the commit so the instructions never ship inside the PR.

Grounding the prompt in code that exists

Asked about a codebase it cannot see, a model will produce a plausible file at a plausible line number. The answer is not a longer prompt, it is retrieval: before any design or plan gets written, the ticket is turned into evidence pulled out of the real repositories.

Lexical retrieval, and no embeddings on purpose

Seeds are extracted from the ticket and the selected expert dossiers by regex, backticked spans and camelCase identifiers, capped at eight. Each one is searched against GitHub's own code index, scoped per repository, and hits come back as twelve line windows around the match. There is no vector store, because an identifier is a lexical object and exact match already answers the question that embeddings would answer worse.

The pack is bounded, and bounded in bytes

Twelve searches, six files, twelve thousand bytes. Those are byte ceilings rather than token budgets by choice: there is no tokenizer in the service, and a cap that is exact and free to enforce beats a token estimate that has to be recomputed per model family and is wrong for at least one of them.

A citation the model is structurally unable to fake

Every reference the agent is allowed to cite is created by the retrieval tool itself and never by the model, so the citable set is closed by construction. Whatever it cites anyway is scraped back out of the draft and resolved against the default branch, and anything that does not exist is flagged in the output instead of shipping as a confident wrong line number.

Multi-model, and what makes models substitutable

Seven model families do the same work here. Making that swap mean anything is the actual engineering, because the vendors agree on almost nothing that matters.

Where they refuse to be the same

The vendors' CLIs are incompatible in every detail that matters, so the shared interface is kept deliberately tiny. They disagree on how they authenticate, on the instruction filename, on whether they can see an image, on what a reasoning-effort knob is called, and on what a model is even named. The port covers only the three things they genuinely share, and every difference stays inside its own adapter, because widening it to cover the rest is how you get an abstraction that lies.

A model id is not portable, so tiers are the currency

Model names do not cross vendors, so the system picks a tier and resolves a concrete id only for whichever family ends up running. A task asks for high, not for a name, so if the run moves to another vendor it still asks for high and that vendor's own model answers. The exception is a model someone pinned by hand: it cannot survive the move, so it is dropped and the ticket says what ran instead.

Health is measured, not configured

A configured key proves nothing, so every backend gets a real cheap call and the verdict, classified as quota, auth, a failed turn, an upstream error or unreachable, is what drives the fallback. The ping names no model on purpose, testing the key against the CLI's own default rather than a pinned id that may itself have been retired and would condemn a healthy backend. When something is down another mode of the same vendor is tried first, because staying with the vendor keeps the model, and the reason follows the run into the metrics and onto the ticket.

Capabilities are negotiated, not assumed

Each adapter declares what it can do, and when it cannot the system degrades in one place instead of failing. Reasoning effort is normalized once and mapped to each vendor's own knob; an adapter that cannot see images has them stripped and runs text-only rather than erroring. Writing that rule once means a new adapter inherits it for free, provided it answers honestly, and the dangerous one is the adapter that claims vision it does not have.

Giving the agent a browser

A backend agent checks its work by running tests. A frontend agent cannot, because the interesting failures are visual. So for frontend repos the workflow boots the app before the agent runs, and hands it a browser through MCP: navigate, click, type, snapshot the accessibility tree, screenshot, read the console and the network. Non-frontend runs keep the backend allow-list unchanged, so the browser is not a global capability grant.

The limit that shaped the design

The MCP server drives the page but cannot intercept its traffic, so request and websocket stubbing are not available as tools; when a task needs them the agent writes an ephemeral Playwright script instead. Knowing where the tool boundary sits mattered more than adding tools: an agent that believes it can stub a request it cannot stub writes a test that passes for the wrong reason. The descriptor is backend-agnostic, so adding a tool server does not fork per provider.

Grading the work

Tests passing proves a diff is internally consistent, not that it solved the ticket. An agent that patches the symptom produces green everything and the wrong change. So the work is graded in three separate places.

  • Before implementation, a red phase derives failing tests from the ticket, so there is a definition of done that predates the patch.
  • After implementation, a separate test-authoring pass runs, because the model that just convinced itself the change is correct is its worst reviewer.
  • After the PR exists, a simplify pass seeds a fix-and-re-review loop bounded at five rounds, because an unbounded self-review loop spends money until someone notices.

CI gets one repair attempt, grounded

If CI goes red the agent gets exactly one pass at fixing it, because a retry loop against a genuinely broken change burns a full agent budget per round. A log tail is a weak prompt for that pass, so when the failing check is the static-analysis gate the prompt carries the exact violations, rule ids and thresholds, verified to belong to the commit being fixed rather than to someone else's code. The same gate guards Veranda's own pull requests.

Why a workflow engine and not a job queue

One run is hours long and the pod running it is rolled on every deploy, so on a job queue a rollout mid-run loses the clone, the branch and everything the model was paid to produce. The problem is durability, not orchestration. Every step is an activity with its own timeout and retry, and a worker that dies resumes from the last completed one instead of re-billing the model.

Deduplication falls out of the same engine. The poller sees the same issue every thirty seconds, and every pod-local claim, an in-memory set, a tracker file, is empty after a rollout, so the first tick after a deploy would open a second PR. The guard asks the engine's visibility index instead, the one piece of state that outlives the pod.

How it ships

One image, two binaries, two Kubernetes Deployments: the bot polls Linear and holds the Slack socket, the worker runs activities, so agent capacity scales without touching ingestion. Nothing calls in, so there is no Ingress and no public endpoint, and the single HTTP port exists only for the kubelet's probes and the Prometheus scrape. The vendor CLIs are pinned into the image, which makes upgrading a coding agent a deploy and rolling one back an image tag.

Why no agent framework

The agent loop was not mine to write. The vendor CLIs already do tool use and context management, and they are the thing being evaluated, so wrapping them in a second loop is a worse copy of the easy part. What was left over, durability across a restart and a provider abstraction with live probing, is exactly what a framework does not offer: its execution model is in-process and ephemeral, where the requirement was durable and resumable.

The layering is ports and adapters, chosen so the cost of adding a provider is knowable in advance: an adapter, a factory case, a probe and some catalog entries, never the port or the resolver.

Test-first, for a specific reason

The failure modes are combinatorial and expensive to reach live: provider times mode times availability times vision support. Verifying that a revoked key falls back to another vendor, drops the now-invalid model label and posts the reason to the ticket should not require revoking a key, so the adapters expose seams for the command runner and the login step and the whole matrix runs in unit tests in seconds.

What the dashboard measures

Tiers double as the cost lever: short structured calls resolve to the cheapest tier the running backend serves while implementation gets the expensive one, and the catalog carries a rate per model id, so spend is attributed per task rather than discovered at the end of the month. The rest is shaped around the questions asked when something looks wrong.

QuestionWhat answers it
Where does time go?Duration per phase, agent run duration, review loop iterations, feedback rounds
What is it costing?Tokens in and out, USD per task, model selection by phase and reason
Is a provider degrading?Backend resolutions by requested label, resolved backend and reason, which surfaces silent fallbacks
Is the output any good?Coverage delta, CI results, checks failed by name and repo, runs that produced no changes at all
Resolutions labelled by reason are the one I would add first to any agent system: they are how you find out that half your runs quietly moved to a fallback provider a week ago.

What I would tell the next person building one

  • Reliability is the product. The model writes the patch; everything else is making a multi-hour job survive a rollout, a revoked key, a flaky test and a red pipeline.
  • Bound every loop and name the bound. Five review rounds, ten feedback rounds, one CI repair. An agent with no stopping condition spends money and credibility at the same rate.
  • Instrument the decisions, not just the outcomes. Knowing a run succeeded is far less useful than knowing which backend it resolved to and why.

Next project

Quadruped Walking Robot