Research brief · 2026-08-30

AI Pitlane

An open-source runtime that treats an AI pipeline as a graph you can see, validate and re-wire — where the model behind any node is a config field, a browser is optional, and an AI client can drive the whole thing over MCP.

Get the code github.com/freePerro/ai-pitlane ↗
Apache-2.0Python 3.11+2 runtime deps1,678 tests · no networkevery budget ships at $0124 MCP tools
Premise

Why a pitlane

The pitlane is the one stretch of a racetrack with a speed limiter. It is where the fastest machinery on the circuit gets worked on — wheels off, parts swapped, sent back out — under rules nobody argues with: a hard speed cap, defined lanes, and a crew chief who decides when the car moves.

That is the design brief. Coding agents are fast machinery, and most orchestration tools respond by making them more autonomous — daemons, retries, auto-escalation. AI Pitlane goes the other way. Pipelines are explicit graphs. Every shipped graph launches from a button, most cap paid spend at $0.00, and nothing runs unattended unless a human turns three separate keys. The interesting part is that slowing the lane down did not make it less capable — it made the pipeline itself something an AI can safely be handed the keys to (§ AI drives it).

Concretely, it is a local-first Python package: a stdlib HTTP dashboard on 127.0.0.1:9191 where you draw and launch graphs, a one-shot queue drainer for terminals and timers, and a stdio MCP server exposing the same surface to AI clients. Two runtime dependencies (watchfiles, requests); the repo's own docs call adding a third "a decision, not a convenience."

How it works · 1

The graph is the program

A pipeline is a validated DAG stored as plain JSON. A node is {id, role, kind, config}; an edge says what feeds what. The registry defines 8 roles and roughly 80 plugin kinds — supervisors, implementers, reviewers, scanners, routers, halt detectors, budget enforcers, and DAG primitives like foreach and collect.

The load-bearing decision is that the backend behind a node is a field, not an architecture. An implementer node might be kind: claude_cli today and kind: gemma_local (a Gemma on a local vLLM server) tomorrow; the swap never touches an edge. Local models, Claude, Codex, and OpenAI or Cerebras specialists sit in the same graph as peers.

trigger manual ▶ work_orders picker implementer kind: ______ reviewer local_ai the backend is a config field claude_cli codex gemma_local · vLLM :11440 budget enforcer paid dispatch over the cap → refused, recorded as the node's error
The autoloop shape: a picker pulls one work-order, an implementer commits to a target repo, a reviewer checks it. Which vendor does the implementing is one field; every paid dispatch passes the project's budget enforcers first.

Graphs live at ai-queue/operations/projects/<name>.graph.json, and every save auto-commits, so the pipeline's history is git history. Editing the file is equivalent to editing in the browser. Two rules keep the canvas honest:

How it works · 2

What a run actually is

Preview launch lists nodes, warnings, scan targets and the target repo before anything moves. ▶ Launch then runs the graph in a transient systemd-run --user unit, so Abort kills the entire process tree — including any vendor CLI still billing underneath it. On the canvas, nodes light up as they go: pulsing outline running, solid green complete, red failed, grey skipped because a router chose another branch.

Everything that happened is written to ai-queue/operations/mode_runs/<run_id>/state.json with per-node outputs. The dashboard only reads that file — there is no hidden state to drift from it, and the repo's standing rule is that nobody edits that directory by hand. It is the record of what happened, the same way a timing sheet is.

How it works · 3

The speed limiter

Budgets are enforced, not observed. Every project ships with its paid caps at zero; free, local plugins run under those caps and paid ones are refused — recorded as the node's error — until a human raises the cap. Two enforcer kinds ship:

Both run in hard or soft mode; either refuses the call at the cap, and soft additionally names a configured fallback in the error for a router — or a human — to act on. Nothing is re-dispatched automatically.

Unattended is off by default, three keys deep. The systemd timers that would drain the queue every minute are rendered in the repo but installed on no machine; the drain timer passes --require-opt-in, which skips any graph that hasn't declared a reconcile trigger; and no shipped graph declares one. Installing a unit, adding that trigger, or raising a cap each turn a computer into something that spends money without being asked — the docs treat all three as decisions, not defaults.

How it works · 4

Giving it work

The working loop is queue-based. A work-order is a markdown file with frontmatter — slice_id, status, priority, depends_on — plus a goal, acceptance criteria, and a files_in list whose paths are inlined from the target repo so the agent sees real code, not a summary. Orders are born draft; nothing is eligible until a human flips one to pending.

Point a project at any git repo you are willing to git reset --hard (an implementer will commit there), drop an order in its queue directory, and either press Launch or run the drainer:

.venv/bin/python -m orchestrator.drain --dry-run   # what would run; changes nothing
.venv/bin/python -m orchestrator.drain             # run ONE eligible slice, then exit
.venv/bin/python -m orchestrator.drain --reap      # mark provably-dead runs failed; requeues nothing

