SLOPSHOPPER

aichemist-beads-band

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

newbandcommandprocesstimer
★ 1v3.10.0MITupdated 2026-10-05Anras573/AIchemist/mods/beads-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · aichemist-beads-band
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ 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 › /beads-band ⎿ aichemist-beads-band: Beads band hidden. Run /beads-band to show it again. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

AIchemist

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.

Installation

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.

What's Included

Agents — Specialized agents for the Task tool:

  • Code Review — Parallel review with Jira integration and confidence scoring
  • TypeScript/React — Full-stack TypeScript, React, Next.js, Node.js
  • .NET — C#/.NET with async patterns, SOLID, DDD, testing
  • DDD — Domain-Driven Design guidance (language-agnostic)

Skills — Context-aware capabilities:

  • Ticket Flow — End-to-end workflow for taking a Jira ticket from definition to reviewed implementation
  • Brainstorming — Structured design dialogue with a hard gate before any implementation
  • Beads — AI-native task tracking with automatic sidecar storage outside your repo
  • Code Review — Parallel review agents with confidence scoring and Jira integration
  • Jira — Issue management with confirmation gates for write operations
  • Mermaid — Generate diagrams (flowcharts, sequence, ER, C4, and more) as markdown
  • PostgreSQL — Safe database querying with blocked writes by default
  • Daily Note — Interact with your Obsidian daily note for journaling and task tracking
  • Capture — Quick capture of thoughts and code snippets to Obsidian
  • Research — Search your Obsidian vault for context during coding sessions
  • Playwright — Browser automation and web testing via playwright-cli
  • Calendar — Calendar queries and meeting prep briefings for Microsoft 365 (via m365 CLI) and iCloud/CalDAV
  • Markitdown — Convert remote URLs and local files to clean markdown
  • MemPalace — Persistent local memory (vector + knowledge graph) across sessions
  • Tool Preferences — Consistent tool selection patterns

Mods — Optional Claude Code-only add-ons, installed separately; they add to the skills above and never replace them:

  • Calendar Status — Current and next meeting in the status line, via the Calendar skill's calendar.sh, for Microsoft 365 or iCloud (claude plugin install aichemist-calendar-status@aichemist)
  • Beads Band — In-progress and ready beads tasks above the prompt, via the Beads skill's beads-db.sh (claude plugin install aichemist-beads-band@aichemist)
  • PR Review Pane — /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.

Quick Start

# 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

Philosophy

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:

  • Composable — Works well independently and in combination
  • Documented — Clear purpose and usage instructions
  • Practical — Solves real problems in daily workflows

License

See LICENSE for details.

Source 3 files
hooks/register.tsx 113 lines
1import { 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}
113
hooks/beads.ts 73 lines
1// 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}
73
types/index.d.ts 21 lines
1export 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