Band above the prompt: the worktree's active spec, its stage and its task progress, read through cq specs status --no-fetch. Writes no spec or branch and never…

Faixa acima do prompt do Claude Code: a spec ativa da worktree, o estágio dela e o progresso das tasks, sem gastar uma tool call por consulta.
spec 1521 · approved · 3/7 · claude-quenching-1521-plugin-quenching-hud-minimo
A faixa aparece quando o branch atual é plan/<id>-<handle>; em qualquer outro branch ela some. Ela se atualiza em session.start e a cada turn.complete.
Só dois comandos, por argv fixo e sem shell, e o id vem de dígitos do nome do branch:
git branch --show-currentcq specs status --spec <id> --no-fetch --jsonO plugin não escreve em spec nem em branch e não registra comando, botão nem hook de ferramenta. --no-fetch faz o cq specs status ler só o ref local refs/remotes/origin/quenching que já existe: sem fetch, sem rede, sem ssh, sem escrita de ref e sem cache. A faixa mostra o estado da última vez que outro comando cq buscou origin, então pode estar defasada. Sem esse ref, ou num backend que não é o git (github, azure), o comando recusa e a faixa some, sem chamar a API do GitHub.
cq no PATH, o que o plugin quenching garante quando está ativo. Sem cq a faixa some, sem erro.
/plugin install quenching-hud --marketplace holetz/claude-quenching
claude plugin validate plugins/quenching-hud
claude plugin test plugins/quenching-hudhooks/register.tsx 58 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { HudLine } from '../types'
5import { basename, formatLine, lineFromStatus, specIdFromBranch } from './hud'
6
7const line = atom({ plugin: 'quenching-hud', key: 'line' } as const, null as HudLine | null)
8
9async function refresh($: EngineInterface) {
10 try {
11 const cwd = await $.session.cwd()
12 const branch = await $.process.run(['git', 'branch', '--show-current'], { cwd })
13 const id = branch.exitCode === 0 ? specIdFromBranch(branch.stdout) : null
14
15 if (id === null) {
16 await update($, line, () => null)
17
18 return
19 }
20
21 const status = await $.process.run(['cq', 'specs', 'status', '--spec', String(id), '--no-fetch', '--json'], { cwd })
22 const next = status.exitCode === 0 ? lineFromStatus(id, status.stdout, basename(cwd)) : null
23 await update($, line, () => next)
24 } catch {
25 await update($, line, () => null)
26 }
27}
28
29export const register: Register = on => {
30 on('session.start', async ($, e, next) => {
31 await refresh($)
32
33 return next(e)
34 })
35
36 on('turn.complete', async ($, e, next) => {
37 await refresh($)
38
39 return next(e)
40 })
41
42 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
43 const current = await read($, line)
44
45 if (current === null) {
46 return next(e)
47 }
48
49 const { Box, Text } = $.ui.resolve(e)
50
51 return (
52 <Box>
53 <Text dimColor>{formatLine(current)}</Text>
54 </Box>
55 )
56 })
57}
58hooks/hud.ts 36 lines1import type { HudLine } from '../types'
2
3const BRANCH = /^plan\/(\d+)-/
4
5/** The spec id a `plan/<id>-<handle>` branch carries; digits only, so it is safe in an argv. */
6export const specIdFromBranch = (branch: string): number | null => {
7 const match = BRANCH.exec(branch.trim())
8
9 return match ? Number(match[1]) : null
10}
11
12export const formatLine = (line: HudLine): string =>
13 `spec ${line.id} · ${line.stage} · ${line.checked}/${line.total} · ${line.worktree}`
14
15/** Builds a line from `cq specs status --spec <id> --json` stdout; null when it does not parse. */
16export const lineFromStatus = (
17 id: number,
18 stdout: string,
19 worktree: string
20): HudLine | null => {
21 try {
22 const status = JSON.parse(stdout)
23 const tasks = status?.tasks
24
25 if (typeof status?.stage !== 'string' || typeof tasks?.checked !== 'number' || typeof tasks?.total !== 'number') {
26 return null
27 }
28
29 return { id, stage: status.stage, checked: tasks.checked, total: tasks.total, worktree }
30 } catch {
31 return null
32 }
33}
34
35export const basename = (path: string): string => path.replace(/\/+$/, '').split('/').pop() ?? path
36types/index.d.ts 14 lines1export type HudLine = {
2 id: number
3 stage: string
4 checked: number
5 total: number
6 worktree: string
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'quenching-hud': { line: HudLine | null }
12 }
13}
14