SLOPSHOPPER

spec-flow

[mod] [skills] Turns a feature request into a spec, one approved step at a time: clarify the request (asking you what is unclear), survey every affected screen…

newpanebandguardcommandtoast
v?no licenseupdated 2026-10-02japan-da-man/claude-code/plugins/spec-flow
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · spec-flow
│ ┃ spec-flow ✕ › fix the failing auth test and add an audit log call │ ┃ まだ要望がありません。 │ ┃ 「〇〇な機能が欲しい」と話しかけるか、/spec- ⏺ Read(src/auth.ts) │ ┃ flow:spec <要望> で始めます。 ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /specs │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · spec-flow
まだ要望がありません。 「〇〇な機能が欲しい」と話しかけるか、/spec-flow:spec <要望> で始めます。
README

claude-code

japan-da-man の Claude Code プラグイン(mods を含む)を管理するマーケットプレイス。

プラグイン

| 名前 | 内容 | | :- | :- | | japan-da-man-mods | まとめてインストール用。下のプラグインを全部入れる | | todo-mod | /todo で TODO パネルを開く。チェックを入れたタスクを Claude に依頼する | | token-weather | コンテキストの埋まり具合を天気でプロンプトの上に表示する(☀ Clear → ☁ Cloudy → ☂ Showers → ☇ Storm → ↯ Compact soon)。直近12ターンのグラフと、直前のターンの増減(▲ +98.3k last turn)付き。/weather で表示切り替え | | usage-stats | /stats で利用統計のパネルを開く。セッション数・メッセージ数・トークン数・利用日数・連続日数・よく使うモデル・日ごとのヒートマップ | | blast-radius | 危険なシェルコマンドを実行前に止め、何が消えるか・変わるかをペインに出して Proceed / Cancel で選ばせる(10分答えがなければ止める)。rm -r/-f、git reset --hard、force push、git clean、変更の破棄、git branch -D、git stash drop/clear、マイグレーション、find -delete、xargs rm。bash -c / eval の中も調べる。/blast-radius で履歴 | | replay-theater | ターンの中で Claude が行ったファイル編集を記録する。ターンが終わるとプロンプトの上に ▶ Replay: N edits が出て、r か /replay でペインを開き、編集を 1 件ずつ差分で見られる(Prev / Next / Close) | | spec-flow | 「〇〇な機能が欲しい」と話しかけるか /spec-flow:spec <要望> で、要求整理 → 影響画面確認 → 要件定義 → 機能案・UI案 を 1 段階ずつ承認しながら進める。曖昧な点はヒアリングし、影響調査と案出しはサブエージェントが行う。成果物は docs/specs/<id>/(UI 案の HTML モックは Desktop なら Artifact にも公開。--local で公開しない)。/specs でチェックリストを開き、チェックで承認・修正依頼もできる | | dev-flow | 汎用の開発フロー。skills plan(小さなタスクに分けて承認をもらってから着手)・tdd(RED → GREEN → REFACTOR)・debug(再現 → 仮説 → 検証で原因を確かめてから直す)・review(重要度つきの差分レビュー)。サブエージェント code-explorer(横断調査)・test-runner(テスト結果の要約)・reviewer(別コンテキストでレビュー、修正はしない)が長い出力をメインに持ち込まない | | dev-extras | 他の OSS から選んで取り込んだ skill(すべて MIT、出典は各 README と licenses/)。ponytail(最小の実装に絞る)・ponytail-review(作り込みすぎだけを探す)・grill-me(質問攻めで計画を詰める)・handoff(バックグラウンドエージェントに引き継ぐ)・verification-before-completion(検証してから完了と言う)・security-audit(Cloudflare 製のセキュリティ監査) | | mod-dev | skill add-mod。「〇〇する mod を作って」で雛形・テスト・登録まで行う。marketplace リポジトリでは marketplace.json / bundle / README も更新し、それ以外では単体の mod を作る |

インストール

claude plugin marketplace add japan-da-man/claude-code
claude plugin install japan-da-man-mods@japan-da-man

1つずつ入れる場合は claude plugin install <名前>@japan-da-man。

更新:

claude plugin marketplace update japan-da-man
claude plugin update <名前>@japan-da-man

