Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
98eea73
feat(ghost): the second agent — the desk summons a red ghost to play …
srivtx Sep 4, 2026
1adc9aa
fix(ghost): the desk head's exact red twin + the player that wasn't r…
srivtx Sep 4, 2026
4212680
feat(coco): hide ≠ stop, controls on demand, coco's own ear
srivtx Sep 4, 2026
1079867
feat(coco): its own chat window, three-button card, hide in the header
srivtx Sep 4, 2026
241764a
feat(coco): the chat drops downward, a double-click sinks the video t…
srivtx Sep 4, 2026
b3158b5
feat(coco): a second double-click brings the screen back
srivtx Sep 4, 2026
e42e930
feat(coco): the fresh page — the desk panel's reset, inherited
srivtx Sep 4, 2026
ab78e5e
fix(coco): the background sink keeps the hum, and the chat body scrolls
srivtx Sep 4, 2026
d53582e
feat(coco): ask coco directly — the crate answers in its own chat
srivtx Sep 4, 2026
34e2ed7
feat(agents): the seventh surface — the two characters live, the hand…
srivtx Sep 4, 2026
82e91f6
feat(agents): the researched frame — rail, spotlight, ping; fix(coco)…
srivtx Sep 4, 2026
5556359
feat(persona): coco talks, moco knows what hums, and the frame is sewn
srivtx Sep 4, 2026
9236f2d
fix(coco): one flow column (no overlap), clean dblclick, full-width v…
srivtx Sep 4, 2026
4294698
feat(agents): the felt desk — grain, seams, dye, and the sheen inside…
srivtx Sep 4, 2026
514a0f4
fix(coco): the wrapper is the head's width — panels grow leftward, ne…
srivtx Sep 4, 2026
326d82f
fix(coco): the panels flip like moco's — sky below drops them, a low …
srivtx Sep 4, 2026
1315296
feat(agents): the woven desk mat — the stage wears its own cloth, and…
srivtx Sep 4, 2026
7e36b49
fix(agents): one stitch voice per object — the band thread sews the s…
srivtx Sep 4, 2026
5c9b720
feat(chat): moco and coco understand any phrasing of what's playing —…
srivtx Sep 4, 2026
4a1bbcf
fix(coco): the player waits for its mount and never poisons the ready…
srivtx Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions ENGINEERING_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
3 changes: 3 additions & 0 deletions JUDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down
70 changes: 70 additions & 0 deletions docs/DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions scripts/verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 16 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,29 @@ 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;

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,
Expand Down
74 changes: 73 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
Loading
Loading