Markdown-tracked task orchestration: grill a task to a spec, scout the repo into a context pack (read-only repo-scout agent), track the epic in a local…

Markdown-tracked task orchestration for non-trivial work. The main agent grills and scouts the task, stores the epic under a git-ignored .orchestration/ directory, plans dependency waves, dispatches self-managing workers, and converges blocking specialist gates. No external tracker, account, or authentication is required.
The compact state machine lives in skills/orchestrate/SKILL.md. Only SKILL.md auto-loads; it requires explicit on-demand absolute reads of bundled references. Routing predicates are authoritative only in references/routing.md, store/concurrency only in references/store-protocol.md, and phase detail in the intake/execution references.
<repo-root>/.orchestration/.gitignore contains *; the root .gitignore is untouched.PROJECT.md is the long-lived per-repo resume index.<epic>/EPIC.md owns epic ACs, context, decisions, convergence records, and append-only session IDs.<epic>/issues/NN-slug.md owns one chunk's frontmatter, current Description/spec, and append-only Comments.repo-scout — read-only context pack. Per scope it precomputes files/reuse plus applicable documented standards, owning docs, non-test quality commands, test surfaces, and solution-reuse signals. Empty results are explicit.solution-reuse-scout — conditional, read-only pre-worker research for custom mechanisms, dependencies/integrations, and likely repository/native/library/package solutions.impl-planner, council-member, design-lead — retain their conditional planning, architecture, and UI design responsibilities.md-worker — lifecycle/status/retry coordinator. It builds one chunk and dispatches only gates enabled by persisted predicates.code-standards-checker — checks only supplied documented standards/clauses; it never discovers standards or runs commands.quality-gates-checker — runs only supplied non-test quality commands; it never runs tests or reviews standards.md-reviewer — checks ACs, functional correctness, scope, actual state, and root-cause correctness.implementation-quality-reviewer — blocking reuse/simplification/root-cause quality gate for substantive source changes only.test-specialist, regression-checker, wcag-reviewer, and visual-fidelity review retain their existing explicit predicates.docs-maintainer remains both editor and auditor, dispatched only for docs-only work or non-empty scoped owning docs.Every dispatched specialist appends its own verdict and relays failed writes. Every inapplicable specialist is recorded as skipped: <reason> rather than a fabricated pass.
repo-docs plugin, not from this one: repo-scout, impl-planner, council-member, design-lead, and solution-reuse-scout use its mcp__plugin_repo-docs_repo-docs__* tools (find_docs, list_docs, read_doc, find_libs, and get_blast_radius / get_file_dependents for which files a change affects). claude/install.sh installs repo-docs automatically alongside this plugin; if its tools are not callable, tell the user to run claude plugin install repo-docs@ai-setup. /reindex and /repo-docs-ignore now live in claude/plugins/repo-docs/commands/.skills/orchestrate/ — compact dispatcher, references/ (routing, store-protocol, intake-design, execution, platform), and canonical templates/ for PROJECT, EPIC, and issues.skills/ — brainstorm (pins the outcome/approach before Gate/Intake on new work), grilling, domain-modeling, and design/accessibility companions.commands/ — /orchestrate.agents/ — the specialists cataloged above and their contract test.The existing repository-owned agent-browser skill is not copied into this plugin: its source contains HCP-specific startup guidance and assumes a separately installed global CLI/browser, so copying it would not make the plugin self-contained or portable. The solution scout prefers direct repo/docs/web tools and retains WebFetch/WebSearch. OpenCode uses its already-installed agent-browser skill when browser interaction is genuinely required.
None for tracking. The store is created automatically. The repo-docs plugin must be installed for repo grounding (its dependencies install on its own SessionStart); WCAG lookups use the bundled CLI skill on demand. If the filesystem is read-only, the workflow reports that persistence is unavailable and falls back to in-session tracking.
/epics opens a pane with the .orchestration/ epics in the working directory: open epics as cards with a progress bar and each chunk's status, closed epics behind "Show closed".
hooks/register.tsx 127 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Epic } from '../types'
5
6const PANE = 'epics'
7const STORE = '.orchestration'
8const STATUS_COLOR: Record<string, string> = {
9 Todo: 'gray',
10 'In Progress': 'yellow',
11 'In Review': 'cyan',
12 Done: 'green',
13 Canceled: 'gray',
14}
15const epics = atom({ plugin: 'orchestrate', key: 'epics' } as const, [] as Epic[])
16const showDone = atom({ plugin: 'orchestrate', key: 'showDone' } as const, false)
17
18const field = (text: string, key: string) => text.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim() ?? ''
19const heading = (text: string) => text.match(/^# (.+)$/m)?.[1]?.trim() ?? ''
20const isClosed = (epic: Epic) => epic.issues.length > 0 && epic.issues.every(one => one.status === 'Done' || one.status === 'Canceled')
21
22async function load($: EngineInterface) {
23 if (!(await $.fs.exists(STORE))) return update($, epics, () => [])
24 const found: Epic[] = []
25 for (const dir of await $.fs.list(STORE)) {
26 if (dir.kind !== 'dir' || !(await $.fs.exists(`${STORE}/${dir.name}/EPIC.md`))) continue
27 const epicText = await $.fs.read(`${STORE}/${dir.name}/EPIC.md`)
28 const issueDir = `${STORE}/${dir.name}/issues`
29 const files = (await $.fs.exists(issueDir)) ? await $.fs.list(issueDir) : []
30 const issues = []
31 for (const file of files.filter(one => one.name.endsWith('.md')).sort((a, b) => a.name.localeCompare(b.name))) {
32 const text = await $.fs.read(`${issueDir}/${file.name}`)
33 issues.push({ id: field(text, 'id') || file.name, title: heading(text), status: field(text, 'status'), wave: field(text, 'wave') })
34 }
35 found.push({ slug: dir.name, title: heading(epicText) || dir.name, issues })
36 }
37 return update($, epics, () => found)
38}
39
40export const register: Register = on => {
41 on('session.start', async ($, e, next) => {
42 await $.command.register({ name: 'epics', description: 'Show this repo\'s orchestration epics and their chunks in a pane' })
43
44 return next(e)
45 })
46
47 on('command.run', { command: 'epics' }, async $ => {
48 await load($)
49 await $.ui.open({ id: PANE, title: 'Epics' })
50
51 return { text: 'Epics pane opened.' }
52 })
53
54 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
55 const { Box, Button, Text } = $.ui.resolve(e)
56 const all = await read($, epics)
57 const isShowingDone = await read($, showDone)
58 const open = all.filter(one => !isClosed(one))
59 const closed = all.filter(isClosed)
60
61 return (
62 <Box flexDirection="column" paddingX={1}>
63 <Box justifyContent="space-between">
64 <Text bold>Epics</Text>
65 <Text dimColor>
66 {open.length} open · {closed.length} closed
67 </Text>
68 </Box>
69 <Box gap={2} marginTop={1}>
70 <Button key="refresh" plain hotkey="r" label="Refresh" onPress={() => load($)} />
71 <Button
72 key="toggle-done"
73 plain
74 hotkey="d"
75 label={isShowingDone ? 'Hide closed' : 'Show closed'}
76 onPress={() => update($, showDone, value => !value)}
77 />
78 </Box>
79 {all.length === 0 && (
80 <Box marginTop={1}>
81 <Text dimColor italic>
82 No .orchestration/ store in this folder.
83 </Text>
84 </Box>
85 )}
86 {open.map(epic => {
87 const done = epic.issues.filter(one => one.status === 'Done').length
88
89 return (
90 <Box key={epic.slug} flexDirection="column" marginTop={1} paddingX={1} borderStyle="round" borderColor="cyan">
91 <Box justifyContent="space-between">
92 <Text bold wrap="truncate-end">
93 {epic.title}
94 </Text>
95 <Text color="green">
96 {'■'.repeat(done)}
97 <Text dimColor>{'□'.repeat(epic.issues.length - done)}</Text> {done}/{epic.issues.length}
98 </Text>
99 </Box>
100 {epic.issues.map(issue => (
101 <Box key={`${epic.slug}-${issue.id}`} gap={1}>
102 <Text color={STATUS_COLOR[issue.status] ?? 'white'} bold>
103 {issue.status.padEnd(11)}
104 </Text>
105 <Text dimColor>w{issue.wave}</Text>
106 <Text wrap="truncate-end" strikethrough={issue.status === 'Canceled'}>
107 {issue.title || issue.id}
108 </Text>
109 </Box>
110 ))}
111 </Box>
112 )
113 })}
114 {isShowingDone &&
115 closed.map(epic => (
116 <Box key={epic.slug} gap={1} marginTop={1}>
117 <Text color="green">✓</Text>
118 <Text dimColor wrap="truncate-end">
119 {epic.title} · {epic.issues.length} {epic.issues.length === 1 ? 'chunk' : 'chunks'}
120 </Text>
121 </Box>
122 ))}
123 </Box>
124 )
125 })
126}
127types/index.d.ts 9 lines1export type Issue = { id: string; title: string; status: string; wave: string }
2export type Epic = { slug: string; title: string; issues: Issue[] }
3
4declare module 'claude-code' {
5 interface PluginState {
6 orchestrate: { epics: Epic[]; showDone: boolean }
7 }
8}
9