Shows the autonomous-mode and ship-goal status in the status line, above the prompt, and via /autonomy-status, in Cursor Dark colors.

Claude Code mod that shows the autonomous-mode and ship-goal status.
Read relative to the session's project root ($.session.root()):
.claude/agent-harness.json or a .claude/state/ directory marks a harness-aware repo (shows ○ manual when autonomy is off)..claude/state/autonomy.on present means autonomy is ON..claude/state/goal.json holds the ship goal (status is active, escalated or done).Outside a harness-aware repo (none of the three exist) the mod shows nothing.
$.ui.status renders as plain text under the prompt and the host offers no footer component for styled output, so the capsule lives in the band. ● AUTONOMOUS (accent background, bold dark text) while autonomy is on, ○ manual (muted background) otherwise; when a goal exists, a second row with the clipped goal and PR #n.PromptHint hook shows fix i/max · stop blocks n just under the textarea, then the engine's own hint line (pills stay live), then the status (escalated: <reason> in the escalated color, or done; omitted while active). Nothing is added when there is no goal to show./autonomy-status: multi-line summary of every field.State refreshes every 3 s, on prompt submit and after each tool call.
Palette from the community "Cursor Dark Theme" (publisher flawlessnitin), file themes/my-custom-theme-color-theme.json of https://github.com/flawlessnitin/cursor-theme, theme label "Cursor Dark Theme".
| Role | Hex | Theme key |
|---|---|---|
| Accent: badge background (ON), active, PR | #4c9df3 | button.background, textLink.foreground |
| Badge text | #141414 | terminal.background |
| Done | #a3be8c | terminal.ansiGreen |
| Escalated | #bf616a | terminal.ansiRed, errorForeground |
| Muted text | #828282 | sideBar.foreground (#cccccc99) flattened over #141414 |
The theme defines no descriptionForeground, so muted text uses the flattened sideBar.foreground, VS Code's own description-text fallback tier. focusBorder is transparent in this theme, so the accent comes from the button and link blue. The palette lives in one const, PALETTE, in hooks/model.ts.
claude plugin validate .
claude plugin test .hooks/register.tsx 124 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Snapshot } from '../types'
5import {
6 AUTONOMY_FLAG,
7 GOAL_FILE,
8 HARNESS_MARKERS,
9 isBandVisible,
10 parseGoal,
11 shownGoal,
12 summary,
13 toastsFor,
14} from './model'
15import { countersView, goalHeaderView, modeView, statusView } from './views'
16
17const POLL_MS = 3000
18const COMMAND = 'autonomy-status'
19
20const snapshotAtom = atom({ plugin: 'autonomy-status', key: 'snapshot' } as const, null)
21const startedAtAtom = atom({ plugin: 'autonomy-status', key: 'startedAt' } as const, 0)
22
23const readSnapshot = async ($: EngineInterface, startedAt: number): Promise<Snapshot> => {
24 const root = await $.session.root()
25 const goalPath = `${root}/${GOAL_FILE}`
26 const [markers, isOn, hasGoal] = await Promise.all([
27 Promise.all(HARNESS_MARKERS.map(marker => $.fs.exists(`${root}/${marker}`))),
28 $.fs.exists(`${root}/${AUTONOMY_FLAG}`),
29 $.fs.exists(goalPath),
30 ])
31 const hasHarness = markers.some(Boolean)
32 if (!hasGoal) return { hasHarness, isOn, goal: null, isGoalFresh: false }
33 const [text, stat] = await Promise.all([$.fs.read(goalPath), $.fs.stat(goalPath)])
34 return { hasHarness, isOn, goal: parseGoal(text), isGoalFresh: stat.mtimeMs >= startedAt }
35}
36
37const refreshOnce = async ($: EngineInterface): Promise<Snapshot | null> => {
38 try {
39 const next = await readSnapshot($, await read($, startedAtAtom))
40 const prev = await read($, snapshotAtom)
41 await update($, snapshotAtom, () => next)
42 if (prev !== null) toastsFor(prev, next).forEach(text => $.ui.toast(text))
43 return next
44 } catch (error) {
45 $.ui.log(`autonomy-status: refresh failed: ${String(error)}`, { to: 'debug' })
46 return null
47 }
48}
49
50let running: Promise<Snapshot | null> | undefined
51
52const refresh = ($: EngineInterface): Promise<Snapshot | null> => {
53 running ??= refreshOnce($).finally(() => {
54 running = undefined
55 })
56 return running
57}
58
59export const register: Register = on => {
60 on('session.start', async ($, e, next) => {
61 const now = await $.clock.now()
62 await update($, startedAtAtom, started => started || now)
63 await $.command.register({
64 name: COMMAND,
65 description: 'Show the autonomous-mode and goal status',
66 })
67 await refresh($)
68 $.clock.every(POLL_MS, () => void refresh($))
69
70 return next(e)
71 })
72
73 on('prompt.submit', ($, e, next) => {
74 void refresh($)
75
76 return next(e)
77 })
78
79 on('tool.call', async ($, e, next) => {
80 const ran = await next(e)
81 void refresh($)
82
83 return ran
84 })
85
86 on('command.run', { command: COMMAND }, async $ => {
87 const snapshot = await refresh($)
88
89 return { text: snapshot === null ? 'Autonomy status unavailable.' : summary(snapshot) }
90 })
91
92 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
93 const snapshot = await read($, snapshotAtom)
94 if (e.props.hasSurvey || snapshot === null || !isBandVisible(snapshot)) return next(e)
95
96 const table = $.ui.resolve(e)
97 const { Box } = table
98
99 return (
100 <Box flexDirection="column">
101 <Box>{modeView(table, snapshot)}</Box>
102 {goalHeaderView(table, snapshot)}
103 </Box>
104 )
105 })
106
107 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
108 const snapshot = await read($, snapshotAtom)
109 const engineHint = await next(e)
110 if (snapshot === null || shownGoal(snapshot) === null) return engineHint
111
112 const table = $.ui.resolve(e)
113 const { Box } = table
114
115 return (
116 <Box flexDirection="column">
117 {countersView(table, snapshot)}
118 {engineHint}
119 {statusView(table, snapshot)}
120 </Box>
121 )
122 })
123}
124hooks/model.ts 92 lines1import type { Goal, GoalStatus, Snapshot } from '../types'
2
3export const PALETTE = {
4 accent: '#4c9df3',
5 onAccent: '#141414',
6 done: '#a3be8c',
7 escalated: '#bf616a',
8 muted: '#828282',
9} as const
10
11export const HARNESS_MARKERS = ['.claude/agent-harness.json', '.claude/state'] as const
12export const AUTONOMY_FLAG = '.claude/state/autonomy.on'
13export const GOAL_FILE = '.claude/state/goal.json'
14
15const GOAL_CLIP = 60
16const STATUSES: readonly GoalStatus[] = ['active', 'escalated', 'done']
17
18const asCount = (value: unknown): number =>
19 typeof value === 'number' && Number.isFinite(value) ? value : 0
20
21const asText = (value: unknown): string => (typeof value === 'string' ? value : '')
22
23export const clip = (text: string, max: number = GOAL_CLIP): string =>
24 text.length <= max ? text : `${text.slice(0, max - 1)}…`
25
26export const parseGoal = (text: string): Goal | null => {
27 let raw: unknown
28 try {
29 raw = JSON.parse(text)
30 } catch {
31 return null
32 }
33 if (typeof raw !== 'object' || raw === null) return null
34 const fields = raw as Record<string, unknown>
35 const status = STATUSES.find(one => one === fields.status)
36 if (status === undefined) return null
37 const pr = fields.pr
38 return {
39 goal: asText(fields.goal),
40 criteria: Array.isArray(fields.criteria) ? fields.criteria.map(asText) : [],
41 branch: asText(fields.branch),
42 pr: typeof pr === 'number' || (typeof pr === 'string' && pr !== '') ? String(pr) : null,
43 headSha: asText(fields.head_sha),
44 fixIterations: asCount(fields.fix_iterations),
45 maxFixIterations: asCount(fields.max_fix_iterations),
46 stopBlocks: asCount(fields.stop_blocks),
47 totalBlocks: asCount(fields.total_blocks),
48 status,
49 reason: asText(fields.reason),
50 }
51}
52
53export const shownGoal = (s: Snapshot): Goal | null =>
54 s.goal !== null && (s.goal.status !== 'done' || s.isGoalFresh) ? s.goal : null
55
56export const isBandVisible = (s: Snapshot): boolean => s.hasHarness || s.isOn || shownGoal(s) !== null
57
58const prLabel = (goal: Goal): string => (goal.pr === null ? '' : ` #${goal.pr}`)
59
60export const summary = (s: Snapshot): string => {
61 if (!s.hasHarness && !s.isOn && s.goal === null) {
62 return 'No autonomy state in this project.'
63 }
64 const lines = [`Autonomy: ${s.isOn ? 'ON' : 'off'}`]
65 const goal = s.goal
66 if (goal === null) return [...lines, 'Goal: none'].join('\n')
67 lines.push(
68 `Goal: ${goal.goal}`,
69 `Status: ${goal.status}${goal.reason === '' ? '' : ` (${goal.reason})`}`,
70 `Branch: ${goal.branch === '' ? '-' : goal.branch}`,
71 `PR: ${goal.pr === null ? '-' : `#${goal.pr}`}`,
72 `Head: ${goal.headSha === '' ? '-' : goal.headSha}`,
73 `Fix iterations: ${goal.fixIterations}/${goal.maxFixIterations}`,
74 `Stop blocks: ${goal.stopBlocks} (total ${goal.totalBlocks})`,
75 )
76 if (goal.criteria.length > 0) {
77 lines.push('Criteria:', ...goal.criteria.map(one => ` - ${one}`))
78 }
79 return lines.join('\n')
80}
81
82export const toastsFor = (prev: Snapshot, next: Snapshot): string[] => {
83 const toasts: string[] = []
84 if (prev.isOn !== next.isOn) toasts.push(`Autonomy ${next.isOn ? 'ON' : 'OFF'}`)
85 const before = prev.goal?.status
86 const goal = next.goal
87 if (goal === null || before === goal.status) return toasts
88 if (goal.status === 'escalated') toasts.push(`Ship escalated: ${goal.reason || 'no reason given'}`)
89 if (goal.status === 'done') toasts.push(`Goal done: PR${prLabel(goal)} merged`)
90 return toasts
91}
92hooks/views.ts 52 lines1import type { EngineInterface, RenderElement } from 'claude-code'
2
3import type { Goal, Snapshot } from '../types'
4import { PALETTE, clip, shownGoal } from './model'
5
6export type Constructors = Pick<ReturnType<EngineInterface['ui']['resolve']>, 'Box' | 'Text'>
7
8export const modeView = ({ Text }: Constructors, snapshot: Snapshot): RenderElement =>
9 Text({
10 backgroundColor: snapshot.isOn ? PALETTE.accent : PALETTE.muted,
11 color: PALETTE.onAccent,
12 bold: true,
13 children: snapshot.isOn ? ' ● AUTONOMOUS ' : ' ○ manual ',
14 })
15
16const statusColor = (goal: Goal): string =>
17 goal.status === 'done'
18 ? PALETTE.done
19 : goal.status === 'escalated'
20 ? PALETTE.escalated
21 : PALETTE.accent
22
23export const goalHeaderView = ({ Box, Text }: Constructors, snapshot: Snapshot): RenderElement | null => {
24 const goal = shownGoal(snapshot)
25 if (goal === null) return null
26 const head = [
27 goal.goal !== '' ? Text({ children: clip(goal.goal) }) : null,
28 goal.pr !== null ? Text({ color: PALETTE.accent, children: ` PR #${goal.pr}` }) : null,
29 ]
30 return Box({ children: Text({ children: head }) })
31}
32
33export const countersView = ({ Box, Text }: Constructors, snapshot: Snapshot): RenderElement | null => {
34 const goal = shownGoal(snapshot)
35 if (goal === null) return null
36 return Box({
37 children: Text({
38 color: PALETTE.muted,
39 children: `fix ${goal.fixIterations}/${goal.maxFixIterations} · stop blocks ${goal.stopBlocks}`,
40 }),
41 })
42}
43
44export const statusView = ({ Box, Text }: Constructors, snapshot: Snapshot): RenderElement | null => {
45 const goal = shownGoal(snapshot)
46 if (goal === null || goal.status === 'active') return null
47 const reason = goal.status === 'escalated' && goal.reason !== '' ? `: ${goal.reason}` : ''
48 return Box({
49 children: Text({ color: statusColor(goal), bold: true, wrap: 'wrap', children: `${goal.status}${reason}` }),
50 })
51}
52types/index.d.ts 29 lines1export type GoalStatus = 'active' | 'escalated' | 'done'
2
3export type Goal = {
4 goal: string
5 criteria: readonly string[]
6 branch: string
7 pr: string | null
8 headSha: string
9 fixIterations: number
10 maxFixIterations: number
11 stopBlocks: number
12 totalBlocks: number
13 status: GoalStatus
14 reason: string
15}
16
17export type Snapshot = {
18 hasHarness: boolean
19 isOn: boolean
20 goal: Goal | null
21 isGoalFresh: boolean
22}
23
24declare module 'claude-code' {
25 interface PluginState {
26 'autonomy-status': { snapshot: Snapshot | null; startedAt: number }
27 }
28}
29