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

A Claude Code mod: a one-line band above the prompt showing the project's current goal and open feedback.
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.- [ ] items in docs/feedback.md.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.
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.
hooks/register.tsx 58 lines1import { 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}
58hooks/parse.ts 26 lines1import 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}
26types/index.d.ts 14 lines1export 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