SLOPSHOPPER

goal-orchestrator

Ship a goal (an issue and its sub-issues) as one PR in Claude Code goal mode: intake asks which models and effort to use, whether you will be away, the finish…

newguardtoaststatusprompttool
A shopper browsing a rack in a slop shop
README

goal-orchestrator

A Claude Code plugin that ships a goal (an issue and its sub-issues, a plan doc, or a written goal) as one PR on one branch with a team of sub-agents, held to its finish line by Claude Code goal mode (/goal).

Why goal mode

A long run drifts if only a prompt holds the finish line. /goal <condition> re-checks the condition after every turn and starts another turn until it is met, survives --resume, and waits while sub-agents run. This plugin turns it on for you after the intake, so the finish line you chose is the one the session is held to.

Real working cases

1. Going to sleep: start it with the plugin.

/goal-orchestrator:run WAN-346 and its sub-issues
  1. Intake asks four questions: implementers (for example Sonnet · medium), reviewers (Opus · xhigh), Away / asleep, and PR, merge when green. Then it asks which final results you want.
  2. The plugin writes the finish line into RUN.md and calls its start_goal tool. That queues /goal <finish line> exactly as if you typed it, and the turn ends.
  3. /goal starts at once and the run continues on its own: recon, waves of implementers in their own worktrees, a review after each wave, the final gate, the PR, CI driven to green, the merge, the tracker and the final report.
  4. You wake up to a merged PR, or a PR with exactly what blocks it, and the results you asked for. /goal shows the goal as achieved.

2. You prefer typing /goal yourself.

/goal Using goal-orchestrator, ship WAN-346 and its sub-issues as one PR: merged into main with CI green, WAN-346..364 Done with checkpoints, final report posted. I'm asleep: Sonnet medium builds, Opus xhigh reviews.

Claude invokes the plugin's run skill inside your goal, skips the questions the condition already answers, and goes. The plugin sees it is already inside a goal and does not set another.

3. Headless or CI.

claude -p "/goal Using goal-orchestrator, ship WAN-400 as one PR, CI green, PR open. --auto" --permission-mode auto

What happens inside the run

  1. Intake and goal: models and effort per role, away or around, finish line, results; then goal mode is armed.
  2. Recon and plan: the repo's steering docs and every issue; waves of lanes that never edit the same files at the same time.
  3. Waves: implementers work in the background, each in a worktree created from the feature branch, with their own database and port.
  4. Review ladder: each step is cheap where it can be, deep where it matters.
  5. Lane gate (every lane, no agent): before a lane merges, the orchestrator re-runs the checks the implementer reported and compares the counts. It also checks the diff against the Done-when items, the files the lane owns and the repo's invariants. A claim that doesn't reproduce sends the lane back.
  6. Lane review (high-risk lanes only): a reviewer checks the lane on its own before it merges. High-risk means data integrity, security, schema, money or concurrency.
  7. Wave review: after a wave merges, a reviewer looks for lanes that clash and checks the wave against its issues.
  8. Final review: a whole-diff review. For UI goals, a visual and motion audit against the design reference runs before the PR opens.
  9. Final gate: typecheck, lint, unused code, full unit and integration, schema verification, full e2e. Flaky tests are root-caused.
  10. Deliver: a PR with Issues, Decisions, Migrations, Ops notes and Tests. CI driven to green, the merge if you chose it, tracker checkpoints, and your final results.

After a compaction or resume, the plugin's system-prompt section and RUN.md bring the run back to the step it was on.

Changes

  • 1.2.0: the review ladder. Every lane passes a lane gate, and high-risk lanes get their own review before they merge. The wave review now focuses on how lanes interact. The UI audit is required before the PR opens.
  • 1.1.0: goal mode. The start_goal tool queues a real /goal, and the finish line is kept in the system prompt.
  • 1.0.0: first release.

Install

From GitHub (after pushing this folder to a repo):

/plugin marketplace add <owner>/<repo>
/plugin install goal-orchestrator@goal-orchestrator-marketplace

From a local folder:

/plugin marketplace add ./goal-orchestrator-marketplace
/plugin install goal-orchestrator@goal-orchestrator-marketplace

Or for one session: claude --plugin-dir ./goal-orchestrator-marketplace/plugins/goal-orchestrator.

Claude Code cloud sessions (claude.ai/code)

Cloud sessions do not install plugins that a repository's .claude/settings.json enables, so install it in the cloud environment's setup script (environment menu in the session title bar → Edit → Setup script). The script runs before Claude starts. Put the lines after #!/bin/bash:

#!/bin/bash
claude plugin marketplace add WaniiDev/goal-orchestrator || true
claude plugin install goal-orchestrator@goal-orchestrator-marketplace --scope user || true

