Jaedan Persaud
Home
In progressSince September 2026

Groundwork

Making the second project cheaper than the first

Groundwork is the shared starting point for everything I build: a token contract, a lint rule that enforces it, a registry of components projects copy in and own, and the agent kit that installs all of it into a new repo. It exists because I kept paying for the same work twice.

contract tokens
43
registry items
24
packages on npm
3
lifecycle skills
7

Why it needed to exist

I noticed it somewhere around the fourth project on the same stack. Each one started from a blank Next app, picked its colours out of the air, re-themed shadcn by hand, and then spent its first week rediscovering traps I had already hit — the same build config, the same App Router gotchas, the same afternoon spent working out why one component was fighting dark mode.

None of it was hard. That is exactly what made it expensive. Hard problems get attention and get solved once; the tax on re-deciding solved problems gets paid quietly, a day at a time, and never appears as a line item. One project had a 426-line document whose only job was describing its own colours — written by hand, and already drifting from the CSS it was supposed to describe.

So I set one test: the second project should be cheaper than the first. It wasn't. Everything below is an attempt to make that true.

What it is

A token contract, not a palette

One file names 43 semantic tokens and states what each is for. It deliberately holds no values — a project's own theme does that. Two projects can look nothing alike and still describe themselves in the same vocabulary, and the stylesheet and the docs are generated from the contract rather than maintained alongside it.

A lint rule with teeth

An ESLint plugin fails on any colour outside the contract, reading its allowlist from the contract itself. A rule nobody can drift from turned out to be worth more than a document everybody agrees with.

A registry, not a component library

24 components served as a shadcn registry. Projects copy them in and own the copy. Published versions are immutable and committed, so an update arrives as a three-way merge against the version you installed and your local edits survive it. Importing a component takes your edits away; copying one takes your updates away. This keeps both.

The agent kit

Skills for the loop a feature actually runs through, gotchas tagged by stack, and three interview prompts that produce a project's overview, architecture and build plan. The prompts are plain markdown and assume no tooling, so the most useful part is not locked behind installing the CLI.

The decisions behind it

  1. 1

    Names are shared, values are not

    The tempting version of this ships colours. That makes every project look like the same project, and the first time one needs to look different you either fork the system or break it. Roles are what genuinely transfer between projects. Values are what should differ, and pretending otherwise is how design systems get abandoned.

  2. 2

    Copy, don't import

    A component you import is a component you cannot fix at 2am. Copying looks like duplication right up until a project needs one thing changed, and then it is the difference between a one-line edit and a fork. The cost is that updates have to be merged rather than pulled, which is why the registry commits every published version as a merge base.

  3. 3

    The build enforces it, not the docs

    Every convention I have written down has eventually been ignored, including by me. The ones that held were the ones that failed a build. So the contract generates its own documentation, a hook refuses edits to generated files, and the registry build refuses to publish changed content under a version number that already shipped.

  4. 4

    A component earns its place on the second ask

    Nothing enters the registry because it might be reusable. It enters when a second project actually wants it. The alternative is a foundation full of abstractions designed around a single caller, which is how a shared layer becomes the thing everyone routes around.

  5. 5

    When the rule loses, the rule is wrong

    If following the contract makes a real app visibly worse, that is a finding about the contract, not a licence for a local exception. Exceptions are how a lint rule quietly becomes an allowlist of workarounds. I write the finding down and change the rule instead.

Where it stands

  • Token contract, lint rule and 24 registry items — published to npm and deployed
  • The CLI end to end: a fresh repo initialised, interviewed by the kickoff prompts, and its first feature built and merged
  • The first consumer project moved off its hand-written colour document and onto the contract
  • Groundwork is built with its own kit, so every skill it ships has been used to build the thing that ships it
  • A docs pass, so the site stops describing an older CLI than the one on npm
  • A second consumer, to prove one update can land across two projects at once

What changed once it existed

The 426-line colour document is gone. A project's entire palette is now one theme file measured against a contract it cannot drift from, and the lint rule catches the drift I used to find in code review three weeks later.

A new repo starts with its context, its guardrails and its loop already in place instead of an empty config file and good intentions. That is the part I underrated: most of what made the second project expensive was not code, it was the absence of anything telling a new repo how this one works.

Building the kit with the kit turned out to be the only review that matters. Several skills looked reasonable written down and did not survive being used, and those got deleted rather than shipped.

Built with

TypeScriptBunNext.js 16Tailwind CSSshadcn registryESLintnpm

Where the line is

Groundwork is a foundation, not a framework. It owns tokens, primitives, patterns and the loop it is built with. It does not own product decisions, backend choices, or anything a single project would be better off deciding for itself — the moment it starts doing that, it becomes the thing I have to work around.

Have a look

If you are solving the same problem in your own stack, or you want this kind of foundation for your team, email me at hi@jaedan.me.