# Designing infrastructure for AI-assisted product development

> The pattern I ship to make design systems agents can read: shared components, synced tokens, closed-world registry.

- Author: Alejandro Haydar
- Published: 2026-07-22
- Category: Product Design
- Canonical: https://alejandroastroport.netlify.app/showcases/tios-design-system/

---
import { Image } from "astro:assets";
import CodeBlock from "../../components/mdx/CodeBlock";
import storybookTiosButton from "../../assets/images/tios/storybook-tios-button.png";
import previewComponentsButtons from "../../assets/images/tios/preview-components-buttons.png";
import agentMetaRegistry from "../../assets/images/tios/agent-meta-registry.png";
import pencilModePrimary from "../../assets/images/tios/pencil-mode-primary.png";
import diagramSvg from "../../assets/images/tios/diagram.svg?url";



Thought Industries needed a design system that could support both human designers and AI-powered workflows. I restructured the system with shared components and synchronized design tokens, creating a reliable foundation that improved maintainability and enabled AI to generate more consistent, production-ready interfaces.

> Agents don't fail because they lack creativity. They fail because nothing tells them what exists.

Every team that leans into AI-assisted development hits the same failure pattern. Agents produce UI that looks plausible and breaks the product contract — they import primitives the app doesn't ship, hardcode palette values, and compose components in ways the app doesn't support. The fix isn't another component. It's a bounded system agents can actually read.

Let me share the pattern that shipped. Thought Industries is a concrete example of it working. What follows is what the pattern is, how it played out there, and why it holds up on the next team too.



- **Role:** Design Engineer
- **Team:** Alejandro (design system contract, agent metadata, design-tool sync), a platform engineer (`@ti/ui` scaffold, `cn()`, Storybook 10), and an engineering partner testing agentic cards
- **Timeline:** Phase 0 started July 1, 2026; Jul 9 and Jul 22 syncs moved the system into `packages/ui/`; Phase 1 asset migration completed in July 2026
- **Skills spotlighted:** design systems, design-to-code tooling, agentic UI infrastructure

---

## The problem AI-assisted product teams hit

Most product teams already have components. What they don't have is an **inventory** anything — human or machine — can point at. Agents don't intuit the current stack. They extrapolate from whatever training data is closest to it (usually generic ShadCN docs) and generate code that doesn't compile, doesn't fit the product, or both.

At Thought Industries, that showed up in three specific failure modes I now watch for on every AI-assisted project:

- **Missing primitives:** Agents imported `Alert`, `Select`, or `Form` patterns from ShadCN docs even when the app did not expose them.
- **Off-system styling:** Agents reached for arbitrary Tailwind values like `bg-blue-500`, bypassing tenant theming.
- **Invalid composition:** Agents nested components in ways that looked normal in training data but violated product conventions.

### Two audiences need the same fix. 

<figure class="breakout my-8">
  <img src={diagramSvg} alt="Diagram: designers, engineers, and coding agents all pointing at one shared closed-world inventory instead of separate sources of truth." class="w-full h-auto" />
  
</figure>

Designers and engineers need one source of truth. Agents need a closed world they can't hallucinate outside of. The constraints that make this pattern harder — the ones I plan around every time:
- A platform team may already be scaffolding a shared UI package.
- Storybook or the design-tool pipeline may be mid-migration.
- Multiple product surfaces (learner, manager, admin) need to share tokens.
- Design canvas, CSS tokens, and component code have to stay in sync while migration happens one piece at a time.

---

## The choice: one home, not two

The tempting move is to build a fresh design system package you own. It ships faster, avoids merge friction, and lets you set every convention. It's also the move that quietly creates two design systems the team has to reconcile forever.

I made that mistake in Phase 0 at Thought Industries. I spun up a parallel `packages/design-system/` package with its own Storybook 8 instance, 17 scaffold stories, and a port-6007 workflow. It worked locally. It was architecturally wrong.

When the platform team's real scaffold landed — one shared `@ti/ui` package, a common `cn()` utility, the Storybook 10 upgrade — the conflict wasn't a merge conflict. It was a product-architecture conflict. Every future design-system change would need to reconcile two Storybooks, two package configs, and two import paths.

