Personal coding standards: skills, hooks, and agents reusable across projects

Personal coding standards, best practices, and coding agent configuration for use across projects. This repo acts as a single source of truth for language conventions, development practices, project management SOPs, and AI agent behavior rules.
coding_agent_standards/
├── languages/ Language-specific standards
├── practices/ Cross-cutting practices
├── .claude/ Project-local Claude Code config (skills, settings) for this repo
├── claude/ Claude Code plugin source (exported to other projects)
│ ├── .claude-plugin/ Plugin manifest
│ ├── agents/ Subagent definitions
│ ├── commands/ Slash commands
│ ├── hooks/ hooks.json, the guard scripts it wires up, and the work-queue progress mod
│ ├── project-management/ Plans, proposals, design, tracking, and commit chunking standards
│ ├── rules/ Agent behavior rules
│ ├── scripts/ Standalone scripts (statusline)
│ ├── settings/ Permission whitelist templates per ecosystem
│ ├── skills/ Skill definitions
│ ├── tests/ Tests for the plugin's hooks module
│ └── types/ State contract for the plugin's hooks module
├── codex/ Codex skills with no Claude Code equivalent (exported as a Codex plugin)
├── mods/ Claude Code mods, each its own opt-in plugin in the same marketplace
├── profiles/ Composable project profiles
├── rumdl/ Shared markdown lint config and runner
├── vale/ Shared prose lint styles and runner
└── bin/ Utility scripts
Standards for each language/framework covering naming, project structure, error handling, testing, and tooling:
1z.md) -- stack-oriented functional language, naming conventions, testing, quotation-based control flowgo.md) -- packages, error wrapping, concurrency, build patternscgo.md) -- C FFI bridge layers, type conversions, memory management, gomobileswift.md) -- SwiftUI, async/await, SPM, Go/C library integrationrust.md) -- modules, workspaces, error handling with thiserror/anyhowzig.md) -- allocators, error sets, build system, embedded testscss.md) -- cascade layers, reset, logical properties, OKLCH tokens, fluid type and space, layout patternssvelte.md) -- SvelteKit, Svelte 5 runes, Tailwind, protobuf RPCtypescript.md) -- strict tsconfig, ESM, Biome for format and lint, pnpmphaser.md) -- scenes, pooling, atlases, isometric projectionpixijs.md) -- v8 application setup, scene graph, ticker, asset bundlesprotobuf.md) -- file layout, naming, schema evolution, field numbering, buf-based code generationpostgres.md) -- data types, database-enforced constraints, relationships, indexes, denormalizationCross-cutting concerns that apply regardless of language:
Standards for planning and tracking work:
Rules, hooks, settings, scripts, commands, agents, and skills for Claude Code:
rules/) -- decision-making boundaries, work discipline, git workflow, commit/PR style, writing voicehooks/) -- hooks.json wires auto-formatting on edit and a test/lint reminder on stop. The block-broad-find, block-redirection, and cg-check guard scripts ship alongside it, plus opt-in hooks for project-management references, golden files, and the Zig cache; see claude/hooks/README.md for which are wired by default. hooks.json also loads work-queue-progress.tsx, a mod that shows a work-queue run's progress above the prompt and in a /queue panesettings/) -- permission whitelist templates to copy into a project's .claude/settings.local.json, one per language ecosystemscripts/) -- standalone scripts not tied to a hook, currently the statuslineskills/) -- specialized agents for code review, testing, linting, Makefile maintenance, releases, proposals, etc.agents/) -- subagent definitions invoked by name: implementer, which writes one milestone for work-queue, and reviewer, for post-milestone diff reviewcommands/) -- slash commands for driving a phase milestone (work-on), working through a queue of milestones planned up front (work-queue), summarizing a session as a commit message (summary), and chunking an integration branch into PRs (next-pr)Commands stay thin. The rules they enforce live in claude/rules/ and claude/project-management/, and a command reads them at ${CLAUDE_PLUGIN_ROOT}/... rather than restating them. Keeping one copy is what stops /summary and /work-on from drifting apart on the same commit-message rules.
Note the distinction between .claude/ and claude/:
.claude/ is the standard Claude Code project config directory. Skills and settings here apply when working in this repo (e.g., standards-synthesizer for onboarding new languages).claude/ contains skills, commands, hooks, rules, and settings exported to other projects that reference this repo via --add-dir or @import.codex/ holds skills that only run under Codex, packaged as a Codex plugin. A skill lands here when it depends on something Codex has and Claude Code does not. expand-lore-wiki is the current example. It calls Codex's built-in imagegen skill, and Claude Code has no image generation to port it to.
Codex skills carry an agents/openai.yaml next to SKILL.md. That file holds the display name and short description Codex shows in its own UI, and Claude Code ignores it.
Composable profiles that bundle the right standards for a given project type. Each profile imports a baseline plus relevant language and practice standards:
| Profile | Stack |
|---|---|
go-service | Go backend services |
go-svelte-fullstack | Go + Svelte + protobuf |
go-cgo-library | Go libraries with C FFI |
swift-macos-app | SwiftUI macOS apps |
swift-cli | Swift command-line tools |
rust-project | Rust projects |
1z-project | 1z standalone projects |
1z-interpreter | 1z interpreter (Zig + 1z) |
zig-project | Zig projects |
svelte-app | Svelte/SvelteKit apps |
ts-game | Browser games in TypeScript, engine-agnostic base |
ts-phaser-game | ts-game plus Phaser |
ts-pixi-game | ts-game plus PixiJS |
oss-contrib | Lightweight profile for contributing to repos you don't own |
Import a profile from your project's CLAUDE.md:
@~/projects/coding_agent_standards/profiles/go-service.md
For repos you don't own, use CLAUDE.local.md (auto-gitignored by Claude Code) to avoid committing personal standards:
@~/projects/coding_agent_standards/profiles/oss-contrib.md
The skills, hooks, and agents under claude/ are packaged as a Claude Code plugin named coding-standards. The marketplace manifest lives at the repo root (.claude-plugin/marketplace.json) and points at ./claude as the plugin source.
Only claude/ is copied into the plugin cache. An installed plugin cannot read files outside its own directory, so anything a skill needs at runtime has to live inside it. That is why project-management/ sits under claude/ rather than at the repo root, and why profiles/baseline.md imports it from there.
Install once, inside any Claude Code session. Run the two commands as separate prompts. A slash command takes the whole rest of the input as its argument. Pasting both lines at once makes /plugin marketplace add read the second line as part of the repo name, and it fails with is not a valid GitHub owner/repo shorthand.
First register the marketplace:
/plugin marketplace add ripta/coding_agent_standards
Then install the plugin from it:
/plugin install coding-standards@coding-standards
The coding-standards@coding-standards spelling is not a typo. The plugin and the marketplace share a name. The part before @ is the plugin from claude/.claude-plugin/plugin.json. The part after is the marketplace from .claude-plugin/marketplace.json.
Choose user scope when prompted so the plugin is available in all your projects. A local clone works as the marketplace source too (/plugin marketplace add ~/projects/coding_agent_standards), in which case updates track your clone instead of GitHub.
Plugin skills and commands are namespaced: invoke them as /coding-standards:<name>. If you keep a copy of a skill in ~/.claude/skills/, or a command in ~/.claude/commands/, both versions will appear -- delete the personal copy once the plugin version works for you.
Installed plugins are cached copies, not live references. After changes land in the repo:
/plugin marketplace update coding-standards
The plugin has no pinned version; Claude Code versions it by git commit SHA, so every update pulls the latest commit.
To iterate on a skill without the cache in the way, launch Claude Code with the plugin loaded directly from source:
claude --plugin-dir ~/projects/coding_agent_standards/claude
Edit freely, then run /reload-plugins in the session to pick up changes immediately. Once satisfied, commit and push, and installed copies catch up via /plugin marketplace update.
mods/ holds mods: plugins built from TypeScript function hooks that can draw UI inside Claude Code. Each is its own plugin in the same marketplace, so none of them ride along with coding-standards. The exception is the work-queue progress mod, which serves /work-queue and so ships inside coding-standards. Install a mods/ plugin by name:
/plugin install gopher-spinner@coding-standards
gopher-spinner draws a pixel-art gopher in place of the spinner line while a turn runs. It hops during tool calls, looks around while thinking, and blinks while replying. It replaces the whole line, so the elapsed time and token count are not shown. It draws in the terminal only. /gopher opens a pane that plays every frame. Redraw the gopher by editing the pixel rows in hooks/frames.ts.
Check a mod with claude plugin validate mods/<name> and claude plugin test mods/<name>. To iterate with hot reload, launch with claude --plugin-dir ~/projects/coding_agent_standards/mods/<name>.
The skills under codex/ are packaged as a Codex plugin, also named coding-standards. Its manifest is codex/.codex-plugin/plugin.json. The marketplace manifest is .agents/plugins/marketplace.json at the repo root, pointing at ./codex.
Codex discovers ~/.agents/plugins/marketplace.json implicitly, but not a repo-local one. Register this repo once:
codex plugin marketplace add ~/projects/coding_agent_standards
codex plugin add coding-standards@coding-standards
Start a new thread afterward. That is the boundary where Codex picks up new skills.
Validate against Codex's ingestion contract before pushing a manifest change:
python3 ~/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py codex
Two differences from the Claude Code plugin are worth knowing:
version and caches by it. There is no commit-SHA equivalent. Iterating locally means rewriting the version to 0.1.0+codex.<token> and re-running codex plugin add.hooks field in plugin.json, so claude/hooks/ has no counterpart on this side.Two linters ship as shared config plus a runner, one directory each. Both are copy-in rather than import, since neither tool supports config inheritance.
cp ~/projects/coding_agent_standards/rumdl/rumdl.toml.template .rumdl.toml
~/projects/coding_agent_standards/rumdl/lint-markdown
cp ~/projects/coding_agent_standards/vale/vale.ini.template .vale.ini
~/projects/coding_agent_standards/vale/lint-prose
rumdl/ checks markdown structure, including the 120-column prose width. vale/ checks prose shape in markdown and in code comments. They do not overlap. Each runner defaults to diff scoping against the base ref, so a repo with a backlog can adopt them without a burn-down first. See rumdl/README.md and vale/README.md.
The bin/check-setup script validates that a project has standards properly configured. It can be wired up as a Claude Code SessionStart hook to run automatically when you open a project.
If this repo doesn't live at ~/projects/coding_agent_standards, create a symlink:
ln -s /actual/path/to/coding_agent_standards ~/coding-standards
hooks/work-queue-progress.tsx 350 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Milestone, MilestoneStatus, Phase, Run, Step, Worker } from '../types'
5
6const run = atom({ plugin: 'coding-standards', key: 'run' } as const, null)
7const workers = atom({ plugin: 'coding-standards', key: 'workers' } as const, [])
8
9export const TOOL = 'work_queue_status'
10export const PANE = 'work-queue'
11const ACCENT = '#56b4e0'
12const LABEL = ' work-queue '
13const RULE_LEAD = '──'
14// The pane lists this many recent subagents.
15const WORKER_HISTORY = 8
16
17const STATUSES: readonly MilestoneStatus[] = ['QUEUED', 'IN PROGRESS', 'DONE', 'HELD', 'BLOCKED']
18const STEPS: readonly Step[] = [
19 'implementing', 'reviewing', 'fixing', 'committing', 'phase-review', 'final-review', 'finished',
20]
21
22const GLYPH: Record<MilestoneStatus, string> = {
23 QUEUED: '○',
24 'IN PROGRESS': '●',
25 DONE: '✓',
26 HELD: '⏸',
27 BLOCKED: '✗',
28}
29const COLOR: Partial<Record<MilestoneStatus, string>> = {
30 'IN PROGRESS': ACCENT,
31 DONE: 'green',
32 BLOCKED: 'red',
33}
34
35const INPUT_SCHEMA = {
36 type: 'object',
37 properties: {
38 phases: {
39 type: 'array',
40 description: 'Every queued phase, in run order',
41 items: {
42 type: 'object',
43 properties: {
44 id: { type: 'string', description: 'Phase number, e.g. "59"' },
45 title: { type: 'string' },
46 held: { type: 'string', description: 'Why the phase is held, when it is' },
47 blocked: { type: 'string', description: 'Why the phase is blocked, when it is' },
48 milestones: {
49 type: 'array',
50 items: {
51 type: 'object',
52 properties: {
53 id: { type: 'string', description: 'Phase milestone, e.g. "59.3"' },
54 title: { type: 'string' },
55 status: { type: 'string', enum: STATUSES },
56 commit: { type: 'string', description: 'Short SHA once committed' },
57 },
58 required: ['id', 'status'],
59 },
60 },
61 },
62 required: ['id', 'milestones'],
63 },
64 },
65 current: {
66 type: 'object',
67 description: 'What the coordinator is doing now',
68 properties: {
69 milestone: { type: 'string', description: 'Phase milestone being worked, e.g. "59.3"' },
70 step: { type: 'string', enum: STEPS },
71 },
72 required: ['step'],
73 },
74 },
75 required: ['phases'],
76}
77
78const DESCRIPTION = [
79 'Report /work-queue run progress so the person can see it above the prompt.',
80 'Send the whole queue every time. Each call replaces the previous snapshot.',
81 'Call it after the plan is approved, and whenever a milestone status or the current step changes.',
82 'Send step "finished" when the run is done.',
83].join(' ')
84
85export function parseRun(input: Record<string, unknown>, now: number): Run | string {
86 if (!Array.isArray(input.phases)) return 'phases must be an array'
87 const phases: Phase[] = []
88 for (const raw of input.phases as unknown[]) {
89 const p = raw as Partial<Phase>
90 if (typeof p?.id !== 'string' || !Array.isArray(p.milestones)) {
91 return 'each phase needs an id and a milestones array'
92 }
93 const milestones: Milestone[] = []
94 for (const m of p.milestones as Partial<Milestone>[]) {
95 if (typeof m?.id !== 'string' || !STATUSES.includes(m.status as MilestoneStatus)) {
96 return `milestone ${String(m?.id)} needs an id and a status of ${STATUSES.join(', ')}`
97 }
98 milestones.push({ id: m.id, title: m.title, status: m.status as MilestoneStatus, commit: m.commit })
99 }
100 phases.push({ id: p.id, title: p.title, held: p.held, blocked: p.blocked, milestones })
101 }
102 const cur = input.current as Run['current'] | undefined
103 if (cur !== undefined && !STEPS.includes(cur.step)) {
104 return `current.step must be one of ${STEPS.join(', ')}`
105 }
106 return { phases, current: cur, updatedAt: now }
107}
108
109export function milestonesOf(r: Run): Milestone[] {
110 return r.phases.flatMap(p => p.milestones)
111}
112
113export function countOf(r: Run, status: MilestoneStatus): number {
114 return milestonesOf(r).filter(m => m.status === status).length
115}
116
117// The headline: what runs now, then the tallies worth a glance.
118export function headline(r: Run): string {
119 const all = milestonesOf(r)
120 const parts: string[] = []
121 const step = r.current?.step
122 if (step === 'finished') {
123 parts.push('finished')
124 } else if (step !== undefined) {
125 parts.push(r.current?.milestone ? `${r.current.milestone} ${step}` : step)
126 }
127 parts.push(`${countOf(r, 'DONE')}/${all.length} done`)
128 const held = r.phases.filter(p => p.held).length
129 const blocked = r.phases.filter(p => p.blocked).length
130 if (held > 0) parts.push(`${held} held`)
131 if (blocked > 0) parts.push(`${blocked} blocked`)
132 return parts.join(' · ')
133}
134
135// The chips that fit in `columns`, kept around the current milestone. Each
136// chip is its glyph, the id, and one column of gap.
137export function visibleChips(r: Run, columns: number): { chips: Milestone[]; before: number; after: number } {
138 const all = milestonesOf(r)
139 const width = (m: Milestone) => m.id.length + 2
140 const total = all.reduce((n, m) => n + width(m), 0)
141 if (total <= columns) return { chips: all, before: 0, after: 0 }
142
143 const room = columns - 8 // "+N " on each side
144 let at = all.findIndex(m => m.status === 'IN PROGRESS')
145 if (at < 0) at = Math.max(0, all.findIndex(m => m.status === 'QUEUED'))
146 let lo = at
147 let hi = at + 1
148 let used = width(all[at] as Milestone)
149 while (true) {
150 const next = hi < all.length ? width(all[hi] as Milestone) : Infinity
151 const prev = lo > 0 ? width(all[lo - 1] as Milestone) : Infinity
152 if (Math.min(next, prev) + used > room) break
153 if (next <= prev) {
154 used += next
155 hi += 1
156 } else {
157 used += prev
158 lo -= 1
159 }
160 }
161 return { chips: all.slice(lo, hi), before: lo, after: all.length - hi }
162}
163
164// A few words on what a subagent's tool call touched.
165export function describeCall(tool: string, input: Record<string, unknown>): string {
166 const pick = input.file_path ?? input.path ?? input.command ?? input.pattern
167 if (typeof pick !== 'string') return tool
168 const brief = typeof input.command === 'string' ? pick : (pick.split('/').pop() ?? pick)
169 return `${tool} ${brief.length > 40 ? `${brief.slice(0, 39)}…` : brief}`
170}
171
172function isStatusTool(tool: string): boolean {
173 return tool.endsWith(`__${TOOL}`)
174}
175
176export const register: Register = on => {
177 on('session.start', async ($, e, next) => {
178 await $.tool.register({ name: TOOL, description: DESCRIPTION, inputSchema: INPUT_SCHEMA })
179 await $.command.register({
180 name: 'queue',
181 description: 'Show the /work-queue run in a pane (/queue clear hides the band)',
182 })
183 return next(e)
184 })
185
186 // The run's only gate is plan approval, so the status tool never prompts.
187 on('tool.check', ($, e, next) => (isStatusTool(e.tool) ? { decision: 'allow' } : next(e)))
188
189 on('tool.call', async ($, e, next) => {
190 const tool = String(e.tool)
191 const input = e as unknown as Record<string, unknown>
192
193 if (isStatusTool(tool)) {
194 if (e.agentId !== undefined) {
195 return { deny: 'Only the /work-queue coordinator reports run status.' }
196 }
197 const parsed = parseRun(input, await $.clock.now())
198 if (typeof parsed === 'string') return { deny: `${TOOL}: ${parsed}` }
199 await update($, run, () => parsed)
200 const n = milestonesOf(parsed).length
201 return { result: `Recorded ${n} milestones in ${parsed.phases.length} phases.` }
202 }
203
204 const agentId = e.agentId
205 if (agentId !== undefined && (await read($, workers)).some(w => w.agentId === agentId)) {
206 const lastTool = describeCall(tool, input)
207 // A fix round reaches the same agent by SendMessage, so a finished one
208 // turns active again on its next call.
209 await update($, workers, list =>
210 list.map(w =>
211 w.agentId === agentId ? { ...w, isActive: true, toolCalls: w.toolCalls + 1, lastTool } : w,
212 ),
213 )
214 }
215 return next(e)
216 })
217
218 on('agent.spawn', async ($, e, next) => {
219 const spawned = await next(e)
220 const current = await read($, run)
221 const isRunning = current !== null && current.current?.step !== 'finished'
222 if (spawned.agentId !== undefined && e.parentAgentId === undefined && isRunning) {
223 const worker: Worker = {
224 agentId: spawned.agentId,
225 type: e.subagentType.replace(/^.*:/, ''),
226 description: e.description,
227 isActive: true,
228 toolCalls: 0,
229 }
230 await update($, workers, list => [...list, worker].slice(-WORKER_HISTORY))
231 }
232 return spawned
233 })
234
235 on('turn.complete', async ($, e, next) => {
236 const agentId = e.agentId
237 if (agentId !== undefined && (await read($, workers)).some(w => w.agentId === agentId)) {
238 await update($, workers, list =>
239 list.map(w => (w.agentId === agentId ? { ...w, isActive: false } : w)),
240 )
241 }
242 return next(e)
243 })
244
245 on('command.run', { command: 'queue' }, async ($, e) => {
246 if (e.args.trim() === 'clear') {
247 await update($, run, () => null)
248 await update($, workers, () => [])
249 await $.ui.close({ id: PANE })
250 return { text: 'Cleared the work-queue progress.' }
251 }
252 if ((await read($, run)) === null) {
253 return { text: 'No /work-queue run has reported progress in this session.' }
254 }
255 await $.ui.open({ id: PANE, title: 'Work queue', closeOnEscape: true })
256 return { text: 'Work queue pane opened. Esc closes it.' }
257 })
258
259 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
260 const r = await read($, run)
261 if (r === null || e.props.hasSurvey) return next(e)
262
263 const { Box, Text } = $.ui.resolve(e)
264 const columns = e.props.bodyColumns
265 const { chips, before, after } = visibleChips(r, columns)
266 const active = (await read($, workers)).filter(w => w.isActive).at(-1)
267 const rows = e.props.maxRows
268 // The headline doubles as a rule, setting the band off from the transcript.
269 const title = ` ${headline(r)} `
270 const fill = Math.max(0, columns - RULE_LEAD.length - LABEL.length - RULE_LEAD.length - title.length)
271
272 return (
273 <Box flexDirection="column">
274 <Box flexDirection="row">
275 <Text dimColor>{RULE_LEAD}</Text>
276 <Text bold color={ACCENT}>{LABEL}</Text>
277 <Text dimColor>{RULE_LEAD}</Text>
278 <Text wrap="truncate-end">{title}</Text>
279 {fill > 0 && <Text dimColor>{'─'.repeat(fill)}</Text>}
280 </Box>
281 {rows >= 2 && (
282 <Box flexDirection="row" gap={1}>
283 {before > 0 && <Text dimColor>+{before}</Text>}
284 {chips.map(m => (
285 <Text
286 key={`chip-${m.id}`}
287 color={COLOR[m.status]}
288 dimColor={m.status === 'QUEUED' || m.status === 'HELD'}
289 bold={m.status === 'IN PROGRESS'}
290 >
291 {GLYPH[m.status]}{m.id}
292 </Text>
293 ))}
294 {after > 0 && <Text dimColor>+{after}</Text>}
295 </Box>
296 )}
297 {rows >= 3 && active !== undefined && (
298 <Text dimColor wrap="truncate-end">
299 ↳ {active.type} · {active.description} · {active.toolCalls} tools
300 {active.lastTool ? ` · ${active.lastTool}` : ''}
301 </Text>
302 )}
303 </Box>
304 )
305 })
306
307 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
308 const { Box, Text } = $.ui.resolve(e)
309 const r = await read($, run)
310 if (r === null) return <Text dimColor>No run has reported progress.</Text>
311 const list = await read($, workers)
312
313 return (
314 <Box flexDirection="column">
315 <Text bold>{headline(r)}</Text>
316 {r.phases.map(p => (
317 <Box key={`phase-${p.id}`} flexDirection="column" marginTop={1}>
318 <Text bold color={ACCENT} wrap="truncate-end">
319 Phase {p.id}{p.title ? ` · ${p.title}` : ''}
320 </Text>
321 {p.held && <Text dimColor wrap="wrap">⏸ held: {p.held}</Text>}
322 {p.blocked && <Text color="red" wrap="wrap">✗ blocked: {p.blocked}</Text>}
323 {p.milestones.map(m => (
324 <Text
325 key={`row-${m.id}`}
326 color={COLOR[m.status]}
327 dimColor={m.status === 'QUEUED' || m.status === 'HELD'}
328 wrap="truncate-end"
329 >
330 {GLYPH[m.status]} {m.id}{m.commit ? ` ${m.commit}` : ''}{m.title ? ` ${m.title}` : ''}
331 </Text>
332 ))}
333 </Box>
334 ))}
335 {list.length > 0 && (
336 <Box flexDirection="column" marginTop={1}>
337 <Text bold>Subagents</Text>
338 {list.map(w => (
339 <Text key={`worker-${w.agentId}`} dimColor={!w.isActive} wrap="truncate-end">
340 {w.isActive ? '●' : '✓'} {w.type} · {w.description} · {w.toolCalls} tools
341 {w.lastTool ? ` · ${w.lastTool}` : ''}
342 </Text>
343 ))}
344 </Box>
345 )}
346 </Box>
347 )
348 })
349}
350types/index.d.ts 48 lines1export type MilestoneStatus = 'QUEUED' | 'IN PROGRESS' | 'DONE' | 'HELD' | 'BLOCKED'
2
3export type Step =
4 | 'implementing'
5 | 'reviewing'
6 | 'fixing'
7 | 'committing'
8 | 'phase-review'
9 | 'final-review'
10 | 'finished'
11
12export type Milestone = {
13 id: string
14 title?: string
15 status: MilestoneStatus
16 commit?: string
17}
18
19export type Phase = {
20 id: string
21 title?: string
22 held?: string
23 blocked?: string
24 milestones: Milestone[]
25}
26
27export type Run = {
28 phases: Phase[]
29 current?: { milestone?: string; step: Step }
30 updatedAt: number
31}
32
33// A subagent the coordinator spawned while a run was going.
34export type Worker = {
35 agentId: string
36 type: string
37 description: string
38 isActive: boolean
39 toolCalls: number
40 lastTool?: string
41}
42
43declare module 'claude-code' {
44 interface PluginState {
45 'coding-standards': { run: Run | null; workers: Worker[] }
46 }
47}
48