modscode / Gallery / openspec-governance
openspec-governance
OpenSpec lifecycle governance for Claude Code: stage band, fail-closed edit gate, task progress and an append-only execution log.
What it does
- Draws a band above the prompt, a pane, toasts
- Adds /openspec
- Watches Write, Edit, NotebookEdit, Read calls and can refuse them
- Hooks session.start, turn.complete, ui.close
Good points
- No network calls
- Starts no processes
- Written in TypeScript
Code to start from
Two kinds: a starter modscode writes from the design above, and the lines of the mod's own code that draw it.
Starter (register.tsx)
import type { Register } from 'claude-code'
// Drawn like openspec-governance by whoisclebs (pane, band, toast), as modscode read it from the mod's code:
// https://modscode.com/gallery/whoisclebs-openspec-governance/
// Written by modscode from that reading, not copied from the mod: use it as you like.
// Each value below stands for one the mod works out at run time; put in your own.
export const register: Register = on => {
// the band above the prompt; what else goes there is drawn under it
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const { Box, Text, Button } = $.ui.resolve(e)
const name = '…'
const changeCount = '…'
const stage = '…'
const tasksDone = '…'
const tasksTotal = '…'
const length = '…'
const hint = '…'
return (
<Box flexDirection="column">
<Box>
<Text bold>
{"OpenSpec "}
</Text>
<Text>
{name}
</Text>
<Text dimColor>
{" (+"}
{changeCount}
)
</Text>
<Text dimColor>
{" · "}
</Text>
<Text color="cyan">
{stage}
</Text>
<Text dimColor>
{" · "}
</Text>
<Text>
████████░░░░░░░░
{" "}
{tasksDone}
/
{tasksTotal}
</Text>
<Text dimColor>
{" · "}
</Text>
<Text color="red">
{`gate closed (${length})`}
</Text>
<Text dimColor>
{" · next: "}
{hint}
</Text>
<Text>
{" "}
</Text>
<Button label="Details" />
</Box>
{await next(e)}
</Box>
)
})
// the pane: open it with $.ui.open({ id: 'my-pane', title: "OpenSpec" })
on('ui.render', { component: 'Pane', requestId: 'my-pane' }, async ($, e) => {
const { Box, Text, Input, Button } = $.ui.resolve(e)
const query = '…'
const length = '…'
const name = '…'
const stage = '…'
const tasksDone = '…'
const tasksTotal = '…'
const hits = '…'
const stage2 = '…'
const tasksDone2 = '…'
const tasksTotal2 = '…'
const hits2 = '…'
const stage3 = '…'
const tasksDone3 = '…'
const tasksTotal3 = '…'
const hits3 = '…'
const stage4 = '…'
const STAGE_EVIDENCE = '…'
const stage5 = '…'
const STAGE_EVIDENCE2 = '…'
const stage6 = '…'
const STAGE_EVIDENCE3 = '…'
const reason = '…'
const reason2 = '…'
const reason3 = '…'
const item = '…'
const item2 = '…'
const item3 = '…'
const tasksDone4 = '…'
const tasksTotal4 = '…'
const text = '…'
const text2 = '…'
const text3 = '…'
const length2 = '…'
const sum = '…'
const reduce = '…'
const capability = '…'
const specPath = '…'
const scenarios = '…'
const name2 = '…'
const scenarios2 = '…'
const name3 = '…'
const scenarios3 = '…'
const name4 = '…'
const capability2 = '…'
const specPath2 = '…'
const scenarios4 = '…'
const name5 = '…'
const scenarios5 = '…'
const name6 = '…'
const scenarios6 = '…'
const name7 = '…'
const capability3 = '…'
const specPath3 = '…'
const scenarios7 = '…'
const name8 = '…'
const scenarios8 = '…'
const name9 = '…'
const scenarios9 = '…'
const name10 = '…'
const time = '…'
const kind = '…'
const detail = '…'
const kind2 = '…'
const detail2 = '…'
const kind3 = '…'
const detail3 = '…'
const name11 = '…'
const stage7 = '…'
const STAGE_EVIDENCE4 = '…'
const stage8 = '…'
const STAGE_EVIDENCE5 = '…'
const stage9 = '…'
const STAGE_EVIDENCE6 = '…'
const reason4 = '…'
const reason5 = '…'
const reason6 = '…'
const item4 = '…'
const item5 = '…'
const item6 = '…'
const tasksDone5 = '…'
const tasksTotal5 = '…'
const text4 = '…'
const text5 = '…'
const text6 = '…'
const length3 = '…'
const capability4 = '…'
const specPath4 = '…'
const scenarios10 = '…'
const name12 = '…'
const scenarios11 = '…'
const name13 = '…'
const scenarios12 = '…'
const name14 = '…'
const capability5 = '…'
const specPath5 = '…'
const scenarios13 = '…'
const name15 = '…'
const scenarios14 = '…'
const name16 = '…'
const scenarios15 = '…'
const name17 = '…'
const capability6 = '…'
const specPath6 = '…'
const scenarios16 = '…'
const name18 = '…'
const scenarios17 = '…'
const name19 = '…'
const scenarios18 = '…'
const name20 = '…'
const kind4 = '…'
const detail4 = '…'
const kind5 = '…'
const detail5 = '…'
const kind6 = '…'
const detail6 = '…'
const name21 = '…'
const stage10 = '…'
const STAGE_EVIDENCE7 = '…'
const stage11 = '…'
const STAGE_EVIDENCE8 = '…'
const stage12 = '…'
const STAGE_EVIDENCE9 = '…'
const reason7 = '…'
const reason8 = '…'
const reason9 = '…'
const item7 = '…'
const item8 = '…'
const item9 = '…'
const tasksDone6 = '…'
const tasksTotal6 = '…'
const text7 = '…'
const text8 = '…'
const text9 = '…'
const length4 = '…'
const capability7 = '…'
const specPath7 = '…'
const scenarios19 = '…'
const name22 = '…'
const scenarios20 = '…'
const name23 = '…'
const scenarios21 = '…'
const name24 = '…'
const capability8 = '…'
const specPath8 = '…'
const scenarios22 = '…'
const name25 = '…'
const scenarios23 = '…'
const name26 = '…'
const scenarios24 = '…'
const name27 = '…'
const capability9 = '…'
const specPath9 = '…'
const scenarios25 = '…'
const name28 = '…'
const scenarios26 = '…'
const name29 = '…'
const scenarios27 = '…'
const name30 = '…'
const kind7 = '…'
const detail7 = '…'
const kind8 = '…'
const detail8 = '…'
const kind9 = '…'
const detail9 = '…'
return (
<Box flexDirection="column">
<Box borderStyle="round" paddingX={1}>
<Text bold>
{"Filter "}
</Text>
<Input placeholder="type to filter changes, tasks, specs" value={`${query}`} autoFocus />
</Box>
<Box>
<Button label="Clear" />
<Text>
{" "}
</Text>
<Button label="Refresh" />
<Text dimColor>
{" live: re-reads every few seconds while open"}
</Text>
</Box>
<Text>
{" "}
</Text>
<Text bold underline>
Changes (
{length}
)
</Text>
<Box>
<Button label={`▶ ${name}`} />
<Text>
{" "}
{stage}
{" "}
████████░░░░░░░░
{" "}
{tasksDone}
/
{tasksTotal}
{" "}
</Text>
<Text color="red">
gate closed
</Text>
<Text color="yellow">
{" "}
{hits}
{" match(es)"}
</Text>
<Text>
{" "}
</Text>
<Button label="Hide" />
</Box>
<Box>
<Button label={`▶ ${name}`} />
<Text>
{" "}
{stage2}
{" "}
████████░░░░░░░░
{" "}
{tasksDone2}
/
{tasksTotal2}
{" "}
</Text>
<Text color="red">
gate closed
</Text>
<Text color="yellow">
{" "}
{hits2}
{" match(es)"}
</Text>
<Text>
{" "}
</Text>
<Button label="Hide" />
</Box>
<Box>
<Button label={`▶ ${name}`} />
<Text>
{" "}
{stage3}
{" "}
████████░░░░░░░░
{" "}
{tasksDone3}
/
{tasksTotal3}
{" "}
</Text>
<Text color="red">
gate closed
</Text>
<Text color="yellow">
{" "}
{hits3}
{" match(es)"}
</Text>
<Text>
{" "}
</Text>
<Button label="Hide" />
</Box>
<Box flexDirection="column">
<Text>
{" "}
</Text>
<Text bold color="cyan">
{"━━ "}
{name}
{" (focus)"}
</Text>
<Box flexDirection="column">
<Text bold underline>
Stage
</Text>
<Box>
<Text color="green">
✔
{" "}
{stage4}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE}
</Text>
</Box>
<Box>
<Text color="green">
✔
{" "}
{stage5}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE2}
</Text>
</Box>
<Box>
<Text color="green">
✔
{" "}
{stage6}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE3}
</Text>
</Box>
<Text>
{" "}
</Text>
<Text bold underline>
Gate
</Text>
<Text color="red">
closed: edits outside openspec/ are denied
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason}
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason2}
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason3}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item2}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item3}
</Text>
<Text>
{" "}
</Text>
</Box>
<Text bold underline>
{"Tasks "}
████████░░░░░░░░
{" "}
{tasksDone4}
/
{tasksTotal4}
{` (showing ${length} of ${length})`}
</Text>
<Text dimColor>
{" No tasks yet."}
</Text>
<Text dimColor>
{" No task matches."}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text2}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text3}
</Text>
<Text>
{" "}
</Text>
<Text bold underline>
{"Specs "}
{length2}
{" file(s) · "}
{`${sum}${length}`}
{" requirement(s) · "}
{`${sum}${reduce}`}
{" scenario(s)"}
</Text>
<Text color="red">
{" No specs yet: the gate has no acceptance criteria."}
</Text>
<Text dimColor>
{" No spec matches."}
</Text>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability}
</Text>
<Text dimColor>
{" "}
{specPath}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios}
{") "}
{name2}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios2}
{") "}
{name3}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios3}
{") "}
{name4}
</Text>
</Box>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability2}
</Text>
<Text dimColor>
{" "}
{specPath2}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios4}
{") "}
{name5}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios5}
{") "}
{name6}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios6}
{") "}
{name7}
</Text>
</Box>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability3}
</Text>
<Text dimColor>
{" "}
{specPath3}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios7}
{") "}
{name8}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios8}
{") "}
{name9}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios9}
{") "}
{name10}
</Text>
</Box>
<Box flexDirection="column">
<Text>
{" "}
</Text>
<Text bold underline>
Recent activity
</Text>
<Text dimColor>
{" No activity matches."}
</Text>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind}
</Text>
<Text wrap="truncate-end">
{detail}
</Text>
</Box>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind2}
</Text>
<Text wrap="truncate-end">
{detail2}
</Text>
</Box>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind3}
</Text>
<Text wrap="truncate-end">
{detail3}
</Text>
</Box>
</Box>
</Box>
<Box flexDirection="column">
<Text>
{" "}
</Text>
<Text bold color="cyan">
{"━━ "}
{name11}
{" (focus)"}
</Text>
<Box flexDirection="column">
<Text bold underline>
Stage
</Text>
<Box>
<Text color="green">
✔
{" "}
{stage7}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE4}
</Text>
</Box>
<Box>
<Text color="green">
✔
{" "}
{stage8}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE5}
</Text>
</Box>
<Box>
<Text color="green">
✔
{" "}
{stage9}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE6}
</Text>
</Box>
<Text>
{" "}
</Text>
<Text bold underline>
Gate
</Text>
<Text color="red">
closed: edits outside openspec/ are denied
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason4}
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason5}
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason6}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item4}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item5}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item6}
</Text>
<Text>
{" "}
</Text>
</Box>
<Text bold underline>
{"Tasks "}
████████░░░░░░░░
{" "}
{tasksDone5}
/
{tasksTotal5}
{` (showing ${length} of ${length})`}
</Text>
<Text dimColor>
{" No tasks yet."}
</Text>
<Text dimColor>
{" No task matches."}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text4}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text5}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text6}
</Text>
<Text>
{" "}
</Text>
<Text bold underline>
{"Specs "}
{length3}
{" file(s) · "}
{`${sum}${length}`}
{" requirement(s) · "}
{`${sum}${reduce}`}
{" scenario(s)"}
</Text>
<Text color="red">
{" No specs yet: the gate has no acceptance criteria."}
</Text>
<Text dimColor>
{" No spec matches."}
</Text>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability4}
</Text>
<Text dimColor>
{" "}
{specPath4}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios10}
{") "}
{name12}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios11}
{") "}
{name13}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios12}
{") "}
{name14}
</Text>
</Box>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability5}
</Text>
<Text dimColor>
{" "}
{specPath5}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios13}
{") "}
{name15}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios14}
{") "}
{name16}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios15}
{") "}
{name17}
</Text>
</Box>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability6}
</Text>
<Text dimColor>
{" "}
{specPath6}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios16}
{") "}
{name18}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios17}
{") "}
{name19}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios18}
{") "}
{name20}
</Text>
</Box>
<Box flexDirection="column">
<Text>
{" "}
</Text>
<Text bold underline>
Recent activity
</Text>
<Text dimColor>
{" No activity matches."}
</Text>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind4}
</Text>
<Text wrap="truncate-end">
{detail4}
</Text>
</Box>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind5}
</Text>
<Text wrap="truncate-end">
{detail5}
</Text>
</Box>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind6}
</Text>
<Text wrap="truncate-end">
{detail6}
</Text>
</Box>
</Box>
</Box>
<Box flexDirection="column">
<Text>
{" "}
</Text>
<Text bold color="cyan">
{"━━ "}
{name21}
{" (focus)"}
</Text>
<Box flexDirection="column">
<Text bold underline>
Stage
</Text>
<Box>
<Text color="green">
✔
{" "}
{stage10}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE7}
</Text>
</Box>
<Box>
<Text color="green">
✔
{" "}
{stage11}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE8}
</Text>
</Box>
<Box>
<Text color="green">
✔
{" "}
{stage12}
</Text>
<Text dimColor>
{" "}
{STAGE_EVIDENCE9}
</Text>
</Box>
<Text>
{" "}
</Text>
<Text bold underline>
Gate
</Text>
<Text color="red">
closed: edits outside openspec/ are denied
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason7}
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason8}
</Text>
<Text color="red" wrap="wrap">
{" • "}
{reason9}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item7}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item8}
</Text>
<Text dimColor wrap="wrap">
{" ○ missing "}
{item9}
</Text>
<Text>
{" "}
</Text>
</Box>
<Text bold underline>
{"Tasks "}
████████░░░░░░░░
{" "}
{tasksDone6}
/
{tasksTotal6}
{` (showing ${length} of ${length})`}
</Text>
<Text dimColor>
{" No tasks yet."}
</Text>
<Text dimColor>
{" No task matches."}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text7}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text8}
</Text>
<Text wrap="truncate-end">
{" [x] "}
?
{" "}
{text9}
</Text>
<Text>
{" "}
</Text>
<Text bold underline>
{"Specs "}
{length4}
{" file(s) · "}
{`${sum}${length}`}
{" requirement(s) · "}
{`${sum}${reduce}`}
{" scenario(s)"}
</Text>
<Text color="red">
{" No specs yet: the gate has no acceptance criteria."}
</Text>
<Text dimColor>
{" No spec matches."}
</Text>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability7}
</Text>
<Text dimColor>
{" "}
{specPath7}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios19}
{") "}
{name22}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios20}
{") "}
{name23}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios21}
{") "}
{name24}
</Text>
</Box>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability8}
</Text>
<Text dimColor>
{" "}
{specPath8}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios22}
{") "}
{name25}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios23}
{") "}
{name26}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios24}
{") "}
{name27}
</Text>
</Box>
<Box flexDirection="column">
<Box>
<Text bold>
{" "}
{capability9}
</Text>
<Text dimColor>
{" "}
{specPath9}
</Text>
</Box>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios25}
{") "}
{name28}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios26}
{") "}
{name29}
</Text>
<Text color="red" wrap="truncate-end">
{" "}
•
{" ("}
{scenarios27}
{") "}
{name30}
</Text>
</Box>
<Box flexDirection="column">
<Text>
{" "}
</Text>
<Text bold underline>
Recent activity
</Text>
<Text dimColor>
{" No activity matches."}
</Text>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind7}
</Text>
<Text wrap="truncate-end">
{detail7}
</Text>
</Box>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind8}
</Text>
<Text wrap="truncate-end">
{detail8}
</Text>
</Box>
<Box>
<Text dimColor>
{` ${time} `}
</Text>
<Text color="red">
{kind9}
</Text>
<Text wrap="truncate-end">
{detail9}
</Text>
</Box>
</Box>
</Box>
</Box>
)
})
on('session.start', async ($, e, next) => {
const name = '…'
const length = '…'
await $.ui.toast(`openspec-governance: "${name}" fails the gate (${length}); edit allowed (warn mode)`)
return next(e)
})
}
The mod's own code
Shown under the mod's licence (MIT); its text is below. Keep the notice if you copy these lines.
hooks/register.tsx lines 260–266 · Band
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const all = await read($, overview)
const current = currentOf(all, await read($, focus))
if (all === null || current === null || e.props.hasSurvey) return next(e)
return bandView($.ui.resolve(e), current, all.changes.length, () => openPane($))
})hooks/lib/views.tsx lines 42–67 · Band
export const bandView = (ui: Ui, change: ChangeInfo, changeCount: number, onDetails: () => void) => {
const { Box, Button, Text } = ui
const isBlocked = change.gateReasons.length > 0
const hint = change.missing[0]
return (
<Box>
<Text bold>OpenSpec </Text>
<Text>{change.name}</Text>
{changeCount > 1 ? <Text dimColor> (+{changeCount - 1})</Text> : null}
<Text dimColor> · </Text>
<Text color="cyan">{change.stage}</Text>
<Text dimColor> · </Text>
<Text>
{progressBar(change.tasksDone, change.tasksTotal)} {change.tasksDone}/{change.tasksTotal}
</Text>
<Text dimColor> · </Text>
<Text color={isBlocked ? 'red' : 'green'}>
{isBlocked ? `gate closed (${change.gateReasons.length})` : 'gate open'}
</Text>
{hint === undefined ? null : <Text dimColor> · next: {hint}</Text>}
<Text> </Text>
<Button key="details" label="Details" onPress={onDetails} />
</Box>
)
}hooks/register.tsx lines 268–290 · Pane
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
const all = await read($, overview)
const current = currentOf(all, await read($, focus))
const ui = $.ui.resolve(e)
if (!('Input' in ui)) return next(e)
if (all === null || current === null) {
const { Text } = ui
return <Text dimColor>No active OpenSpec change. Read or edit a file under openspec/changes/<name>/ to pick one.</Text>
}
return detailView(
ui,
{ overview: all, current, query: await read($, query), shown: await read($, shown) },
{
search: value => update($, query, () => value),
focus: name => update($, focus, (): Focus => ({ root: all.root, change: name })),
toggle: name =>
update($, shown, list => (list.includes(name) ? list.filter(one => one !== name) : [...list, name])),
refresh: () => refreshFocused($),
},
)
})hooks/lib/views.tsx lines 190–253 · Pane
export const detailView = (ui: InputUi, context: PaneContext, actions: PaneActions) => {
const { Box, Button, Input, Text } = ui
const { overview, current, query, shown } = context
const isFiltering = query.trim() !== ''
return (
<Box flexDirection="column">
<Box borderStyle="round" paddingX={1}>
<Text bold>Filter </Text>
<Input
key="filter"
placeholder="type to filter changes, tasks, specs"
value={query}
autoFocus
onInput={value => actions.search(value)}
onSubmit={value => actions.search(value)}
/>
</Box>
<Box>
<Button key="clear" label="Clear" onPress={() => actions.search('')} />
<Text> </Text>
<Button key="refresh" label="Refresh" onPress={() => actions.refresh()} />
<Text dimColor> live: re-reads every few seconds while open</Text>
</Box>
<Text> </Text>
<Text bold underline>Changes ({overview.changes.length})</Text>
{overview.changes.map(change => {
const isCurrent = change.name === current.name
const isShown = shown.includes(change.name)
const hits = isFiltering ? matchCount(change, query) : null
const isBlocked = change.gateReasons.length > 0
return (
<Box key={`change-${change.name}`}>
<Button
key={`focus-${change.name}`}
label={`${isCurrent ? '▶' : ' '} ${change.name}`}
onPress={() => actions.focus(change.name)}
/>
<Text dimColor={hits === 0}>
{' '}
{change.stage} {progressBar(change.tasksDone, change.tasksTotal)} {change.tasksDone}/{change.tasksTotal}{' '}
</Text>
<Text color={isBlocked ? 'red' : 'green'}>{isBlocked ? 'gate closed' : 'gate open'}</Text>
{hits === null ? null : <Text color="yellow"> {hits} match(es)</Text>}
{isCurrent ? null : <Text> </Text>}
{isCurrent ? null : (
<Button
key={`toggle-${change.name}`}
label={isShown ? 'Hide' : 'Show'}
onPress={() => actions.toggle(change.name)}
/>
)}
</Box>
)
})}
{visibleChanges(overview, current, shown).map(change =>
changeBlock(ui, change, query, change.name === current.name),
)}
</Box>
)
}hooks/lib/views.tsx lines 70–187 · Pane
const changeBlock = (ui: Ui, change: ChangeInfo, query: string, isCurrent: boolean) => {
const { Box, Text } = ui
const isFiltering = query.trim() !== ''
const isBlocked = change.gateReasons.length > 0
const current = (STAGE_ORDER as readonly string[]).indexOf(change.stage)
const tasks = change.tasks.filter(task => matchesQuery(query, task.id, task.text))
const rows = change.logTail.filter(row => matchesQuery(query, row.kind, row.detail))
const specs = change.specs
.map(spec => {
const isWholeMatch = matchesQuery(query, spec.capability, specPath(spec.file))
const requirements = isWholeMatch
? spec.requirements
: spec.requirements.filter(requirement => matchesQuery(query, requirement.name))
return { spec, requirements, isMatch: isWholeMatch || requirements.length > 0 }
})
.filter(entry => entry.isMatch)
const requirementCount = change.specs.reduce((sum, spec) => sum + spec.requirements.length, 0)
const scenarioCount = change.specs.reduce(
(sum, spec) => sum + spec.requirements.reduce((inner, requirement) => inner + requirement.scenarios, 0),
0,
)
return (
<Box key={`block-${change.name}`} flexDirection="column">
<Text> </Text>
<Text bold color="cyan">
━━ {change.name}
{isCurrent ? ' (focus)' : ''}
</Text>
{isFiltering ? null : (
<Box flexDirection="column">
<Text bold underline>Stage</Text>
{STAGE_ORDER.map((stage, index) => (
<Box key={`stage-${change.name}-${stage}`}>
<Text
color={index < current ? 'green' : index === current ? 'cyan' : undefined}
dimColor={index > current}
bold={index === current}
>
{index < current ? '✔' : index === current ? '▶' : '○'} {stage}
</Text>
<Text dimColor> {STAGE_EVIDENCE[stage]}</Text>
</Box>
))}
<Text> </Text>
<Text bold underline>Gate</Text>
<Text color={isBlocked ? 'red' : 'green'}>
{isBlocked ? 'closed: edits outside openspec/ are denied' : 'open'}
</Text>
{change.gateReasons.map(reason => (
<Text key={`gate-${change.name}-${reason}`} color="red" wrap="wrap"> • {reason}</Text>
))}
{change.missing.map(item => (
<Text key={`missing-${change.name}-${item}`} dimColor wrap="wrap"> ○ missing {item}</Text>
))}
<Text> </Text>
</Box>
)}
<Text bold underline>
Tasks {progressBar(change.tasksDone, change.tasksTotal)} {change.tasksDone}/{change.tasksTotal}
{isFiltering ? ` (showing ${tasks.length} of ${change.tasks.length})` : ''}
</Text>
{change.tasks.length === 0 ? <Text dimColor> No tasks yet.</Text> : null}
{change.tasks.length > 0 && tasks.length === 0 ? <Text dimColor> No task matches.</Text> : null}
{tasks.map((task, index) => (
<Text key={`task-${change.name}-${index}`} dimColor={task.isDone} wrap="truncate-end">
{task.isDone ? ' [x] ' : ' [ ] '}
{task.id ?? '?'} {task.text}
</Text>
))}
<Text> </Text>
<Text bold underline>
Specs {change.specs.length} file(s) · {requirementCount} requirement(s) · {scenarioCount} scenario(s)
</Text>
{change.specs.length === 0 ? <Text color="red"> No specs yet: the gate has no acceptance criteria.</Text> : null}
{change.specs.length > 0 && specs.length === 0 ? <Text dimColor> No spec matches.</Text> : null}
{specs.map(({ spec, requirements }) => (
<Box key={`spec-${change.name}-${spec.file}`} flexDirection="column">
<Box>
<Text bold>{' '}{spec.capability}</Text>
<Text dimColor> {specPath(spec.file)}</Text>
</Box>
{requirements.map((requirement, index) => (
<Text
key={`req-${change.name}-${spec.file}-${index}`}
color={requirement.scenarios === 0 ? 'red' : undefined}
wrap="truncate-end"
>
{' '}
{OP_MARK[requirement.op] ?? '•'} ({requirement.scenarios}) {requirement.name}
</Text>
))}
</Box>
))}
{change.logTail.length === 0 ? null : (
<Box flexDirection="column">
<Text> </Text>
<Text bold underline>Recent activity</Text>
{change.logTail.length > 0 && rows.length === 0 ? <Text dimColor> No activity matches.</Text> : null}
{rows.map((row, index) => (
<Box key={`log-${change.name}-${index}`}>
<Text dimColor>{` ${row.time.slice(11, 19)} `}</Text>
<Text color={row.kind === 'gate-denied' ? 'red' : 'cyan'}>{row.kind.padEnd(15)}</Text>
<Text wrap="truncate-end">{row.detail}</Text>
</Box>
))}
</Box>
)}
</Box>
)
}Licence text (MIT)
MIT License Copyright (c) 2026 whoisclebs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
What its code calls
writes filestool calls
The design, read from code
Drawn from the mod's source at the commit read, not from a run: a list is drawn three times, a condition takes its first branch that draws in the desktop app, and a value only the run time knows is shown as a made-up sample (dotted underline) or left out. Colours and type are this site's.
What its code shows
Read from the source at commit fb6252b33ecb on 2026-10-06. The first group decides whether a mod is listed; the rest is for you to weigh. A later commit is not read until the next crawl.
| Decides the listing: code you can check | |
|---|---|
| Entry read | Every module hooks/hooks.json names was read.hooks/register.tsx |
| Parses | Every file parses as TypeScript or JavaScript.10 files parsed |
| All its code is in the mod | Every import is a file of the mod or the engine's own module; no packages, no require.all its code is in the mod |
| Readable | No minified lines, no eval or new Function, no import of a computed path, no encoded blobs.no minified code, no eval, no encoded blobs |
| Calls in the open | Network and programs are reached as $.http.fetch and $.process.run in place, never passed around.every call outside goes through $ in the open |
| For you to weigh before installing | |
| Where it draws | The terminal, the desktop app, both, or nothing (hooks only: a guard or a prompt rewriter), as its code says. Shown, never a reason to leave a mod out.draws in the terminal and the desktop app, the same in both |
| Install line | Whether its repository's own .claude-plugin/marketplace.json lists it, so it installs from that marketplace with the lines on its page.listed in its repository's .claude-plugin/marketplace.json: installs from there |
| Programs on Windows | Whether every program it starts through $.process exists on Windows (tail, date, open, osascript, /usr/… do not).runs no program a Windows PC lacks |
| Network | Whether every fetch names its https host in the code.no network |
| Programs | Whether every program it starts is named in the code and is not a shell, an interpreter or a network tool.starts no programs |
| Reads and sends | Whether it both reads files or environment variables and reaches the network.reaches no network |
| Keys in the source | Whether the source holds what looks like an API key or a private key.no keys in the source |
| Licence | An open licence (MIT, Apache-2.0, BSD, ISC, Unlicense, 0BSD, CC0) with its text lets modscode show its code; otherwise the code is linked.MIT |
This is a reading of the code text, not a security review: it does not run the mod, and it cannot see what a program the mod starts does. The check before installing is yours. How mods are chosen
Install it
Check it yourself before you install it. Mods run unsandboxed, with your permissions, and modscode does not vouch for what this code does. Read the code below (or run claude plugin validate . in its folder), weigh what it reaches, and install the commit you read.
Its repository is its marketplace. Add that marketplace (in the terminal with these commands, or in the desktop app's plugin settings) and install the mod from it:
/plugin marketplace add whoisclebs/openspec-governance/plugin install openspec-governance@openspec-governanceThis installs the marketplace's latest commit, which may not be the one read here (fb6252b33ecb). Compare them before you install.
Ask Claude about it
With the modscode connector added, Claude can read this design, what its code reaches and its code, and build on them. Ask it like this:
Read the modscode design "whoisclebs-openspec-governance" with its code and make me a pane like itSearch them from Claude: the MCP connector
Similar designs
-
Ask Claude…2h 10m · █████░░░✓ build ✓ build ✓ buildJavaKotlin3 Python■ ok 3 ■ ok 3 ■ ok 3 Razor proven 3 Razor proven 3 Razor proven 3Phases ──────── 3/3build Pythonbuild Pythonbuild Python… 3 more phasesLua ──────── 3/Kotlin■ the whole upgrade Ruby■ build build Ruby■ build build Ruby■ build build Ruby … 3 moreNext ──────── PythonRuby (by hand)Podsclick a button, or ctrl+x tab then Tab, EnterAttention ──────── 3! Scala ! Scala ! Scala … 3 moreSession ──────── okJava
-
slop Edit1 of 3accepteditrejectAsk Claude…
-
Ask Claude…Skins0 Your promptadd rate limiting to the hub api0 Tool rows: finished, running, failedYour prompt Spinner0 Folded group of reads and searches●main · note 3 · note 3 · note 30 Edit diff cardDesktop only; the terminal keeps Claude Code's diff.0 Shell output cardDesktop only.0 Code block cardDesktop only; the terminal keeps Claude Code's markdown.0 TableAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentCopyAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentCopyAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentAccentCopy0 SpinnerAccent SpinnerSpinnerSpinner… 2h 10m0 Band above the prompt: normal, then nudging to compact█████░░░ 42% build█████░░░ 42% build█████░░░ 42% buildContext is 42% full0 Compact now0 Band above a questionTable BashBashBash0 Turn footer (terminal)3 build in 2h 10m · Grep · +3 −3
-
Ask Claude…agent-discoverbuildbuildbuildRefreshMCP Browse/3Tab / shift+Tab move · Enter presses · ↑↓ scroll · Esc back to the promptBrowse...Audit asks:DeclineAcceptDeclineCancelAudit asks:DeclineAcceptDeclineCancelAudit asks:DeclineAcceptDeclineCancel
-
Ask Claude…Fanyi settingsOnAPI · OpusenOn this MacAPIHost src/ SaveKey K · paste a new one, Enter SaveOpus↻ RefreshCustom model model id, for an API that lists none UseFanyi settingsHostClose
-
Ask Claude…Glossary BrowserGlossary openContextsContextsContexts▲ prevWrite1/Glossary▼ nextContexts