I killed the parallel package and moved the work into `@ti/ui`. That decision is now the first thing I check on any new design-systems engagement: **is there already a home for this? If yes, join it. If no, negotiate one before building.**

### Key decisions

**Merge into the existing shared package, not a parallel one.** One name, one Storybook, one utility surface. Validated in Jul 9 and Jul 22 syncs with the platform team.

**Make the registry the source of truth.** `.ai/meta.ts` re-exports the component inventory, composition rules, token map, Storybook catalog, and agent hints. A code review pass tightened the types so Storybook and agents read from the exact same object.

**Keep design artifacts beside implementation.** Feature designs live near their components. The shared design-tool library stays in one canvas with a token sync script. That kept the canvas close without making it a competing source of truth.

<CodeBlock
  client:visible
  filename="packages/ui/"
  hideLineNumbers
  code={`packages/ui/
├── design.md
├── tios/                     ← HSL tokens + preview HTML
├── .ai/meta.ts               ← agent entry point
├── designs/shadeCN+ti.lib.pen
└── src/components/button/    ← co-located primitive`}
/>

---

## The pattern: a closed world agents can read

### The registry, in code

The core artifact is `.ai/meta.ts`. It's the thing that turns "we have a design system" into "agents have to use it." Five schemas, one shared vocabulary:

| Schema | Purpose |
|--------|---------|
| **ATOMS** | Enumerated components with import paths and migration phase; agents cannot invent primitives outside this list |
| **RELATIONSHIPS** | Valid parent-child composition and strict hierarchy (TableCell inside TableRow, DialogTitle required) |
| **DESIGN_TOKENS** | Approved Tailwind classes mapped to semantic tokens; blocks `bg-blue-500` and arbitrary values |
| **STORYBOOK** | Catalog of story titles, state args, and root atoms; shared by Storybook and agent context |
| **AI_HINTS** | Inline system instructions: icon library, variant separator, no emoji in copy, flex+gap spacing |

<figure class="full-width my-8" style="display:block;">
  <Image src={agentMetaRegistry} alt="Three layers of agent control: registry entry (.ai/meta.ts), closed-world component schema (ATOMS), and inline generation rules (AI_HINTS) — the contract Cursor agents read before generating UI." class="w-full h-auto" />
  <figcaption class="text-[13px] text-neutral-500 mt-2 max-w-3xl mx-auto px-6">Three files, one contract: `meta.ts` is the entry point, `atoms.ts` is the closed world, `ai-hints.ts` is the inline rulebook. If a component isn't in ATOMS, it doesn't exist.</figcaption>
</figure>

### What changes in generated code

Same prompt: *"add a destructive confirmation dialog to the enrollment card."*

**Before (no registry, generic training data):**

<CodeBlock 
  client:visible
  language="tsx"
  filename="Before — no registry"
  highlightedLines={[1, 6, 8]}
  code={`import { Alert, AlertDescription } from "@/components/ui/alert";   // ❌ doesn't exist
import { Dialog } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

<Dialog>
  <Alert className="bg-red-500 text-white">   // ❌ hardcoded palette
    <AlertDescription>Are you sure? This can't be undone.</AlertDescription>
    <Button variant="danger">Delete</Button>  // ❌ variant not in enum
  </Alert>
</Dialog>`}
/>

**After (`meta.ts` in context):**

<CodeBlock
  client:visible
  language="tsx"
  filename="After — meta.ts in context"
  highlightedLines={[2, 9, 11, 13]}
  code={`import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,  // ✅ from ATOMS
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

<Dialog>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Remove enrollment?</DialogTitle>   // ✅ RELATIONSHIPS require DialogTitle
    </DialogHeader>
    <p className="text-muted-foreground">This can't be undone.</p>  // ✅ DESIGN_TOKENS semantic class
    <DialogFooter>
      <Button variant="destructive">Remove</Button>    // ✅ variant in enum
    </DialogFooter>
  </DialogContent>
</Dialog>`}
/>

The difference isn't visual polish. The "before" fails at import time and bypasses the design system. The "after" uses real primitives, semantic tokens, and a supported Button variant. `AI_HINTS.global` makes the fallback explicit: if a component isn't in ATOMS, stop and ask instead of inventing one.

### Token bridge and design-tool sync

