Shows Maestro external lane runs above the prompt.

Maestro started as an adaptation of DannyMac180/fable-advisor (MIT) and has since been rewritten around its own doctrine.
Maestro turns a Claude Code session into an architect-orchestrator: the session decomposes the problem, writes specs, routes the actual typing to other models, and judges the verification evidence — it almost never writes the code itself. Implementation goes to cheaper or external lanes (GPT-5.6 Luna and Grok 4.6 by default, Claude Opus or Sonnet as fallback), each launched from a spec with its own reviewer checking the diff before the architect accepts it. The point is cost: keep the expensive model (Fable 5.1) for judgment — decomposition, interface design, routing, and reading reviews — and spend cheaper or external tokens on volume.
The optional maestro-lanes mod polls /tmp/maestro-lanes/ and shows each Luna, Grok, Astra, or research run above the prompt with its task suffix, state, elapsed time, and latest output line. Load it with claude --plugin-dir mods/lanes, or install maestro-lanes from this marketplace. Function hooks are early access. Function hooks load only with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in the environment (e.g. CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir mods/lanes).
This is tuned to my own setup and subscriptions: a Claude subscription used from Claude Code (the session runs Fable 5.1; Opus/Sonnet lanes), an OpenAI subscription used through the Codex CLI (GPT-5.6 Luna and GPT-6 Astra lanes), and an xAI Grok subscription used through the Grok Build CLI (Grok 4.6 lanes). If you don't have one of these, the corresponding lanes report unavailable and the doctrine falls back as documented below. Nothing here is a benchmark or a recommendation; it is the routing that keeps my weekly Claude limit alive.
| Lane | Runs | Claude subagent? | Used when |
|---|---|---|---|
maestro:luna-lane skill | GPT-5.6 Luna, effort max, via codex exec | No — architect launches codex directly from Bash | Half of day-to-day/cheap work; slow (~100 steps) but cheap |
maestro:grok-lane skill | Grok 4.6, effort medium, via the Grok CLI | No — architect launches grok directly from Bash | The other half of day-to-day work; preferred when wall-clock matters |
maestro:astra-lead skill | GPT-6 Astra, effort high, via codex exec | No — architect launches codex directly from Bash | A whole self-contained objective with a command that proves it done, on request, or in EXTERNAL-ONLY mode; followed by mandatory advisor final review |
maestro:grok-research skill | Grok 4.6, effort medium, plan mode (read-only) | No | Investigating a question across code/docs/web; falls back to opus-researcher |
maestro:lane-runner agent | Claude Haiku 4.5, effort medium, foreground forwarder to the external lane CLI | Yes — thin forwarder | Use when the run should appear in Claude Code's native agent list; direct Bash remains the default |
maestro:opus-heavy-implementer agent | Claude Opus 5, effort high | Yes | Complex algorithms, concurrency, migrations, security-sensitive code, many-file changes. Outside the tally; diff reviewed by codex-peer |
maestro:opus-implementer agent | Claude Opus 5, effort medium | Yes | Fallback when luna/grok is unavailable, or on explicit request; diff reviewed by codex-peer |
maestro:opus-reviewer agent | Claude Opus 5, effort medium | Yes | Reviews every luna/grok diff: reads git diff, re-runs the spec's verification, returns ship/fix |
maestro:opus-researcher agent | Claude Opus 5, effort medium | Yes | Research fallback when grok-research is unavailable; read-only |
maestro:codex-peer agent | GPT-6 Astra, high reasoning, via codex exec (wrapper model: haiku) | Yes (thin forwarder) | Cross-vendor discussion/second opinion; reviews opus-implementer/opus-heavy-implementer diffs and Fable-led final deliverables |
maestro:advisor agent | Fable 5.1, effort high | Yes | Commitment-boundary decisions; mandatory final review of Astra-led work |
maestro:sonnet-implementer agent | Claude Sonnet 5, effort medium | Yes | Not routed by the doctrine — kept for manual use only |
For native Claude Code visibility, dispatch maestro:lane-runner with lane, spec file, and project; it forwards the same spec in the foreground and preserves the lane's own report. It costs a little Claude quota while Haiku waits, so the direct Bash launch remains the default and lane-runner is the alternative when the agent row matters.
Day-to-day implementation (simple to reasonably complex, plus all cheap work) splits 50/50 between the luna and grok lanes; the architect keeps a running lanes: luna N / grok M tally and corrects toward 50/50 as it drifts, preferring grok when wall-clock matters and luna for cheap volume work. Heavy code goes to opus-heavy-implementer, outside that tally. Every luna/grok diff goes to opus-reviewer before it's accepted — it re-runs the spec's verification command and returns ship/fix; diffs from the opus lanes go to codex-peer instead, so the reviewer is never the implementer's own vendor. Once the Claude 5-hour usage window hits 75%, mode flips to EXTERNAL-ONLY: all implementation runs through luna, grok and astra, and a PreToolUse gate denies the opus/sonnet lanes outright. When a lane reports unavailable, rate-limited or timed out, its share moves to the other external lane (the 50/50 tally pauses); opus-implementer is used only when both external lanes are down, or the user explicitly accepts a Claude lane for a time-critical spec. The architect itself keeps only decomposition, interface design, spec writing, routing, and judging verification evidence — never the typing.
A statusline script (scripts/usage-statusline.sh) reads Claude Code's rate-limit data on every prompt and records the 5-hour and 7-day usage percentages to ~/.claude/maestro/usage.json. A hook (scripts/usage-gate.sh, wired in hooks/hooks.json) reads that file on every UserPromptSubmit and injects a maestro usage: … mode: … note into the prompt, and on every PreToolUse for an Agent/Task call it denies opus-implementer, opus-heavy-implementer, opus-reviewer, opus-researcher and sonnet-implementer once the 5-hour window is at or above the threshold. The threshold is set by the env var MAESTRO_EXTERNAL_ONLY_AT (default 75; the older FABLE_ADVISOR_EXTERNAL_ONLY_AT and FABLE_ADVISOR_GROK_ONLY_AT are still accepted as fallbacks). advisor, codex-peer and the read-only Explore agent are never gated — they stay available in EXTERNAL-ONLY mode because the point of the mode is preserving Claude quota for judgment, not shutting the session down.
Every implementation prompt carries the same five parts, because lanes share none of the architect's conversation context: objective, files, interfaces, constraints, verification. The Constraints section always ends with the ponytail simplicity block (doctrine/ponytail-block.md) pasted in verbatim, plus the karpathy guidelines block (doctrine/karpathy-block.md) — the external lanes don't load this plugin's skills, so both doctrines have to travel inside the spec file itself. The opus agents carry the same ladder directly in their own instructions and apply it even if a spec forgets to paste it.
git clone https://github.com/ricardosuman/maestro.git
cd maestro && ./install.sh
install.sh checks that the claude and jq CLIs are present and that it can reach the (private) repo over git, then: adds this repo as a plugin marketplace and installs the maestro plugin; adds and installs the companion plugins (openai-codex, ponytail, karpathy-skills); writes the orchestration doctrine block (doctrine/CLAUDE-orchestration.md) and the always-on-skills block into ~/.claude/CLAUDE.md; copies scripts/usage-statusline.sh into ~/.claude/maestro/; merges ~/.claude/settings.json (per-model effort levels, a statusLine entry pointing at the usage script, and a permissions.allow rule for the grok binary); and, when run interactively, offers to register the Obsidian MCP server.
The grok lanes call the grok binary by its literal absolute path, so add the permission rule install.sh prints — Bash(/Users/<you>/.grok/bin/grok:*) — to permissions.allow in ~/.claude/settings.json if it isn't merged automatically. The luna/astra/codex-peer lanes need the Codex CLI installed and logged in (npm i -g @openai/codex, then codex login); the grok/grok-research lanes need the Grok CLI installed and logged in (grok login). Without one of these, the corresponding lanes report unavailable and the doctrine's fallback rules apply.
Restart Claude Code (or start a new session) after installing or updating — hooks, skills and agents only load on a fresh session.
Bump the version in .claude-plugin/plugin.json, push, then on each machine:
claude plugin marketplace update maestro && claude plugin update maestro@maestro
Restart Claude Code afterward.
.claude-plugin/marketplace.json plugin marketplace entry
.claude-plugin/plugin.json plugin manifest (name, version, description)
agents/advisor.md Fable 5.1 commitment-boundary advisor
agents/codex-peer.md cross-vendor discussion peer + reviewer (GPT-6 Astra via Codex CLI)
agents/opus-heavy-implementer.md Claude Opus 5 high — heavy/correctness-critical implementation
agents/opus-implementer.md Claude Opus 5 medium — fallback implementation
agents/opus-researcher.md Claude Opus 5 medium — research fallback
agents/opus-reviewer.md Claude Opus 5 medium — reviews every luna/grok diff
agents/sonnet-implementer.md Claude Sonnet 5 medium — manual-use simple implementer
doctrine/CLAUDE-always-on-skills.md loads ponytail + karpathy skills every session
doctrine/CLAUDE-orchestration.md the routing doctrine merged into ~/.claude/CLAUDE.md
doctrine/karpathy-block.md karpathy guidelines block pasted into every spec
doctrine/ponytail-block.md simplicity ladder block pasted into every spec
hooks/hooks.json wires usage-gate.sh to UserPromptSubmit and PreToolUse
install.sh one-shot installer
scripts/refresh-doctrine.sh replaces a doctrine block inside a CLAUDE.md file
scripts/usage-gate.sh injects the usage note; denies opus/sonnet in EXTERNAL-ONLY mode
scripts/usage-statusline.sh records usage.json and prints the statusline
skills/astra-lead/SKILL.md how to hand a whole objective to GPT-6 Astra
skills/grok-lane/SKILL.md how to drive the Grok 4.6 day-to-day lane
skills/grok-research/SKILL.md how to drive Grok 4.6 read-only research
skills/luna-lane/SKILL.md how to drive the GPT-5.6 Luna day-to-day lane
skills/orchestration/SKILL.md the full routing doctrine
MIT (see LICENSE).
hooks/register.ts 213 lines1import type { EngineInterface, Register, RenderInput, Timer } from 'claude-code'
2
3const LANE_DIR = '/tmp/maestro-lanes'
4const POLL_MS = 2_000
5const FINISHED_MS = 10 * 60 * 1_000
6const STALE_MS = 65 * 60 * 1_000
7const LANE_FILE = /^(luna|grok|astra|research)\.(.+)$/
8const EXIT_LINE = /^maestro-exit:\s*(-?\d+)$/i
9const RATE_LIMITED =
10 /usage limit|rate limit|quota|429|402|payment required|balance exhausted|unauthorized|not logged in/i
11
12const MODEL_OF = {
13 luna: 'GPT-5.6 Luna max',
14 grok: 'Grok 4.6 medium',
15 astra: 'GPT-6 Astra high',
16 research: 'Grok 4.6 medium (plan)',
17} as const
18
19type Lane = keyof typeof MODEL_OF
20
21type LaneFile = {
22 lane: Lane
23 suffix: string
24 mtimeMs: number
25 firstSeenMs: number
26 body: string
27}
28
29type LaneState = {
30 sessionStartMs: number | undefined
31 firstSeen: Map<string, number>
32 rows: string[]
33 stamp: string
34 isRefreshing: boolean
35}
36
37function elapsedOf(ms: number): string {
38 const seconds = Math.max(0, Math.floor(ms / 1_000))
39
40 if (seconds < 60) {
41 return `${seconds}s`
42 }
43
44 return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, '0')}s`
45}
46
47function rowOf(file: LaneFile, now: number): string | null {
48 const lines = file.body
49 .split(/\r?\n/)
50 .map(line => line.trim())
51 .filter(line => line !== '')
52 const exit = lines.findLast(line => EXIT_LINE.test(line))?.match(EXIT_LINE)
53 const exitCode = exit?.[1] === undefined ? undefined : Number(exit[1])
54 const isFinished = exitCode !== undefined
55
56 if (isFinished && now - file.mtimeMs > FINISHED_MS) {
57 return null
58 }
59
60 let state = 'running'
61
62 if (exitCode === 0) {
63 state = 'done'
64 } else if (exitCode !== undefined) {
65 // ponytail: tail-20 heuristic, a structured exit reason from the lane if it misfires
66 state = lines.slice(-20).some(line => RATE_LIMITED.test(line))
67 ? 'rate-limited'
68 : `failed (${exitCode})`
69 } else if (now - file.mtimeMs > STALE_MS) {
70 // ponytail: mtime heuristic, a pid file if it misfires
71 state = 'stale'
72 }
73
74 const elapsed = isFinished
75 ? file.mtimeMs - file.firstSeenMs
76 : now - file.firstSeenMs
77 const output = lines.findLast(line => !EXIT_LINE.test(line))
78 const row = `${file.lane} ${file.suffix} · ${MODEL_OF[file.lane]} · ${state} · ${elapsedOf(elapsed)}`
79
80 return output === undefined ? row : `${row} · ${output}`
81}
82
83async function rowsOf(
84 $: EngineInterface,
85 state: LaneState,
86 sessionStartMs: number,
87 now: number,
88): Promise<string[]> {
89 let entries
90
91 try {
92 entries = await $.fs.list(LANE_DIR)
93 } catch {
94 return []
95 }
96
97 const files = await Promise.all(
98 entries.map(async entry => {
99 const match = entry.kind === 'file' ? entry.name.match(LANE_FILE) : null
100
101 if (!match) {
102 return null
103 }
104
105 const path = `${LANE_DIR}/${entry.name}`
106
107 try {
108 const stat = await $.fs.stat(path)
109
110 if (stat.kind !== 'file' || stat.mtimeMs < sessionStartMs) {
111 return null
112 }
113
114 // $.fs.stat has no birth time: a run starts when a poll first sees it.
115 if (!state.firstSeen.has(path)) {
116 state.firstSeen.set(path, now)
117 }
118
119 return {
120 lane: match[1] as Lane,
121 suffix: match[2] ?? '',
122 mtimeMs: stat.mtimeMs,
123 firstSeenMs: state.firstSeen.get(path) ?? now,
124 body: await $.fs.read(path),
125 }
126 } catch {
127 return null
128 }
129 }),
130 )
131
132 return files
133 .map(file => (file === null ? null : rowOf(file, now)))
134 .filter((row): row is string => row !== null)
135}
136
137function bandOf(
138 $: EngineInterface,
139 e: RenderInput<'AbovePrompt', 'terminal'>,
140 rows: readonly string[],
141) {
142 const { Box, Text } = $.ui.resolve(e)
143
144 return Box({
145 flexDirection: 'column',
146 children: rows.map(row => Text({ wrap: 'truncate-end', children: row })),
147 })
148}
149
150async function refresh(
151 $: EngineInterface,
152 state: LaneState,
153 invalidate: boolean,
154): Promise<void> {
155 if (state.sessionStartMs === undefined || state.isRefreshing) {
156 return
157 }
158
159 state.isRefreshing = true
160
161 try {
162 const now = await $.clock.now()
163 const nextRows = await rowsOf($, state, state.sessionStartMs, now)
164 const nextStamp = nextRows.join('\n')
165 const changed = nextStamp !== state.stamp
166
167 state.rows = nextRows
168 state.stamp = nextStamp
169
170 if (invalidate && changed) {
171 $.ui.invalidate('ui.render')
172 }
173 } finally {
174 state.isRefreshing = false
175 }
176}
177
178export const register: Register = (on, _options) => {
179 const state: LaneState = {
180 sessionStartMs: undefined,
181 firstSeen: new Map(),
182 rows: [],
183 stamp: '',
184 isRefreshing: false,
185 }
186 let poll: Timer | undefined
187
188 on('session.start', async ($, e, next) => {
189 state.sessionStartMs = await $.clock.now()
190 poll?.cancel()
191 await refresh($, state, false)
192 poll = $.clock.every(POLL_MS, () => {
193 void refresh($, state, true)
194 })
195
196 return next(e)
197 })
198
199 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
200 if (e.props.hasSurvey || e.surface !== 'terminal') {
201 return next(e)
202 }
203
204 await refresh($, state, false)
205
206 if (state.rows.length === 0) {
207 return next(e)
208 }
209
210 return bandOf($, e, state.rows)
211 })
212}
213