Decisions you have already made
The rule that decides what belongs in the kit is a single test — does this encode a decision you have already made a hundred times, or one the next prototype should be free to make — and it is what separates a kit from a framework nobody asked for.
The baseline run leaves you holding a list of expensive phases, and the obvious next move is to put all of them in the kit. That move is the one this lesson exists to stop, because a kit containing everything expensive is not a kit any more. It is a framework, written by one person, for a set of products that do not exist yet — and it will cost you more time than it saves on the first prototype whose shape it did not anticipate.
The distinction is not about size. A large kit can be fine and a small one can be poisonous. It is about what kind of thing each item encodes. HouseWarm’s review gate is a decision you have made: a human approves per-field before anything downstream fires. Which fields, what confidence threshold, whether the gate blocks the whole batch or one record — those are decisions the next prototype has to make, and every one of them you bake in is a wall the next prototype will have to climb.
One question decides every item
For each candidate, ask exactly this:
Does this encode a decision I have already made a hundred times — or a decision the next prototype should be free to make?
The test is deliberately binary and deliberately unkind. “It depends on the project” is a failing answer, not a nuanced one: it means the decision is not yet made, which means it belongs to the prototype and not to the kit. Worked through the phase list from the baseline run:
- Project scaffold, TypeScript config, lint, format. Already decided, a hundred times. In.
- Design tokens and the component layer. Already decided — you have shipped design systems; you are not going to reconsider your primitives under a three-hour clock. In.
- An auth stub. Already decided, because in a prototype the answer is always the same: a fake session, a user-switcher, no real identity provider. In. Note that this is only a settled decision because the course scoped real auth out; if the kit were for production it would be the opposite answer.
- A trace view and a review gate. The existence of both is decided — every agentic surface needs a way to see what the agent did and a way for a human to stop it. In, as generic shells.
- Which fields the gate approves, and at what threshold. Not decided. That is the product question the prototype is being built to ask. Out.
- The agent’s tool set and its domain model. Not decided, and never will be in advance. Out, emphatically.
- A canonical set of run states. This is the dangerous one, because it looks settled and is not. Whether a run can be paused, resumed, partially rejected or rolled back is a product decision that differs per portfolio company, and a kit that hard-codes one answer quietly forecloses the others.
Why a wrong item in a kit costs more than a wrong item in an app
Sandi Metz’s “duplication is far cheaper than the wrong abstraction” is usually quoted as advice about extracting shared code too early. Her mechanism is what matters here: someone extracts a reasonable abstraction, a near-miss requirement arrives, a second developer adds a parameter and a conditional rather than reconsidering the abstraction, and the process repeats until the thing is unmaintainable. Nobody in that story behaves unreasonably. Each individual step is the cheap one.
A starter kit is that pattern with the feedback loop cut. In an application, a wrong abstraction sits in one place, and Metz’s remedy — re-inline it at each call site, delete what is not needed, re-abstract from what you now understand — is available to you. In a kit, the wrong abstraction has already been copied into every prototype you started since you added it, and there is no path back.
That is not a metaphor; it is a mechanical property of how kits get distributed. GitHub’s own docs are explicit that a repository created from a template starts with a single commit, and that branches created from a template have unrelated histories, so you cannot create pull requests or merge between them. A fork keeps a merge path to its parent. A template deliberately does not. So every prototype you generate is a permanent, unreachable copy of whatever the kit believed on the day you cloned it — which is exactly what you want for a disposable prototype, and exactly what makes a wrong item in the kit expensive rather than annoying.
The posture worth copying, and its honest cost
shadcn/ui is the best-known statement of this posture in the React ecosystem, and it says so directly in its own documentation: “This is not a component library. It is how you build your component library.” Components are copied into your project rather than installed as a dependency, which the docs justify on the grounds that you end up wrapping library components and writing workarounds to override styles otherwise. That is a vendor documenting its own product, and the argument is self-serving in the ordinary way — but the structural claim is the same one this lesson is making, and it is testable rather than rhetorical.
Be equally honest about what the posture costs. Code you own is code that does not get upstream fixes. shadcn’s answer to that is tooling — the CLI now ships migrate subcommands precisely because owned copies drift — which is an admission that ownership has a bill attached, not a refutation. Your kit inherits the same bill. What it buys is that no item in the kit can hold a prototype hostage: anything wrong in a generated repo can be deleted in place without negotiating with a package.
How an item earns its place
The inclusion test tells you what is allowed in the kit. It does not tell you what should actually be there, and the difference matters because plenty of legitimately-settled decisions are not worth the weight. Two rules do the rest of the work, and both are enforced by the practice log rather than by judgement:
- The two-run rule. A candidate earns its place after it has appeared in the “what the kit should have had” column of two separate runs, against two different target domains. One run tells you about that target. Two starts telling you about the work.
- The removal column is the one that keeps the kit a kit. Every run also fills in “what the kit had that got in the way,” and anything named there twice comes out. A kit with no removal mechanism only grows, and a kit that only grows becomes the framework this lesson opened by warning about — not through any single bad decision, but through a hundred individually reasonable additions. Which is Metz’s mechanism exactly.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Name the asymmetry between leaving something out of the kit and putting the wrong thing in.
Check your answer
Leaving something out costs minutes, once, in the one prototype that wanted it — and it announces itself immediately, because you sit there building the missing thing. Putting the wrong thing in costs a smaller number of minutes in every prototype from then on, and it never announces itself at all, because the symptom is the next prototype feeling slightly awkward rather than anything visibly breaking.
Which is why the test is biased toward exclusion, and why the removal column in the practice log matters more than the addition column even though it is the one nobody fills in.
Hands on
The kit manifest, version zero
Done when: A manifest file exists listing every candidate from the baseline run as IN or OUT, each with the inclusion test applied in one sentence — including at least three deliberate exclusions with what each would have cost if included.
- Take the phase table from your baseline run and turn every phase into one or more concrete candidate items. “Auth stub” is a candidate; “yak-shaving” is not, so decompose that row into what actually happened during it.
- For each candidate, write the inclusion test verdict as a sentence that names the decision: not “IN — useful” but “IN — encodes that a prototype session is faked with a user-switcher and never touches a real identity provider.” If you cannot name the decision, the item fails.
- Force at least three OUT verdicts, and for each one write what including it would have cost — specifically, which future prototype it would have constrained. If nothing is coming out, you are describing a framework and have not applied the test.
- Mark every IN item with the phase it removes from the three-hour clock, using the measured minutes from your baseline run rather than an estimate. An item that removes no measured phase is a candidate for the two-run rule, not an inclusion — flag it PENDING rather than IN.
- Do not build anything yet. The manifest is the artifact; a kit built before the manifest is a kit built from the baseline run’s irritations.
- Bring the manifest into the chat. I will argue specifically with your IN list, and hardest with anything marked IN that is really a product decision wearing an infrastructure name — run states and gate semantics are where that hides.
What this does not cover
The manifest decides what the kit contains on the day you write it, and says nothing about the day six months later when you clone it and the first hour goes to finding out what broke. That failure has nothing to do with whether the inclusion test was applied well — a perfectly chosen kit rots at the same rate as a badly chosen one. Naming that cost and putting a budget against it is the maintenance-tax lesson, which closes this module.
The manifest also lists a trace view and a review gate as items without saying what either of them talks to. Both need a backend that streams something, and standing up a real agent graph is the single slowest way to get one. The fake-backend module builds the mock they run against, and the lesson on wiring the trace and the gate once is where those two manifest entries stop being entries and start being code.
Read this next — primary source
The Wrong AbstractionSandi Metz, 20 January 2016 — free, about ten minutes
This lesson borrows one sentence. The post itself is mostly the mechanism behind it — a step-by-step account of how a good abstraction rots, written from the point of view of each successive developer, none of whom does anything unreasonable. That framing is what makes it useful here rather than merely quotable: nobody in the story is careless, and the abstraction still ends up unmaintainable. Reading the whole thing gives you the pattern to recognise in your own kit six months from now, when the parameter you are about to add will feel entirely justified.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.