Tokens live as HSL in the canonical CSS file because the design tool edits them that way. Production Tailwind expects RGB triplets. A one-way bridge converts once:

- Designers keep editing HSL in the canonical token file.
- Engineers consume RGB downstream.
- Tenant primary colors swap through a `data-primary` attribute instead of per-customer CSS forks.

A sync script pushes CSS variables back into the design tool's Primary axis. Without it, drift is quiet: a designer picks a color in the canvas that no longer matches production because someone changed a semantic token weeks earlier. The sync reduces that drift window to the length of one commit.

<figure class="breakout my-8">
  <Image src={pencilModePrimary} alt="Mode Primary in Pencil: seven tenant themes mapped to tiOS CSS variables (--primary, --ring, sidebar tokens), with live button variants on canvas." />
  <figcaption class="text-[13px] text-neutral-500 mt-2">Mode Primary in Pencil — seven tenant themes wired to CSS variables. Sync scripts push token changes into the canvas so design and code stay one artifact apart.</figcaption>
</figure>

### Fast previews before Storybook

Every primitive gets a plain-HTML preview served by a Python one-liner. It's the fastest way to check variants and size tokens without booting Storybook or Vite. On any large legacy monorepo the feedback loop matters more than the fancy tool — I ship the preview server on day one.

<figure class="breakout my-8">
  <Image src={previewComponentsButtons} alt="tiOS preview HTML — five button variants, size tokens (32/48), and disabled state from the canonical design contract in packages/ui/tios/preview/." />
  <figcaption class="text-[13px] text-neutral-500 mt-2">The canonical Button contract, rendered from `tios/preview/components-buttons.html`. Five variants, two sizes, one disabled state — the reference designers and engineers point at.</figcaption>
</figure>

With Storybook 10 in `packages/ui/.storybook/`, the build verifies the tiOS sidebar grouping and the tenant primary toolbar. Stories reference the meta catalog instead of hardcoding args, so the catalog scales as remaining LMS primitives move over one at a time.

---

## What this pattern delivers

Phase 1 didn't pretend adoption was done. The LMS still imports from `@/components/ui/*`, and controlled measurement belongs in Phase 2. What shipped is the foundation the rest of the work depends on:

- **One package target.** The `packages/design-system/` vs. `packages/ui/` debate ended. The design system conversation now happens in one place.
- **Storybook 10 unblocked.** Design system assets merged without reviving old Storybook config, and the registry passed review for type safety.
- **Agent behavior improved in tested Cursor sessions.** Once `meta.ts` was in context, agents stopped proposing off-registry ShadCN imports in the tasks I ran. This is an observed pattern, not a controlled metric yet.
- **Migration path clarified.** Scaffold → asset migration → co-located stories → visual gating, paced by the LMS refactor.

Concrete scope at Thought Industries:

- **60 components** registered in ATOMS, from Badge through Toaster.
- **17 Storybook catalog entries** in the meta module.
- **1 live co-located CSF3 story** for Button.

That gap between "registered" and "live in Storybook" is intentional. The closed-world contract belongs in front of every agent before Storybook coverage is complete.

**What this shape of work delivers to a business:** tenant theming through variants instead of per-customer CSS forks, a practical path for AI-assisted UI generation that stays on-system, and a shared vocabulary for design, engineering, and coding agents. Velocity impact depends on Phase 2 adoption and instrumentation.

---

## The lesson I keep

The obvious retrospective note is "sync earlier." The more useful lesson is about ownership.

A package I controlled end-to-end felt safer than co-authoring one that was still moving. Design systems don't reward private ownership. They reward shared contracts. `meta.ts` matters more than any single Button or Dialog because it makes everyone else's work compose — designers, engineers, Storybook, the design tool, and coding agents all read from the same inventory.

The right unit of work isn't a package I own. It's a contract the team can point at. That reframe is what I bring to the next engagement.

---

## The deliverable is the contract

The substance of this work is the closed-world contract: a typed registry that gives agents a bounded vocabulary of components, tokens, and composition rules.

The story of this work is the pivot: I killed a parallel package and merged into a shared scaffold so the team could point at one artifact instead of reconciling two.

This is the version of design-systems work I trust now. The contract is the deliverable. The code matters because it makes the contract real. Thought Industries is one example of the pattern working. The pattern is what I bring to the next team.