SLOPSHOPPER

orchestrate

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…

newpanecommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · orchestrate
│ ┃ Epics ✕ › fix the failing auth test and add an audit log call │ ┃ Epics 0 open · 0 closed │ ┃ ⏺ Read(src/auth.ts) │ ┃ r: Refresh d: Show closed ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ No .orchestration/ store in this folder. ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /epics │ ⎿ orchestrate: Epics pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Epics
Epics 0 open · 0 closed r: Refresh d: Show closed No .orchestration/ store in this folder.
README

Orchestration Claude Plugin

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.

Markdown store

  • <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.
  • Every agent receives explicit absolute store paths. Comments use append-only writes; the worker/orchestrator remains the single status writer.

Responsibility catalog

  • 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.

Layout

  • Repo grounding comes from the separate 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.
  • Other 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.

Browser research portability

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.

Prerequisites

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.

Mod

/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".

Source 2 files
hooks/register.tsx 127 lines
1import { 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}
127
types/index.d.ts 9 lines
1export 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