SLOPSHOPPER

queue

A thin band above the prompt showing what is in flight and the next 3 queued tickets (the orchestrator's proposed order, else the top of ready), read from the…

newbandprocess
v0.1.0MITupdated 2026-10-09DerekHertz/DimSumDen/mods/queue
A shopper browsing a rack in a slop shop
README

[!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.

🥟 Dim Sum Den

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.

CI License Platform Built with React Three Fiber Runs on Claude Code

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: Bao at the center, agents at their stations

The den in Demo mode (?demo=den), captured from the running app.


What it is

  • A team, not a swarm. Nine roles (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.
  • A kitchen, roughly. Roles sit at stations named for a dim sum kitchen. The Pass (orchestrator, product, architect) decides and sequences. Steamers (developer, scout) build and dig. Tea & Pantry (qa, security) test and guard. Front of House (designer, herald) keeps things looking right and drafts the public posts.
  • A relay for every code ticket. qa writes failing tests, a developer makes them pass, qa verifies, a risk check decides whether security reviews, and the orchestrator opens the PR and merges on green CI (ADR 0002).
  • Gates you can't miss. Agents stop before pushing, adding a dependency, or editing an ADR or their own role files. You approve a ticket once; the orchestrator runs its whole relay and only comes back for a visual check, a design question, a ticket that failed twice, or a red merge.
  • A board you can diff. Tickets are markdown under .scratch/, claimed with lock files and handed off through a board CLI (ADR 0003, ADR 0008). No database, no hosted service.
  • The den. 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 mode. Open ?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.
  • Cheap decisions, logged before trusted. Routine relay choices (which model a developer needs, how deep qa verifies) go to Jev, a small typed-decision model, through 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).

Requirements

  • Node.js and npm.
  • Claude Code on your own subscription, to run the agents (ADR 0001).
  • git, and the GitHub CLI (gh) for the orchestrator's PRs and CI checks.
  • Optional: Playwright's Chromium, for npm run smoke:ui.

Linux, macOS and WSL. It's a local tool, and the bridge binds to 127.0.0.1 only.

Run locally

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

Controls

KeyAction
Walk buttonEnter walk mode (the on-screen W A S D pad works on touch screens)
W A S D / arrowsWalk; hold Shift to go faster
Mouse dragLook around; double-click to grab the pointer again
TabFree the cursor to click a card
EscClose the transcript, or leave walk mode
FOpen the transcript of the agent you're standing near
TMessage that agent
A / DApprove or deny the request it's waiting on
J / KNext or previous request on a card
MJump to the note field on a card
Ctrl/Cmd + EnterSend the card's action
Ctrl/Cmd + KFocus the input bar
+ / − / wheel / pinchZoom the overview camera

How it works

 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
  • Bridge. 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.
  • Mechanical checks. The rules agents skip most are enforced by scripts, not by asking nicely: 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).
  • Spend you can see. Every agent run is logged to .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).

Numbers

All of these can be re-run, as of 9d3e57c.

  • 917 commits on main, 128 of them merges: git log --oneline 9d3e57c | wc -l, then add --merges.
  • 231 test files: find apps packages scripts -name "*.test.mjs" -not -path "*/node_modules/*" | wc -l.
  • 20 recorded decisions in docs/adr/.
  • 573 logged agent runs across 122 tickets, about 32.6 million tokens: the kind:"cell" rows of .scratch/usage.jsonl, summing tokens (17 early rows carry no token count).

Development

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.

More

  • 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.

License

MIT

Source 3 files
hooks/register.tsx 49 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Queue } from '../types'
5import { renderBand } from './render.mjs'
6
7const queue = atom({ plugin: 'queue', key: 'queue' } as const, null)
8
9// The band's data comes from scripts/queue.mjs (board files only, no network). The mod shells out to
10// it in the session's directory; ORGANISM_ROOT, when set, points it at the main checkout. A failing
11// script clears the band: it renders nothing rather than a stale or broken line.
12async function refresh($: any) {
13  try {
14    const root = await $.env.get('ORGANISM_ROOT')
15    const r = await $.process.run(['node', 'scripts/queue.mjs', '--json'], root ? { env: { ORGANISM_ROOT: root } } : undefined)
16    const next = r.exitCode === 0 ? (JSON.parse(r.stdout) as Queue) : null
17    await update($, queue, () => next)
18  } catch {
19    await update($, queue, () => null).catch(() => {})
20  }
21}
22
23export const register: Register = on => {
24  on('session.start', async ($, e, next) => {
25    await refresh($)
26    return next(e)
27  })
28
29  on('turn.complete', async ($, e, next) => {
30    await refresh($)
31    return next(e)
32  })
33
34  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
35    const q = await read($, queue)
36    const text = q ? renderBand(q) : ''
37    if (!text) return next(e)
38
39    const { Box, Text } = $.ui.resolve(e)
40    return (
41      <Box flexDirection="column">
42        {text.split('\n').map((line, i) => (
43          <Text key={i} dimColor>{line}</Text>
44        ))}
45      </Box>
46    )
47  })
48}
49
hooks/render.mjs 33 lines
1// organism-infra/212: the band text for the board's queue. Pure and dependency-free: a mod runs with
2// no Node. Input is what scripts/queue.mjs --json returns: { inFlight, ready, waitingOnUser, blocked, proposed }.
3//   now: 212 Queue mod (developer) · 213 Next one (qa verify)
4//   next: 143 Steering adapter · 210 Cell budget · 211 Token economics
5// At most two lines, each at most MAX characters. Anything that is not a queue renders "".
6const MAX = 120;
7const NEXT = 3;
8
9const num = (ref) => /(?:^|\/)0*(\d+)/.exec(String(ref ?? ""))?.[1] ?? "";
10const rows = (v) => (Array.isArray(v) ? v.filter((r) => r && typeof r === "object" && !Array.isArray(r) && typeof r.ref === "string") : []);
11
12function clip(text) {
13  const chars = [...text];
14  return chars.length <= MAX ? text : `${chars.slice(0, MAX - 1).join("").trimEnd()}…`;
15}
16
17const flight = (r) => {
18  const who = r.cell ? ` (${r.cell}${r.mode ? ` ${r.mode}` : ""})` : "";
19  return `${num(r.ref)} ${r.title ?? ""}${who}`.trim();
20};
21
22export function renderBand(queue) {
23  if (!queue || typeof queue !== "object" || Array.isArray(queue)) return "";
24  const inFlight = rows(queue.inFlight);
25  const proposed = rows(queue.proposed);
26  const next = (proposed.length ? proposed : rows(queue.ready)).slice(0, NEXT);
27
28  const out = [];
29  if (inFlight.length) out.push(clip(`now: ${inFlight.map(flight).join(" · ")}`));
30  if (next.length) out.push(clip(`next: ${next.map((r) => `${num(r.ref)} ${r.title ?? ""}`.trim()).join(" · ")}`));
31  return out.join("\n");
32}
33
types/index.d.ts 9 lines
1export type QueueRow = { ref: string; title: string; priority?: number; rank?: number; cell?: string | null; mode?: string | null }
2export type Queue = { inFlight: QueueRow[]; ready: QueueRow[]; waitingOnUser: QueueRow[]; blocked: QueueRow[]; proposed: QueueRow[] }
3
4declare module 'claude-code' {
5  interface PluginState {
6    queue: { queue: Queue | null }
7  }
8}
9