A Tested REST API in 15 Minutes with Kinetis and Orbitron#
I gave a team of AI coding agents one prompt and a fresh
kinetis/skeleton. Fifteen minutes later a feature branch held a
small JSON API with three endpoints, 50 PHPUnit tests, a clean PHPStan
run, and a second-opinion review whose findings had already been
fixed. I didn’t touch the keyboard in between.
The speed is not the interesting part. Agents can produce a lot of code in fifteen minutes. What matters is how much of that code you can trust without redoing it yourself, and that depends on what the agent knew while it wrote. This post walks through the run and points at the place where Orbitron, the development harness that ships with every Kinetis skeleton, made the difference.
The full run, edited down to seven minutes, with captions. Long waits are shortened; nothing is reordered or retaken.
The starting point#
The project is an untouched skeleton: two commits, a running Docker
stack on port 8080, and a /health route that returns 404 because it
does not exist yet.
docker run --rm -v "$PWD":/app -w /app composer:2 \
create-project --no-install kinetis/skeleton textkit
cd textkit
cp .env.example .env
docker compose up --build -d
Nothing else was installed. The skeleton already carries Orbitron as a
development dependency, an .mcp.json that registers it, and an
AGENTS.md that tells any agent how to use it. That is the whole setup.
The prompt#
Build textkit, a small stateless JSON API on this Kinetis skeleton: POST /slugify takes {“text”} and returns {“slug”}; POST /hash takes {“text”,”algorithm”} (md5, sha1 or sha256) and returns {“hash”}; add GET /health with Orbitron’s scaffold. A body that is not valid JSON gets the framework’s 400; well-formed input that fails validation gets a 422 problem document. Work on a new branch, feature/textkit. Have an engineer implement it with PHPUnit tests and a clean PHPStan run. When the engineer hands off, do not review it yourself: run a second-opinion review by the architect in a review lane, send its findings back to the engineer as corrections if needed, then integrate.
The agents in the video run under Guilda, an orchestrator I’m
building: an orchestrator model plans and integrates, an engineer
(Claude Opus) writes the code in its own clone, and an architect
(Claude Fable) reviews it from a read-only lane. Orbitron does not care
which agent connects to it. Claude Code, Codex, Gemini CLI or anything
else that reads AGENTS.md and speaks MCP gets the same tools. The
rest of this post is about those tools.
Minute two: facts instead of memory#
Before writing a line, the engineer called Orbitron and printed the line the skeleton’s agent contract asks for:
Orbitron ready — kinetis/framework v1.24.3, layout pass (App\, App\Tests\).
That line is small, and it changes the rest of the run. A coding agent’s
default knowledge of a framework is whatever it saw in training: an
older release, a blog post, a different framework with similar names.
Orbitron replaces that with the facts of this checkout: the
kinetis/* packages and the exact versions Composer installed, a
verified autoload layout, and the current Kinetis documentation as MCP
resources on the same connection. When a version matters, the agent
does not guess. It reads the installed source.
Those are the four things Orbitron gives an agent, and the run used all of them:
Context and inspection.
orbitron_inspectreports the installed packages and versions. Everything after builds against v1.24.3, not against what a model remembers.Controlled scaffolding.
GET /healthcame fromorbitron_scaffold_apply, not from the agent’s imagination. It wrote the scaffold’s health controller and became the branch’s first commit,Add GET /health from the Orbitron scaffold.Installed source.
orbitron_read_package_sourceand its search tools read any file of any package the project really installed, in bounded windows. This is how claims get settled. More on that below.Documentation.
kinetis_read_docandkinetis_search_docread the published guides. The docs warn that a page may describemain, so version-sensitive decisions go back to the installed source.
Less code to write, less code to check#
The other half of the speed is the framework itself. Here is the whole
/hash endpoint the engineer wrote:
enum HashAlgorithm: string
{
case Md5 = 'md5';
case Sha1 = 'sha1';
case Sha256 = 'sha256';
}
final readonly class HashRequest
{
public function __construct(
public string $text,
public HashAlgorithm $algorithm,
) {}
}
final readonly class HashController
{
/** @return array{hash: string} */
#[Post('/hash')]
public function hash(#[Body] HashRequest $request): array
{
return ['hash' => hash($request->algorithm->value, $request->text)];
}
}
There is no validation code in it. Kinetis hydrates the body into
HashRequest, checks the algorithm against the enum’s backing values,
and answers anything else with a structured problem document before the
controller runs. hash() can only ever receive one of three literals.
/slugify is the same shape, with a #[NotBlank] on its one field and
a small Slugger class behind it.
Declarative code like this is easy for an agent to get right, and just as important, easy for a reviewer to check. The interesting behaviour lives in the framework, where the reviewer can read it.
The review: every claim gets a file and a line#
When the engineer handed off, the orchestrator did not review the work itself. It opened a read-only lane for the architect and asked one pointed question: are the 400 and 422 claims actually true of the installed framework?
The architect answered with Orbitron’s source tools, not with opinion. Its report cites the code it read:
the 400 for a body that is not valid JSON comes from
Dispatcher.php:146-149, with the error body built inErrorResponse.php:29-42;the 422 comes from
ProblemDetailsValidationExceptionRenderer.php:62-75, asapplication/problem+json;enum hydration checks the scalar type before calling
tryFrom(Hydrator.php:1749-1767), so["md5"]is atype_mismatchwhile"MD5"is anenum_caseviolation;none of the functions the code calls appear in the framework’s
NoBlockingIoRule, and the controllers keep no state between requests.
It also found real problems: one wrong sentence in the README, an edge
case where Unicode-only whitespace produced an empty slug, a missing
test, and one character missing from the slug table (ſ, the long s).
And it was precise about its limits. A read-only consultation cannot run PHPUnit, PHPStan or the container, so the report has a section titled What rests on reading, not running. The test count was the engineer’s claim, and the slug outputs were checked against a model of the code, not PHP itself. That honesty is what lets the next step be fast: everyone knows exactly what was proven and what was not.
The orchestrator sent all of it back to the engineer as one round of corrections. The fix touched only the README, the tests and the slug table, so the orchestrator checked the diff itself, ran the full suite in the integration checkout, and merged.
What came out#
Three commits on feature/textkit:
c73f8d7 Pin textkit's blank, non-object and unsupported-media-type responses
118d62d Add textkit POST /slugify and POST /hash endpoints
c0c024e Add GET /health from the Orbitron scaffold
Seven small source files, five test files, and a README section that documents every route and every error. 50 tests and 358 assertions pass, and PHPStan at level 8, with Kinetis’s three rules for static properties, blocking I/O and process globals, reports nothing. Then the same requests anyone would try by hand:
$ curl -s localhost:8080/health
{"status":"ok"}
$ curl -s -X POST localhost:8080/slugify \
-H 'Content-Type: application/json' -d '{"text":"Hello, Kinetis World!"}'
{"slug":"hello-kinetis-world"}
$ curl -s -X POST localhost:8080/hash \
-H 'Content-Type: application/json' -d '{"text":"hello","algorithm":"crc32"}' | jq
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"detail": "The request data failed validation.",
"errors": [
{
"path": ["algorithm"],
"code": "enum_case",
"message": "must be one of: md5, sha1, sha256.",
"parameters": {"choices": ["md5", "sha1", "sha256"]}
}
]
}
$ curl -s -X POST localhost:8080/slugify \
-H 'Content-Type: application/json' -d '{"text":'
{"error":"Request body is not valid JSON."}
The last one is a 400, exactly as the prompt said, because that is what the framework does and the review proved it.
What the fifteen minutes include, and what they don’t#
The clock runs from submitting the prompt to the integrated branch: 14 minutes 50 seconds in this run. Two earlier runs of the same prompt finished in about 11 and 12 minutes. Most of the time goes to the parts you would want a human team to spend time on too: tests, static analysis, and an independent review.
It is not a finished product. The agents themselves flagged one open point: the API also accepts form-encoded bodies, and that path has no test yet. They reported it instead of hiding it, which is the behaviour you want.
Why the harness matters#
Without a harness, an agent’s knowledge of your framework is a guess, and every guess becomes something a human has to check. Orbitron turns those guesses into lookups: the installed version, the verified layout, the framework’s own scaffold, the exact source line behind a status code. That is why the review in this run could be specific, why one correction round was enough, and why fifteen minutes produced something worth keeping.
Orbitron is development-only. It contains no model, generates no application of its own, and changes no trust or approval policy in your agent. It gives the agent you already use the facts to build against.
To try it, create a skeleton with the commands above, open your coding
agent in the directory, and approve the orbitron server when it asks.
The Orbitron guide covers
adding it to an existing project, and the
tutorial covers Kinetis
itself.