drain takes the coordinator lock, so it cannot race the dashboard; if the lock is held it exits 0 and does nothing. The implementer commits to the target repo and the loop stops at a draft PR — review stays human. poll_issues closes the loop from the other end, turning labeled GitHub issues into work-orders.

One honest gap, filed and documented rather than papered over: there is no crash recovery. An interrupted run stays running until --reap marks it failed, and its work-order needs a human to set it back to pending. In a tool this shaped, a requeue that happens by itself would be a bug.

How it works · 5

When no kind fits, write one

The registry's ~80 kinds are each tied to a purpose — a scanner wraps a specific tool, an implementer drives a specific agent. The escape hatch is primitive.python_script: the blank node. Drag it onto the canvas and write Python straight into its config. The contract is three names — inputs (a dict of upstream node id → that node's output) and config are already in scope; assign result to whatever downstream nodes should see:

# `inputs` and `config` are already in scope.
result = {"ok": all(r.get("passed") for r in inputs.values())}

The snippet runs in its own subprocess interpreter with a real timeout_s (default 60), nothing auto-imported beyond the standard library, and no orchestrator internals reachable — so a snippet that loops forever or calls sys.exit has a blast radius of one node, not the run. It plays the role n8n's Code node and ComfyUI's custom nodes play elsewhere; the shipped testbed graph uses one as its audit gate, deciding in plain Python whether a slice's checks actually passed.

When a snippet grows into something you'll reuse, promote it to a real plugin. The dashboard ships a Monaco-based plugin editor at /editor: a New Plugin form scaffolds a registered (role, kind) skeleton into the plugin pack, lifecycle badges track it from scaffold-only to implemented, and the toolbar runs Validate, Smoke Test and Reload Registries so a new kind shows up in the palette without restarting the server. The same loop is exposed over MCP (scaffold_plugin → validate_plugin → test_plugin → reload_plugin_pack) — which matters in the next section.

The point

Have an AI talk right to it

The browser is one client. The other is a stdio MCP server — line-delimited JSON-RPC over stdin/stdout — exposing 124 tools that cover the same surface as the dashboard: projects, nodes, edges, templates, launches, runs, artifacts, plugins, triggers, health. It is not a chat feature bolted to the side; it is the second door into the same room. Registering it with an MCP-capable client is one line:

claude mcp add -s user ai-pitlane -- "$PWD/.venv/bin/python" -m orchestrator.mcp_server

After that you can stop drawing graphs and start describing them. A working session looks like this:

Three conventions make that safe to hand to a model:

There is a genuinely useful recursion in here: an AI assistant designing, wiring, launching and monitoring pipelines of other AI agents — Claude assembling a graph in which a local Gemma does the work and a local reviewer checks it. Read-only MCP resources (orchestrator://projects, orchestrator://runs/latest, orchestrator://project/<name>/graph) give the client ambient context without tool calls, and apply_graph_diff applies a whole multi-op edit as one transaction. And because the plugin-authoring loop is on the tool surface too, an AI client that finds no kind for the job can scaffold one, implement it, smoke-test it, reload the registry, and wire the new node into the graph it is building — end to end, without a human opening an editor.

In practice

What you can do with it

Shipped graphWhat it doesPaid cap
defaultsupervisor → local implementer → local reviewer; the first-run graph — works with nothing installed, costs nothing$0 hard
red_teamscan a chosen target, verify findings locally, write a markdown findings report$0 hard
autoloopdrop a work-order, walk away: picker → implementer → checks, ending at a draft PR$5 hard
testbedthe same loop pointed at a repo that is not the orchestrator's own$5 hard
multivendorClaude, Codex and a local model cooperating in one graph$5 hard
blank / plugin_demoan empty canvas, and a visual reference for router and config widgets$0 hard

The patterns those shapes unlock, in rough order of how often I reach for them:

Boundaries

What it refuses to do

Run it

Kick the tires in ten minutes

git clone https://github.com/freePerro/ai-pitlane.git
cd ai-pitlane
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest orchestrator/tests -q   # 1,678 tests, ~35 s, no network, no model
.venv/bin/python -m orchestrator                   # dashboard on 127.0.0.1:9191

The suite stubs every subprocess and model call, so it passes on a machine with no vendors installed at all. First launch of the default graph with no local model server running fails with a structured error — that's the expected outcome, it costs nothing, and the run record shows exactly which node said why. From there: /help on the dashboard for tutorials and generated references, /connections for what's wired up, and infra/mcp/README.md for the full MCP tool list.

Grounded 2026-08-30 against the public repo at github.com/freePerro/ai-pitlane (Apache-2.0). Where this page and the code disagree, the code wins. Companion pages: the 3090 coding-stack brief and the bake-off it drove.