Your CLAUDE.md, actually followed: compile your instructions into linked atomic statements, plan every task by walking them, and learn from corrections through…

The director remembers how your team works and makes every agent session work that way. It keeps your decisions, conventions and methods as a small memory of linked rules (When / Do / Because); before an agent plans, it walks that memory and hands the agent the rules that apply, and the plan cites them; it sends each task to the right place — the frontier model, a model on your machine, another tool — and refuses to send confidential work anywhere you have not cleared; and after a task it turns your corrections into lessons that you accept or reject, each one a versioned change you can undo. It ships as a Claude Code plugin (this folder) and as a local harness with a two-lane interface.
What to expect. With today's strongest models the director does not add general good practice — the model already has it. It pays where the model cannot know: your own decisions and conventions, your privacy rules, and an auditable record of what was learned. Every measurement, the flat ones included, and the status history: how it works, and what we measured.
<img src="docs/img/results.png" alt="Three bar charts. D0, planning the team's way on 14 situations: today's session 0.589, today's session told to walk its files 0.768, the director 0.929. D2, learning from tasks on 8 held-out probes: before 0.0, after learning 0.781, and D0 re-checked afterwards 0.964. D3, learning the lane: public-facing drafts sent to the frontier before 0.0, after one move 1.0, and the internal control kept local 1.0." width="100%">

The Evolving Agents Toolkit (2025, legacy/eat/) had a SystemAgent, ArchitectZero, Smart Memory, a SmartLibrary and a SmartAgentBus, Firmware and an EvolutionStrategist — and three test functions. The director keeps each idea and pins it to a measurement: the SystemAgent plans by walking the memory and leaves the loop to the harness; ArchitectZero's design comes from the team's own rules; Smart Memory becomes linked When / Do / Because statements; the library and the bus become lanes; Firmware becomes rules enforced in code; evolution becomes a lesson that passes a gate and lands as an agentvcs patch. The table, piece by piece: how it works; the analysis: docs/EVOLUTION.md.
It plans by walking the memory. The index names the situations a task is in; each entry statement links, inside its own line, to the next one that applies; the plan's every step cites the statement it follows.

It learns from a task without breaking what it knew. After a task, the correction it received becomes a drafted statement; the statement passes a gate (it lints clean, and a person accepts it); the edit is sealed in agentvcs's ledger as a patch; the statement is filed where the next walk will find it (D2, D2b).

It learns where work runs. A plan waits; the user can move any task between the frontier and the local lane; the move goes into the next reflection, and the director plans the next similar task in that lane (D3).

