SLOPSHOPPER

maestro-lanes

Shows Maestro external lane runs above the prompt.

newbandtimer
v0.1.0MITupdated 2026-09-19ricardosuman/maestro/mods/lanes
A shopper browsing a rack in a slop shop
README

maestro

Maestro started as an adaptation of DannyMac180/fable-advisor (MIT) and has since been rewritten around its own doctrine.

What it is

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.

Lanes band

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

Who this is for

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.

How work is routed

LaneRunsClaude subagent?Used when
maestro:luna-lane skillGPT-5.6 Luna, effort max, via codex execNo — architect launches codex directly from BashHalf of day-to-day/cheap work; slow (~100 steps) but cheap
maestro:grok-lane skillGrok 4.6, effort medium, via the Grok CLINo — architect launches grok directly from BashThe other half of day-to-day work; preferred when wall-clock matters
maestro:astra-lead skillGPT-6 Astra, effort high, via codex execNo — architect launches codex directly from BashA 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 skillGrok 4.6, effort medium, plan mode (read-only)NoInvestigating a question across code/docs/web; falls back to opus-researcher
maestro:lane-runner agentClaude Haiku 4.5, effort medium, foreground forwarder to the external lane CLIYes — thin forwarderUse when the run should appear in Claude Code's native agent list; direct Bash remains the default
maestro:opus-heavy-implementer agentClaude Opus 5, effort highYesComplex algorithms, concurrency, migrations, security-sensitive code, many-file changes. Outside the tally; diff reviewed by codex-peer
maestro:opus-implementer agentClaude Opus 5, effort mediumYesFallback when luna/grok is unavailable, or on explicit request; diff reviewed by codex-peer
maestro:opus-reviewer agentClaude Opus 5, effort mediumYesReviews every luna/grok diff: reads git diff, re-runs the spec's verification, returns ship/fix
maestro:opus-researcher agentClaude Opus 5, effort mediumYesResearch fallback when grok-research is unavailable; read-only
maestro:codex-peer agentGPT-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 agentFable 5.1, effort highYesCommitment-boundary decisions; mandatory final review of Astra-led work
maestro:sonnet-implementer agentClaude Sonnet 5, effort mediumYesNot 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.

The usage gate

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.

The spec contract and the simplicity block

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.

Install

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.

Updating

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.

Files

.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

License

MIT (see LICENSE).

Source 1 files
hooks/register.ts 213 lines
1import 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