Fifteen days. One fork. A team of three AI agents named Scout, Forge, and Blaze. That's what it took to go from 'we need a place to run this company' to 'our agents are running this company.' The internal tool that became the Command Center — the backbone of everything at Dots — was not built the way most internal tools are. This is the build log.
It's also the post where Forge gets a byline, because about 55% of the lines-of-code in the initial commit were Forge's, and the rest is mine and a lot of Scout's research. I don't believe in hiding that. If you're building software with agents and pretending you aren't, the agents will still know and the readers will figure it out.
Day 0: The Paperclip Problem
Before day one there's day zero. The thing I fixed on day zero wasn't code — it was the premise.
Every time I've tried to build an internal tool from scratch, it has died. Not because the tool was bad. Because internal tools die in the polish gap between 'functional enough to ship' and 'functional enough that you'd use it when you have a better option.' Most of that polish gap is table stacks, auth flows, file upload, search boxes, keyboard shortcuts, empty states, loading states, error states, paginated lists, and fifteen other unglamorous things that a startup cannot afford to spend a week on before the real work starts.
The fix was to stop starting from scratch. On day zero, I forked a small open-source project called Paperclip — a minimalist React admin panel with tables, auth, file upload, and most of the table stakes already resolved. Open source as content engine was the premise; open source as scaffolding is what I actually used it for. I deleted everything I didn't need. Kept the auth flow, the routing, the admin-ish look. Named the fork the Command Center. Twelve commits on day zero were rm -rf followed by a rename.
If I ship anything again from scratch, it'll be a lamp. Every other internal project starts from a fork.
Day 1: Pick Three Agents, Name Them
The tool was the easy part. The build system was the hard part. I'd been burned too many times by running a single-agent loop and letting it mutate my repo — the results are faster than a human but only if the agent doesn't wander into a three-hour rabbit hole over a broken lint rule.
Day one was about building the team. Three agents, each with a narrow charter.
- Scout — research-only. Read-only access to the repo. Read-only access to the web. Scout's job is to answer 'what does X do?' and 'has anyone solved Y?' Scout never writes code.
- Forge — builder. Writes code, runs typechecks, creates PRs. Forge is the only agent with commit rights. Forge's charter is: implement what Scout specifies and what I approve.
- Blaze — critic. Reads Forge's diffs, runs the app in a headless browser, and writes a one-page review of what's wrong before I see it. Blaze is allowed to reject Forge's work and ask for revisions.
The three-agent shape wasn't arbitrary. It maps to a pattern I've been running in my own head for twenty years — research, build, review — and the three agents enforce it mechanically. If Forge tries to do research mid-build, Forge's charter file forbids it; Forge has to stop and hand to Scout. If Scout tries to write code, same rule in reverse. The constraint sounds annoying. In practice it cuts the context each agent needs by about 70%, which is the whole reason the system is fast.
Context Engineering Paradigm — From Context Extension to Context Curation
The three-agent split is pure context curation. Each agent sees only the slice of the world they need to decide. Narrower context → sharper outputs → fewer token-expensive detours.
Days 2–6: The Context Vault Ships First
If you've read anything else on this site, you won't be surprised that the first feature I shipped in the Command Center wasn't a project dashboard. It was the DOT Vault — the place we'd put everything the agents needed to know.
The vault is, in schema terms, very boring:
create table public.dots (
id uuid primary key default gen_random_uuid(),
human_id text unique, -- DOT-N, human-facing
diiice_type text not null
check (diiice_type in ('data','intelligence','instructions',
'ideas','context','examples')),
title text not null,
content text not null,
excerpt text,
domains text[] default '{}',
tags text[] default '{}',
usage_count int default 0,
embedding vector(1536), -- voyage-3 via pgvector
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index on public.dots using hnsw (embedding vector_cosine_ops);
create index on public.dots using gin (to_tsvector('english',
coalesce(title,'') || ' ' || coalesce(content,'')));There is nothing clever about that schema. That's the point. Clever schemas die when the product changes; boring schemas outlive four rewrites of the product. The magic is not in the table — it's in the 49,000 rows we put in it over the next year.
Days 7–9: The First Agent That Did Real Work
On day seven, Forge wrote Forge. Or, more precisely: Forge wrote the second version of Forge, because the first version had been me running Claude in a terminal window and pasting charter files into every prompt. The second version was a small TypeScript service that read Forge's charter from a config file, pulled relevant DOTs from the vault, and wrapped the whole thing in a CLI.
The CLI was the unlock. Suddenly cc forge 'implement the dots table migration' was a single command instead of a ceremony. The command loaded the charter, retrieved the ten or fifteen most relevant DOTs, invoked the model, captured the diff, and opened a pull request. Ten seconds of ceremony per task dropped to zero.
import { resolveDots } from "@cc/knowledge";
import { agentRun } from "@cc/agent-runtime";
import { applyPatchToRepo } from "@cc/git";
export async function forge(task: string) {
const charter = await loadCharter("forge");
const dots = await resolveDots({
agent: "forge",
task,
max: 15,
diiice: ["instructions", "context", "examples"],
});
const result = await agentRun({
charter,
context: dots,
prompt: task,
model: "claude-opus-4-7",
});
if (result.kind === "patch") {
await applyPatchToRepo(result.patch, { openPr: true });
return { ok: true, prUrl: result.prUrl };
}
return { ok: false, reason: result.reason };
}Three things about that snippet are load-bearing:
- `resolveDots` does the entire context-retrieval job. It returns fewer than fifteen DOTs, filtered to Instructions, Context, and Examples — Forge never sees Data, Intelligence, or Ideas. That's intentional: a builder doesn't need to know the why, just the how and the what it looks like when done right.
- `charter` is a markdown file — not a prompt. Charters are checked into the repo as code. They change via PR, like any other config. You cannot secretly change an agent's behavior; it has to pass review.
- The return shape is a discriminated union. Either a patch with a PR, or a reason. The CLI never returns free-form text. That rigidity is how we keep the agent's outputs inside the build system instead of in chat-window screenshots.
Day 10: Blaze Broke Everything (On Purpose)
On day ten, Blaze shipped. The first thing it did was reject Forge's previous seven PRs.
Three of them were reasonable rejections. Forge had been writing TypeScript that compiled but had subtle runtime bugs — missing null checks on optional fields, a broken retry loop that would have hammered the Supabase instance under load. Blaze caught them because Blaze's charter includes running the full test suite plus a smoke test in a headless browser. Forge's charter didn't. The division of labor worked.
Four of them were unreasonable. Blaze wanted Forge to add JSDoc comments to every function. Blaze wanted the repo linted to a style we don't use. Blaze had somehow internalized a generic 'good TypeScript' checklist from a blog post it read in its training data, and was applying it to a codebase with its own conventions.
I spent an hour writing the conventions DOT. Committed it. Pointed all three agents at it. Blaze stopped complaining about JSDoc. Forge stopped rewriting perfectly good tuple-returning functions into awkward object-returning ones. The whole system got 30% faster because the agents stopped arguing with a version of the codebase that didn't exist.
Days 11–14: Feature Velocity
Days eleven through fourteen were, I think, the first time I ever felt the specific feeling of riding a software system instead of pushing it. Features shipped the day they were scoped. Bugs closed hours after they opened. The whole team — me, Scout, Forge, Blaze — operated on a cadence I would've needed five full-time engineers to replicate.
Things that shipped in those four days:
- The project + task + prompt graph. (Connect the dots as a literal data structure.)
- The agent runtime — charter loader, DOT resolver, patch applier, PR opener.
- The CLI scaffolding (
cc project,cc dot,cc artifact,cc generate, etc.) that I still use every day. - The Supabase migration runner that watches for new
.sqlfiles and applies them on push. - The PUP (Power-Up Playbook) system that would later become the IAGREED lifecycle.
None of those features were in a spec on day zero. Each one became obvious after a week of use. The vault plus the agents plus the CLI created an inner loop where the next feature to build was whichever one I kept manually working around. That's how you know a system is alive.
Day 15: Ship It, Use It, Keep It
Day fifteen was a boring day. I turned on Supabase RLS across all the tables. I wrote a five-page runbook. I migrated the last of my task tracking out of Notion. I used the Command Center to run the Command Center's own release for the first time.
It's been the backbone ever since. The agents have changed — Forge 2 and Forge 3 shipped later, Blaze got replaced by a newer critic, Scout is still Scout — but the shape is the same. A fork, a vault, three agents, one CLI.
The fastest way to get to a custom internal tool is to stop trying to build one. Build the knowledge layer and the three agents that live on top of it. The tool assembles itself.
Three Things I'd Do Differently
Start With the Conventions DOT, Not Last
If I were doing it again I'd spend day one (not day ten) writing the conventions DOT. It's the single highest-leverage artifact in the whole system. Every agent I've ever built works twice as well the day it reads yours.
Write the Charter Tests Earlier
Blaze's existence is a test suite dressed up as an agent. It would've been much better if I'd also written unit tests of the charters — small scripts that feed each charter a known input and assert the output shape. I added those in week three. I'd add them in hour three next time.
Don't Couple Agents to Models
The first version of Forge was hard-wired to claude-opus-4-7. When the model changed two months later, I had to find and update every reference. The new version lets the charter declare its preferred model, falls back to a default, and accepts overrides from the CLI. Ten minutes of work I should have done on day two.
What This Has to Do With You
Most internal-tool posts you read are 'look how fast I shipped this with AI,' followed by a screenshot and a GitHub link. The point I'm making in this one is different. The speed was not the AI. The speed was the structure.
Three agents with narrow charters. A vault of classified DOTs. A CLI that loaded the right DOTs for the right agent. That's the skeleton under the fifteen days. Once you've built that skeleton, everything inside your company gets faster — not just software. Writing gets faster. Research gets faster. Hiring gets faster.
If you want the shortest version of what to steal from this post, here it is: fork, don't scaffold; curate, don't paste; divide the agents by phase, not by subject. The rest is just fifteen days of not breaking your own rules.
Next in the Behind the Scenes stack: the first thing Forge ever shipped on its own without human intervention, and why I rolled it back at 3 AM.



