A mod that shows the progress of the issue you are working on: when the agent reads or edits a GitHub issue with gh, it reads the issue's checklists (- [ ] / -…

English | 日本語
A Claude Code mod that shows, above the prompt, the progress of the acceptance criteria and checklists in the GitHub issue you are working on. You can see how many criteria are done and which remain without opening the issue page.
claude plugin marketplace add insession-space/claude-ship
claude plugin install acceptance-progress@claude-ship
Restart Claude Code and it loads. The mod is the hooks module named under modules in hooks/hooks.json (hooks/acceptance.tsx). A Claude Code without mods support does not load that module, so nothing happens there. The mod reads issue bodies with gh, so run gh auth login first.
When the agent touches an issue that has a checklist with gh, a band appears above the prompt.
╭──────────────────────────────────────────────────────────────────────────╮
│ #33 Show progress on screen Acceptance criteria ████░░░░ 2/6 │
│ ○ claude plugin test passes │
│ ○ listed in the README │
│ ○ existing tests stay green │
│ 1 more │
│ Open questions 0/2 │
╰──────────────────────────────────────────────────────────────────────────╯
## Open questions and the like) get one last row with each heading and its countA checklist under a heading Acceptance criteria / Definition of done / 受け入れ条件 / 受入条件 / 完了条件 counts as the acceptance criteria, and so do the checklists under its subheadings (### Functional and the like). When no heading matches, every checklist in the body counts. Items are lines starting with - [ ] / * [ ] / 1. [ ], ticked with [x] or [X]; lines inside code fences and HTML comments are not counted.
When the main agent's command succeeds, the mod reads that issue's body back from GitHub with gh issue view --json:
gh issue view <N> / gh issue edit <N> / gh issue comment <N> (-R owner/repo and issue URLs are read too)gh issue create (the issue at the URL it prints)It shows the last issue touched. ship-session's issue-loop ticks each acceptance criterion in the body as soon as it verifies it, so the count moves forward each time.
It neither reads again nor switches the band for:
gh command a subagent runs (so the band does not jump whenever a delegate reads a related issue). Boxes a subagent ticks show up with /acceptancegh issue view with no number (the form that infers it from the branch)-R and no URL that runs cd or sets GH_REPO before gh (it is unknown which repository the number belongs to)gh on the second line or later (such as a heredoc's text), and a -R that is not owner/repoIt does not poll. A box someone ticks on GitHub directly shows up the next time the issue is touched, or with /acceptance.
| Command | What it does |
|---|---|
/acceptance | Read the shown issue again |
/acceptance 42 / /acceptance owner/repo#42 / /acceptance <issue URL> | Show that issue |
/acceptance off | Hide the band |
/acceptance on | Bring the band back |
gh is missing, not logged in, or the network fails: the previous band stays, marked "refresh failed". The same goes for a failed switch to another issue, so the band shows it is no longer currentThe body's text is only displayed; it is never read as a command or a setting.
Taken from Claude Code's language setting, then the language of your latest message, then English.
claude plugin validate plugins/acceptance-progress
claude plugin test plugins/acceptance-progresshooks/acceptance.tsx 184 lines1// 作業中の Issue の受け入れ条件とチェックリストの進捗を、入力欄の上のバンドに出す mod。
2// エージェントが gh で触った Issue の本文を GitHub から読み直し、済んだ数と残りの項目を描く。
3import { atom, read, update } from 'claude-code'
4import type { EngineInterface, Register } from 'claude-code'
5
6import type { Lang, TrackedIssue } from '../types'
7import type { IssueRef } from './checklist'
8import {
9 LABELS,
10 bar,
11 countItems,
12 hasJapanese,
13 isNotFound,
14 issueFromJson,
15 langFromSetting,
16 mainGroups,
17 parseCommandArg,
18 refFromCommand,
19 refFromUrl,
20 viewArgv,
21} from './checklist'
22
23const issue = atom({ plugin: 'acceptance-progress', key: 'issue' } as const, null as TrackedIssue | null)
24const isHidden = atom({ plugin: 'acceptance-progress', key: 'isHidden' } as const, false)
25const lang = atom({ plugin: 'acceptance-progress', key: 'lang' } as const, null as Lang | null)
26
27/** バンドに並べる未完了の項目の上限。残りは「ほか N 件」にまとめる */
28const MAX_OPEN_ROWS = 3
29
30// 最後に始めた取得の番号。再読み込みで 0 に戻るが、走っていた取得もそこで捨てられる
31let latestFetch = 0
32
33type Fetched = { kind: 'ok'; issue: TrackedIssue } | { kind: 'not-found' } | { kind: 'failed'; reason: string }
34
35/** gh で本文を読み直す。gh が無い・未認証・ネットワーク失敗は failed、Issue が無いのは not-found。 */
36const fetchIssue = async ($: EngineInterface, ref: IssueRef): Promise<Fetched> => {
37 try {
38 const { exitCode, stdout, stderr } = await $.process.run(viewArgv(ref), { timeoutMs: 15_000 })
39 if (exitCode !== 0) return isNotFound(stderr) ? { kind: 'not-found' } : { kind: 'failed', reason: stderr.trim() }
40 const parsed = issueFromJson(stdout)
41 return parsed === null ? { kind: 'failed', reason: 'unreadable output' } : { kind: 'ok', issue: parsed }
42 } catch (err) {
43 return { kind: 'failed', reason: String(err) }
44 }
45}
46
47/**
48 * Issue を読み直して表示を差し替える。Issue が無い・チェックリストが無いときは消し、
49 * 読めなかったときは前回の表示を残して失敗の印を付ける。別の Issue へ切り替えようとして
50 * 失敗したときも印を付け、残っている表示が最新ではないことを示す。
51 */
52const refresh = async ($: EngineInterface, ref: IssueRef): Promise<Fetched> => {
53 // 取得は並んで走ることがある(並列の Bash 呼び出し、コマンドとエージェントの重なり)。
54 // 後から始めた取得が先に終わったとき、先に始めた古い取得の結果で上書きしない
55 const mine = ++latestFetch
56 const got = await fetchIssue($, ref)
57 if (mine !== latestFetch) return got
58 await update($, issue, cur => {
59 if (got.kind === 'ok') return got.issue.groups.length === 0 ? null : got.issue
60 if (got.kind === 'not-found') return null
61 return cur === null ? cur : { ...cur, error: got.reason }
62 })
63
64 return got
65}
66
67export const register: Register = on => {
68 // settings の `language` が決まっていればそれを使い、無ければプロンプトの文字で決める
69 let isLangFromSettings = false
70
71 on('session.start', async ($, e, next) => {
72 const fromSettings = langFromSetting((await $.settings.read()).language)
73 isLangFromSettings = fromSettings !== null
74 if (fromSettings !== null) await update($, lang, () => fromSettings)
75 await $.command.register({
76 name: 'acceptance',
77 description: 'Show the acceptance criteria progress of a GitHub issue above the prompt',
78 argumentHint: '[number | owner/repo#N | URL | off | on]',
79 })
80
81 return next(e)
82 })
83
84 on('prompt.submit', async ($, e, next) => {
85 // ユーザーが打った文だけを見る(タスク通知などエンジン由来の文は英語で来る)
86 const isUserText = e.origin.kind === 'composer' || e.origin.kind === 'bridge'
87 if (isUserText && !isLangFromSettings) await update($, lang, () => (hasJapanese(e.text) ? 'ja' : 'en'))
88
89 return next(e)
90 })
91
92 on('command.run', { command: 'acceptance' }, async ($, e) => {
93 const l = LABELS[(await read($, lang)) ?? 'en']
94 const arg = parseCommandArg(e.args)
95 if (arg.kind === 'usage') return { text: l.usage }
96 if (arg.kind === 'off' || arg.kind === 'on') {
97 await update($, isHidden, () => arg.kind === 'off')
98 return { text: arg.kind === 'off' ? l.hidden : l.shown }
99 }
100
101 const cur = await read($, issue)
102 const ref = arg.kind === 'show' ? arg.ref : cur === null ? null : { repo: cur.repo, number: cur.number }
103 if (ref === null) return { text: l.none }
104 await update($, isHidden, () => false)
105 const got = await refresh($, ref)
106 if (got.kind === 'not-found') return { text: l.notFound(ref.number) }
107 if (got.kind === 'failed') return { text: l.fetchFailed(ref.number) }
108 if (got.issue.groups.length === 0) return { text: l.noChecklist(ref.number) }
109 const { done, total } = countItems(mainGroups(got.issue.groups))
110 return { text: l.showing(ref.number, done, total) }
111 })
112
113 // メインエージェントが gh で触った Issue を読み直す。サブエージェントの呼び出しでは切り替えない
114 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
115 const ran = await next(e)
116 if (ran.deny !== undefined || ran.isError === true || e.agentId !== undefined) return ran
117
118 const created = /\bgh\s+issue\s+create\b/.test(e.command) ? refFromUrl(ran.text ?? '') : null
119 const ref = refFromCommand(e.command) ?? created
120 if (ref === null) return ran
121 // リポジトリを書いていないコマンドが表示中と同じ番号を指すなら、表示中のリポジトリとみなす。
122 // エージェントが別のディレクトリへ cd していても、セッションのカレントで読み違えないため
123 const cur = await read($, issue)
124 await refresh($, ref.repo === null && cur?.number === ref.number ? { ...ref, repo: cur.repo } : ref)
125
126 return ran
127 })
128
129 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
130 const cur = await read($, issue)
131 if (e.props.hasSurvey || cur === null || (await read($, isHidden))) return next(e)
132
133 // 他のプラグイン(agent-cast や ship-session のバンド)が描くものの下に並べる
134 const below = await next(e)
135 const { Box, Text } = $.ui.resolve(e)
136 const l = LABELS[(await read($, lang)) ?? 'en']
137 const main = mainGroups(cur.groups)
138 const others = cur.groups.filter(g => !main.includes(g))
139 const { done, total } = countItems(main)
140 const open = main.flatMap(g => g.items).filter(i => !i.isDone)
141
142 return (
143 <Box flexDirection="column">
144 {below}
145 <Box flexDirection="column" borderStyle="round" borderColor="green" paddingX={1}>
146 <Box justifyContent="space-between" columnGap={2}>
147 <Text bold wrap="truncate-end">
148 #{cur.number} {cur.title}
149 </Text>
150 <Box flexShrink={0}>
151 <Text>
152 <Text dimColor>{main[0]?.isAcceptance === true ? l.acceptance : l.other} </Text>
153 <Text color="green">{bar(done, total, 12)}</Text> <Text bold>{done}/{total}</Text>
154 </Text>
155 </Box>
156 </Box>
157 {open.length === 0 ? (
158 <Text color="green">✔ {l.allDone}</Text>
159 ) : (
160 open.slice(0, MAX_OPEN_ROWS).map(i => (
161 <Text wrap="truncate-end">
162 <Text dimColor>○ </Text>
163 {i.text}
164 </Text>
165 ))
166 )}
167 {open.length > MAX_OPEN_ROWS && <Text dimColor>{l.more(open.length - MAX_OPEN_ROWS)}</Text>}
168 {others.length > 0 && (
169 <Text dimColor wrap="truncate-end">
170 {others
171 .map(g => {
172 const c = countItems([g])
173 return `${g.heading ?? l.noHeading} ${c.done}/${c.total}`
174 })
175 .join(' · ')}
176 </Text>
177 )}
178 {cur.error !== null && <Text color="yellow">⚠ {l.failed}</Text>}
179 </Box>
180 </Box>
181 )
182 })
183}
184hooks/checklist.ts 281 lines1// Issue 本文のチェックリストと、gh コマンドが指す Issue を読み取る純粋関数。
2// 描画(acceptance.tsx)から切り離して、解析だけをテストできるようにしている。
3import type { ChecklistGroup, ChecklistItem, Lang, TrackedIssue } from '../types'
4
5/**
6 * 受け入れ条件の見出し。create-issue のテンプレートの見出しと、その英訳。
7 * 見出し全体で比べる(`Non-acceptance criteria` を拾わない)。後ろの補足のカッコだけは許す
8 */
9const ACCEPTANCE_HEADING =
10 /^(受け入れ条件|受入条件|完了条件|acceptance criteria|definition of done)\s*(\(.*\)|(.*))?\s*:?$/i
11
12/** フェンスの行。記号の並びと、その後ろ(info 文字列)を取る */
13const FENCE = /^\s{0,3}(`{3,}|~{3,})(.*)$/
14const HEADING = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
15const ITEM = /^\s*(?:[-*+]|\d+[.)])\s+\[([ xX])\]\s+(.*)$/
16
17/** 項目の文からマークダウンの記号を落として1行にする。 */
18export const plainText = (text: string): string =>
19 text
20 .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
21 .replace(/(\*\*|__|~~|`)/g, '')
22 .replace(/\s+/g, ' ')
23 .trim()
24
25/**
26 * 本文のチェックリストを見出しごとにまとめる。コードフェンスの中は数えない。
27 * 受け入れ条件のグループを先頭に、残りは本文の順に並べる。
28 */
29export const parseChecklist = (body: string): ChecklistGroup[] => {
30 const groups: ChecklistGroup[] = []
31 let heading: string | null = null
32 // 受け入れ条件の見出しの深さ。その下の小見出し(`### 機能` など)も受け入れ条件として数える
33 let acceptanceLevel: number | null = null
34 let current: ChecklistItem[] | null = null
35 // 開いているフェンスの記号の並び(CommonMark と同じく、同じ記号で同じ長さ以上の行が閉じる)
36 let fence: string | null = null
37
38 // HTML コメントの中は GitHub に表示されないので数えない。フェンスの中の `<!--` は例示なので数えない対象にしない
39 let isInComment = false
40 for (const raw of body.split(/\r?\n/)) {
41 let line = raw
42 if (isInComment) {
43 const end = line.indexOf('-->')
44 if (end < 0) continue
45 isInComment = false
46 line = line.slice(end + 3)
47 }
48 const f = FENCE.exec(line)
49 if (fence === null && f === null && line.includes('<!--')) {
50 line = line.replace(/<!--[\s\S]*?-->/g, '')
51 const open = line.indexOf('<!--')
52 if (open >= 0) {
53 isInComment = true
54 line = line.slice(0, open)
55 }
56 }
57 if (fence !== null) {
58 const closes = f !== null && f[1]?.[0] === fence[0] && (f[1]?.length ?? 0) >= fence.length && f[2]?.trim() === ''
59 if (closes) fence = null
60 continue
61 }
62 // バッククォートのフェンスは info 文字列にバッククォートを含まない(行頭のインラインコードと区別する)
63 if (f !== null && !(f[1]?.startsWith('`') && f[2]?.includes('`'))) {
64 fence = f[1] ?? null
65 continue
66 }
67
68 const h = HEADING.exec(line)
69 if (h !== null) {
70 const level = h[1]?.length ?? 1
71 heading = plainText(h[2] ?? '')
72 if (ACCEPTANCE_HEADING.test(heading)) acceptanceLevel = level
73 else if (acceptanceLevel !== null && level <= acceptanceLevel) acceptanceLevel = null
74 current = null
75 continue
76 }
77 const item = ITEM.exec(line)
78 if (item === null) continue
79 if (current === null) {
80 current = []
81 groups.push({ heading, isAcceptance: acceptanceLevel !== null, items: current })
82 }
83 current.push({ text: plainText(item[2] ?? ''), isDone: item[1] !== ' ' })
84 }
85
86 return [...groups.filter(g => g.isAcceptance), ...groups.filter(g => !g.isAcceptance)]
87}
88
89/**
90 * 進捗として大きく出すグループ。受け入れ条件の見出しがあればそれだけ、
91 * 無ければ全部のチェックリスト。
92 */
93export const mainGroups = (groups: readonly ChecklistGroup[]): ChecklistGroup[] => {
94 const acceptance = groups.filter(g => g.isAcceptance)
95 return acceptance.length > 0 ? acceptance : [...groups]
96}
97
98/** グループの項目のうち、済んだ数と全体の数。 */
99export const countItems = (groups: readonly ChecklistGroup[]): { done: number; total: number } => {
100 const items = groups.flatMap(g => g.items)
101 return { done: items.filter(i => i.isDone).length, total: items.length }
102}
103
104export type IssueRef = { repo: string | null; number: number }
105
106const ISSUE_URL = /https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)/
107const REPO = /^[\w.-]+\/[\w.-]+$/
108
109/** Issue の URL からリポジトリと番号を取る。 */
110export const refFromUrl = (text: string): IssueRef | null => {
111 const m = ISSUE_URL.exec(text)
112 return m === null ? null : { repo: m[1] ?? null, number: Number(m[2]) }
113}
114
115/** 値を取るフラグ(`-R owner/repo` の `-R` など)。この直後の語は Issue 番号として読まない */
116const VALUE_FLAGS = new Set([
117 '-R', '--repo', '--json', '-q', '--jq', '-t', '--template', '--title', '-b', '--body', '-F', '--body-file',
118 '--add-label', '--remove-label', '--add-assignee', '--remove-assignee', '--add-project', '--remove-project',
119 '-m', '--milestone', '-e', '--editor-cmd',
120])
121
122/**
123 * `gh issue view|edit|comment <N>` が指す Issue。番号を書いていないコマンド
124 * (ブランチから推定する形など)と、別のコマンドは null。
125 */
126export const refFromCommand = (command: string): IssueRef | null => {
127 // 1行目だけを読む。2行目以降は heredoc の本文やエコーした文のことがあり、
128 // そこに書かれた `gh issue view 1 -R ...` を実行されたコマンドとして読まないため
129 const firstLine = command.split(/\r?\n/, 1)[0] ?? ''
130 // 引用符の中(タイトルや本文)の数字を番号と取り違えないよう、先に1語の印に置き換える。
131 // 消してしまうと `--title "…" 7` の値が無くなり、番号の 7 をフラグの値として読み飛ばす。
132 // `--repo "acme/app"` のように空白を含まない値は、引用符だけを外して残す
133 const bare = firstLine.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, q => {
134 const inner = q.slice(1, -1)
135 return /^[\w.\/#:-]+$/.test(inner) ? inner : ' _ '
136 })
137 // コマンドの位置(行頭か、`&&` `;` `|` の直後)にある gh だけを読む
138 const m = /(?:^|&&|\|\||[;|])\s*gh\s+issue\s+(?:view|edit|comment)\b(.*)/.exec(bare)
139 if (m === null) return null
140 // gh より前で `cd` したり GH_REPO を渡したりしたコマンドは、どのリポジトリを指すか分からない。
141 // セッションのカレントで読むと別のリポジトリの同じ番号を出すので、-R か URL が無ければ読まない
142 const before = bare.slice(0, m.index + m[0].search(/gh\s/))
143 const isElsewhere = /(?:^|[\s;&|])(?:cd|pushd)\s|\bGH_REPO=/.test(before)
144 const tokens = (m[1] ?? '').split(/\s*(?:\|\||&&|[|;&])\s*/)[0]?.split(/\s+/).filter(Boolean) ?? []
145
146 let repo: string | null = null
147 let ref: IssueRef | null = null
148 for (let i = 0; i < tokens.length; i++) {
149 const t = tokens[i] ?? ''
150 const [flag, inline] = t.split('=', 2) as [string, string | undefined]
151 if (flag === '-R' || flag === '--repo') {
152 repo = inline ?? tokens[i + 1] ?? null
153 if (inline === undefined) i++
154 continue
155 }
156 if (VALUE_FLAGS.has(flag)) {
157 if (inline === undefined) i++
158 continue
159 }
160 if (ref !== null || t.startsWith('-')) continue
161 const n = /^#?(\d+)$/.exec(t)?.[1]
162 ref = n !== undefined ? { repo: null, number: Number(n) } : refFromUrl(t)
163 }
164 if (ref === null) return null
165 // `-R` は `owner/repo` の形だけを受け付ける。gh は `HOST/OWNER/REPO` も読むので、
166 // 形を確かめないと任意のホストへ gh が問い合わせる
167 const repoOk = repo === null || REPO.test(repo)
168 if (ref.repo === null && !repoOk) return null
169 const resolved = ref.repo ?? repo
170 if (resolved === null && isElsewhere) return null
171
172 return { repo: resolved, number: ref.number }
173}
174
175/** 本文を読み直す gh コマンドの引数。 */
176export const viewArgv = (ref: IssueRef): string[] => [
177 'gh',
178 'issue',
179 'view',
180 String(ref.number),
181 ...(ref.repo === null ? [] : ['-R', ref.repo]),
182 '--json',
183 'number,title,body,url',
184]
185
186/** Issue が無い・見られない失敗か(ネットワークや認証の失敗と分ける)。 */
187export const isNotFound = (stderr: string): boolean =>
188 // `command not found`(gh が無い)などの汎用の not found は含めない。それは failed として前回の表示を残す
189 /could not resolve to an? (issue|pullrequest|repository)|http 404|is a pull request/i.test(stderr)
190
191/**
192 * `gh issue view --json number,title,body,url` の出力を表示用に組み立てる。
193 * 読めない出力は null。
194 */
195export const issueFromJson = (stdout: string): TrackedIssue | null => {
196 let data: unknown
197 try {
198 data = JSON.parse(stdout)
199 } catch {
200 return null
201 }
202 if (data === null || typeof data !== 'object') return null
203 const { number, title, body, url } = data as Record<string, unknown>
204 if (typeof number !== 'number' || typeof url !== 'string') return null
205
206 return {
207 repo: refFromUrl(url)?.repo ?? null,
208 number,
209 title: typeof title === 'string' ? title : '',
210 url,
211 groups: parseChecklist(typeof body === 'string' ? body : ''),
212 error: null,
213 }
214}
215
216/** `/acceptance` の引数。 */
217export type CommandArg = { kind: 'refresh' } | { kind: 'off' } | { kind: 'on' } | { kind: 'show'; ref: IssueRef } | { kind: 'usage' }
218
219export const parseCommandArg = (args: string): CommandArg => {
220 const a = args.trim()
221 if (a === '') return { kind: 'refresh' }
222 if (/^(off|hide)$/i.test(a)) return { kind: 'off' }
223 if (/^(on|show)$/i.test(a)) return { kind: 'on' }
224 const n = /^#?(\d+)$/.exec(a)?.[1]
225 if (n !== undefined) return { kind: 'show', ref: { repo: null, number: Number(n) } }
226 const short = /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(a)
227 if (short !== null) return { kind: 'show', ref: { repo: short[1] ?? null, number: Number(short[2]) } }
228 const url = refFromUrl(a)
229 return url === null ? { kind: 'usage' } : { kind: 'show', ref: url }
230}
231
232/** 文字列に日本語(かな・漢字)が入っているか。 */
233export const hasJapanese = (text: string): boolean => /[-ヿ一-鿿]/.test(text)
234
235/** settings の `language` から表示言語を決める。決まらなければ null。 */
236export const langFromSetting = (language: unknown): Lang | null => {
237 if (typeof language !== 'string' || language.trim() === '') return null
238 return /^(ja([-_].*)?|日本語?|japanese)$/i.test(language.trim()) ? 'ja' : 'en'
239}
240
241/** `done / total` の進捗バー。 */
242export const bar = (done: number, total: number, width: number): string => {
243 const filled = total === 0 ? 0 : Math.round((done / total) * width)
244 return '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled))
245}
246
247export const LABELS = {
248 ja: {
249 acceptance: '受け入れ条件',
250 other: 'チェック',
251 noHeading: '(見出しなし)',
252 allDone: 'すべて完了',
253 more: (n: number) => `ほか ${n} 件`,
254 failed: '読み直しに失敗',
255 usage: '使い方: /acceptance [Issue 番号 | owner/repo#N | URL | off | on]',
256 hidden: '受け入れ条件の表示を隠しました。/acceptance on で戻します',
257 shown: '受け入れ条件の表示を戻しました',
258 none: 'まだ Issue を読んでいません。/acceptance <番号> で指定できます',
259 noChecklist: (n: number) => `#${n} にチェックリストがありません`,
260 notFound: (n: number) => `#${n} が見つからないか、見る権限がありません`,
261 fetchFailed: (n: number) => `#${n} の本文を読めませんでした`,
262 showing: (n: number, done: number, total: number) => `#${n} の受け入れ条件 ${done}/${total}`,
263 },
264 en: {
265 acceptance: 'Acceptance criteria',
266 other: 'Checklist',
267 noHeading: '(no heading)',
268 allDone: 'All done',
269 more: (n: number) => `${n} more`,
270 failed: 'refresh failed',
271 usage: 'Usage: /acceptance [issue number | owner/repo#N | URL | off | on]',
272 hidden: 'Hid the acceptance criteria. /acceptance on brings them back',
273 shown: 'Showing the acceptance criteria again',
274 none: 'No issue read yet. Pick one with /acceptance <number>',
275 noChecklist: (n: number) => `#${n} has no checklist`,
276 notFound: (n: number) => `#${n} was not found, or you cannot see it`,
277 fetchFailed: (n: number) => `Could not read #${n}`,
278 showing: (n: number, done: number, total: number) => `#${n} acceptance criteria ${done}/${total}`,
279 },
280} as const
281types/index.d.ts 44 lines1/** チェックリストの1項目(`- [ ]` / `- [x]` の1行)。 */
2export type ChecklistItem = {
3 /** マークダウンの記号を落とした項目の文 */
4 text: string
5 isDone: boolean
6}
7
8/** 見出し1つ分のチェックリスト。 */
9export type ChecklistGroup = {
10 /** 直前の `##`〜`####` 見出し。見出しより前の項目は null */
11 heading: string | null
12 /** 受け入れ条件の見出し(`受け入れ条件` / `Acceptance criteria` など)か */
13 isAcceptance: boolean
14 items: ChecklistItem[]
15}
16
17/** 表示中の Issue。GitHub の本文から読んだもの。 */
18export type TrackedIssue = {
19 /** `owner/name`。コマンドにも URL にも無ければ null(セッションのカレントのリポジトリ) */
20 repo: string | null
21 number: number
22 title: string
23 url: string
24 /** 受け入れ条件のグループが先頭に来る */
25 groups: ChecklistGroup[]
26 /** 最後に読み直しに失敗したときの理由。成功したら null に戻す */
27 error: string | null
28}
29
30/** 表示言語 */
31export type Lang = 'ja' | 'en'
32
33declare module 'claude-code' {
34 interface PluginState {
35 'acceptance-progress': {
36 /** 表示中の Issue。チェックリストを持たない Issue と、まだ何も読んでいないときは null */
37 issue: TrackedIssue | null
38 /** `/acceptance off` で隠したか */
39 isHidden: boolean
40 lang: Lang | null
41 }
42 }
43}
44