agent-infra control plane as a band above the prompt, with an answerable questions pane

hooks/register.tsx 192 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Question, Snapshot } from '../types'
5
6// The control plane the SessionStart digest prints as text, drawn instead
7// above the prompt where it costs the model nothing. Data: `pulse funnel
8// --json` (0.4s) + `questions_view --json` + the inbox `pulse tick` writes;
9// never `pulse status`, which takes ~45s.
10
11const PANE = 'questions'
12const REFRESH_MS = 10 * 60_000
13const STALE_MS = 30 * 86_400_000
14
15const snap = atom({ plugin: 'control-plane', key: 'snap' } as const, null)
16const questions = atom({ plugin: 'control-plane', key: 'questions' } as const, [])
17const isHidden = atom({ plugin: 'control-plane', key: 'isHidden' } as const, false)
18const note = atom({ plugin: 'control-plane', key: 'note' } as const, '')
19
20type $ = EngineInterface
21
22async function paths($: $) {
23 const home = (await $.env.get('HOME')) ?? '/Users/alien'
24 return {
25 repo: `${home}/Projects/agent-infra`,
26 uv: `${home}/.local/bin/uv`,
27 inbox: `${home}/.claude/control-plane-inbox.md`,
28 }
29}
30
31async function runJson($: $, script: readonly string[]): Promise<any> {
32 const p = await paths($)
33 const r = await $.process.run([p.uv, 'run', 'python3', ...script], { cwd: p.repo, timeoutMs: 60_000 })
34 if (r.exitCode !== 0) throw new Error(`${script[0]} exit ${r.exitCode}: ${r.stderr.trim().slice(-120)}`)
35 return JSON.parse(r.stdout)
36}
37
38export function driftCount(inbox: string): number {
39 const section = inbox.split(/^## /m).find(s => s.startsWith('Drift flags'))
40 return section ? (section.match(/^- \*\*/gm) ?? []).length : 0
41}
42
43async function refresh($: $): Promise<void> {
44 try {
45 const [funnel, view] = await Promise.all([
46 runJson($, ['scripts/pulse.py', 'funnel', '--json']),
47 runJson($, ['scripts/questions_view.py', '--json']),
48 ])
49 const now = await $.clock.now()
50 const qs: Question[] = view.questions.map((q: Question) => ({
51 id: q.id, source: q.source, prompt: q.prompt, created: q.created, ref: q.ref,
52 }))
53 const { inbox } = await paths($)
54 let drift = 0
55 let inboxAgeH: number | null = null
56 if (await $.fs.exists(inbox)) {
57 inboxAgeH = Math.floor((now - (await $.fs.stat(inbox)).mtimeMs) / 3_600_000)
58 drift = driftCount(await $.fs.read(inbox))
59 }
60 const fresh: Snapshot = {
61 questions: qs.length,
62 stale: qs.filter(q => q.created !== '' && now - Date.parse(q.created) > STALE_MS).length,
63 rsi: funnel.rsi_close_pending,
64 quarantine: funnel.quarantine_pending,
65 drift,
66 inboxAgeH,
67 error: null,
68 }
69 await update($, questions, () => qs)
70 await update($, snap, () => fresh)
71 } catch (err) {
72 const error = String(err).slice(0, 160)
73 await update($, snap, s => ({
74 ...(s ?? { questions: 0, stale: 0, rsi: 0, quarantine: 0, drift: 0, inboxAgeH: null }),
75 error,
76 }))
77 }
78}
79
80async function answer($: $, q: Question, text: string): Promise<void> {
81 const value = text.trim()
82 if (value === '') return
83 const p = await paths($)
84 const r = await $.process.run(
85 [p.uv, 'run', 'python3', 'scripts/human_md_answer.py', q.ref, value],
86 { cwd: p.repo, timeoutMs: 30_000 },
87 )
88 await update($, note, () =>
89 r.exitCode === 0 ? `Answered: ${q.prompt.slice(0, 60)}` : `Not written: ${r.stderr.trim().slice(-120)}`,
90 )
91 await refresh($)
92}
93
94const openPane = ($: $) => $.ui.open({ id: PANE, title: 'Questions for you', focus: true, closeOnEscape: true })
95
96export const register: Register = on => {
97 on('session.start', async ($, e, next) => {
98 if (e.isInteractive) {
99 await $.command.register({ name: 'questions', description: 'Open the questions pane (answer HUMAN.md asks inline)' })
100 await $.command.register({ name: 'control-plane', description: 'Refresh and show the control-plane band' })
101 void refresh($)
102 $.clock.every(REFRESH_MS, () => void refresh($))
103 }
104 return next(e)
105 })
106
107 on('command.run', { command: 'questions' }, async $ => {
108 await refresh($)
109 await openPane($)
110 return { text: 'Questions pane opened.' }
111 })
112
113 on('command.run', { command: 'control-plane' }, async $ => {
114 await update($, isHidden, () => false)
115 await refresh($)
116 const s = await read($, snap)
117 return { text: s?.error ? `[DEGRADED] ${s.error}` : 'Control-plane band refreshed.' }
118 })
119
120 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
121 const s = await read($, snap)
122 if (e.props.hasSurvey || s === null || (await read($, isHidden))) return next(e)
123 if (s.error === null && s.questions + s.rsi + s.quarantine + s.drift === 0) return next(e)
124
125 const { Box, Button, Text } = $.ui.resolve(e)
126 return (
127 <Box>
128 {s.error !== null ? (
129 <Text color="warning">[DEGRADED] control plane: {s.error} </Text>
130 ) : (
131 <Text dimColor>▸ control plane{s.inboxAgeH === null ? '' : ` ${s.inboxAgeH}h`} </Text>
132 )}
133 {s.questions > 0 && (
134 <Button
135 key="questions"
136 label={`questions ${s.questions}${s.stale > 0 ? ` (${s.stale} stale)` : ''}`}
137 onPress={() => void openPane($)}
138 />
139 )}
140 {s.rsi > 0 && (
141 <Button
142 key="rsi"
143 label={`rsi close ${s.rsi}`}
144 onPress={() => void $.prompt.fill({ text: '/rsi close', mode: 'replace' })}
145 />
146 )}
147 <Text dimColor> quarantine {s.quarantine} · drift {s.drift} </Text>
148 <Button key="hide" label="hide" plain dimColor onPress={() => void update($, isHidden, () => true)} />
149 </Box>
150 )
151 })
152
153 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
154 const els = $.ui.resolve(e)
155 const { Box, Button, Text } = els
156 const qs = await read($, questions)
157 const msg = await read($, note)
158 return (
159 <Box flexDirection="column">
160 {msg !== '' && <Text color="success">{msg}</Text>}
161 {qs.length === 0 && <Text dimColor>No open questions.</Text>}
162 {qs.map(q => (
163 <Box key={q.id} flexDirection="column" marginBottom={1}>
164 <Text bold>{q.prompt}</Text>
165 <Text dimColor>
166 {q.source} · {q.created} · {q.ref}
167 </Text>
168 {q.source === 'human-md' && 'Input' in els ? (
169 <els.Input
170 key={`answer-${q.id}`}
171 placeholder="your answer; Enter writes it into HUMAN.md"
172 onSubmit={value => void answer($, q, value)}
173 />
174 ) : (
175 <Button
176 key={`open-${q.id}`}
177 label="ask Claude about it"
178 onPress={() =>
179 void $.prompt.fill({
180 text: `Read ${q.ref} and give me the decision it needs, with your recommendation.`,
181 mode: 'replace',
182 })
183 }
184 />
185 )}
186 </Box>
187 ))}
188 </Box>
189 )
190 })
191}
192types/index.d.ts 18 lines1export type Snapshot = {
2 questions: number
3 stale: number
4 rsi: number
5 quarantine: number
6 drift: number
7 inboxAgeH: number | null
8 error: string | null
9}
10
11export type Question = { id: string; source: string; prompt: string; created: string; ref: string }
12
13declare module 'claude-code' {
14 interface PluginState {
15 'control-plane': { snap: Snapshot | null; questions: Question[]; isHidden: boolean; note: string }
16 }
17}
18