[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…

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)をまとめてやる。手でやる場合:
plugins/<name>/ に .claude-plugin/plugin.json と中身を置く.claude-plugin/marketplace.json の plugins にエントリを追加する(name は plugin.json と同じにする)plugins/japan-da-man-mods/.claude-plugin/plugin.json の dependencies にも名前を足すplugin.json に version を書いていないので、コミットごとに新しいバージョンとして扱われる。
mods は Claude Code v2.1.287 以降が必要。
token-weather・blast-radius・replay-theater は、Anthropic の claude-code-playground にある同名の mod(Apache-2.0)をもとに手を加えたもの。各プラグインに LICENSE を同梱し、各 hooks/register.js の先頭に元のコードの著作権表示と、こちらで変えた点を書いている。
hooks/register.js 252 lines1// 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}
252types/index.d.ts 15 lines1declare 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