Our Rust backend, MCP server, worker and CLI became TypeScript in three days, with 1,141 of 1,142 tests ported and the test suite down from 46 minutes to 7. One agent, 62 tasks, one spec it could not edit.

SDD Vault is a hosted store for Spec Driven Development documents: the constitution, specs, plans and tasks that AI agents work from, served to the agents over MCP so they read the current spec instead of a file on a branch. We built it with the method it sells. This is the story of the biggest change we made that way: moving every server-side deliverable from Rust to TypeScript, with agents doing the work and specs driving them.

Why rewrite a working backend

By late October the product was 87 specs in. The server side was a Rust workspace of about 91,000 lines: a core crate with every business rule and 16 Diesel migrations, an axum API with 124 documented operations, SCIM, the browser session and a live socket, an rmcp MCP server with 18 tools, a worker on RabbitMQ, and the sddvault CLI.

It worked. It was also the slowest part of every ticket. A full make test in one of our build bots took 46 minutes, 25 of them compiling test binaries before the first test ran. A one-line change waited minutes for a build. When most of your contributors are agents that run the suite after every task, build time is the tax on everything.

We were not in production yet, so the database could be wiped instead of migrated. The owner decided on 2026-10-28: every Rust deliverable moves to TypeScript on Node, with patterns we already knew (NestJS, CQRS, zod, TypeORM, Jest, pnpm, an OpenAPI document generated by the API and a client generated from it), keeping every functionality and every contract.

The spec came first, and it was the contract

The first thing written was not code. It was spec 031, "TypeScript backend", in the same shape as every other spec in the vault:

  • Context and goal, with the numbers above.
  • Actors: the developer and the agent who build and test; the person in the app who must see no change; the agent on MCP who must see the same tools, inputs, outputs and errors; the operator who deploys.
  • A table of every current piece, what it existed for, the decision, and the justification. The axum API: "replace with NestJS keeping paths, methods, bodies, status codes, error bodies, cookies and the socket protocol", because the app, the CLI, the Keycloak theme and the browser tests depend on them.
  • 15 functional requirements and 3 non-functional ones. FR-11 said every Rust test is ported or its absence is justified, one by one. NFR-1 said the suite runs in a fraction of the baseline.

The constitution had to change too: principle 2 named Rust. The spec amended it to v4.0, and that amendment was itself a task with a test. Agents cannot edit the constitution in SDD Vault; only an owner can, and the change is a recorded revision.

Then the plan, the research (every technical doubt answered against the official docs or the reference code, with the source cited), and the tasks: 62 of them, each under 30 minutes, each with the FRs it covers, a parallelism marker and a literal "Done when".

The 62 tasks of spec 031, all done, each linked to its FRs

How the agents worked

The agent was a Claude Code session in a Linux container with 3 CPUs and 8 GB (the "bot"), never on a laptop and never against staging, connected to the vault over MCP. The loop per task, 62 times:

  1. claim_task. The task shows as taken in the app, with the agent and the time.
  2. Read the spec, the plan and the research through get_document. Not the whole project, just what the task needs.
  3. Write the test first, then the code. Run that package's tests and the linter.
  4. Commit, mark_task_done with the commit hash. The task row links the FRs it closes.
  5. Stop and wait for "next".

A person said "next" 62 times, reviewed the diffs, and made the decisions the spec left open. The agent's rule for those was the one every agent in the vault has: every doubt is asked or recorded, never guessed. The ported-tests ledger (ported-tests.md) grew task by task, one row per Rust file, and is the proof for FR-11.

Revisions of ported-tests.md: one per task, each with the agent and the time

The rewrite was one agent. Running several at once on one spec is what the product is for (task claims, live presence, optimistic locking on every document), and it is what the demo video shows; this story is about the spec being the contract.

The baseline, measured, not remembered

Task T01 was: measure the Rust suite in a bot, record it in research.md. Result on main 34f67c6, host under load from other bots:

Command Time
make test, Rust 2,784 s (46 min 24 s), stopped in the performance checks
of which compiling test binaries about 25 min

Everything after was compared to this.

What came out