bundle に増えたプラグインを入れるときは、update → install の順で実行する(update だけだと増えた分は入らず、bundle が failed to load になる):

claude plugin marketplace update japan-da-man
claude plugin update japan-da-man-mods@japan-da-man
claude plugin install japan-da-man-mods@japan-da-man

セッション内なら update のあと /reload-plugins でもよい。

開発

インストールせずに作業中のディレクトリを読み込む(保存するとホットリロードされる):

claude --plugin-dir ./plugins/todo-mod

チェック:

claude plugin validate .
claude plugin validate ./plugins/todo-mod

テスト(tests/*.test.ts があるプラグイン):

cd plugins/<name> && claude plugin test

プラグインの追加

mod-dev を入れた状態でこのリポジトリを開き「〇〇する mod を追加して」と頼むと、skill add-mod が下の手順(雛形・テスト・marketplace / bundle / README 更新・validate)をまとめてやる。手でやる場合:

  1. plugins/<name>/ に .claude-plugin/plugin.json と中身を置く
  2. .claude-plugin/marketplace.json の plugins にエントリを追加する(name は plugin.json と同じにする)
  3. まとめて入るようにするなら plugins/japan-da-man-mods/.claude-plugin/plugin.json の dependencies にも名前を足す

plugin.json に version を書いていないので、コミットごとに新しいバージョンとして扱われる。

mods は Claude Code v2.1.287 以降が必要。

公式 mod をもとにしたもの

token-weather・blast-radius・replay-theater は、Anthropic の claude-code-playground にある同名の mod(Apache-2.0)をもとに手を加えたもの。各プラグインに LICENSE を同梱し、各 hooks/register.js の先頭に元のコードの著作権表示と、こちらで変えた点を書いている。

Source 2 files
hooks/register.js 252 lines
1// spec-flow: docs/specs/*/progress.json を読んで、要望ごとの 4 段階をチェックリストで見せる。
2// 進行そのものは spec スキルが行い、この mod は見せることと承認・差し戻しを送ることだけをする。
3// mod がなくてもスキルだけで最後まで進められる(progress.json が唯一の正)。
4import { atom, read, update } from 'claude-code'
5
6const PANE_ID = 'spec-flow'
7const SPECS_DIR = 'docs/specs'
8const SKILL = 'spec-flow:spec'
9// ペインに出す要望の数
10const MAX_SPECS = 6
11
12const specsAtom = atom({ plugin: 'spec-flow', key: 'specs' }, [])
13
14// 段階の状態ごとの印
15export const MARKS = { approved: '[x]', review: '[?]', in_progress: '[~]', pending: '[ ]' }
16
17// progress.json の中身を、表示に使う形にする。形が違えば null
18export function summarize(id, progress) {
19  if (!progress || !Array.isArray(progress.steps)) return null
20  return {
21    id,
22    title: String(progress.title || id),
23    updatedAt: String(progress.updatedAt || progress.createdAt || ''),
24    chosen: progress.chosen ?? null,
25    steps: progress.steps.map((s) => ({
26      id: String(s.id),
27      label: String(s.label || s.id),
28      status: MARKS[s.status] ? s.status : 'pending',
29      file: String(s.file || ''),
30    })),
31  }
32}
33
34export function isDone(spec) {
35  return spec.steps.every((s) => s.status === 'approved')
36}
37
38// 今の段階: 承認されていない最初の段階。全部承認済みなら null
39export function currentStep(spec) {
40  return spec.steps.find((s) => s.status !== 'approved') ?? null
41}
42
43// 帯に出す要望: 終わっていないもののうち一番新しいもの
44export function activeSpec(specs) {
45  return specs.find((s) => !isDone(s)) ?? null
46}
47
48export function stepLine(step) {
49  const note = { review: '  確認待ち', in_progress: '  作業中' }[step.status] ?? ''
50  return `${MARKS[step.status]} ${step.label}${note}`
51}
52
53// 承認したあと Claude に送る文。スキルは progress.json を読み直して次の段階から続ける。
54// / で始まる文はコマンドとして扱われ送れないので、スキル名を文中で頼む
55export function approvalPrompt(spec, step) {
56  return `${SKILL} スキルで spec「${spec.title}」(${SPECS_DIR}/${spec.id})を続けてください。チェックリストで「${step.label}」を承認しました。progress.json を読み直して次の段階に進んでください。`
57}
58
59export function revisePrompt(spec, step, text) {
60  return `${SKILL} スキルで spec「${spec.title}」(${SPECS_DIR}/${spec.id})の「${step.label}」を次のとおり直して、もう一度確認待ちにしてください。\n${text}`
61}
62
63export function resumePrompt(spec) {
64  return `${SKILL} スキルで spec「${spec.title}」(${SPECS_DIR}/${spec.id})を続けてください。`
65}
66
67// Claude に頼む。await しない: Claude が作業中だとターンが終わるまで解決しないため
68function ask($, text) {
69  $.prompt.submit({ text }).catch(() => $.ui.toast('spec-flow: Claude に送れませんでした'))
70}
71
72// progress.json の 1 段階を承認済みにした次の中身を返す
73export function approve(progress, stepId, now) {
74  return {
75    ...progress,
76    updatedAt: now,
77    steps: progress.steps.map((s) => (s.id === stepId ? { ...s, status: 'approved', approvedAt: now } : s)),
78  }
79}
80
81// docs/specs を読み直して state に置く。読めないものは飛ばす
82async function scan($) {
83  const specs = []
84  try {
85    if (await $.fs.exists(SPECS_DIR)) {
86      for (const entry of await $.fs.list(SPECS_DIR)) {
87        if (entry.kind !== 'dir') continue
88        try {
89          const spec = summarize(entry.name, JSON.parse(await $.fs.read(`${SPECS_DIR}/${entry.name}/progress.json`)))
90          if (spec) specs.push(spec)
91        } catch {
92          // progress.json がないか壊れている
93        }
94      }
95    }
96  } catch {
97    // 読めなければ空のまま
98  }
99  specs.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0))
100  await update($, specsAtom, () => specs)
101}
102
103// チェックを入れた段階を承認して、Claude に続きを頼む
104async function approveStep($, spec, step) {
105  const path = `${SPECS_DIR}/${spec.id}/progress.json`
106  try {
107    const progress = JSON.parse(await $.fs.read(path))
108    const now = new Date(await $.clock.now()).toISOString()
109    await $.fs.write(path, JSON.stringify(approve(progress, step.id, now), null, 2) + '\n')
110  } catch {
111    $.ui.toast(`spec-flow: ${path} を書き換えられませんでした`)
112    return
113  }
114  await scan($)
115  $.ui.toast(`spec-flow: 「${step.label}」を承認しました`)
116  ask($, approvalPrompt(spec, step))
117}
118
119export function register(on) {
120  on('session.start', async ($, e, next) => {
121    const r = await next(e)
122    await scan($)
123    await $.command.register({ name: 'specs', description: 'spec-flow: 要望ごとの進み具合を開く', immediate: true })
124    return r
125  })
126
127  on('command.run', { command: 'specs' }, async ($) => {
128    await scan($)
129    await $.ui.open({ id: PANE_ID, title: 'spec-flow', focus: true, closeOnEscape: true })
130    return {}
131  })
132
133  // スキルが progress.json を書いたら、すぐ表示に反映する
134  on('tool.call', async ($, e, next) => {
135    const r = await next(e)
136    if ((e.tool === 'Write' || e.tool === 'Edit') && String(e.file_path || '').endsWith('/progress.json')) await scan($)
137    return r
138  })
139
140  on('turn.complete', async ($, e, next) => {
141    const r = await next(e)
142    if (!e.agentId) await scan($)
143    return r
144  })
145
146  // プロンプトの上: 進行中の要望の 4 段階
147  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
148    const spec = activeSpec(await read($, specsAtom))
149    if (!spec || e.props.hasSurvey) return next(e)
150    const { Box, Text, Button } = $.ui.resolve(e)
151    const marks = spec.steps.map((s, k) =>
152      Text({ key: `s${k}`, color: s.status === 'approved' ? 'green' : s.status === 'review' ? 'yellow' : undefined, dimColor: s.status === 'pending', children: `${MARKS[s.status]} ${s.label}` }),
153    )
154    const line = Box({
155      flexDirection: 'row',
156      gap: 2,
157      paddingX: 1,
158      children: [
159        Text({ color: 'cyan', bold: true, wrap: 'truncate-end', children: `◆ ${spec.title}` }),
160        ...((e.props.bodyColumns ?? 80) >= 90 ? marks : []),
161        Button({ key: 'open-specs', label: 'Specs', hotkey: 's', onPress: () => $.ui.open({ id: PANE_ID, title: 'spec-flow', focus: true, closeOnEscape: true }) }),
162      ],
163    })
164    // 他の mod が帯に描くものも残す
165    const rest = await next(e)
166    return rest ? Box({ flexDirection: 'column', children: [line, rest] }) : line
167  })
168
169  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
170    if (e.requestId !== PANE_ID) return next(e)
171    const { Box, Text, Button, Input } = $.ui.resolve(e)
172    const specs = (await read($, specsAtom)).slice(0, MAX_SPECS)
173    if (!specs.length) {
174      return Box({
175        flexDirection: 'column',
176        children: [
177          Text({ children: 'まだ要望がありません。' }),
178          Text({ dimColor: true, children: `「〇〇な機能が欲しい」と話しかけるか、/${SKILL} <要望> で始めます。` }),
179        ],
180      })
181    }
182
183    const blocks = specs.map((spec, n) => {
184      const current = currentStep(spec)
185      const rows = spec.steps.map((step, k) => {
186        const key = `${spec.id}-${step.id}`
187        const file = step.file ? `  ${SPECS_DIR}/${spec.id}/${step.file}` : ''
188        // 確認待ちの段階だけ押せるチェックボックスにする
189        if (step.status === 'review') {
190          return Box({
191            key,
192            flexDirection: 'row',
193            gap: 1,
194            children: [
195              Button({ key: `approve-${key}`, label: '[ ]', plain: true, autoFocus: n === 0, onPress: () => approveStep($, spec, step) }),
196              Text({ color: 'yellow', children: `${step.label}  確認待ち(チェックで承認して次へ)` }),
197              Text({ dimColor: true, wrap: 'truncate-start', children: file }),
198            ],
199          })
200        }
201        return Text({
202          key,
203          color: step.status === 'approved' ? 'green' : undefined,
204          dimColor: step.status === 'pending',
205          wrap: 'truncate-end',
206          children: stepLine(step) + (step.status === 'approved' ? file : ''),
207        })
208      })
209
210      const extra = []
211      if (current?.status === 'review') {
212        extra.push(
213          Input({
214            key: `revise-${spec.id}`,
215            label: '直してほしい点',
216            placeholder: `「${current.label}」への修正を書いて Enter`,
217            value: '',
218            submitLabel: '送る',
219            onSubmit: (value) => {
220              const text = value.trim()
221              if (!text) return
222              $.ui.toast(`spec-flow: 「${current.label}」の修正を依頼しました`)
223              ask($, revisePrompt(spec, current, text))
224            },
225          }),
226        )
227      } else if (current) {
228        extra.push(Button({ key: `resume-${spec.id}`, label: '続きを進める', onPress: () => ask($, resumePrompt(spec)) }))
229      } else if (spec.chosen) {
230        extra.push(Text({ dimColor: true, children: `採用案: ${spec.chosen}` }))
231      }
232
233      return Box({
234        key: spec.id,
235        flexDirection: 'column',
236        marginBottom: 1,
237        children: [
238          Text({ bold: true, color: isDone(spec) ? 'green' : 'cyan', children: `${isDone(spec) ? '✔' : '◆'} ${spec.title}` }),
239          Text({ dimColor: true, children: `${SPECS_DIR}/${spec.id}` }),
240          ...rows,
241          ...extra,
242        ],
243      })
244    })
245
246    return Box({
247      flexDirection: 'column',
248      children: [...blocks, Text({ dimColor: true, children: '[x] 承認済み · [?] 確認待ち · [~] 作業中 · Tab で移動 · Esc で閉じる' })],
249    })
250  })
251}
252
types/index.d.ts 15 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'spec-flow': {
4      // docs/specs/*/progress.json の要約(更新の新しい順)
5      specs: Array<{
6        id: string
7        title: string
8        updatedAt: string
9        chosen: string | null
10        steps: Array<{ id: string; label: string; status: string; file: string }>
11      }>
12    }
13  }
14}
15