SLOPSHOPPER

task-planner

Plan work with an Opus planner agent and track the steps in a pane

newpanecommandtoastagents
v0.1.0no licenseupdated 2026-10-07alexatennea/claude-mods/task-planner
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · task-planner
│ ┃ Plan ✕ › fix the failing auth test and add an audit log call │ ┃ No plan yet. Try /plan <goal>. │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ 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 │ │ › /plan │ ⎿ task-planner: Usage: /plan <what you want to build or fix> │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Plan
No plan yet. Try /plan <goal>.
README

Claude Code mods

Four mods that work together: plan first, then code with each task sent to a model that fits how hard it is, and watch it all happen.

ModWhat it does
task-planner/plan <goal> runs an Opus planner agent (read-only) that returns a checklist of focused tasks, each rated easy/medium/hard. The steps appear in a Plan pane.
coding-agentsAdds a coder subagent and tells the main session to hand each focused coding task to it, one at a time.
model-routerBefore each coder starts, Haiku rates the task and the coder runs on Haiku (easy), Sonnet (medium) or Opus (hard). Falls back to Sonnet if rating fails; a model the session asks for explicitly wins.
mod-monitorA Mods pane: in the desktop app an animated control room (you → session → mods → Haiku/Sonnet/Opus) with messages flying along the wires, busy mods pulsing, running agents linked to their model, and the model mix; in the terminal an emoji/colour version. Hover anything for details.

They need Claude Code on your own computer (terminal claude, or the Code tab of the desktop app). Cloud sessions in the Claude app don't load plugins.

Mods pane in the desktop app

Install

In a Claude Code session:

/plugin install task-planner --marketplace alexatennea/claude-mods
/plugin install coding-agents --marketplace alexatennea/claude-mods
/plugin install model-router --marketplace alexatennea/claude-mods
/plugin install mod-monitor --marketplace alexatennea/claude-mods

Already added the marketplace before a mod existed? Refresh it first: /plugin marketplace update void-mods. After an update to a mod you have, run /plugin update <mod>.

Answer y to add the marketplace the first time, then pick user scope so they're on in every session. Start a new session (or run /reload-plugins) and they're active.

How to use

  1. Open your project and start Claude Code there (cd my-project && claude). Use a wide terminal: panes show beside the conversation from about 144 columns; in a narrower one, open them with the commands below.
  2. Watch: /mods opens the Mods pane.
  3. Plan: /plan add password reset to the login page. The planner (Opus) reads the code and returns numbered steps rated easy/medium/hard; the Plan pane opens with them. Nothing is edited yet.
  4. Review the plan. Ask for changes in plain words ("split step 3", "skip the email step") if needed.
  5. Build: say "go ahead". The session hands each step to a coder. For each one the router asks Haiku to rate it, then the coder runs on Haiku, Sonnet or Opus. In the Mods pane you'll see e.g.: `` model-router → haiku: Task: Add reset route … haiku → model-router: medium session → coding-agents:coder (sonnet): Add reset route coding-agents:coder (sonnet) → session: done in 41s: Changed 2 files … ``
  6. Track progress: /plan-done 2 ticks off step 2; /plan-view reopens the Plan pane.
  7. Check spend: /routing shows how many tasks went to each model.

You don't have to use /plan: asking for any coding work ("fix the failing date test") also goes through coder + router.

Commands

CommandModDoes
/plan <goal>task-plannerPlan a goal with the Opus planner
/plan-viewtask-plannerOpen the Plan pane
/plan-done Ntask-plannerTick (or untick) step N
/routingmodel-routerTasks per model so far
/modsmod-monitorOpen the Mods pane

Configure

model-router options (in /config, or pluginConfigs.model-router.options in settings):

  • easyModel / mediumModel / hardModel: defaults haiku / sonnet / opus (aliases always mean the latest of each)
  • classifierModel: default haiku
  • routedAgents: comma-separated agent types to route, default coding-agents:coder. Add more coding agents here as you build them.

Develop

