Claude Code mod: shows your in-progress and ready beads tasks in a band above the prompt, read through the beads skill's tools/beads-db.sh

Transmuting raw AI capabilities into golden solutions
A plugin for Claude Code and GitHub Copilot CLI with custom agents and skills for AI-assisted development.
Claude Code:
claude plugin install aichemist
GitHub Copilot CLI:
copilot plugin install aichemist
See docs/installation.md for alternative installation methods and MCP server configuration.
Agents — Specialized agents for the Task tool:
Skills — Context-aware capabilities:
playwright-clim365 CLI) and iCloud/CalDAVMods — Optional Claude Code-only add-ons, installed separately; they add to the skills above and never replace them:
calendar.sh, for Microsoft 365 or iCloud (claude plugin install aichemist-calendar-status@aichemist)beads-db.sh (claude plugin install aichemist-beads-band@aichemist)/pr-pane shows the PR Review Loop's state, CI and unresolved threads in a live pane (claude plugin install aichemist-pr-review-pane@aichemist)See docs/ for detailed documentation.
# Review your current branch (skill auto-invoked by natural language)
# "Do a code review" or /code-review
# See your recent Jira tickets
# "Show my Jira tickets from last week" or /jira
# The agents work automatically via Task tool
# e.g., "Review this PR for DDD patterns" invokes the DDD agent
Like the alchemists of old who sought to transform base metals into gold, AIchemist aims to refine and combine AI building blocks into powerful, practical solutions. Each component is crafted to be:
See LICENSE for details.
hooks/register.tsx 113 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { BeadsSnapshot } from '../types'
5import { bandLines, parseTasks } from './beads'
6
7const COMMAND = 'beads-band'
8
9const snapshot = atom({ plugin: 'aichemist-beads-band', key: 'snapshot' } as const, null as BeadsSnapshot | null)
10const isHidden = atom({ plugin: 'aichemist-beads-band', key: 'isHidden' } as const, false)
11
12// Re-reads the database through the beads skill's own tools/beads-db.sh, so
13// the band always shows the database the skill uses. Read-only throughout:
14// the script runs without --init and every bd call passes --readonly, so a
15// repo or sidecar the skill has not set up stays untouched.
16async function refresh($: EngineInterface): Promise<void> {
17 const next = await fetchSnapshot($)
18 await update($, snapshot, () => next)
19}
20
21async function fetchSnapshot($: EngineInterface): Promise<BeadsSnapshot | null> {
22 try {
23 const resolved = await $.process.run(['bash', `${$.plugin.root}/tools/beads-db.sh`])
24 // Exit 3: no database for this repo yet. Anything else non-zero: the
25 // script failed. Either way there is nothing to show.
26 const db = resolved.stdout.trim()
27 if (resolved.exitCode !== 0 || db === '') {
28 return null
29 }
30
31 // One at a time: bd's embedded Dolt takes a lock per call.
32 const inProgress = await $.process.run(['bd', '--db', db, '--readonly', 'list', '--status', 'in_progress', '--json'])
33 const ready = await $.process.run(['bd', '--db', db, '--readonly', 'ready', '--json'])
34 if (inProgress.exitCode !== 0 || ready.exitCode !== 0) {
35 return null
36 }
37
38 return { inProgress: parseTasks(inProgress.stdout), ready: parseTasks(ready.stdout) }
39 } catch {
40 // bd or bash missing, or a call timed out.
41 return null
42 }
43}
44
45export const register: Register = (on, options) => {
46 const refreshMs = Math.max(15, Number(options.refreshSeconds ?? 60)) * 1000
47 let poll: Timer | undefined
48
49 on('session.start', async ($, e, next) => {
50 if (!e.isInteractive) {
51 return next(e)
52 }
53
54 await $.command.register({
55 name: COMMAND,
56 description: 'Show or hide the beads band (in-progress and ready tasks above the prompt)',
57 })
58 // session.start can fire again without a reload; don't stack timers.
59 poll?.cancel()
60 poll = $.clock.every(refreshMs, () => void refresh($))
61 $.clock.after(1, () => void refresh($))
62
63 return next(e)
64 })
65
66 // Claude often changes tasks during a turn (claim, close, create), so read
67 // again once each turn ends.
68 on('turn.complete', async ($, e, next) => {
69 const result = await next(e)
70 void refresh($)
71
72 return result
73 })
74
75 on('command.run', { command: COMMAND }, async $ => {
76 const hidden = !(await read($, isHidden))
77 await update($, isHidden, () => hidden)
78 if (!hidden) {
79 void refresh($)
80 }
81
82 return { text: hidden ? 'Beads band hidden. Run /beads-band to show it again.' : 'Beads band shown.' }
83 })
84
85 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
86 const tasks = await read($, snapshot)
87 if (e.props.hasSurvey || tasks === null || (await read($, isHidden))) {
88 return next(e)
89 }
90
91 const lines = bandLines(tasks.inProgress, tasks.ready, Math.max(20, e.props.bodyColumns - 10))
92 if (lines.length === 0) {
93 return next(e)
94 }
95
96 const { Box, Button, Text } = $.ui.resolve(e)
97 const shown = lines.slice(0, Math.max(1, e.props.maxRows))
98
99 return (
100 <Box flexDirection="row" justifyContent="space-between">
101 <Box flexDirection="column" flexGrow={1}>
102 {shown.map((line, index) => (
103 <Text key={`line-${index}`} dimColor={!line.startsWith('▶')} wrap="truncate-end">
104 {line}
105 </Text>
106 ))}
107 </Box>
108 <Button key="hide" hotkey="h" dimColor label="Hide" onPress={() => update($, isHidden, () => true)} />
109 </Box>
110 )
111 })
112}
113hooks/beads.ts 73 lines1// Pure helpers for the beads band: no `$`, so tests call them directly.
2// Input is what `bd ready --json` and `bd list --status in_progress --json`
3// print (bd 1.x): an array of issues with id, title, status and priority.
4
5import type { BeadsTask } from '../types'
6
7type RawIssue = {
8 id?: unknown
9 title?: unknown
10 status?: unknown
11 priority?: unknown
12}
13
14export function parseTasks(stdout: string): BeadsTask[] {
15 let raw: unknown
16 try {
17 raw = JSON.parse(stdout)
18 } catch {
19 return []
20 }
21 if (!Array.isArray(raw)) {
22 return []
23 }
24
25 return (raw as RawIssue[])
26 .filter(issue => typeof issue.id === 'string' && typeof issue.title === 'string')
27 .map(issue => ({
28 id: issue.id as string,
29 title: (issue.title as string).trim(),
30 status: typeof issue.status === 'string' ? issue.status : 'open',
31 priority: typeof issue.priority === 'number' ? issue.priority : 4,
32 }))
33 // bd priorities run 0 (highest) to 4; keep bd's order within a priority.
34 .map((task, index) => ({ task, index }))
35 .sort((a, b) => a.task.priority - b.task.priority || a.index - b.index)
36 .map(({ task }) => task)
37}
38
39function truncate(text: string, max: number): string {
40 return text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1))}…`
41}
42
43function describe(task: BeadsTask, titleMax: number): string {
44 return `P${task.priority} ${task.id} ${truncate(task.title, titleMax)}`
45}
46
47// Up to two lines: what is in progress, and what is ready next. Each fits
48// `columns` cells; empty when there is nothing to show.
49export function bandLines(
50 inProgress: readonly BeadsTask[],
51 ready: readonly BeadsTask[],
52 columns: number,
53): string[] {
54 const lines: string[] = []
55 const titleMax = Math.max(12, Math.floor(columns / 3))
56
57 if (inProgress.length > 0) {
58 const more = inProgress.length > 1 ? ` (+${inProgress.length - 1} more)` : ''
59 lines.push(truncate(`▶ In progress: ${describe(inProgress[0]!, titleMax)}${more}`, columns))
60 }
61
62 // A task in progress is never also listed as ready.
63 const busy = new Set(inProgress.map(t => t.id))
64 const next = ready.filter(t => !busy.has(t.id))
65 if (next.length > 0) {
66 const shown = next.slice(0, 3).map(t => describe(t, titleMax)).join(' · ')
67 const more = next.length > 3 ? ` · +${next.length - 3} more` : ''
68 lines.push(truncate(`◇ Ready (${next.length}): ${shown}${more}`, columns))
69 }
70
71 return lines
72}
73types/index.d.ts 21 lines1export type BeadsTask = {
2 id: string
3 title: string
4 priority: number
5 status: string
6}
7
8export type BeadsSnapshot = {
9 inProgress: BeadsTask[]
10 ready: BeadsTask[]
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 'aichemist-beads-band': {
16 snapshot: BeadsSnapshot | null
17 isHidden: boolean
18 }
19 }
20}
21