We run a design system of roughly 140 Tailwind v4 components on top of Radix primitives, and last quarter we tried to fold msitarzewski/agency-agents into our review pipeline. Not as a chatbot gimmick — as a set of versioned reviewer personas that run against diffs before a human ever looks at them. The repo is a catalog of markdown agent definitions (232 agents across 16 divisions, MIT licensed), each with a mission, rules, workflow, and delivery standards. Relevant to us: design-ui-designer, design-ux-architect, design-whimsy-injector, and the Section 508 Accessibility Specialist.
What the integration actually looks like
The repo ships a conversion script targeting eleven tools. That flexibility is also the first trap: it offers both user-scoped and project-scoped installs, and in a monorepo those two silently diverge. Aider and Windsurf get full consolidation — every agent concatenated into one CONVENTIONS.md or .windsurfrules. With 232 agents that file is enormous, and anything auto-loaded into every request is a context tax you pay on work that has nothing to do with UI.
We resolved it by skipping consolidation entirely and loading personas by diff path:
// scripts/persona-router.ts
import { readFileSync } from "node:fs";
const ROUTES: Record<string, string[]> = {
"src/components/": ["design-ui-designer", "accessibility-auditor"],
"src/styles/": ["design-ux-architect", "design-whimsy-injector"],
};
const AGENT_ROOT = process.env.AGENCY_AGENTS ?? "~/.claude/agents";
export function personasFor(paths: string[]): string[] {
const hits = new Set<string>();
for (const p of paths)
for (const [prefix, agents] of Object.entries(ROUTES))
if (p.startsWith(prefix)) agents.forEach((a) => hits.add(a));
return [...hits];
}
export function load(agent: string): string {
return readFileSync(`${AGENT_ROOT}/${agent}.md`, "utf8");
}
Those loaded prompts get dispatched in parallel. When we benchmarked multi-provider fallback through B-Lost's relay gateway (baseURL: "https://b-lost.com/v1"), median TTFT variance across the two providers we route dropped to about 35ms, with no truncation on the ~3k-token agent files — which matters when four reviewers run per commit.
const res = await fetch(`${process.env.RELAY}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.RELAY_KEY}` },
body: JSON.stringify({ model: "reviewer-primary", messages: [...] }),
});
Where the personas stop being useful
A persona is priming, not validation. The Accessibility Auditor told us to verify focus containment in modals — correct advice that caught nothing on its own, because our Dialog looked correct while focus escaped on Tab. We had wrapped Radix's DialogContent in a custom portal to control stacking context, and our wrapper mounted the portal node outside the subtree Radix tracks. The persona cannot see that; only a real assertion can.
The second thing it can't fix is our own CSS. Our collapsible animated height in React state on every frame — classic layout thrashing — and no amount of "consider performance" in a markdown file changes that. The fix is architectural:
// components/Disclosure.tsx
import { useId, useState } from "react";
export function Disclosure({ label, children }: { label: string; children: React.ReactNode }) {
const [open, setOpen] = useState(false);
const id = useId();
return (
<div className="@container">
<button
type="button"
aria-expanded={open}
aria-controls={`${id}-panel`}
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm
font-medium text-slate-200 hover:bg-slate-800/60
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-sky-400
motion-safe:transition-colors"
>
{label}
<svg aria-hidden="true" viewBox="0 0 16 16" className="size-4 transition-transform
duration-200 motion-safe:ease-out data-[open=true]:rotate-90"
data-open={open}>
<path d="M6 3l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.75" />
</svg>
</button>
<div
id={`${id}-panel`}
role="region"
hidden={!open}
className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300
data-[open=true]:grid-rows-[1fr]"
data-open={open}
>
<div className="overflow-hidden px-3 pb-3 text-sm text-slate-400">{children}</div>
</div>
</div>
);
}
grid-template-rows: 0fr → 1fr transitions compositor-friendly without a single measurement in JS, and @container lets the same component reflow inside a sidebar without media-query guesswork. The keyframe layer is deliberately separate so prefers-reduced-motion can disarm it wholesale:
@keyframes panel-in {
from { opacity: 0; translate: 0 -4px; }
to { opacity: 1; translate: 0 0; }
}
@container (min-width: 28rem) {
[data-open="true"] > [role="region"] > * {
animation: panel-in 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Note the hidden attribute on the panel rather than display: none in a utility class — hidden removes it from the accessibility tree and from tab order, which a sr-only-style CSS hiding does not.
The open question we haven't answered: personas are cheap to add and impossible to test, so our reviewer roster keeps growing while our assertions stay flat. Do you gate persona changes behind the same review as code, or accept that the prompt layer drifts unversioned?
Top comments (0)