Final run of the TypeScript branch, 2026-10-29, same bot:

Rust TypeScript
make test 2,784 s 416 s
one module's unit tests (core/domain/markdown, 47 tests) build first, then seconds 1.5 s
all core unit tests (414) 7.2 s

Tests: 1,142 Rust tests in 218 files. 1,141 ported. The one not ported needs an HTTP request with no socket peer, which supertest cannot produce; the rule it tested is covered by another test, and the ledger says so. Five tests a static audit found missing were ported in a single later commit and passed on their first run.

Deliverable Rust tests Ported
core 781 781
api 189 188
worker 20 20
mcp 63 63
cli 85 85
test kit 4 4

Performance thresholds were kept from Rust, every one:

Check Threshold TypeScript
p95 document read plus project status < 100 ms 66 ms
usage report by project, agent, token < 300 ms 103, 163, 172 ms
agent guide for 50 specs and 1,000 tasks < 300 ms 10 ms
audit table over 100,000 entries, p95 < 500 ms 148 ms
import of 100 specs < 10 s 1.0 s
CLI import of 100 specs < 30 s 1.6 s
200 KB spec read, 10 concurrent callers, p95 < 100 ms 49 ms

Node runs the API on one thread where Rust used the bot's three cores. The 200 KB read was over the limit under host load until a fourth optimisation pass; the research file records each pass and what it changed.

Contract: an automated parity test checks every path, method, parameter and schema of the new OpenAPI document against the frozen Rust one. The browser app and the CLI were not changed. The MCP tools kept names, inputs, outputs and errors.

Traceability of spec 031: every FR with the tests that cover it

What the agents got wrong, and how we know

The validation document lists it. The useful part of SDD is not that agents never fail; it is that every failure is a row in a table with a cause and a commit.

  • A browser test flaked on a toast shown twice. Cause: a flake already recorded in an earlier spec's validation, not this branch. Evidence: passes 3 of 3 alone, the flow is unchanged.
  • A SCIM test restarted the dev DNS container, which recreated its network interface, and Chromium dropped requests with ERR_NETWORK_CHANGED. Fixed in 57d34c1: dnsmasq restarts inside the container. 56 of 56 afterwards.
  • The concurrent read check passed at host load 2 and failed at load 5. Fixed in ac5cf2b after profiling about 8 ms of CPU per read pair on one thread.

And the deviations we accepted on purpose, each listed in the spec's validation: malformed JSON and non-numeric query parameters now answer 422 with a JSON body where axum answered 400 plain text; repeated query parameters take the last value where serde refused them. The app and the CLI never sent those. The agent reported each one; a person accepted it; the record is in the vault.

Why the spec mattered more than the model

Three things made this work, and none of them is the model.

The spec was the only source of truth, and the agent could not change it. Asked to make a parity test pass, an agent's cheapest move is to relax the contract. In a repo, that is an edit to a markdown file in the same commit as the code. In the vault, the spec has a status, approval is recorded with a name and a time, and an editor's agent cannot write the constitution at all. A refusal comes back as a typed error the agent can read, and the attempt is in the audit trail.

Demo data: an editor's agent tries to write the constitution and is refused

Tasks were small and literal. 62 tasks under 30 minutes, each with a "Done when" a test could check. The agent never had to decide what finished looked like.

Every doubt was asked, never guessed. The research file answers each technical question against the official documentation or the reference code, with the source cited. The decisions a person made (the accepted deviations, the separate performance suite) are written down next to the evidence.

By the numbers

  • 99 specs in the vault, 1,305 commits, 24 days from the first commit to today.
  • Spec 031: 62 tasks by one Claude Code session, 1,141 of 1,142 tests ported, make test at 15 % of the baseline, every performance threshold kept.
  • Spec written 2026-10-28. Validation written 2026-10-29. Last task closed 2026-10-30.

The spec, plan, research, tasks and validation of this rewrite are a project in our own vault, and they export to plain markdown. If you want to see them, ask in the comments.

SDD Vault is at https://sddvault.com. Free for 3 people and 1 project, no card. Your agent connects with one URL: https://{org}-mcp.sddvault.com/mcp.