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…

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).
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.
1. Going to sleep: start it with the plugin.
/goal-orchestrator:run WAN-346 and its sub-issues
Sonnet · medium), reviewers (Opus · xhigh), Away / asleep, and PR, merge when green. Then it asks which final results you want.RUN.md and calls its start_goal tool. That queues /goal <finish line> exactly as if you typed it, and the turn ends./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./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
After a compaction or resume, the plugin's system-prompt section and RUN.md bring the run back to the step it was on.
start_goal tool queues a real /goal, and the finish line is kept in the system prompt.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.
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).
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./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.
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.start_goal tool queues a real /goal./goal clear.hooks/register.test.ts (claude plugin test).hooks/register.ts 96 lines1import { 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}
96types/index.d.ts 16 lines1/** 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