modscode EN

modscode / 図鑑 / openspec-governance

openspec-governance

OpenSpec lifecycle governance for Claude Code: stage band, fail-closed edit gate, task progress and an append-only execution log.

確かめやすいコードターミナル+デスクトップペインバンドトーストBoxButtonInputText

何をするか

  • プロンプトの上の帯・欄・トーストを描く
  • /openspec を足す
  • Write・Edit・NotebookEdit・Read の呼び出しを見張る(止めることがある)
  • session.start・turn.complete・ui.closeにフックする

良いところ

  • ネットワークに出ない
  • プロセスを起こさない
  • TypeScript で書かれている

作り始めるためのコード

2 種類あります。上のデザインから modscode が書いた雛形と、その Mod 自身のコードのうち描いている部分です。

雛形(register.tsx)

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)
  })
}

Mod 自身のコード

Mod のライセンス(MIT)に従って載せています。全文は下にあります。写すときは著作権表示を残してください。

hooks/register.tsx 260〜266 行 · バンド
  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 42〜67 行 · バンド
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 268〜290 行 · ペイン
  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/&lt;name&gt;/ 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 190〜253 行 · ペイン
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 70〜187 行 · ペイン
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>
  )
}
ライセンスの全文(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.

コードが呼ぶもの

ファイル書き込みツール呼び出し

コードから読み取ったデザイン

読み取ったコミットのソースから描いたもので、動かした結果ではありません。一覧は 3 回描き、条件はデスクトップアプリで描く最初の分岐を取り、動かしたときにだけ決まる値は仮の例(点線の下線)にするか省きます。色と書体はこのサイトのものです。

コードから読めること

2026-10-06 に読んだコミット fb6252b33ecb のソースから。最初のまとまりが載せる条件で、残りは自分で判断するための材料です。後のコミットは、次に読むまで見ていません。

載せる条件: 確かめやすいコード
入口を読んだhooks/hooks.json が名指すモジュールをすべて読んだ。hooks/register.tsx
解析できるすべてのファイルが TypeScript か JavaScript として解析できる。10 ファイルを解析
コードが Mod の中で完結import は Mod 自身のファイルかエンジンのモジュールだけ。パッケージも require もない。コードはすべて Mod の中
読める圧縮した行・eval・new Function・計算した道の import()・符号化した塊がない。圧縮・eval・符号化した塊なし
呼び出しが見える通信とプログラム起動はその場で $.http.fetch・$.process.run と書き、他へ渡さない。外への呼び出しはすべてその場の $ で
入れる前に自分で判断する材料
描く場所コードから読んだ、描く先。ターミナル・デスクトップアプリ・両方・何も描かない(フックだけ: 見張りやプロンプトの書き換え)のどれか。出すだけで、外す理由にはしない。ターミナルとデスクトップアプリに同じように描く
入れる手順リポジトリ自身の .claude-plugin/marketplace.json にこの Mod が載っていて、ページに書いた手順でそのマーケットプレイスから入れられるか。リポジトリの .claude-plugin/marketplace.json に載っており、そこから入れられる
Windows のプログラム$.process で起動するプログラムが Windows にあるか(tail・date・open・osascript・/usr/… などは無い)。Windows にないプログラムを使わない
通信fetch の送り先(https のホスト)がコードに書いてあるか。通信なし
プログラム起動するプログラムの名前がコードに書いてあり、シェル・インタプリタ・通信用の道具でないか。プログラムを起動しない
読んで送るファイルや環境変数を読み、かつ通信するか。通信しない
ソースの鍵ソースに API キーや秘密鍵らしきものがあるか。ソースに鍵なし
ライセンスオープンなライセンス(MIT・Apache-2.0・BSD・ISC・Unlicense・0BSD・CC0)で全文があれば、modscode にコードを載せる。違えばリンクだけ。MIT

コードの文面を読んだだけで、安全の審査ではありません。動かしてはおらず、起動するプログラムの中で何が起きるかも見えません。入れる前の確認はあなたの役目です。 載せる条件

入れる

入れる前に、必ず自分で確かめてください。Mod はサンドボックスなしで、あなたの権限で動きます。modscode はこのコードの安全を保証しません。下のコードを読み(またはフォルダーで claude plugin validate .)、触るものを見て、読んだコミットを入れてください。

リポジトリがそのままマーケットプレイスです。このマーケットプレイスを足し(ターミナルでは次のコマンド、デスクトップアプリではプラグインの設定)、そこから Mod を入れます。

/plugin marketplace add whoisclebs/openspec-governance
/plugin install openspec-governance@openspec-governance

入るのはマーケットプレイスの最新のコミットで、ここで読んだもの(fb6252b33ecb)と違うことがあります。入れる前に見比べてください。

Claude に聞く

modscode のコネクタを入れると、Claude がこのデザイン・コードが触るもの・コードを読んで、それをもとに作れます。たとえば:

modscode のデザイン「whoisclebs-openspec-governance」をコードごと読んで、同じようなペインを作って

Claude から探す: MCP コネクタ

似たデザイン