Shows the regin send_to_user inbox's unread count above the prompt

A workbench for building software with AI coding agents. Plan the feature, run the agents, see what they did, count what they got wrong.
🌐 regin.ccday.top — the official project site.
⚠️ Early beta. regin is under active development and breaking changes are expected at any time. Database schemas, settings keys, hook contracts, skill bundles, and CLI flags may all change without backward-compatible shims. Pin a commit if you need stability.
regin runs one loop around the coding agent you already use:
Spec → Run → Watch → Count
│ │ │ └─ defects declared against the done-claim; rounds-to-done per feature
│ │ └─ every session traced: tools, edits, tokens, time, grades
│ └─ task runs launched on Claude or Kimi, steered and stopped from /live
└─ a feature row carries the spec, its acceptance tests, and the slices cut from it
It is organized in four layers. The first two are where you spend the day; the last two make the runs inspectable and keep the agent on-spec.
| Layer | What it covers | Dashboard |
|---|---|---|
| Workbench | feature → spec → acceptance tests → tasks → done-claim; the defect ledger | /overview (Pipeline), /inbox |
| Runs | launching task runs, steering live sessions, the agent's messages back to you | /live, /inbox |
| Observability | session traces, time and cost analytics, post-hoc grades, audit | /trace, /audit |
| Guidance | patterns & skills, rule engines in hooks, cross-session memory, topic wikis | /patterns, /rules, /memory, /repos/:name/topics |
A feature is a row. Its spec lives on that row (or on its tracker issue when the feature is tracked remotely), and its acceptance items are named tests in the repo — red before the build, green at ship. The spec is cut into slices; each slice becomes a task with its dependencies, and tasks run in dependency order. When the work is called done, that is a done-claim, and anything found wrong afterwards is a defect declared against the feature.
The number the pipeline is held to is rounds-to-done: one build plus each time a done-claim was walked back. Only declarations feed it — nothing is inferred from prose, branch names or timing.
/overview is the Pipeline — Overview, Features and Tasks behind one tab strip; defects are reached from the Overview and the Features triage rail. /features/:id is a feature's ledger.regin feature (where a feature stands and what it cost), regin task (capture, inspect, check a slice table); runs are armed and launched from the dashboard, regin defects (what a feature got wrong, and what escaped).A task is armed with a provider, a model and a launch mode, then started from the dashboard. Claude runs go through the Python Claude Agent SDK, so regin owns the process and keeps a typed channel to it — an AskUserQuestion is answered from the browser rather than by typing into a terminal. Kimi Code runs over ACP or in a tmux pane on regin's own tmux server.
/live follows a running session: its current state, recent steps, any question it is blocked on, with steer and Stop.send_to_user tool; messages land in /inbox and can fan out to a webhook, Telegram or Lark. CLI: regin messages, regin bridge./live renders a phone-sized card that can answer permission prompts while you are away from the desk. Rough edges and breaking changes are expected; see docs/agent-bridge-design.md.Internals: Agent SDK tier, Provider launch capabilities, Agent Messages.
Hooks and transcripts produce a stream of events: tool calls, model thoughts, file edits, hook decisions, token usage. regin ingests that stream into a session trace you can filter by tool, agent and phase, replay, and roll up (tokens by tool, skill reads, MCP calls).
/trace/sessions lists sessions; each opens into a timeline and a conversation view.regin stats time shows where build time goes (attended vs. agent vs. human); regin stats session <trace-id> ranks why one session was slow; regin stats sessions exports per-session rows.regin grade scores finished sessions against a rubric. /audit is the ops history of regin itself.Internals: Turn & Token Tracing, Session Grader.
The supporting layer: what the agent is told before it acts, and what pushes back when it drifts.
regin pattern promote packages a proven pattern as a versioned skill. Dashboard: /patterns, /skills.PostToolUse hooks, so a violation is reported to the agent in the same turn as the edit. Dashboard: /rules; CLI: regin rules, regin hooks./memory; CLI: regin memory./repos/:name/topics; CLI: regin topics, regin wiki.Internals: Patterns, Rule engines, Agent Memory, docs/topics/proposals.md.
git clone https://github.com/caiuswang/regin && cd regin
./scripts/setup.sh
.venv/bin/python cli/regin.py serve # dashboard on http://localhost:8321
Manual setup, settings, server mode and the CLI reference: docs/setup.md.
| Agent | Status |
|---|---|
| Claude Code | Full: hooks, rule engines, trace, memory, and task runs through the Agent SDK. |
| Kimi Code | Task runs only (ACP or tmux), with known limits — runs are unattended and a steer is cancel-then-re-prompt. See Run a task with Kimi. |
| Codex, generic | Provider adapters exist; cannot be armed for task runs. See Agent provider architecture. |
Not a chat UI, not a model, not a hosted service. It assumes you already have a coding agent and a codebase, and runs locally beside them.
hooks/register.tsx 115 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Inbox, InboxMessage } from '../types'
5
6const REGIN = '/Users/taowang/regin'
7const PANE = 'regin-inbox'
8const LIMIT = 200
9const inbox = atom({ plugin: 'regin-band', key: 'inbox' } as const, null)
10
11type Row = { id: number; title: string | null; body: string | null; msg_type: string; created_at: string }
12
13export function parseInbox(stdout: string): Inbox | null {
14 let rows: Row[]
15 try {
16 rows = JSON.parse(stdout)
17 } catch {
18 return null
19 }
20 if (!Array.isArray(rows)) return null
21 const messages = rows.map((r): InboxMessage => ({
22 id: r.id,
23 title: r.title || (r.body ?? '').split('\n')[0]!.slice(0, 80),
24 type: r.msg_type,
25 createdAt: r.created_at,
26 }))
27 return { unread: messages.length, messages }
28}
29
30function regin($: EngineInterface, ...args: string[]) {
31 return $.process.run([`${REGIN}/.venv/bin/python`, `${REGIN}/cli/regin.py`, ...args], { cwd: REGIN })
32}
33
34// Refreshed on session start, at each turn's end and after an action rather
35// than on a timer: the count only matters when the person is about to read it.
36// The list's scope (non-test, non-dismissed) is the one `read --all` clears;
37// `messages stats` also counts test rows, so it never reached 0 after the press.
38async function refresh($: EngineInterface) {
39 const { exitCode, stdout, isStdoutTruncated } = await regin(
40 $, 'messages', 'list', '--unread', '--json', '--limit', String(LIMIT + 1),
41 )
42 const box = exitCode === 0 && !isStdoutTruncated ? parseInbox(stdout) : null
43 await update($, inbox, (): Inbox | null => box)
44}
45
46async function openPane($: EngineInterface) {
47 await refresh($)
48 return $.ui.open({ id: PANE, title: 'regin inbox' })
49}
50
51async function markAllRead($: EngineInterface) {
52 await regin($, 'messages', 'read', '--all')
53 await refresh($)
54}
55
56function count(box: Inbox) {
57 return box.unread > LIMIT ? `${LIMIT}+` : String(box.unread)
58}
59
60export const register: Register = on => {
61 on('session.start', async ($, e, next) => {
62 const started = await next(e)
63 await $.command.register({ name: PANE, description: 'Show unread regin inbox messages in a pane' })
64 await refresh($)
65 return started
66 })
67
68 on('turn.complete', async ($, e, next) => {
69 const done = await next(e)
70 await refresh($)
71 return done
72 })
73
74 on('command.run', { command: PANE }, async $ => {
75 await openPane($)
76 return { text: 'regin inbox opened.' }
77 })
78
79 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
80 const box = await read($, inbox)
81 if (e.props.hasSurvey || box === null || box.unread === 0) return next(e)
82
83 const { Box, Button, Text } = $.ui.resolve(e)
84 return (
85 <Box>
86 <Text dimColor>regin inbox: {count(box)} unread </Text>
87 <Button key="open-inbox" label="Open" onPress={() => void openPane($)} />
88 </Box>
89 )
90 })
91
92 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
93 const { Box, Button, Text } = $.ui.resolve(e)
94 const box = await read($, inbox)
95 if (box === null) return <Text dimColor>regin inbox unavailable.</Text>
96 if (box.unread === 0) return <Text dimColor>No unread messages.</Text>
97
98 const room = Math.max(1, (e.viewport?.rows ?? 24) - 6)
99 return (
100 <Box flexDirection="column">
101 <Box>
102 <Text>{count(box)} unread </Text>
103 <Button key="mark-all-read" label="Mark all read" onPress={() => markAllRead($)} />
104 </Box>
105 {box.messages.slice(0, room).map(m => (
106 <Box key={`m${m.id}`}>
107 <Text dimColor>{m.createdAt.slice(5, 16).replace('T', ' ')} {m.type.padEnd(8)} </Text>
108 <Text wrap="truncate">{m.title}</Text>
109 </Box>
110 ))}
111 </Box>
112 )
113 })
114}
115types/index.d.ts 9 lines1export type InboxMessage = { id: number; title: string; type: string; createdAt: string }
2export type Inbox = { unread: number; messages: InboxMessage[] }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'regin-band': { inbox: Inbox | null }
7 }
8}
9