A thin band above the prompt that counts down to den v1: bar, done/total and the next critical-path ticket, read from the board.

[!WARNING] Work in progress. Dim Sum Den is built for one person's workflow (mine), and the agents in it rebuild it every day. Expect breaking changes: keys that move, panels that get redrawn, features that come and go without notice. If it's close to what you want, fork it and bend it into what you need.
A local control room where a small team of AI agents builds software, and you sit at the Pass.
Agents work tickets through a strict relay on a plain-file board, stop at gates only you can open, and perch on Bao, a big plush panda, in a 3D den that shows who is doing what at a glance.
Run locally · Controls · How it works · Numbers · Development · Glossary · Decisions
npm ci && npm run ui # then open http://127.0.0.1:4317/?demo=den

The den in Demo mode (?demo=den), captured from the running app.
product, architect, orchestrator, developer, scout, qa, security, designer, herald), each a Claude Code session running one ticket in its own git worktree. A role is a markdown file in .claude/agents/ that fixes its model, tools, skills and done criteria..scratch/, claimed with lock files and handed off through a board CLI (ADR 0003, ADR 0008). No database, no hosted service.apps/ui is a React Three Fiber scene of Bao and the agents, with a dashboard for the queue, gates and usage. Walk up to an agent to read its transcript, message it, or approve or deny what it's waiting on.?demo=den or press Watch the demo to replay a recorded session into the den. It makes no bridge calls and reads no tokens, and every action that would steer a real agent is greyed out.scripts/jev.mjs. It runs in shadow mode: each pick is logged beside what the relay actually did, and any error or missing key falls back to the current rule (ADR 0010).git, and the GitHub CLI (gh) for the orchestrator's PRs and CI checks.npm run smoke:ui.Linux, macOS and WSL. It's a local tool, and the bridge binds to 127.0.0.1 only.
git clone https://github.com/DerekHertz/DimSumDen.git
cd DimSumDen
npm ci
npm run ui # builds the UI, then starts the bridge on port 4317
Open <http://127.0.0.1:4317/>. With no agents running, add ?demo=den to watch a recorded session.
Run an agent as its own Claude Code session:
claude --agent orchestrator # proposes tickets and runs the relay
claude --agent developer # or any other role in .claude/agents/
npm run next-session # prints the command to resume from the latest handoff
| Key | Action |
|---|---|
| Walk button | Enter walk mode (the on-screen W A S D pad works on touch screens) |
| W A S D / arrows | Walk; hold Shift to go faster |
| Mouse drag | Look around; double-click to grab the pointer again |
| Tab | Free the cursor to click a card |
| Esc | Close the transcript, or leave walk mode |
| F | Open the transcript of the agent you're standing near |
| T | Message that agent |
| A / D | Approve or deny the request it's waiting on |
| J / K | Next or previous request on a card |
| M | Jump to the note field on a card |
| Ctrl/Cmd + Enter | Send the card's action |
| Ctrl/Cmd + K | Focus the input bar |
| + / − / wheel / pinch | Zoom the overview camera |
you ── approve ticket ──▶ orchestrator ──▶ qa specify ──▶ developer ──▶ qa verify ──▶ risk-check ──▶ PR ──▶ merge on green
│ │
└──── board (.scratch/, locks, handoffs) ◀────────────────┘
│
bridge (127.0.0.1:4317, /state, /events) ──▶ den UI
apps/bridge reads the board, streams changes over server-sent events at /events, and serves /state and /metrics (ADR 0011). What you do in the den (approvals, messages) goes back as requests that the orchestrator acts on.npm run risk-check, npm run check:bom, npm run session-check, and a release gate that won't let an agent drop a ticket without a handoff (ADR 0009)..scratch/usage.jsonl with its model, tokens and time. npm run spend totals them per ticket and per role.Not built yet: launching agents from the den itself, and runtimes other than Claude Code (ADR 0004 sketches the adapter).
All of these can be re-run, as of 9d3e57c.
main, 128 of them merges: git log --oneline 9d3e57c | wc -l, then add --merges.find apps packages scripts -name "*.test.mjs" -not -path "*/node_modules/*" | wc -l.docs/adr/.kind:"cell" rows of .scratch/usage.jsonl, summing tokens (17 early rows carry no token count).npm test # node:test across apps, packages and scripts (runs check:bom first)
npm run ui:dev # Vite dev server for the UI alone
npm run bridge # the bridge alone
npm run smoke # HTTP smoke check
npm run smoke:ui # browser smoke check (Playwright + Chromium)
npm run risk-check # decides whether a branch needs a security review
npm run board -- status <feature>/<NN-slug>
Layout: apps/ui (React + React Three Fiber, built by Vite, the only build step), apps/bridge (localhost server), apps/organism-infra (the board CLI), apps/ci-cd (dev server and smoke checks), scripts/ (relay tooling). Everything else is Node ESM run with node --test.
CONTEXT.md: the glossary (organism, station, cell, genome, Bao, perch and the rest).docs/adr/: every recorded decision and why it was made.docs/agents/: how agents use the board, worktrees and process hygiene.hooks/register.tsx 45 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Progress } from '../types'
5import { renderBand } from './render.mjs'
6
7const progress = atom({ plugin: 'north-star', key: 'progress' } as const, null)
8
9// The band's numbers come from scripts/north-star.mjs (board files only, no network). The mod
10// shells out to it in the session's directory; ORGANISM_ROOT, when set, points it at the main checkout.
11async function refresh($: any) {
12 try {
13 const root = await $.env.get('ORGANISM_ROOT')
14 const r = await $.process.run(['node', 'scripts/north-star.mjs', '--json'], root ? { env: { ORGANISM_ROOT: root } } : undefined)
15 if (r.exitCode === 0) await update($, progress, () => JSON.parse(r.stdout) as Progress)
16 } catch {
17 // keep the last reading
18 }
19}
20
21export const register: Register = on => {
22 on('session.start', async ($, e, next) => {
23 await refresh($)
24 return next(e)
25 })
26
27 on('turn.complete', async ($, e, next) => {
28 await refresh($)
29 return next(e)
30 })
31
32 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
33 const p = await read($, progress)
34 const text = p ? renderBand(p) : ''
35 if (!text) return next(e)
36
37 const { Box, Text } = $.ui.resolve(e)
38 return (
39 <Box>
40 <Text dimColor>{text}</Text>
41 </Box>
42 )
43 })
44}
45hooks/render.mjs 14 lines1// organism-infra/209: the band text for den v1 progress. Pure and dependency-free: a mod runs with
2// no Node. Input is what scripts/north-star.mjs returns: { done, total, remaining, next }.
3// ████████░░░░ v1 8/12 · next: 143
4const WIDTH = 12;
5
6export function renderBand(progress) {
7 const { done, total, next } = progress ?? {};
8 if (!total) return "";
9 const fill = done >= total ? WIDTH : Math.min(WIDTH - 1, Math.round((done / total) * WIDTH));
10 const bar = "█".repeat(fill) + "░".repeat(WIDTH - fill);
11 const num = next ? /(?:^|\/)0*(\d+)/.exec(next)?.[1] : null;
12 return `${bar} v1 ${done}/${total}${num ? ` · next: ${num}` : ""}`;
13}
14types/index.d.ts 8 lines1export type Progress = { done: number; total: number; remaining: number; next: string | null }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'north-star': { progress: Progress | null }
6 }
7}
8