diff --git a/ENGINEERING_LOG.md b/ENGINEERING_LOG.md
index 6d11dad..9f19dd3 100644
--- a/ENGINEERING_LOG.md
+++ b/ENGINEERING_LOG.md
@@ -887,3 +887,54 @@ references shipped products only, and all numbers regenerate from the new
catalog (GMV ₹60,530 · 13 captures · net@1M ₹9,30,93,000).
---
+
+## 2026-09-04 — D7: the second agent — the ghost is summoned over the wire, not prompt-typed
+
+**What happened:** the request was "ask the desk agent to play a song, and a
+second agent comes and performs it." The cheap version of that is an LLM
+writing a YouTube URL into chat. That version costs tokens on every command,
+hallucinates track names, and makes the second agent a puppet of the first.
+The honest version makes the handoff structural: the desk agent *calls* the
+ghost over an event bus, the way it calls tools.
+
+**The call it came from:** the desk agent already had a quota discipline
+(regex brain catches shopping commands at zero tokens; the LLM only meets
+casual chat). Music text must never reach the model — "play Africa by Toto"
+is a command, not a conversation. So the ghost got its own rules brain
+(`music/brain.ts`): deterministic intent parsing for play/skip/pause/resume/
+stop/louder/quieter, with mood words ("something chill") mapped to fixed,
+curated search strings — replayable bit-for-bit, zero tokens, forever. The
+LLM brain is now skipped entirely when the rules brain catches music.
+
+**The wall that mattered:** resolving a song name to a playable track without
+scraping. The answer is the official YouTube Data API v3, server-side only
+(the key never reaches the browser), embeddable-filtered, with the embed
+player riding *visible* inside the ghost's control bubble — no 1px hidden
+frames, no re-built player UI. Search costs 100 quota units, durations 1
+more; the free tier covers hundreds of summons a day. No key: the desk says
+so calmly and the ghost stays in the floor — the refusal is an honesty
+label, not an error.
+
+**The bugs this round caught before they shipped:**
+1. **The arrive state was a trap.** The first draft set `state: "arriving"`
+ and never advanced it — the beat bob is gated on `"out"`, so the ghost
+ would have materialized and stood frozen forever. Arrive now schedules
+ its own successor (the same lesson as the toast beats, D5-era).
+2. **The eye mood cycle hit a temporal dead zone.** The rest-timer closure
+ referenced its own container before initialization. Rewritten so every
+ timer has a declared home and a clear-all teardown.
+3. **A sed edit ate the "ceiling rides with the intent" comment** while
+ inserting the music case — caught by re-reading the diff, restored in
+ the same commit. Comments are load-bearing in this repo.
+
+**The contract that grew:** the band (`--band`) is the product's one
+non-verdict hue, owned by the ghost the way the aurora is owned by the bot
+(DESIGN_SYSTEM §2, §7). Face law inherited: eyes only, no mouth — the ghost
+sings through the bob and the eye states, not through a drawn mouth.
+
+**The test it became:** `tests/music-brain.test.ts` — control verbs as whole
+commands (a "pause" inside a shopping sentence stays shopping), mood words
+resolve to fixed queries, "play X" strips politeness, bare "music" resolves
+to the chill mood.
+
+---
diff --git a/JUDGE.md b/JUDGE.md
index 8b9db47..6bd2f3a 100644
--- a/JUDGE.md
+++ b/JUDGE.md
@@ -59,6 +59,9 @@ numbers read live from the ledger).
Evidence: `make demo` · `make triage` · `data/state/ledger.jsonl` (hash-chained
JSONL — the audit trail IS the database) · `src/components/customs/why.tsx` ·
`src/components/customs/paper.tsx` + `PAPER.md` (twins, AGENTS invariant 11).
+Try the second agent too: tell the desk agent "play africa by toto" — a red
+ghost is summoned over an event bus and performs it; the commands cost zero
+tokens (tests/music-brain.test.ts proves the rules brain owns the path).
### 4 — Failure handling & recovery
Claim: twelve authored attacks are refused (or held) with specific reason
diff --git a/README.md b/README.md
index 23d2852..ff06e66 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,9 @@ the **Why it exists** view and `PAPER.md` §7.
| `src/lib/customs/agent/nlu.ts` | deterministic rules brain |
| `src/lib/customs/agent/llm.ts` | optional LLM brain — measured, never trusted |
| `src/lib/customs/adapters/index.ts` | naive / MCP-style / ACP-style transports |
+| `src/lib/customs/music/brain.ts` | the ghost's rules brain — zero-token music intents |
+| `src/lib/customs/music/youtube.ts` | server-side track resolution (YouTube Data API v3) |
+| `src/lib/customs/music/store.ts` | the client bus the desk agent calls the ghost over |
| `src/lib/customs/fuzz/corpus.ts` | the authored attack corpus |
| `src/lib/customs/meter.ts` | channel P&L + projection (assumptions declared) |
| `src/lib/customs/store/catalog.ts` | Fieldnote Supply — 21 products, integer paise |
@@ -179,6 +182,8 @@ the **Why it exists** view and `PAPER.md` §7.
| `src/components/customs/hero-bot.tsx` | the customs bot — one smooth volume, token-inked, reduced-motion aware |
| `src/components/customs/why.tsx` | why it exists + the architecture (drawn as cards) + desk ledger |
| `src/components/customs/agent-kit.tsx` | the agent kit view — three doors (chat / real MCP / ACP), the kit board, copy-context, live walkthrough |
+| `src/components/customs/agents.tsx` | the agents view — the two characters live, the handoff wire, a working summon |
+| `src/components/customs/music-ghost.tsx` | the second agent — the red ghost the desk summons to play music |
| `src/components/customs/paper.tsx` | the working paper view — a clean document page, numbers live |
| `src/components/customs/playground.tsx` | buyer side: chat, mandate approval, red team |
| `src/components/customs/control-room.tsx` | merchant side: P&L, approvals, the order ledger |
diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md
index 460f158..7248184 100644
--- a/docs/DESIGN_SYSTEM.md
+++ b/docs/DESIGN_SYSTEM.md
@@ -50,6 +50,12 @@ be colored (the single exception: the bot's aurora, §7):
Each has `-ink` (a ~8–10% tint for fills) and `-contrast` (text on solid fills)
companions. A color that does not map to a verdict does not exist here.
+**The one non-verdict hue: the band.** `--band` (`#e0604e` dark / `#c04a3a` light,
+with `-ink`/`-contrast` companions) belongs to the music ghost — the second
+agent — the way the aurora hues belong to the bot. It is a character color,
+not a verdict: it must never appear on stamps, chips, text or borders outside
+the ghost and its transcript chip. Nothing else in the product may wear it.
+
### The sheet
`.sheet` (the paper view) is a **white document in both themes** — x.ai's dark site
@@ -186,6 +192,70 @@ are cute: smooth, quiet, small tells.
lilac, amber at 0.7–0.85 stops, themed total opacity 0.55 dark / 0.38 light)
are the only non-verdict colors in the product. Keep exactly four.
+### The second character: coco, the music ghost — `music-ghost.tsx`
+
+Coco is the desk head's own body in the band red: the same 32×32 round
+volume, the same `desk-eye-g` / `desk-eye` classes (glance for glance,
+blink for blink — the choreography is shared, not copied). It is
+summoned by the desk agent's chat and plays what was asked.
+
+1. **Face law:** eyes only — no mouth, ever (the desk head is mouthless;
+ so is coco). Expression is eye state (wide; happy arcs when the
+ passage earns it) plus body motion. No new parts.
+2. **Twin law:** the silhouette is the desk head's own — one circle, no
+ skirt, no extra geometry. The band fill, the beat bob, and the sheen
+ (the bot's aurora pattern scaled down: a static rect clipped to the
+ circle, the paint drifting via SMIL, paused under reduced motion)
+ are the only departures. Paused music freezes the bob — coco
+ breathes only when the track does.
+3. **Motion budget:** arrive/sink (one 0.9s transform each), the beat
+ bob, the shared eye choreography, the tucked-away fade. That is all.
+4. **Controls** live in one hairline bubble: the YouTube player at
+ visible size (never a hidden or 1px frame), track, channel, a
+ transform-only progress ride, exactly three buttons (play/pause,
+ skip, stop). Mono for the metadata, sans for the title. No close box
+ on the card — the card is dismissed by stop, or hidden with the
+ music still humming. The card stays mounted while a track lives —
+ the background sink is visual only, so the music never dies
+ mid-track; stop alone ends the player, and the next summon starts
+ a fresh one.
+5. **Coco's chat** is the desk agent's panel in miniature, and it opens
+ on a click of coco: the same hairline header (the face at 24px, one
+ status dot — band while humming), the same quiet text close, the
+ same sunken composer well, TypeLine for coco's voice. It drops
+ downward from coco when there is sky below, rises when coco sits
+ low — clearing the card when the card is up (the desk panel's own
+ below/above law). Its ear is the rules brain: control verbs run at
+ zero tokens; a play wish rides the desk's chat pipeline over the
+ wire — the server-side rules brain catches it at zero tokens (the
+ LLM never wakes) and the crate answers in coco's own chat, no desk
+ conversation required. Hide ≠ stop: "hide" (chat word or header button)
+ tucks coco away while the music keeps humming; "give me controls"
+ recalls it. A **double-click** on coco toggles the video: it sinks
+ to the background — the music hums on, coco stays to talk to — and a
+ second double-click brings the screen back (a video the browser
+ still holds back can't sink — tap play first). "give me controls"
+ always works too. A ↻ fresh page fades the conversation before it
+ clears — turning a sheet over, not a wipe; the music is untouched.
+6. The summon is logged in both transcripts as a compact `coco ·` chip
+ with the chosen track — a receipt of the handoff, not decoration.
+7. **The agents view** presents both characters on their OWN woven desk
+ mat — deliberately not the bot's wool (HeroFabric is a hand-cut felt
+ blob with turbulence edges and aurora pastels; the agents' mat is
+ rectangular loom cloth: warp/weft crosshatch, machine hem, two dye
+ pools — ink for moco's cloth, band for coco's). The two cloths are
+ sewn together by a hand running-stitch seam corner to corner, and
+ the seam IS the bus wire — the handoff is the join between the two
+ cloths, never a line drawn on top. Elsewhere the house's craft
+ vocabulary holds (static feTurbulence grain, running-stitch seams,
+ woven-label nameplates, cross-stitch corner tacks, thread = hairline
+ gray except on the ghost's own cloth) under the craft school's
+ discipline (Web Interface Guidelines /
+ animations.dev / Linear's calmer interface): hover changes one
+ quiet thing, the live heads and the one-shot wire ping are the
+ delight budget, and the card/chat/head stack in one flow column
+ below the head so nothing ever overlaps.
+
## 8. Copy voice
Lowercase for actions and system labels ("see the protocol run live",
diff --git a/scripts/verify.mjs b/scripts/verify.mjs
index a1ad21b..cecd460 100644
--- a/scripts/verify.mjs
+++ b/scripts/verify.mjs
@@ -43,6 +43,9 @@ const REQUIRED = [
"src/components/customs/control-room.tsx", "src/components/customs/landing.tsx",
"src/components/customs/why.tsx", "src/components/customs/paper.tsx",
"src/components/customs/agent-kit.tsx",
+ "src/components/customs/music-ghost.tsx",
+ "src/lib/customs/music/brain.ts", "src/lib/customs/music/youtube.ts",
+ "src/lib/customs/music/store.ts",
"src/components/customs/bits.tsx", "src/components/customs/chat-events.tsx",
"src/components/customs/demo-player.tsx",
"src/components/customs/theme.tsx",
diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts
index a21ee63..1809777 100644
--- a/src/app/api/chat/route.ts
+++ b/src/app/api/chat/route.ts
@@ -17,6 +17,9 @@ export async function POST(req: NextRequest) {
message: string;
adapter?: string;
tier?: string;
+ persona?: string;
+
+ music?: { title?: unknown; channel?: unknown; playing?: unknown };
};
const rt = getRuntime();
const adapter = (ADAPTERS.includes(body.adapter as AdapterId) ? body.adapter : "naive") as AdapterId;
@@ -24,7 +27,19 @@ export async function POST(req: NextRequest) {
let session: BuyerSession | undefined = body.sessionId ? rt.sessions.get(body.sessionId) : undefined;
if (!session) session = newSession(rt, (body.tier === "ATTESTED" || body.tier === "MANDATED" ? body.tier : "UNVERIFIED") as BuyerSession["tier"]);
- const result = await agentTurn(rt, session.sessionId, body.message ?? "", adapter);
+ const persona = body.persona === "coco" ? ("coco" as const) : ("desk" as const);
+ /* the ghost's state, if it hums — one compact line of context so the
+ cheap voice can answer ANY phrasing of "what's playing" (the
+ regex brains keep the commands; this is chatter context only) */
+ const music =
+ body.music && typeof body.music.title === "string" && typeof body.music.channel === "string"
+ ? {
+ title: body.music.title.slice(0, 80),
+ channel: body.music.channel.slice(0, 80),
+ playing: body.music.playing === true,
+ }
+ : null;
+ const result = await agentTurn(rt, session.sessionId, body.message ?? "", adapter, { persona, music });
return NextResponse.json({
ok: true,
sessionId: session.sessionId,
diff --git a/src/app/globals.css b/src/app/globals.css
index 88276ff..8b54db3 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -60,6 +60,9 @@
--color-refused-contrast: var(--refused-contrast);
--color-held: var(--held);
--color-held-ink: var(--held-ink);
+ --color-band: var(--band);
+ --color-band-ink: var(--band-ink);
+ --color-band-contrast: var(--band-contrast);
}
/*
@@ -96,6 +99,13 @@
--held: #d6a251;
--held-ink: rgba(214, 162, 81, 0.09);
+ /* the band — the music ghost's one hue. A character color, not a
+ verdict: it belongs to the second agent the way the aurora hues
+ belong to the bot (DESIGN_SYSTEM §7). Nothing else may wear it. */
+ --band: #e0604e;
+ --band-ink: rgba(224, 96, 78, 0.12);
+ --band-contrast: #1c0a07;
+
--sb-thumb: rgba(255, 255, 255, 0.14);
--sb-thumb-hover: rgba(255, 255, 255, 0.28);
--card-hover: #0d0d0d;
@@ -167,6 +177,9 @@ html.light {
--refused-contrast: #f8efe9;
--held: #916a1e;
--held-ink: rgba(145, 106, 30, 0.07);
+ --band: #c04a3a;
+ --band-ink: rgba(192, 74, 58, 0.1);
+ --band-contrast: #fdf1ee;
--sb-thumb: rgba(10, 10, 10, 0.22);
--sb-thumb-hover: rgba(10, 10, 10, 0.4);
@@ -426,6 +439,42 @@ html.light {
.desk-head.thinking .desk-eye-g.e2 .desk-eye {
animation-delay: 1.07s;
}
+
+ /* the music ghost — the desk head's red twin. The eyes ARE the desk
+ head's eyes (shared .desk-eye-g/.desk-eye classes above — the same
+ idle choreography, glance for glance, blink for blink). The ghost
+ adds only: the arrive/sink, and while the track plays, the beat
+ bob. Paused music freezes the bob — the ghost breathes only when
+ the track does. All transform/opacity; kill-switched below. */
+ .ghost-arrive {
+ animation: ghost-arrive 0.9s cubic-bezier(0.25, 1, 0.5, 1) both;
+ }
+ .ghost-sink {
+ animation: ghost-sink 0.9s cubic-bezier(0.25, 1, 0.5, 1) both;
+ }
+ .ghost-stage.ghost-playing {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ghost-bob 1.15s ease-in-out infinite alternate;
+ }
+ .ghost-eye-state,
+ .ghost-eye-arc {
+ transform-box: fill-box;
+ transform-origin: center;
+ transition: opacity 0.35s ease;
+ }
+ @keyframes ghost-arrive {
+ from { transform: translateY(56%) ; opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+ }
+ @keyframes ghost-sink {
+ from { transform: translateY(0); opacity: 1; }
+ to { transform: translateY(56%); opacity: 0; }
+ }
+ @keyframes ghost-bob {
+ from { transform: translateY(-2.4px) rotate(-2deg); }
+ to { transform: translateY(2.4px) rotate(2deg); }
+ }
@keyframes desk-idle {
0%, 12%, 100% { transform: translate(0, 0); }
6%, 9% { transform: translate(0, -3px); }
@@ -948,6 +997,25 @@ html.light {
}
}
+/* the wire's ping — one shot when a summon actually happens */
+.wire-packet {
+ animation: wire-run 0.9s cubic-bezier(0.25, 1, 0.5, 1) 1 both;
+}
+
+@keyframes wire-run {
+ 0% {
+ opacity: 0;
+ transform: scale(0.6);
+ }
+ 35% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(1.25);
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.animate-rise,
.beat-in,
@@ -969,7 +1037,11 @@ html.light {
.hb-glint,
.hb-shadow,
.hb-pom,
- .agent-yarn {
+ .agent-yarn,
+ .ghost-arrive,
+ .ghost-sink,
+ .ghost-stage.ghost-playing,
+ .wire-packet {
animation: none !important;
}
.pulse-dot::after {
diff --git a/src/components/customs/agents.tsx b/src/components/customs/agents.tsx
new file mode 100644
index 0000000..97fa9a8
--- /dev/null
+++ b/src/components/customs/agents.tsx
@@ -0,0 +1,490 @@
+"use client";
+
+/**
+ * agents.tsx — the agents view, in the landing's own grammar: a hero
+ * stage (display type + the two heads large on their woven desk mat,
+ * the summon button), then py-16 sections — the sewn frame, the
+ * handoff log, the cheap pair. The stage fabric is the agents' OWN
+ * object, deliberately not the bot's wool: where HeroFabric is a
+ * hand-cut felt blob with turbulence edges and aurora pastels, this is
+ * a rectangular WOVEN mat — warp/weft crosshatch, machine hem, two
+ * cloths (moco's ink pool, coco's band pool) joined by a hand
+ * running-stitch seam that runs corner to corner. The seam IS the bus
+ * wire: the handoff is the join between the two cloths, the bus node
+ * rides it at center. The craft is the house's felt vocabulary (static
+ * grain, running-stitch seams, woven-label nameplates) held to the
+ * discipline: hover changes one quiet thing; the delight budget is
+ * the live heads + the one-shot ping.
+ */
+import { useId, useState } from "react";
+import { cn } from "@/lib/utils";
+import { DeskHead, GhostButton, InkButton, Reveal, SectionLabel } from "./bits";
+import { GhostBody } from "./music-ghost";
+import { musicBus } from "@/lib/customs/music/store";
+import type { ChatEvent } from "@/lib/customs/agent/loop";
+import type { View } from "./shell";
+
+/** FeltGrain — the desk's material: static desaturated noise at 5% */
+function FeltGrain() {
+ const id = `ag-grain-${useId().replace(/:/g, "")}`;
+ return (
+
+ );
+}
+
+/**
+ * AgentsFabric — the woven desk mat the two agents share. The bot's
+ * mat is hand-cut wool; this is its structural opposite: loom cloth.
+ * Warp and weft as a crosshatch pattern, a machine hem (fine dash —
+ * a different stitch voice from the hand seam), two dye pools (ink
+ * for moco's cloth, band for coco's), one fold, one sheen, and the
+ * hand seam that sews the two cloths together corner to corner —
+ * the bus wire rides that seam. Static throughout; token colors only.
+ */
+function AgentsFabric({ className }: { className?: string }) {
+ const uid = useId().replace(/:/g, "");
+ const mat = `af-mat-${uid}`;
+ const weave = `af-weave-${uid}`;
+ const dyeM = `af-dm-${uid}`;
+ const dyeC = `af-dc-${uid}`;
+ const soft = `af-soft-${uid}`;
+ return (
+
+ );
+}
+
+/** Stitch — a running-stitch seam: SVG dash geometry, static */
+function Stitch({ inset = 6, className, rx = 3 }: { inset?: number; className?: string; rx?: number }) {
+ return (
+
+
+
+ );
+}
+
+/** CornerTack — a cross-stitch X reinforcing the frame's corners */
+function CornerTack({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+/** Nameplate — the woven label: hairline patch, inset stitch, mono name */
+function Nameplate({ name, thread }: { name: string; thread: string }) {
+ return (
+
+
+ {name}
+
+ );
+}
+
+const DESK_SKILLS = [
+ "searches the catalog and builds the cart",
+ "checks out through a signed mandate — the gate holds the money",
+ "red-teams itself: attack: overspend-tier",
+ "floats on every page — click the black head to talk",
+];
+
+const COCO_SKILLS = [
+ "plays what was asked — \u201cplay africa by toto\u201d",
+ "skip · pause · louder · quieter — all at zero tokens",
+ "its own chat — click coco, type straight to it",
+ "hide ≠ stop — it tucks away, the music hums on",
+ "double-click — the video sinks to the background",
+];
+
+const HANDOFF: { tag: string; text: string; loud?: boolean }[] = [
+ { tag: "you", text: "\u201cplay africa by toto\u201d", loud: true },
+ { tag: "brain", text: "caught by the rules brain · 0 tokens · the crate is searched" },
+ { tag: "bus", text: "the music event rides the wire — the handoff" },
+ { tag: "coco", text: "▶ Toto - Africa (Lyrics) — 7clouds" },
+];
+
+function SkillLog({ lines }: { lines: string[] }) {
+ return (
+
+ {lines.map((s) => (
+
+
+ [ok]
+
+ {s}
+
+ ))}
+
+ );
+}
+
+export function AgentsPage({ onEnter }: { onEnter: (v: View) => void }) {
+ const [summoning, setSummoning] = useState(false);
+ const [said, setSaid] = useState(null);
+ /* the wire's ping: one shot, only when a summon actually lands */
+ const [ping, setPing] = useState(false);
+
+ const summon = async () => {
+ if (summoning) return;
+ setSummoning(true);
+ setSaid("searching the crate…");
+ try {
+ const ctrl = new AbortController();
+ const kill = setTimeout(() => ctrl.abort(), 12_000);
+ const res = await fetch("/api/chat", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ message: "play something chill", adapter: "naive" }),
+ signal: ctrl.signal,
+ });
+ clearTimeout(kill);
+ const data = (await res.json()) as { ok: boolean; events?: ChatEvent[] };
+ const music = data.events?.find(
+ (e): e is Extract => "kind" in e && e.kind === "music"
+ );
+ if (music && music.action === "play" && music.tracks.length) {
+ musicBus.emit({ action: "play", tracks: music.tracks, query: music.query, mood: music.mood, error: music.note });
+ setSaid(`coco is on it — ${music.tracks[0].title}`);
+ setPing(true);
+ setTimeout(() => setPing(false), 950);
+ } else {
+ setSaid(music?.note === "no-key" ? "the crate is locked — no YouTube key on this desk." : "nothing surfaced — try again.");
+ }
+ } catch {
+ setSaid("the crate is slow — try again.");
+ } finally {
+ setSummoning(false);
+ }
+ };
+
+ return (
+
+ Two agents share one desk and one wire. moco shops inside a
+ signed mandate; coco plays what was asked and talks back.
+ Summon it right here — the button rides the real bus.
+
+
+
+ {summoning ? "Searching the crate…" : "Summon coco"}
+
+ onEnter("agent")} ariaLabel="open the agent playground" variant="ink" className="h-11 px-5">
+ Open the playground
+
+
+
+ {said ?? "the wire is quiet — press summon and watch it run"}
+
+
+
+ {/* the stage: both heads on their woven mat, the bus seam
+ between them — sewn into the fabric, not drawn on top */}
+
+
+
+
+
+
+
+
+
+ {/* the bus — the wire's node, and the ping's home */}
+
+ bus
+ {ping && }
+
+
+
+
+
+
+ {/* ------------------------------ what they can do ------------------------------ */}
+
+
+
+
+ What they can do.
+
+ every command on the house brain · 0 tokens
+
+
+
+
+
+
+
+
+
+
+ {/* moco's patch — ink felt, gray thread; no border stitch of
+ its own: one stitch voice per object, the frame speaks */}
+
+
+
+
+
+
+
+ on the desk
+
+
+
+
+
+
+
+
+
+ {/* the seam — two cloths sewn by one band-thread running
+ stitch, the bus riding it (the hero mat's law, kept) */}
+
+
+
+ bus
+
+
+ {/* the seam, stacked — the same stitch, horizontal on phones */}
+
+
+
+ bus
+
+
+
+ {/* coco's patch — band dye only; the band thread lives in
+ the seam now, not on the border */}
+
+ one wire, four beats — as the transcript sees it
+
+
+
+ {HANDOFF.map((l) => (
+
+
+ [{l.tag}]
+
+
+ {l.text}
+
+
+ ))}
+
+
+
+
+
+ {/* ------------------------------ how it stays cheap ------------------------------ */}
+
+
+
+ How it stays cheap.
+
+
+
+
+
+ Rules brains
+ 01
+
+
+ Every command — search, add, checkout, play, skip, hide —
+ is caught by deterministic rules. Zero tokens, replayable
+ bit-for-bit.
+
+
+ parseMusicIntent("play africa by toto")
+
+
+
+
+
+
+ The model, on a leash
+ 02
+
+
+ Only casual chat reaches an LLM — the cheap voice, a tiny
+ prompt, a hard ceiling, two personas (moco talks desk,
+ coco talks ghost). Music never touches it.
+
+
+ brainMode() === "rules" // always, for commands
+
+
+
+
+
+
+
+
+ moco floats on every page — click the black head. coco answers when called.
+
+
+ );
+}
diff --git a/src/components/customs/chat-events.tsx b/src/components/customs/chat-events.tsx
index 558810f..02ee779 100644
--- a/src/components/customs/chat-events.tsx
+++ b/src/components/customs/chat-events.tsx
@@ -91,6 +91,8 @@ function ChatEventInner({
switch (event.kind) {
case "step":
return ;
+ case "music":
+ return ;
case "products":
return (
@@ -197,6 +199,48 @@ function ChatEventInner({
}
}
+/**
+ * MusicCard — the handoff receipt: the desk called the ghost, and the
+ * summon lands in the transcript like any other tool call. One hairline
+ * row, the band chip, the track it chose. Controls live on the ghost.
+ */
+function MusicCard({ event }: { event: Extract }) {
+ const label =
+ event.action === "play"
+ ? event.tracks[0]
+ ? "now playing"
+ : "summon refused"
+ : event.action;
+ const track = event.action === "play" ? event.tracks[0] : null;
+ return (
+
- The desk is open. Shopping runs on the house brain — ask it
- for something, or just say hi.
+ The desk is open — moco here. Shopping runs on the house
+ brain — ask for something, or just say hi.