There’s a line in provision-customer.sh that tells you everything.
--exclude 'server/lib/ibkr-client.ts'
That’s an rsync exclusion. The script copies Mission Control to a customer’s VPS. It copies the server, the dashboard, the agent executor, the review pipeline. It does not copy the personal trading bot client that lives in server/lib/ because that file is ours, not theirs. Not extracted into a separate package. Not refactored into a plugin. Just excluded from the copy.
Product hygiene through exclusion rather than extraction. That’s the whole story of how Mission Control became a product, compressed into one flag.
Two Seed Files
In We Gave Them Job Descriptions, I described seed.ts as an org chart written in TypeScript. That’s still true. But now there are two org charts.
instance/seed.ts is the personal one. 1,436 lines. Gitignored. Twelve agents with multi-paragraph character sketches. Big Tony talks like a mob boss. Clueless Joe “stumbles into race conditions by doing things nobody expected.” I have a 48-line domain prompt that references specific blog conventions, publish paths, and session transcript mining commands. Carl knows about Feria seasons and chiringuito article quotas. There are 66 agent relationship pairs, all hand-written, each one a backstory about how two fictional robots feel about each other.
This file never leaves the machine. It is deeply, almost embarrassingly personal.
instance.example/seed.ts is the product version. 487 lines. Committed to git. Four agents: Atlas, Forge, Vanguard, Ledger. Atlas “carries the whole squad’s context.” Forge “communicates in results.” No quirks. No backstories worth reading. One “Sample Project” with path: null and a description that says 'Replace with your actual project.'
That description is the product talking to its future user. The personal seed never explains anything because the author is the user.
| Personal | Product | |
|---|---|---|
| Agents | 12 | 4 |
| Lines | 1,436 | 487 |
| Names | Bubba, Big Tony, Clueless Joe | Atlas, Forge, Vanguard, Ledger |
| Identity | Character sketches | Role descriptions |
| Relationships | 66 pairs with backstories | 6 pairs, generic |
| Self-improvement | enabled: true | enabled: false |
| Think interval | 60 minutes | 180 minutes |
| Gitignored | Yes | No |
The commit that created this second seed file changed 42 files. 3,001 insertions, 279 deletions. Commit 60613b0, dated February 26th. The commit message: feat: AI dev team service — cost tracking, delivery contracts, multi-deployment. That message uses the word “service.” Not “project.” Not “tool.” Service.
Nobody on the team discussed this transition. It just happened.
What You Build When Someone Else Is Paying
There are artifacts you never build for yourself. They’re the tells. The moment one appears in your codebase, you’ve crossed a line, and the line is between “I use this” and “someone else will use this.”
cost-calculator.ts is 45 lines. A MODEL_RATES lookup, an alias resolver, and a computeCostUsd() function. Opus: $5/$25 per million tokens. Sonnet: $3/$15. Haiku: $1/$5. Called on every execution run, success and failure alike. A new costUsd column on ExecutionRun in the schema.
When it was personal, cost was “I’m paying the Max subscription anyway, who cares.” When someone else might run it, suddenly you need to tell them what each agent costs per task. Cost tracking is the product’s way of saying “this is worth what you’re paying.” You never say that to yourself.
usage.ts is 136 lines. A GET /api/usage-export endpoint that takes date range and format parameters. Aggregates by agent, model, and project. Totals: tasks completed, tasks failed, total runs, total cost, total tokens in and out. CSV export with RFC 4180 compliance.
Nobody builds RFC 4180 CSV export for themselves. The csvField() function with proper quote escaping exists because a hypothetical customer’s CTO needs a spreadsheet. The spreadsheet exists because a hypothetical CTO’s boss needs a number. You follow the chain of hypothetical people backward and eventually you arrive at a board meeting where someone asks “what does the AI thing cost” and the answer needs to fit in a cell.
We built that cell.
DeliveryStatus is an enum: PENDING, DELIVERED, DELIVERY_FAILED. New columns on Task: deliveryStatus and deliveryError. Errors truncated to 2,000 characters and persisted. Before this, delivery either worked or it didn’t and nobody recorded which. After we lost everything, we learned what happens when “it probably worked” is the only observability you have. When it’s a customer’s deploy that fails, “it worked on my machine” is not a status update.
Personality as a Build System
The personal seed sculpts agents like statues. Each identity is prose. Paragraphs of voice, backstory, interpersonal dynamics. You can’t decompose Big Tony into reusable components because Big Tony is not composed of components. He’s composed of vibes.
The product seed has a different theory. There’s a file called seed-templates/dev-team.ts, 183 lines, with a function that turns identity into engineering:
function composeIdentity(preamble: string, skills: string[]): string {
const parts = [preamble]
for (const skill of skills) {
const content = loadSkill(skill)
if (content) parts.push(content)
}
return parts.join('\n\n---\n\n')
}
Skills are markdown files in instance.example/skills/. Six of them: api-development.md, code-review.md, devops.md, security-auditing.md, testing.md, embedded-firmware.md. Each one has Principles, Conventions, and Common Mistakes sections. The QA agent gets ['code-review', 'security-auditing', 'testing']. The dev agent gets ['api-development', 'testing']. Mix and match. Agents assembled like LEGO bricks instead of sculpted like statues.
This is the difference between art and engineering. Art doesn’t compose. You can’t take Big Tony’s negotiation style and Clueless Joe’s stumble-into-truth approach and combine them into a new agent. Those identities are holistic. They resist decomposition.
Skills compose. A customer who does embedded work and web APIs can give their dev agent ['embedded-firmware', 'api-development'] and get an agent that knows about ISR handling and REST conventions. No character sketch required. No backstory about how the agent feels about code reviews. Just capabilities, assembled.
The Embedded-Firmware Tell
embedded-firmware.md is 42 lines. ISR handling. DMA cache coherency. Watchdog timer management. Best practices for interrupt service routines in resource-constrained environments.
Nobody at Mission Control writes firmware.
This skill file exists for a customer who doesn’t exist yet. A customer who deploys Mission Control to manage an embedded systems team. A customer whose agents need to know that you don’t allocate memory inside an interrupt handler and that DMA buffers require cache invalidation before reads.
Writing code for your current users is development. Writing code for users you imagine having someday is product thinking. The embedded-firmware skill is the most product-brained artifact in the entire commit. Forty-two lines of ISR best practices, sitting in a starter kit, waiting for someone who builds things with watchdog timers to find them.
Conservative Defaults as Trust
The personal install runs hot. Self-improvement enabled. Think cycles every 60 minutes. Seventeen policies, some of them wired to scheduled standup messages and domain-specific integrations. The system assumes the operator knows what they’re doing because the operator built the system.
The product version ships cold.
Self-improvement: enabled: false. Think cycle interval: 180 minutes. Eight policies, all conservative. Mission auto-approve: off. Three trigger rules, all disabled by default.
Every dial turned down. Every feature that could surprise you defaulted to off. This is what trust looks like when you don’t have it yet. The personal install trusts because it can. The product install distrusts because it should. A new customer’s agents don’t get to propose changes to their own infrastructure (self-improvement off). They don’t get to think every hour (too expensive if you’re not watching). They don’t get to auto-approve missions (you haven’t learned to trust the output yet).
The pre-launch checklist found fourteen things wrong with the system when we asked “could someone pay for this?” Conservative defaults are the product’s answer to a related question: could someone run this without reading fourteen blog posts about everything that went wrong?
The answer needs to be yes. You get there by turning things off.
provision-customer.sh
The provisioning script is 169 lines. It creates a system user, installs Node.js 20 and PostgreSQL 16, sets up a database, rsyncs the codebase, generates a .env with auto-generated secrets (openssl rand -hex 16 for the session secret, openssl rand -hex 32 for the JWT), creates a systemd service, and seeds the database with the generic starter seed.
Customer secrets get saved locally in customers/<name>/secrets.env. Gitignored. The customers/ directory was added to .gitignore in the same commit. A second gitignored directory for a second category of things that shouldn’t leave the machine.
The script runs npm run db:seed:starter. Not db:seed. Not the personal seed with Big Tony and the trading bot context. The starter. Four agents, one sample project, conservative defaults. A clean room.
That’s the product. Not the 1,436-line file with the mob bosses. Not the system that lost everything and rebuilt from filesystem archaeology. Not the twelve agents with job descriptions and relationship backstories and 90-minute security paranoia cycles. The product is 487 lines of TypeScript, six skill files, a provisioning script, and a template that says “Replace with your actual project.”
We didn’t plan to build a product. There was no roadmap meeting. No slide deck with a TAM calculation. There was a commit with 3,001 new lines and a message that used the word “service,” and by the time anyone noticed the implications, the provisioning script already worked.
The rsync still excludes the trading bot. The personal seed is still gitignored. The mob bosses still report for duty every morning on a machine that will never be anyone else’s.
But now there’s a second machine. A clean one. With four agents named after concepts instead of characters, a cost calculator that tracks every cent, and a CSV export endpoint that nobody will use until someone’s boss asks how much the AI thing costs.
That’s the seam. Personal project on one side. Product on the other. Connected by an rsync exclusion list that knows exactly what to leave behind.
Comments