New sessions pick it up; running sessions do not. The setup script clones without your credentials, so this marketplace repository must be public (a private one fails with "could not read Username for 'https://github.com'"; check the session's setup log).

Requirements

  • Claude Code 2.1.293 or newer. The start_goal tool is a function-hooks module, an early-access surface.
  • /goal must be available. It is off when disableAllHooks is set, or when managed settings allow only managed hooks. Without it the plugin falls back to the built-in ProposeGoal tool, or asks you to paste the /goal line it prints.
  • For unattended runs, use auto permission mode, so turns are not stopped by permission prompts.

Use

/goal-orchestrator:run <goal> [--auto]
/goal-orchestrator:status      progress as an icon list
/goal-orchestrator:report      the final results
/goal clear                    end the goal (only you do this)

--auto skips the intake and uses the defaults: Sonnet · medium to build, Opus · xhigh to review, away mode, PR only (it never merges without your yes), and the chat, tracker and PR-body results.

Contents

  • skills/run/: the methodology (SKILL.md) and its references: intake, implementer brief, review briefs, delivery, lessons from real runs.
  • skills/status/, skills/report/: progress and final results formats.
  • agents/: implementer (default Sonnet · medium), reviewer and ui-auditor (default Opus · xhigh). The intake's choices override these per run.
  • hooks/register.ts: the function-hooks module.
  • The start_goal tool queues a real /goal.
  • A system-prompt section keeps the finish line through compaction.
  • It notices /goal clear.
  • Tests are in hooks/register.test.ts (claude plugin test).
Source 2 files
hooks/register.ts 96 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { ActiveGoal } from '../types'
5
6const PLUGIN = 'goal-orchestrator'
7/** `/goal`'s own limit on a condition. */
8const MAX_CONDITION = 4000
9/** `/goal clear` and its aliases. */
10const CLEARS = new Set(['clear', 'stop', 'off', 'reset', 'none', 'cancel'])
11
12const goal = atom({ plugin: 'goal-orchestrator', key: 'goal' } as const, null as ActiveGoal | null)
13
14const DESCRIPTION = [
15  'Turns on Claude Code goal mode for a goal-orchestrator run: queues `/goal <condition>` exactly as if the person typed it.',
16  'Call it once, at the end of the run skill\'s intake, with the finish line the person chose.',
17  'The goal starts when this turn ends: after calling it, say in one line that goal mode is armed and end your turn.',
18  'Write the condition so an evaluator that reads only the conversation can judge it: name each artefact and how it shows (PR merged into main, CI green, tracker issues Done, final report posted), and when to stop as impossible.',
19].join(' ')
20
21function conditionOf(input: unknown): string {
22  const value = (input as { condition?: unknown }).condition
23  return typeof value === 'string' ? value.trim() : ''
24}
25
26function runFileOf(input: unknown): string {
27  const value = (input as { runFile?: unknown }).runFile
28  return typeof value === 'string' ? value.trim() : ''
29}
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    await $.tool.register({
34      name: 'start_goal',
35      description: DESCRIPTION,
36      inputSchema: {
37        type: 'object',
38        properties: {
39          condition: { type: 'string', description: `The /goal completion condition, at most ${MAX_CONDITION} characters.` },
40          runFile: { type: 'string', description: 'Absolute path of the run\'s RUN.md.' },
41        },
42        required: ['condition'],
43      },
44      isDeferred: false,
45    })
46    return next(e)
47  })
48
49  on('tool.call', { tool: 'mcp__goal-orchestrator__start_goal' }, async ($, e) => {
50    const condition = conditionOf(e)
51    if (condition === '') return { deny: `${PLUGIN}: start_goal needs a condition.` }
52    if (condition.length > MAX_CONDITION) {
53      return { deny: `${PLUGIN}: the condition is ${condition.length} characters; /goal takes at most ${MAX_CONDITION}.` }
54    }
55    const setAt = await $.clock.now()
56    await update($, goal, () => ({ condition, runFile: runFileOf(e), setAt }))
57    // A slash command cannot run inside the hook the turn is waiting on: queue it for when the session is idle.
58    $.clock.after(0, () => {
59      void $.command.run({ command: 'goal', args: condition }).catch(() => {
60        $.ui.toast(`${PLUGIN}: /goal could not start. Type /goal and paste the condition from RUN.md.`)
61      })
62    })
63    $.ui.status('goal mode queued')
64    // A registered tool answers with text (or MCP content blocks): the shape core validates and the model reads.
65    return {
66      result: `Goal mode is queued: /goal starts when this turn ends and keeps the run going until this condition is met:\n${condition}\nEnd your turn now.`,
67    }
68  }).catch(() => ({ deny: `${PLUGIN}: start_goal failed; type /goal and paste the condition from RUN.md.` }))
69
70  // `/goal clear` (or a new /goal for something else) ends this run's hold on the finish line.
71  on('command.run', { command: 'goal' }, async ($, e, next) => {
72    const args = e.args.trim()
73    const current = await read($, goal)
74    if (current !== null && args !== '' && (CLEARS.has(args.toLowerCase()) || args !== current.condition)) {
75      await update($, goal, () => null)
76      $.ui.status(undefined)
77    }
78    return next(e)
79  }).catch(($, e, next) => next(e))
80
81  // Keeps the finish line in the system prompt, so it survives a compaction even if the conversation does not.
82  on('prompt.compose', async ($, e, next) => {
83    const composed = await next(e)
84    const current = await read($, goal)
85    if (current === null) return composed
86    const runFile = current.runFile === '' ? 'RUN.md in the scratchpad' : current.runFile
87    const text = [
88      'A goal-orchestrator run is active in this session, in goal mode.',
89      `Its finish line (the /goal condition): ${current.condition}`,
90      `Its state is in ${runFile}: re-read it before acting after a compaction or a resume, then continue the goal-orchestrator:run skill from the step it records.`,
91      'If `/goal` reports the goal achieved or cleared, this section no longer applies.',
92    ].join('\n')
93    return { ...composed, sections: [...composed.sections, { id: `${PLUGIN}:finish-line`, text, scope: 'session' }] }
94  })
95}
96
types/index.d.ts 16 lines
1/** The finish line of the run this session is orchestrating, as `start_goal` set it. */
2export type ActiveGoal = {
3  /** The `/goal` condition, word for word. */
4  condition: string;
5  /** Where the run keeps its state (`RUN.md`), so it can be re-read after a compaction. */
6  runFile: string;
7  /** When the goal was queued, in epoch milliseconds. */
8  setAt: number;
9};
10
11declare module 'claude-code' {
12  interface PluginState {
13    'goal-orchestrator': { goal: ActiveGoal | null };
14  }
15}
16