Holds a permission ask open while regin's web card decides it, and gives it back to the native dialog on release or when regin is unreachable

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./projects/:id holds a project's step stages, each stage's prompt, and the language its task runs write in.regin feature (where a feature stands and what it cost), regin task (capture, inspect, check a slice table, arm a run; runs are launched from the dashboard), regin defects (what a feature got wrong, and what escaped), regin workspace (add, list, remove).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 and CodeBuddy Code runs go over ACP or into 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.regin MCP server — carries send_to_user and the pipeline's tools (features, tasks, defects), so any MCP-speaking agent can read and write the pipeline. regin mcp register wires it, with the memory server, into each provider's config./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; its aspects and judge are configured under Settings → Grading. /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 # needs Python ≥ 3.11 and Node; asks to wire the Claude Code hooks
.venv/bin/python cli/regin.py serve # dashboard on http://localhost:8321
.\scripts\setup.cmd instead (PowerShell or cmd; ./scripts/setup.sh there only opens the file in an editor), then regin serve in a new terminal. Same steps, except the live-session terminal and agent bridge, which need tmux. See docs/setup.md.regin hooks install --provider claude. Without them no session is recorded.git pull && ./scripts/setup.sh; it keeps your database, settings and users and runs regin migrate.regin uninstall (--dry-run to preview, --purge-data to drop data too), then delete the checkout.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. |
| CodeBuddy Code | Hooks, skills, MCP servers and transcript capture, plus task runs over ACP or tmux with a model and permission mode. |
| WorkBuddy | Hooks, skills, MCP servers and transcript capture; cannot be armed for task runs, and its sessions are not offered Resume. |
| 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 151 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, ToolCheckInput, ToolCheckResult } from 'claude-code'
3
4import type { HeldCall } from '../types'
5
6const held = atom({ plugin: 'regin-permit', key: 'held' } as const, [])
7
8// The engine ignores a plugin's allow for these: the dialog shows regardless
9// (F158 P0 Q2), so holding one would only delay that dialog.
10const PERSON_REQUIRED = new Set(['AskUserQuestion', 'ExitPlanMode'])
11
12// `$.process.run`'s own ceiling. Past it the run rejects and the call goes
13// back to the native dialog; regin sets no timeout of its own.
14const HOLD_MS = 600_000
15
16export const EXIT_DECIDED = 0
17export const EXIT_RELEASED = 3
18
19export type HoldOutcome = { op: 'decide'; behavior: 'allow' | 'deny'; reason?: string }
20
21export function parseOutcome(stdout: string): HoldOutcome | null {
22 const line = stdout.trim().split('\n').pop() ?? ''
23 try {
24 const o = JSON.parse(line)
25 if (o?.op === 'decide' && (o.behavior === 'allow' || o.behavior === 'deny')) return o
26 } catch {
27 // not an outcome line
28 }
29 return null
30}
31
32// Same resolution as regin-agents/bin/regin-mcp.sh: $REGIN_HOME, else ~/regin.
33async function reginHome($: EngineInterface): Promise<string> {
34 return (await $.env.get('REGIN_HOME')) || `${await $.env.get('HOME')}/regin`
35}
36
37async function bridgeArgv($: EngineInterface, op: 'hold' | 'release', toolUseId: string) {
38 const home = await reginHome($)
39 const session = await $.session.id()
40 const argv = [`${home}/.venv/bin/python`, `${home}/cli/regin.py`, 'bridge', op,
41 '--session', session, '--tool-use-id', toolUseId]
42 return { argv, home }
43}
44
45async function hold($: EngineInterface, e: ToolCheckInput, toolUseId: string, asked: ToolCheckResult) {
46 const { argv, home } = await bridgeArgv($, 'hold', toolUseId)
47 // `input` exactly as tool.check got it: regin's card keys on its gate_key,
48 // which a later classic PermissionRequest must reproduce from tool_input.
49 const stdin = JSON.stringify({ tool: e.tool, input: e.input, reason: asked.reason })
50 return $.process.run(argv, { cwd: home, stdin, timeoutMs: HOLD_MS })
51}
52
53export function settle(asked: ToolCheckResult, exitCode: number, stdout: string): ToolCheckResult {
54 if (exitCode !== EXIT_DECIDED) return asked
55 const outcome = parseOutcome(stdout)
56 if (outcome === null) return asked
57 return { decision: outcome.behavior, reason: outcome.reason }
58}
59
60async function notify($: EngineInterface, tool: string) {
61 try {
62 await $.ui.notify(`${tool} is waiting for permission — answer on regin or in the terminal`)
63 } catch (err) {
64 $.ui.log(`regin-permit: notify failed: ${String(err)}`, { to: 'debug' })
65 }
66}
67
68// Every way the hold can end — decided, released, regin unreachable, the run
69// rejected at its ceiling — leaves the call answered and the band cleared.
70async function holdForRegin($: EngineInterface, e: ToolCheckInput, toolUseId: string, asked: ToolCheckResult) {
71 const call: HeldCall = { toolUseId, tool: e.tool }
72 await update($, held, (rows: HeldCall[]) => [...rows, call])
73 try {
74 await notify($, e.tool)
75 const { exitCode, stdout, stderr } = await hold($, e, toolUseId, asked)
76 if (exitCode !== EXIT_DECIDED && exitCode !== EXIT_RELEASED) {
77 $.ui.log(`regin-permit: hold exited ${exitCode}: ${stderr}`, { to: 'debug' })
78 }
79 return settle(asked, exitCode, stdout)
80 } catch (err) {
81 $.ui.log(`regin-permit: hold failed: ${String(err)}`, { to: 'debug' })
82 return asked
83 } finally {
84 await update($, held, (rows: HeldCall[]) => rows.filter(r => r.toolUseId !== toolUseId))
85 }
86}
87
88// The hold's own process sees the release and exits 3; this only asks regin
89// to end it. A failed release leaves the hold running, so it is logged.
90async function release($: EngineInterface, toolUseId: string) {
91 const { argv, home } = await bridgeArgv($, 'release', toolUseId)
92 const { exitCode, stderr } = await $.process.run(argv, { cwd: home })
93 if (exitCode !== 0) $.ui.log(`regin-permit: release exited ${exitCode}: ${stderr}`, { to: 'debug' })
94}
95
96// `setting` and `decider` arrive on engines newer than the types this mod is
97// checked against, so they are read off the verdict rather than typed.
98const VERDICT_KEYS = ['decision', 'reason', 'rule', 'hook', 'setting', 'decider', 'ceiling'] as const
99
100// Above post_event's worst case (3 attempts × 500ms + backoff) and the CLI's
101// start-up, well under the 30s default a hung regin would otherwise cost.
102const REPORT_MS = 5_000
103
104// regin stamps this onto the call's tool span as `attributes.permission`. Only
105// the trace reads it, so a failed report is a debug line, never the verdict.
106// Awaited: nothing documents un-awaited `$` work surviving the hook's return.
107async function reportDecision($: EngineInterface, e: ToolCheckInput, toolUseId: string, verdict: ToolCheckResult) {
108 const fields = verdict as unknown as Record<string, unknown>
109 const record: Record<string, unknown> = { tool_use_id: toolUseId, agentId: e.agentId ?? null }
110 for (const key of VERDICT_KEYS) if (fields[key] !== undefined) record[key] = fields[key]
111 try {
112 const home = await reginHome($)
113 const session = await $.session.id()
114 const { exitCode, stderr } = await $.process.run(
115 [`${home}/.venv/bin/python`, `${home}/cli/regin.py`, 'trace', 'decision', '--session', session],
116 { cwd: home, stdin: JSON.stringify(record), timeoutMs: REPORT_MS },
117 )
118 if (exitCode !== 0) $.ui.log(`regin-permit: trace decision exited ${exitCode}: ${stderr}`, { to: 'debug' })
119 } catch (err) {
120 $.ui.log(`regin-permit: trace decision failed: ${String(err)}`, { to: 'debug' })
121 }
122}
123
124export const register: Register = on => {
125 on('tool.check', async ($, e, next) => {
126 const asked = await next(e)
127 if (e.tool_use_id) await reportDecision($, e, e.tool_use_id, asked)
128 if (asked.decision !== 'ask' || !e.tool_use_id || PERSON_REQUIRED.has(e.tool)) return asked
129 return holdForRegin($, e, e.tool_use_id, asked)
130 }).catch(($, e, next) => next(e))
131
132 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
133 const rows = await read($, held)
134 if (rows.length === 0) return next(e)
135
136 const { Box, Button, Text } = $.ui.resolve(e)
137 return (
138 <Box flexDirection="column">
139 {rows.map(r => (
140 <Box key={`held-${r.toolUseId}`}>
141 <Text color="yellow">{r.tool} is waiting for permission on regin </Text>
142 <Button key={`release-${r.toolUseId}`} onPress={() => release($, r.toolUseId)}>
143 Answer here
144 </Button>
145 </Box>
146 ))}
147 </Box>
148 )
149 })
150}
151types/index.d.ts 8 lines1export type HeldCall = { toolUseId: string; tool: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'regin-permit': { held: HeldCall[] }
6 }
7}
8