claude plugin validate model-router
claude plugin test model-router
claude --plugin-dir task-planner --plugin-dir coding-agents --plugin-dir model-router --plugin-dir mod-monitor
Source 2 files
hooks/register.tsx 105 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Difficulty, PlanStep } from '../types'
5
6const PANE = 'task-plan'
7const PLANNER = 'task-planner:planner'
8const STEP = /^\s*[-*]\s*\[( |x|X)\]\s*(.+?)\s*(?:\((easy|medium|hard)\))?\s*$/
9
10const goal = atom({ plugin: 'task-planner', key: 'goal' } as const, '')
11const steps = atom({ plugin: 'task-planner', key: 'steps' } as const, [])
12const plannerIds = atom({ plugin: 'task-planner', key: 'plannerIds' } as const, [])
13
14export function parsePlan(answer: string): PlanStep[] {
15  return answer.split('\n').flatMap(line => {
16    const m = STEP.exec(line)
17    if (!m?.[2]) return []
18    return [{ text: m[2], difficulty: m[3] as Difficulty | undefined, isDone: m[1] !== ' ' }]
19  })
20}
21
22export const register: Register = on => {
23  on('session.start', async ($, e, next) => {
24    await $.command.register({ name: 'plan', description: 'Plan a goal with the Opus planner agent' })
25    await $.command.register({ name: 'plan-done', description: 'Tick off plan step N (e.g. /plan-done 2)' })
26    await $.command.register({ name: 'plan-view', description: 'Show the current plan in a pane' })
27
28    return next(e)
29  })
30
31  on('command.run', { command: 'plan' }, async ($, e) => {
32    const text = e.args.trim()
33    if (!text) return { text: 'Usage: /plan <what you want to build or fix>' }
34    await update($, goal, () => text)
35    await $.prompt.submit({
36      text: `Use the ${PLANNER} agent to plan this goal, then show me the plan and wait for my go-ahead before coding:\n\n${text}`,
37    })
38
39    return { text: `Planning: ${text}` }
40  })
41
42  on('command.run', { command: 'plan-done' }, async ($, e) => {
43    const n = Number.parseInt(e.args.trim(), 10)
44    const list = await read($, steps)
45    if (!Number.isInteger(n) || n < 1 || n > list.length) {
46      return { text: `Usage: /plan-done <1-${Math.max(list.length, 1)}>` }
47    }
48    await update($, steps, all => all.map((s, i) => (i === n - 1 ? { ...s, isDone: !s.isDone } : s)))
49
50    return { text: `Step ${n} toggled.` }
51  })
52
53  on('command.run', { command: 'plan-view' }, async $ => {
54    await $.ui.open({ id: PANE, title: 'Plan' })
55
56    return { text: 'Plan pane opened.' }
57  })
58
59  // Remember which subagents are planners, so their answers can be read as plans.
60  on('agent.spawn', async ($, e, next) => {
61    const started = await next(e)
62    if (e.subagentType === PLANNER && started.agentId) {
63      const id = started.agentId
64      await update($, plannerIds, ids => [...ids, id].slice(-20))
65    }
66
67    return started
68  }).catch(($, e, next) => next(e))
69
70  on('turn.complete', async ($, e, next) => {
71    const ids = await read($, plannerIds)
72    if (e.agentId && ids.includes(e.agentId)) {
73      const plan = parsePlan(e.answer)
74      if (plan.length > 0) {
75        await update($, steps, () => plan)
76        $.ui.toast(`Plan ready: ${plan.length} steps`)
77        void $.ui.open({ id: PANE, title: 'Plan' })
78      }
79    }
80
81    return next(e)
82  })
83
84  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
85    const { Box, Text } = $.ui.resolve(e)
86    const list = await read($, steps)
87    const title = await read($, goal)
88    const done = list.filter(s => s.isDone).length
89
90    return (
91      <Box flexDirection="column">
92        {title !== '' && <Text bold>{title}</Text>}
93        {list.length === 0 && <Text dimColor>No plan yet. Try /plan &lt;goal&gt;.</Text>}
94        {list.length > 0 && <Text dimColor>{done}/{list.length} done</Text>}
95        {list.map((s, i) => (
96          <Text dimColor={s.isDone}>
97            {s.isDone ? '[x]' : '[ ]'} {i + 1}. {s.text}
98            {s.difficulty ? ` · ${s.difficulty}` : ''}
99          </Text>
100        ))}
101      </Box>
102    )
103  })
104}
105
types/index.d.ts 9 lines
1export type Difficulty = 'easy' | 'medium' | 'hard'
2export type PlanStep = { text: string; difficulty?: Difficulty; isDone: boolean }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'task-planner': { goal: string; steps: PlanStep[]; plannerIds: string[] }
7  }
8}
9