Images: docs/img/ — the plates' prompts are in the repository's docs/img/IMAGES.md; results.png is drawn by docs/img/results.py from the briefs' numbers.
| piece | reuses |
|---|---|
| operational memory, atomic statements with links | the memory design of lora-kernel (pages of verifiable statements; the weights hold the navigation, the base holds the content) |
| frontier lane | Claude Code, driven as claude -p — the isolated, audited way agentvcs merge resolve already runs it |
| local lane | lora-kernel's gateway on llama.cpp: local experts that abstain to the frontier |
| refining without breaking | agentvcs: each memory edit is a gated patch; blame says which edit moved a score, bisect finds the one that broke it |
| step | what | gate |
|---|---|---|
| D0 | today's session vs a session that walks the compiled memory, on real situations | baseline below 0.85 (headroom), then treatment ≥ +0.15 — brief |
| D1 | the director's loop and its interface: the director on top (capabilities, plan, conversation), below it the frontier lane (left) and the local lane (right), from one event log | built — below |
| D2 | learning: memory edits proposed after each task, gated on D0's suite, versioned in agentvcs | learns — brief |
| D3 | the user moves a task between lanes; the move becomes a routing statement the director learns | learns the lane — brief |
| D4–D7 | Accept guarded: an edit is kept only if it breaks none of the cases that relied on what it changes, and its own probes show it changes the plan | targeted guard + positive probe — D4, D5, D6, D7 (below) |
| P0 | the walk inside Claude Code, as a plugin | 0.929, the same as the director — brief |
| D8–D10 | can a local 12B walk the memory itself? | not navigable: core recall 0.32 → 0.45, precise (~0.9), but it reaches too little of what each case needs — D8, D9, D10 |
| D11–D12 | do the 12B's plans hold up, on a bank no strategy was designed on? | memory helps, gap remains: 0.68 against Sonnet's 0.92; a plan composed by code did not close it — D11, D12 |
| D13–D14 | retrieval in code (lexical + local embeddings), the plan written by Claude | retrieval partial (0.77 / 0.74); the split planned 0.837 vs 0.892 at 17× less per plan, short of its gate by 0.005 — D13, D14 |
| D15 | Claude writes meta-recipes from the memory; code runs them; the local model only picks a recipe and answers yes/no | recipes work with the 12B: 0.806 vs 0.854, 3 / 3 / 6 per case; not yet with 4B (0.58) or 2B (0.26) — brief |
| D16 | the lanes' facts as one directory of data the statements cite, against the same facts in prose (EVOLUTION step 2) | directory loses: routes as well (0.880 vs 0.864) and D0 holds (0.929), but 5 clearance violations vs 1 and a lane going down followed at 0.762 / 0.786 against 0.8 / 0.9; not built — brief |
| D17 | gated evolution of the recipes from D15's own runs (EVOLUTION step 3) | brief written; runs on the lab's machine (Ollama) — brief |
| H | the director with no person present: a verifier's verdicts in the journal, lessons consolidated and accepted by a declared policy (verified: only from sessions whose verifier passed; all: the ablation) | built and tested offline, not yet run live — below |
| — | where the director should beat other memories, and why: five hypotheses (verified gating, methods with their checks, editing in place, recall, cost), what would show each, and where it should not win | written before the bench measures — analysis |
| DB0 | the director-bench pilot: one family, no memory / native memory / the director, 3 seeds, 90 sessions | brief written — headroom (no-memory repeats its errors) and spread before the full run — brief |
| P1 | an anthropic-managed lane: Managed Agents behind submit / status / fetch, the memory mounted read-only, the walk as a skill, the gate over MCP | planned — D0 through it within 0.03 of 0.929; cost per plan published (EVOLUTION §5) |
| P2 | an antigravity lane: the same artifact on Google's harness | planned — the same bar, and D0's plans judged by the other provider |
/plugin install director --marketplace EvolvingAgentsLabs/evolving-agents
This folder is the plugin (.claude-plugin/plugin.json). Claude Code keeps running the session; the plugin adds only what it lacks:
| piece | what it does |
|---|---|
/director:compile | compiles your CLAUDE.md / AGENTS.md, auto-memory, agents and skills into .director/memory/ (asks first; ~US$5 for ~60 files) |
SessionStart hook | where that memory exists, puts the walk (DIRECTOR.md) into the session: every plan walks the memory and cites what it follows; since 0.2.6 the walk does not end with the plan — at each decision point of the work (a definition, a convention, a value someone will score or reuse) the session searches the memory again and follows it, and what it hands over cites the statements it followed and lists the choices the memory does not hold; it also says how many of the person's messages and corrections no reflection has read yet, and when the session is running an older director than the one installed (restart Claude Code to load it; claude --resume keeps the conversation); silent elsewhere |
UserPromptSubmit hook | every message the person sends goes to the episodic journal as a person entry (keys scrubbed), so a decision made in passing is not lost before /director:reflect reads it; nothing reaches the memory except through the gate. If the installed director changes under a running session, the next prompt says so (restart; claude --resume), once per installed version per session |
PreToolUse hook (Bash) | the public guard: a git commit, git push or gh pr/gh issue text bound for a repository not listed as private is refused when what it publishes carries a term from a private list or something shaped like a key — enforced in code (director/public_guard.py; silent without its private config) |
/director:reflect | reads the journal since the last reflection and turns the person's corrections and decisions (a model, a cap, a tool, "from now on …"), the methods a task reached only after failed attempts (steps with the check that caught each failure, without that task's values) and, since 0.2.6, the scored failures of the agents that did the work (items failing in more than one run: a reference to check, or a definition the team never fixed, proposed as a convention for the person to decide) into edits plus two probes; a lesson the plain runs already follow is reported as redundant, not proposed; nothing touches the memory until you say yes |
/director:recipes | writes (or rewrites) the meta-recipes a local model follows to plan from this memory (recipes.json beside the pages), then lints them and reports how much of the memory they cite (D15); stamps them with the memory version they were written from, so recipes_lint, plan_local and the status line say when an accepted lesson has made them stale |
MCP server director | memory_propose (a candidate copy, linted, diffed), memory_check (D7's positive probe, in the background: TEACHES / REDUNDANT / NO EFFECT), memory_accept (your yes, a clean lint and — if checked — TEACHES; versioned as a gated agentvcs patch when agentvcs is installed; a proposal made before another lesson was accepted is rebased onto the live memory, same id, when none of the statements or Index lines it touches changed — memory_status reports rebased_from and the new diff — and refused with conflict naming them when they did, since accepting would undo what came since; an edit whose situations hold a whole Index line (→ or [[) is refused), memory_reject (your no, or a conflicting proposal closed), memory_lint, memory_status; recipes_lint and plan_local (a plan made by following the recipes with a local model on Ollama, DIRECTOR_LOCAL_MODEL, default gemma4:12b-mlx; free, and abstain when no recipe fits); lanes_list, plan_propose, plan_status, delegate, lane_status, lane_fetch (lanes, below); journal_read and journal_note (the episodic journal, below) |
status line and band (hooks/ui.tsx) | where that memory exists, the status line reads director · 286 statements · 3 learned · 1 to decide · 4 to reflect; when a plan waits, a band above the prompt shows each task with its lane (Run, Move, Cancel); when a lesson waits for a decision, it shows its edits and its check with Diff, Accept and Reject (Accept is your yes, through the same gate as memory_accept; it is offered only when the proposal lints clean and its check, if one ran, says TEACHES), and each delegation not yet brought back with Status and Fetch. What you press reaches the session with your next prompt. Needs a Claude Code with plugin UI modules; on older versions everything else works as before |
live activity (hooks/ui.tsx) | every tool call the session makes is shown as it runs, on its lane: the status line adds frontier ▶ <what runs now> (12s…) │ local ▶ <plan_local or a delegation>, and the band above the prompt shows Director · activity — the walk through the memory (walk → Headroom), the director's own tools, the journal's latest events and each lane's last step. /director-activity hides or shows the band; the status line always keeps the lanes |
What you see in Claude Code. No other window: the director lives in the session. The status line names the memory and what waits for you; a band appears above the prompt only when something does. An illustration, not a capture:
╭─ Director · a plan to run ───────────────────────────────────────────╮
│ Draft the 0.7 release note and summarise this week's internal log │
│ frontier Draft the release note (public) │
│ local Summarise the week's log [Move] │
│ [Run] [Cancel] │
├─ Director · a lesson to decide ──────────────────────────────────────┤
│ Screenshots are cropped before publishing │
│ add Confidentiality#crop_screens · check: TEACHES │
│ [Diff] [Accept] [Reject] │
╰──────────────────────────────────────────────────────────────────────╯
> _
director · 291 statements · 3 learned · 1 to decide · 1 plan waiting
Nothing is delegated before Run; Move offers only a lane the task is cleared for; Accept appears only when the lesson lints clean and its check, if one ran, says TEACHES. What you press reaches the session with your next prompt ("On the director's band, the person: moved task t2 from local to frontier"). The memory is found at $DIRECTOR_MEMORY, else .director/memory/ in the project, else memory/; with none, the plugin stays silent. To use a memory kept elsewhere: DIRECTOR_MEMORY=~/path/to/memory claude.
Measured [ran] 2026-10-17 (P0): the walk through the plugin's hook plans D0's 14 situations at 0.929, the same as the director driving Claude Code (T − P = 0.0; 27 of 28 plans cite the statements they follow), against 0.589 for a session without it.
Lanes. The session is the frontier lane. Other lanes are commands named in a private config ($DIRECTOR_LANES, default ~/.config/director/lanes.json: submit, status and fetch per lane), so the public code knows nothing about what they run. With a person at the session, the plan waits for them: plan_propose puts each task with its lane on the band, where they press Run, Move (the next lane allowed for that task) or Cancel; nothing is delegated before Run, each move is logged in .director/lane-moves.jsonl for the next /director:reflect (D3 learned the lane from such moves), and plan_status tells the session what they did. delegate still sends one task straight to a lane; lane_status and lane_fetch follow it, and the band shows each delegation until it is fetched. A confidential task — flagged, or with an input under a confidential_paths folder — is refused by every lane without "confidential_ok": true, the frontier included: enforced in code (director/lanes.py), not only as a learned statement.
Other harnesses, next. Claude Code is one harness; Managed Agents and the Antigravity agent are the same kind, hosted. All three load SKILL.md and call MCP servers and the memory is plain files, so a managed lane is an adapter behind the same three commands — the memory mounted read-only, lessons returned through the gate, and no cloud lane cleared for confidential work unless you clear it (P1, P2 above).

Not in the plugin: the targeted guard (its cases are this lab's; a project has none until it has run some), lora-kernel's local lane and the two-lane web interface — those stay in director.server below; the band carries the interface's Accept / Reject into the session. Tests: tests/test_plugin.py, tests/test_ui_state.py, tests/test_recipes.py, tests/test_lanes.py, tests/test_plans.py, tests/test_journal.py, tests/test_versions.py, tests/test_headless.py; the band: claude plugin test ..
director.headlessA benchmark run or a nightly job has no one to say yes. director.headless gives it the same gate with the decision declared instead of asked, and keeps everything in one store directory (<store>/.director/memory, the journal and the proposals), so a caller that keeps only that directory between sessions keeps the whole memory:
python -m director.headless init --memory STORE # an empty memory, once
claude -p "<task>" --plugin-dir packages/director ... # the session, with DIRECTOR_MEMORY=STORE/.director/memory
python -m director.headless outcome --memory STORE --task 03 --passed true --trace session.jsonl
python -m director.headless consolidate --memory STORE --policy verified # or --policy all; --check to require TEACHES
outcome writes the verifier's verdict to the journal with a digest of the session's trace: its tool calls in order, the tool errors it met, its final words, its turns.consolidate runs the reflection with Claude Code on the sessions since the last one — under verified only those whose verifier passed (the model never sees the others), under all every one — and each proposal it makes is accepted by gate.accept_by_policy: clean lint and, with --check, TEACHES. The journal records decided_by: policy:verified (or all); the last line printed carries the reflection's tokens and cost.memory_accept still needs the user's yes, and accept_by_policy is reachable from no MCP tool and no band (a test holds that line).Built and tested offline with a fake Claude Code (tests/test_headless.py); not yet run live — DB0 runs it first by hand on one real session.
Your own memory first. director.compile reads only what you name — your CLAUDE.md / AGENTS.md, Claude Code's auto-memory directory, any folder of agents or skills — compiles it with Claude Code (Opus by default; one compile of ~60 files cost about US$5) into pages of atomic statements and an index of situations, lints them, and writes a config if you have none:

python -m director.compile --claude-md ~/your-project/CLAUDE.md \
--memory-dir ~/.claude/projects/<your-project>/memory --workdir ~/your-project
python -m director.server --config ~/.config/director/director.json # http://127.0.0.1:8740
The config is private JSON: memory_dir (the compiled pages, Index.md among them), workdir (where frontier tasks run), log (the event log), local_base_url/local_model (an OpenAI-compatible server), director_model, frontier_model, frontier_tools, plan_budget_usd, task_budget_usd; optionally guard (D0 behind Accept, below), local_kind: "lora-kernel" with lora_kernel (the local lane through lora-kernel's specialists, below) and agentvcs (the executable that versions the memory).
lane-moves.jsonl) and becomes part of what the director reflects on.<log dir>/vcs) and makes it the live memory. Reject leaves the memory as it was.claude -p --output-format stream-json). Read-only tools (Read, Grep, Glob) unless the config grants more.offline.[ran] 2026-10-14, one real request ("check that agentvcs's two READMEs state the same number of golden cases; draft a changelog note"): the director walked 6 pages, planned 2 tasks citing 6 statements, put both on the frontier because the local lane was offline, and the frontier verified 117 = 117 = the cases on disk. Tests: python -m pytest tests -q (a fake claude and a fake local server; in CI).
Accept is guarded by D0, targeted ("guard": {"enabled": true, "mode": "targeted", "k": 3, "config": "<private d0 config>"}, D6): the guard re-plans only the D0 cases whose plans relied on a statement the edit removes or changes in
hooks/ui.tsx 369 lines1// The director's band and status line. The status line names the memory and what waits for a decision; the band
2// above the prompt shows a plan waiting to run (each task's lane; Run / Move / Cancel), a proposed lesson (its edits,
3// its check, Diff / Accept / Reject) and the delegations not yet brought back. Both read director/ui_state.py and stay silent where the project has no director memory.
4import { atom, read, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7import type { Snapshot, Step } from '../types'
8
9const snap = atom({ plugin: 'director', key: 'snap' } as const, null)
10const diff = atom({ plugin: 'director', key: 'diff' } as const, null)
11const busy = atom({ plugin: 'director', key: 'busy' } as const, null)
12const message = atom({ plugin: 'director', key: 'message' } as const, null)
13const notes = atom({ plugin: 'director', key: 'notes' } as const, [] as string[])
14const steps = atom({ plugin: 'director', key: 'steps' } as const, [] as Step[])
15const verbose = atom({ plugin: 'director', key: 'verbose' } as const, true)
16
17const TOOL_PREFIX = 'mcp__plugin_director_director__'
18
19export function statusText(s: Snapshot | null, live: Step[] = [], now = Date.now()): string | undefined {
20 if (!s || !s.memory) return undefined
21 if (s.error) return `director · ${s.error.slice(0, 60)}`
22 const n = s.statements ?? 0
23 const parts = [`director · ${n} statement${n === 1 ? '' : 's'}`, `${s.accepted ?? 0} learned`]
24 const waiting = s.proposals?.length ?? 0
25 if (waiting) parts.push(`${waiting} to decide`)
26 const plans = s.plans?.length ?? 0
27 if (plans) parts.push(`${plans} plan${plans === 1 ? '' : 's'} waiting`)
28 const out = s.delegations?.length ?? 0
29 if (out) parts.push(`${out} delegated`)
30 if (s.recipes_stale) parts.push('recipes stale')
31 if (s.to_reflect) parts.push(`${s.to_reflect} to reflect`)
32 const lanes = laneText(live, s, now)
33 return lanes ? `${parts.join(' · ')} │ ${lanes}` : parts.join(' · ')
34}
35
36const MEMORY_PATH = /\/\.director\/memory\/([^/]+?)\.md$/
37
38// One tool call, seen live: which lane it belongs to and a short label. The director's own steps are its MCP tools
39// and the walk (a read of one of its memory's pages); everything else the session does is the frontier lane, and a
40// plan made on the local model (plan_local) or a task sent to a lane other than the frontier is the local lane.
41export function classify(tool: string, args: Record<string, unknown>): { lane: Step['lane']; label: string } {
42 const str = (k: string) => (typeof args[k] === 'string' ? (args[k] as string) : '')
43 const path = str('file_path') || str('path')
44 const page = path.match(MEMORY_PATH)
45 if (page) return { lane: 'director', label: `walk → ${page[1]}` }
46 if ((tool === 'Grep' || tool === 'Glob') && path.includes('/.director/memory'))
47 return { lane: 'director', label: `search memory: ${str('pattern')}`.slice(0, 80) }
48 if (tool.startsWith(TOOL_PREFIX)) {
49 const name = tool.slice(TOOL_PREFIX.length)
50 if (name === 'plan_local') return { lane: 'local', label: `plan_local: ${str('request')}`.slice(0, 80) }
51 if (name === 'delegate') return { lane: 'local', label: `→ ${str('lane')}: ${str('task')}`.slice(0, 80) }
52 const detail = str('rationale') || str('kind') || str('id') || str('request')
53 return { lane: 'director', label: `${name}${detail ? `: ${detail}` : ''}`.slice(0, 80) }
54 }
55 const base = (f: string) => f.split('/').pop() || f
56 const label =
57 tool === 'Bash'
58 ? str('description') || str('command').split('\n')[0]
59 : tool === 'Read' || tool === 'Edit' || tool === 'Write'
60 ? `${tool} ${base(path)}`
61 : tool === 'Grep' || tool === 'Glob'
62 ? `${tool} ${str('pattern')}`
63 : tool === 'Agent'
64 ? `subagent: ${str('description')}`
65 : tool.startsWith('mcp__')
66 ? tool.split('__').slice(-1)[0]
67 : tool
68 return { lane: 'frontier', label: `${tool === 'Bash' ? '$ ' : ''}${label}`.slice(0, 80) }
69}
70
71function ago(step: Step, now: number): string {
72 const secs = Math.round(((step.ms ?? now - step.t) as number) / 1000)
73 return step.state === 'running' ? `${secs}s…` : step.state === 'error' ? 'failed' : `${secs}s`
74}
75
76// The status line's live half: what each lane is doing now, or did last.
77export function laneText(live: Step[], s: Snapshot | null, now = Date.now()): string {
78 const last = (lane: Step['lane']) => [...live].reverse().find(x => x.lane === lane)
79 const f = last('frontier')
80 const l = last('local')
81 const out = (s?.delegations ?? []).find(d => !d.done)
82 const local = l ? `${l.label} (${ago(l, now)})` : out ? `${out.lane}: ${out.id} (${out.status})` : 'idle'
83 if (!f && !l && !out) return ''
84 return `frontier ▶ ${f ? `${f.label} (${ago(f, now)})` : 'idle'} │ local ▶ ${local}`
85}
86
87type Result = { error?: string } & Record<string, unknown>
88
89let poll: { cancel: () => void } | null = null
90let tick: { cancel: () => void } | null = null
91
92// The status line again from what is already known (no subprocess): called on every step and, while a step runs,
93// every second, so the elapsed time moves.
94async function redraw($: EngineInterface): Promise<void> {
95 const live = await read($, steps)
96 $.ui.status(statusText(await read($, snap), live))
97 const running = live.some(x => x.state === 'running')
98 if (running && !tick) tick = $.clock.every(1_000, () => void redraw($))
99 else if (!running && tick) {
100 tick.cancel()
101 tick = null
102 }
103}
104
105async function record($: EngineInterface, step: Step): Promise<void> {
106 await update($, steps, list => [...list, step].slice(-40))
107 await redraw($)
108}
109
110async function finish($: EngineInterface, t: number, state: Step['state']): Promise<void> {
111 await update($, steps, list => list.map(x => (x.t === t ? { ...x, state, ms: Date.now() - x.t } : x)))
112 await redraw($)
113}
114
115async function run($: EngineInterface, args: string[]): Promise<Result> {
116 const root = await $.session.root()
117 try {
118 const r = await $.process.run(['python3', `${$.plugin.root}/director/ui_state.py`, ...args], {
119 cwd: root,
120 env: { CLAUDE_PROJECT_DIR: root },
121 timeoutMs: args[0] === 'fetch' ? 600_000 : 150_000,
122 })
123 return JSON.parse(r.stdout.trim().split('\n').pop() || '{}') as Result
124 } catch (err) {
125 return { error: `ui_state.py failed: ${String(err).slice(0, 200)}` }
126 }
127}
128
129async function refresh($: EngineInterface): Promise<void> {
130 const s = (await run($, ['status'])) as unknown as Snapshot
131 await update($, snap, () => s)
132 $.ui.status(statusText(s, await read($, steps)))
133 const checking = (s.proposals ?? []).some(p => p.check === 'RUNNING')
134 if (checking && !poll) {
135 poll = $.clock.every(20_000, () => void refresh($))
136 } else if (!checking && poll) {
137 poll.cancel()
138 poll = null
139 }
140}
141
142// An action from a button: run it, say how it went, tell the session on the next prompt, and re-read the state.
143async function act($: EngineInterface, args: string[], note: string | ((r: Result) => string)): Promise<void> {
144 const what = args.join(' ')
145 await update($, busy, () => what)
146 const r = await run($, args)
147 const text = r.error ? `${what}: ${r.error}` : `${what}: done`
148 await update($, message, () => text)
149 if (!r.error) await update($, notes, xs => [...xs, typeof note === 'string' ? note : note(r)])
150 await update($, busy, () => null)
151 $.ui.toast(`director: ${text}`)
152 await refresh($)
153}
154
155// What the session needs to hear after Run: where each task went, and which are its own to do now.
156export function runSummary(r: Result): string {
157 const results = (r.results ?? []) as { id: string; lane: string; status?: string; error?: string }[]
158 return results
159 .map(x =>
160 x.error
161 ? `${x.id} failed on ${x.lane} (${x.error})`
162 : x.lane === 'frontier'
163 ? `${x.id} is yours to do now`
164 : `${x.id} delegated to ${x.lane}`,
165 )
166 .join('; ')
167}
168
169async function showDiff($: EngineInterface, id: string): Promise<void> {
170 const r = await run($, ['diff', id])
171 await update($, diff, () => ({ id, text: String(r.diff ?? r.error ?? '') }))
172}
173
174export const register: Register = on => {
175 on('session.start', async ($, e, next) => {
176 const started = await next(e)
177 // A surface without slash commands still gets the status line and the band.
178 await $.command
179 .register({
180 name: 'director-activity',
181 description: "Show or hide the director's live activity above the prompt (the status line always shows the lanes)",
182 })
183 .catch(() => undefined)
184 await refresh($)
185 return started
186 })
187
188 on('command.run', { command: 'director-activity' }, async $ => {
189 const on_ = !(await read($, verbose))
190 await update($, verbose, () => on_)
191 return { text: `director activity above the prompt: ${on_ ? 'shown' : 'hidden'}` }
192 })
193
194 on('turn.complete', async ($, e, next) => {
195 const done = await next(e)
196 await refresh($)
197 return done
198 })
199
200 // Every tool call, live: recorded on its lane when it starts and closed when it ends, so the status line and the band
201 // show what each lane is doing at every moment. The director's own tools also re-read its state.
202 on('tool.call', async ($, e, next) => {
203 const { lane, label } = classify(e.tool, e as unknown as Record<string, unknown>)
204 const t = Date.now()
205 await record($, { t, lane: e.agentId && lane === 'frontier' ? 'frontier' : lane, label: e.agentId ? `[subagent] ${label}` : label, state: 'running' })
206 try {
207 const result = await next(e)
208 await finish($, t, 'deny' in result && result.deny ? 'error' : result.isError ? 'error' : 'done')
209 if (e.tool.startsWith(TOOL_PREFIX)) void refresh($)
210 return result
211 } catch (err) {
212 await finish($, t, 'error')
213 throw err
214 }
215 }).catch(($, e, next) => next(e))
216
217 // What the person did on the band reaches the model with their next prompt, so the session's picture stays true.
218 on('prompt.submit', async ($, e, next) => {
219 const pending = await read($, notes)
220 if (!pending.length || e.command) return next(e)
221 await update($, notes, () => [])
222 return next({ ...e, context: [...(e.context ?? []), `On the director's band, the person: ${pending.join('; ')}.`] })
223 }).catch(($, e, next) => next(e))
224
225 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
226 const s = await read($, snap)
227 const proposals = s?.proposals ?? []
228 const delegations = s?.delegations ?? []
229 const plans = s?.plans ?? []
230 const live = await read($, steps)
231 const showActivity = (await read($, verbose)) && (live.length > 0 || (s?.recent ?? []).length > 0)
232 if (e.props.hasSurvey || !s?.memory || (!proposals.length && !delegations.length && !plans.length && !showActivity))
233 return next(e)
234
235 const { Box, Button, Text, Code } = $.ui.resolve(e)
236 const shown = await read($, diff)
237 const working = await read($, busy)
238 const said = await read($, message)
239 const p = proposals[0]
240 const plan = plans[0]
241
242 const now = Date.now()
243 const directorSteps = live.filter(x => x.lane === 'director').slice(-5)
244 const journal = (s?.recent ?? []).slice(-3)
245 const laneLine = (lane: Step['lane']) => {
246 const x = [...live].reverse().find(y => y.lane === lane)
247 return x ? `${x.label} · ${ago(x, now)}` : 'idle'
248 }
249
250 return (
251 <Box flexDirection="column">
252 {showActivity && (
253 <Box flexDirection="column">
254 <Text bold>Director · activity</Text>
255 {directorSteps.map(x => (
256 <Text key={`step-${x.t}`} dimColor={x.state !== 'running'}>
257 {x.state === 'running' ? '▶ ' : x.state === 'error' ? '✗ ' : '✓ '}
258 {x.label}
259 </Text>
260 ))}
261 {journal.map(j => (
262 <Text key={`journal-${j.t}-${j.kind}`} dimColor>
263 · {j.kind}: {j.what}
264 </Text>
265 ))}
266 <Text>
267 <Text bold>frontier</Text> ▶ {laneLine('frontier')}
268 </Text>
269 <Text>
270 <Text bold>local</Text> ▶ {laneLine('local')}
271 </Text>
272 </Box>
273 )}
274 {plan && (
275 <Box flexDirection="column">
276 <Text bold>Director · a plan to run{plans.length > 1 ? ` (1 of ${plans.length})` : ''}</Text>
277 {plan.request ? <Text>{plan.request}</Text> : null}
278 {plan.tasks.map(t => (
279 <Box key={`task-${plan.id}-${t.id}`} flexDirection="row" gap={1}>
280 <Text>
281 <Text bold>{t.lane}</Text> {t.task}
282 {t.confidential ? <Text dimColor> (confidential)</Text> : null}
283 </Text>
284 {t.movable ? (
285 <Button
286 key={`move-${plan.id}-${t.id}`}
287 label="Move"
288 onPress={() =>
289 act($, ['plan-move', plan.id, t.id], r => `moved task ${t.id} from ${t.lane} to ${String(r.to)}`)
290 }
291 />
292 ) : null}
293 </Box>
294 ))}
295 <Box flexDirection="row" gap={1}>
296 <Button
297 key={`run-${plan.id}`}
298 label="Run"
299 variant="primary"
300 onPress={() => act($, ['plan-run', plan.id], r => `ran plan ${plan.id}: ${runSummary(r)}`)}
301 />
302 <Button
303 key={`cancel-${plan.id}`}
304 label="Cancel"
305 onPress={() => act($, ['plan-cancel', plan.id], `cancelled plan ${plan.id} (nothing was delegated)`)}
306 />
307 </Box>
308 </Box>
309 )}
310 {p && (
311 <Box flexDirection="column">
312 <Text bold>Director · a lesson to decide{proposals.length > 1 ? ` (1 of ${proposals.length})` : ''}</Text>
313 {p.rationale ? <Text>{p.rationale}</Text> : null}
314 <Text dimColor>{p.edits.join(', ')}</Text>
315 <Text dimColor>
316 {p.check === null ? 'not checked' : `check: ${p.check}`}
317 {p.lint ? ` · ${p.lint} lint problems` : ''}
318 </Text>
319 {!p.acceptable ? <Text color="warning">Cannot be accepted: {p.why_not}</Text> : null}
320 {shown && shown.id === p.id ? <Code source={shown.text || '(no changes)'} format="diff" /> : null}
321 <Box flexDirection="row" gap={1}>
322 <Button
323 key={`diff-${p.id}`}
324 label={shown && shown.id === p.id ? 'Hide diff' : 'Diff'}
325 onPress={() =>
326 shown && shown.id === p.id ? update($, diff, () => null) : showDiff($, p.id)
327 }
328 />
329 {p.acceptable ? (
330 <Button
331 key={`accept-${p.id}`}
332 label="Accept"
333 variant="primary"
334 onPress={() => act($, ['accept', p.id], `accepted proposal ${p.id} (now the live memory)`)}
335 />
336 ) : null}
337 <Button
338 key={`reject-${p.id}`}
339 label="Reject"
340 onPress={() => act($, ['reject', p.id], `rejected proposal ${p.id} (the memory is unchanged)`)}
341 />
342 </Box>
343 </Box>
344 )}
345 {delegations.map(d => (
346 <Box key={`lane-${d.id}`} flexDirection="row" gap={1}>
347 <Text>
348 {d.lane}: {d.id} <Text dimColor>{d.status}</Text>
349 </Text>
350 <Button
351 key={`refresh-${d.id}`}
352 label="Status"
353 onPress={() => act($, ['refresh', d.id], `asked lane ${d.lane} for the status of ${d.id}`)}
354 />
355 {d.done ? (
356 <Button
357 key={`fetch-${d.id}`}
358 label="Fetch"
359 onPress={() => act($, ['fetch', d.id], `fetched delegation ${d.id} from lane ${d.lane}`)}
360 />
361 ) : null}
362 </Box>
363 ))}
364 {working ? <Text dimColor>working: {working}…</Text> : said ? <Text dimColor>{said}</Text> : null}
365 </Box>
366 )
367 })
368}
369types/index.d.ts 47 lines1export type Proposal = {
2 id: string
3 rationale: string
4 edits: string[]
5 lint: number
6 check: string | null
7 probes: number
8 acceptable: boolean
9 why_not: string
10}
11
12export type Delegation = { id: string; lane: string; status: string; done: boolean }
13
14export type PlanTask = { id: string; task: string; lane: string; confidential: boolean; movable: boolean }
15
16export type Plan = { id: string; request: string; tasks: PlanTask[] }
17
18export type Snapshot = {
19 memory: string | null
20 statements?: number
21 accepted?: number
22 proposals?: Proposal[]
23 delegations?: Delegation[]
24 plans?: Plan[]
25 recipes_stale?: boolean
26 to_reflect?: number
27 recent?: { t: string; kind: string; what: string }[]
28 error?: string
29}
30
31/** One step seen live in this session: a tool call on a lane, or a step of the director's own. */
32export type Step = { t: number; lane: 'frontier' | 'local' | 'director'; label: string; state: 'running' | 'done' | 'error'; ms?: number }
33
34declare module 'claude-code' {
35 interface PluginState {
36 director: {
37 snap: Snapshot | null
38 diff: { id: string; text: string } | null
39 busy: string | null
40 message: string | null
41 notes: string[]
42 steps: Step[]
43 verbose: boolean
44 }
45 }
46}
47