SLOPSHOPPER

goal-band

Band above the prompt showing the project's current goal (docs/state.md) and open feedback count (docs/feedback.md)

newband
v0.1.0no licenseupdated 2026-10-25thissayantan/goal-band
A shopper browsing a rack in a slop shop
README

goal-band

A Claude Code mod: a one-line band above the prompt showing the project's current goal and open feedback.

  • Reads docs/state.md (a markdown table whose rows start with | G<n> …| <status> |). The current goal is the first row marked in progress, else next, else the first not done.
  • Counts open - [ ] items in docs/feedback.md.
  • Shows nothing in projects without docs/state.md. Refreshes at session start and after every turn.

Load it in every session: add this folder to CLAUDE_CODE_PLUGIN_DIRS in the env block of ~/.claude/settings.json.

Install

At the prompt of a terminal session:

/plugin install goal-band --marketplace thissayantan/goal-band

From a shell:

claude plugin install goal-band --marketplace thissayantan/goal-band

Answer y to add the marketplace, then pick a scope.

Source 3 files
hooks/register.tsx 58 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { parseState } from './parse'
5
6const status = atom({ plugin: 'goal-band', key: 'status' } as const, null)
7
8const readOr = async ($: EngineInterface, path: string) => {
9  try {
10    return await $.fs.read(path)
11  } catch {
12    return null
13  }
14}
15
16const refresh = async ($: EngineInterface) => {
17  const state = await readOr($, 'docs/state.md')
18  const feedback = (await readOr($, 'docs/feedback.md')) ?? ''
19  await update($, status, () => (state === null ? null : parseState(state, feedback)))
20}
21
22export const register: Register = on => {
23  on('session.start', async ($, e, next) => {
24    await refresh($)
25    return next(e)
26  })
27
28  on('turn.complete', async ($, e, next) => {
29    await refresh($)
30    return next(e)
31  })
32
33  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
34    const s = await read($, status)
35    if (e.props.hasSurvey || s === null || s.total === 0) {
36      return next(e)
37    }
38
39    const { Box, Text } = $.ui.resolve(e)
40    const goal = s.current ?? 'all goals done'
41
42    return (
43      <Box>
44        <Text color="cyan">◆ {goal}</Text>
45        {s.status ? <Text dimColor> · {s.status}</Text> : null}
46        <Text dimColor>
47          {'  '}
48          {s.done}/{s.total} done
49        </Text>
50        <Text color={s.openFeedback > 0 ? 'yellow' : undefined} dimColor={s.openFeedback === 0}>
51          {'  '}
52          feedback: {s.openFeedback} open
53        </Text>
54      </Box>
55    )
56  })
57}
58
hooks/parse.ts 26 lines
1import type { GoalStatus } from '../types'
2
3// Rows look like: | G1 Home | next | notes |
4export const parseState = (state: string, feedback: string): GoalStatus => {
5  const rows = state
6    .split('\n')
7    .filter(line => /^\|\s*G\d+/.test(line))
8    .map(line => line.split('|').map(cell => cell.trim()))
9    .map(([, goal = '', status = '']) => ({ goal, status }))
10
11  const isDone = (s: string) => /^done/i.test(s)
12  const active =
13    rows.find(r => /in progress|active|doing/i.test(r.status)) ??
14    rows.find(r => /^next/i.test(r.status)) ??
15    rows.find(r => !isDone(r.status)) ??
16    null
17
18  return {
19    current: active?.goal ?? null,
20    status: active?.status || null,
21    done: rows.filter(r => isDone(r.status)).length,
22    total: rows.length,
23    openFeedback: (feedback.match(/^\s*- \[ \]/gm) ?? []).length,
24  }
25}
26
types/index.d.ts 14 lines
1export type GoalStatus = {
2  current: string | null
3  status: string | null
4  done: number
5  total: number
6  openFeedback: number
7}
8
9declare module 'claude-code' {
10  interface PluginState {
11    'goal-band': { status: GoalStatus | null }
12  }
13}
14