Skip to content

feat(mcp): coordinator agent with task visibility, pilot/co-pilot control, and expanded options#100

Draft
brooksc wants to merge 39 commits intojohannesjo:mainfrom
brooksc:feature/orchestrator-control-v2
Draft

feat(mcp): coordinator agent with task visibility, pilot/co-pilot control, and expanded options#100
brooksc wants to merge 39 commits intojohannesjo:mainfrom
brooksc:feature/orchestrator-control-v2

Conversation

@brooksc
Copy link
Copy Markdown
Contributor

@brooksc brooksc commented May 4, 2026

Overview

This PR builds on the initial coordinating agent implementation by @cledoux95 (PR #31) with substantial fixes and new capabilities.

The core motivation: I wanted to use a coordinator agent to drive parallel workstreams, but still be able to monitor each sub-task, answer questions when an agent gets stuck, and take over a task when I want to make a decision myself — then hand it back. Think of it like a pilot/co-pilot handoff: explicit, visible, and safe.


Credit

The foundation of this PR is @cledoux95's work in #31, which introduced the coordinating agent concept, the MCP server/client, and the create_task / send_prompt / wait_for_idle / merge_task tools. This PR would not exist without that work.


What's new / changed

Sub-task visibility (addresses the core UX gap)

In #31, sub-tasks created by the coordinator were not visible in the sidebar. The coordinator agent was essentially working in the dark. Now each sub-task spawned via create_task appears as its own sidebar panel, giving you full terminal visibility into what every agent is doing — exactly like a manually created task.

Pilot/co-pilot control handoff

Each coordinated sub-task shows a banner indicating who is driving:

  • "Orchestrator driving" (subtle grey bar) — the coordinator agent has control; you can observe but the agent is running
  • "You have control — orchestrator is paused" (amber warning bar) — you've taken over; the coordinator cannot send further prompts until you explicitly return control

You can click "Take Control" at any time to pause the coordinator for that task, interact with the agent yourself, then click "Return to Orchestrator" when done.

Expanded create_task options

The coordinator agent can now specify:

  • skipPermissions: true — passes --dangerously-skip-permissions to the sub-agent so it runs fully autonomously without tool-approval interruptions
  • gitIsolation: "worktree" | "direct" | "none" — controls git isolation mode (defaults to "worktree" as before)

Fixes to #31

Several bugs were found and fixed while integrating and testing:

Issue Fix
Hardcoded port 7777 — if the port was in use, the MCP server silently failed findFreePort(7777, 7800) tries ports sequentially until one is free
Missing --mcp-config arg — the coordinator agent was spawned without the MCP config flag, so it had no MCP tools and fell back to bash orchestration TaskAITerminal now passes --mcp-config <path> when task.mcpConfigPath is set
Double-spawn — the orchestrator backend called spawnAgent directly AND the renderer's TerminalView also called it, killing the backend spawn and losing the output subscription Removed backend spawnAgent call; orchestrator now uses onPtyEvent('spawn', ...) to subscribe to output each time the renderer spawns the agent — survives restarts too
MCP server not restarted after app restart — persisted coordinator tasks had no server to connect to after restart, causing fetch failed on all MCP tool calls On startup, App.tsx now awaits StartMCPServer for each persisted coordinator task before the agents are spawned, refreshing the config file with the new port/token
deleteTask wrong call signature — the orchestrator called deleteTask with positional args but the function now takes an options object Fixed to deleteTask({ agentIds, branchName, deleteBranch, projectRoot })
Missing REST input validation — the orchestrator API routes accepted arbitrary input Added type guards returning 400 for invalid name, prompt, projectId, skipPermissions, gitIsolation
waitForIdle hangs when human takes controlsetTaskControl('human') left pending waiters stuck until timeout setTaskControl now unblocks pending waitForIdle callers on any control change
mcp_control_changed missing from preload allowlist Added to ALLOWED_CHANNELS in preload.cjs
Better connection error messagesfetch failed gave no actionable info MCPClient now wraps network errors: "Cannot reach Parallel Code at http://127.0.0.1:PORT. Is the app running?"

Tests

Added 25 new tests across two files:

  • electron/mcp/prompt-detect.test.tsstripAnsi and chunkContainsAgentPrompt (the core of waitForIdle idle detection)
  • electron/mcp/orchestrator.test.ts — control handoff logic: sendPrompt blocked/unblocked, waitForIdle behavior under human control, PTY exit propagation

The tests caught the waitForIdle hang bug described above before it shipped.


Test plan

  • Create a new task with Coordinator mode enabled
  • Coordinator agent spawns sub-tasks; each appears as a new sidebar panel
  • Sub-task panels show the "Orchestrator driving" banner
  • Click Take Control — banner switches to amber "You have control"
  • Coordinator cannot send prompts while human has control (MCP tool returns error)
  • Click Return to Orchestrator — coordinator resumes
  • Kill and relaunch the app; open a coordinator task — MCP tools still work (no fetch failed)
  • create_task with skipPermissions: true spawns sub-agent without permission prompts
  • npm run check && npm test — all pass

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brooksc brooksc force-pushed the feature/orchestrator-control-v2 branch from 2e523b0 to b436f19 Compare May 4, 2026 03:27
@brooksc
Copy link
Copy Markdown
Contributor Author

brooksc commented May 5, 2026

@cledoux95 as you initiated this I'd love to get your thoughts on my building further on your work.
@johannesjo this PR as is, is useful but I don't think it's ready yet.

Here's what I'm trying to solve for.

  • I have a medium size project where I'm using (surprise!) parallel claude code to build it
  • I'm using backlog.md to build up a healthy list of items that I curate.
  • Prior to this I'd switch to the browser to view backlog.md's kanban, copy a TASK-###, switch to parallel code, open a new task and paste it in. And repeat until I had 3-5 tasks running.

What I'm hoping to get to:

  1. I have the coordinator agent reviewing what's on the backlog and determining what's next.
  2. It fires off up to X (e.g. 5) paralell claude codes
  3. I can see them and interact with them, take control from the claude code, interact with it and return control
  4. The coordinator monitors for when an agent is done. Flags to me when there are questions I need to address. Review's it's final results and then tells it to commit and rebase.

1-3 is working here (although the take control/return control may need some work)
4 -- the agent has to be prompted to "go check on the agents" and it then does the commit/rebase nicely and closes the window.

I'm thinking I need some webhook or way for agentN to notify the coordinator when it's done (hook...?)

Anyway... this may take some time to polish. @johannesjo What do you think about adding a "Beta" section in settings, when you're ready to integrate it - it can live in Beta for a little while so others can opt-in and shake it out if they want. I think it'll take some time to live with this to continue to refine it. I'm using it myself going forward.

@johannesjo
Copy link
Copy Markdown
Owner

johannesjo commented May 6, 2026

Thank you very much @brooksc (and @cledoux95 !) !! This is impressive work, and the integration fixes on top of #31 are exactly the kind of stuff that's painful to discover from the outside. I want to ship this, but I agree with you that it's not ready as a default-on feature, and I think your "Beta in Settings" instinct is the right framing. Let me make that concrete.

Yes to beta-gating, with a real flag. Specifically:

  • A single experimental.coordinatorMode setting, off by default.
  • When off, the MCP server module is not started (lazy import() on flag flip), the Coordinator option in NewTaskDialog is hidden, and the mcp_control_changed IPC channel isn't exposed to the renderer. Goal: zero footprint for users who don't opt in.
  • Coordinator-specific code lives in its own folders (electron/mcp/, src/components/coordinator/, a dedicated store slice), and the cross-cutting touches (TaskPanel, Sidebar) take a coordinator-aware prop rather than inlining the logic. This keeps future unrelated PRs from having to reason about coordinator state.

Three things I'd want addressed before I merge, even into beta:

  1. Post-restart MCP path – please add an integration test that confirms the port/token rotation actually rewrites the config the agent reads on next launch. That's the most fragile part by design and the unit tests don't cover it.
  2. skipPermissions guardrail--dangerously-skip-permissions is a real footgun. I'd like it to require both the beta flag and an explicit per-task confirmation in the UI, not just an MCP tool argument the coordinator can pass.
  3. Feature flag wraps startup + IPC registration, not just UI affordances – so toggling it off genuinely removes the surface area.

On your unsolved step #4 (coordinator noticing "agent done"): I think Claude Code's Stop / SubagentStop hooks are the right primitive here – a hook posting back to the local MCP server would replace the polling/manual-nudge loop with a real notify channel. Worth exploring before we promote out of beta; not a blocker for landing the beta itself.

Graduation criteria I'd want to hit before flipping the flag on by default:

  • Restart-with-running-coordinator works reliably across macOS and Linux.
  • No leaked PTY/zombie processes over a 24h session.
  • The "agent finished" notification is solved (hooks or equivalent).
  • A few weeks in beta with no P0 bugs from opt-in users.

If you're up for the gating work I'm happy to keep this open and help where useful. And to be clear: I do want to ship this – the pilot/co-pilot pattern is the right shape for human-in-the-loop orchestration, and you've already shaved off a lot of the rakes.

@brooksc
Copy link
Copy Markdown
Contributor Author

brooksc commented May 7, 2026

@johannesjo — thanks for the detailed review, this is exactly the kind of feedback that makes it worth sharing early. Lots to respond to.


Where things stand: working e2e

Since the original PR I've built considerably more on top. Here's what's confirmed working end-to-end:

Coordinator creation & MCP tooling

  • "Coordinator mode" checkbox in New Task dialog; only one active coordinator per project enforced
  • MCP server starts automatically; coordinator agent sees: create_task, list_tasks, get_task_status, send_prompt, wait_for_idle, wait_for_signal_done, get_task_diff, get_task_output, merge_task, close_task
  • Sub-tasks get only signal_done (not create_task etc.) so there's no runaway recursion

Sub-task lifecycle

  • create_task spawns a worktree + agent, injects a [SUB-TASK MODE] preamble so the agent knows its role and constraints
  • Sub-task inherits the coordinator's agent command, args, and --dangerously-skip-permissions state
  • merge_task and close_task work cleanly, including worktree cleanup

Coordinator ↔ sub-agent notification — and why we didn't use Stop hooks

This is where we diverge from your suggestion, and I think for good reason. You suggested Stop/SubagentStop hooks posting back to the MCP server. The problem: Claude Code fires Stop any time the agent pauses — including when it hits a question mid-task or runs out of context. What we actually want is "agent has finished its assigned work and is ready for review."

Instead, sub-agents call the signal_done MCP tool explicitly when they're done. The coordinator receives a staged notification into its PromptInput textarea with a summary of which tasks completed, which branch to review, and whether there was a non-zero exit. The notification auto-fires after a quiet period (default 60s, faster on error) if the coordinator is idle — so the coordinator agent processes it automatically without manual nudging. This is the step 4 from the original description working.

Additional notification paths:

  • If the coordinator calls send_prompt while the user has taken control of a sub-task, it gets an error immediately (not a silent hang). When the user returns control, the coordinator gets a "Task X is back under your control" notification so it can retry without human prompting.
  • If the user closes a sub-task before its prompt even lands, the coordinator still gets notified — previously it would silently think 2 tasks were running when only 1 was.

Pilot / co-pilot control handoff — how it works and two open items

Each sub-task panel shows a banner indicating who's driving. When the coordinator has control the banner reads "Coordinator driving" in grey. Clicking "Take Control" blocks the coordinator's send_prompt from reaching that sub-task — the banner turns amber. Clicking "Return to Coordinator" restores it and automatically notifies the coordinator if it was blocked mid-action.

Two things to note as known gaps: first, the button labels ("Take Control" / "Return to Orchestrator") overstate what's happening — the coordinator agent keeps running, only its ability to write to that terminal is paused. "Pause coordinator" / "Resume coordinator" would be more accurate. Second, the sub-task's PromptInput and raw xterm terminal are not currently gated on control state, so a user could type simultaneously with a coordinator send_prompt. The fix is to disable input when the coordinator has control and surface "Pause coordinator" as the way to unlock it — making the model self-documenting rather than relying on reading the banner.

Unit test coverage
379 tests covering: notification staging, ack/dedup, batch formatting, idle detection, signal_done, waiter resolver leak-on-timeout, per-task projectRoot isolation, spawn settings inheritance, control handoff notification, early-close notification.


What isn't done yet

Beta gating — you're right that zero-footprint opt-in is the right approach. We haven't done the experimental.coordinatorMode flag, lazy MCP server import, or IPC registration gating. That's the right next piece and I'm happy to do it.

Settings dialog placement — with coordinator mode, Docker, verbose logging, and more to come, Settings is getting long. A tabbed settings dialog (General / Experimental) would be a natural home for the beta flag, but that's its own PR and should land independently of this one.

Docker + coordinator — currently mutually exclusive. Sub-tasks spawned by a coordinator run as native host processes, defeating Docker isolation. Two approaches documented in KNOWN-TODOS.md: same container via docker exec, or per-sub-task containers. Prerequisite decision: should coordinator mode force direct git isolation (no worktree)? Architecturally that's cleaner since coordinators don't commit code themselves.

Post-restart MCP path integration test — fair ask. The port/token rotation path is the right thing to test. Will add.

skipPermissions guardrail — also fair. Currently if the coordinator is started with skip-permissions, all sub-tasks inherit it. An explicit per-task confirmation in the UI is the right guardrail. One nuance: for the "40 tasks, spawn 2 at a time" workflow, the user probably wants to grant it once rather than 40 times — so I'd suggest a "propagate skip-permissions to sub-tasks" checkbox that requires explicit opt-in, separate from the per-task flow.

Minor items (in KNOWN-TODOS.md): orphaned sub-task badge UI, re-stage notification after user manually sends an edited prompt, configurable notification delay, control handoff input gating and button renaming.


Happy to tackle the beta gating as the next chunk. Does the experimental.coordinatorMode approach you outlined work as the gating mechanism, or do you have a different shape in mind for the flag?

@brooksc brooksc marked this pull request as draft May 7, 2026 05:06
@brooksc brooksc force-pushed the feature/orchestrator-control-v2 branch from 4057d03 to 0be08ca Compare May 7, 2026 05:11
@johannesjo
Copy link
Copy Markdown
Owner

Thanks @brooksc — the depth of the response (and the 379 tests!) tells me where this is heading, and I'm on board with the shape. A few replies and a few new things I noticed reading the diff today.

signal_done over Stop/SubagentStop — agreed, you're right. Your reasoning on Stop firing for mid-task pauses is correct, and the auto-fire-on-quiet staging is a nicer fit than I'd given it credit for. Withdrawing that suggestion.

Beta gating shape: experimental.coordinatorMode works for me. Single boolean, persisted in app state, off by default. Worth doing as its own follow-up PR after the rest lands, or first-in-line — your call. I'd defer the tabbed Settings dialog as you suggested.

skipPermissions guardrail — the propagation checkbox is the right shape. Good refinement: a "propagate skip-permissions to sub-tasks" checkbox in the New Task dialog, defaulting off even when the coordinator itself was launched with skip-permissions. Don't auto-inherit silently. The 40-tasks-at-a-time workflow is real and worth optimizing for.

A few things I caught on a code pass that haven't come up yet:

  1. The post-restart fix described in the PR body isn't actually in the diff. The description says "App.tsx awaits StartMCPServer for each persisted coordinator task." `git grep StartMCPServer` finds it only in `src/store/tasks.ts` (the create-task path). `mcpConfigPath` is also absent from both `saveState` and `loadState` in `src/store/persistence.ts`, so on next launch `TaskAITerminal` skips `--mcp-config` entirely (it's gated on `props.task.mcpConfigPath` at `TaskAITerminal.tsx:223`) and the worktree's `.mcp.json` carries the previous run's token. Could you verify that test-plan item ("Kill and relaunch the app … MCP tools still work") is actually green today? My read is it would `fetch failed` on the first MCP call.

  2. `CLAUDE.md` mutation in the worktree is risky when combined with skip-permissions. `coordinator.ts` writes a `<!-- parallel-code-subtask-start -->` block into the worktree's `CLAUDE.md` and restores it via `setTimeout(..., 3000)` after first idle (`coordinator.ts:317,328`). A skip-permissions sub-agent that decides to `git add -A && git commit` early on will commit our injection. The `git restore` fallback runs with `stdio: 'ignore'` (line 322), so a failure is invisible. Could we use `--append-system-prompt` (if Claude Code supports it) or write to `.claude/settings.local.json` (already gitignored) instead of mutating a tracked file?

  3. `server.listen('0.0.0.0', ...)` + new mutating REST endpoints. Pre-existing for the mobile-remote feature, where token-bearer auth was guarding a read-mostly surface. With this PR, the same token now also gates `POST /api/tasks` (spawn worktree+process), `POST /api/tasks/:id/merge`, `DELETE /api/tasks/:id`, `POST /api/tasks/:id/prompt`. I think we should either (a) bind to 127.0.0.1 by default and only widen when the user explicitly enables remote mobile, or (b) scope the token so coordinator endpoints require an additional capability the mobile-remote token doesn't get. Option (a) is the smaller change.

  4. `waitForIdle` resolves silently on human takeover. `coordinator.ts:495` returns `Promise.resolve()` immediately when `controlMap.get(taskId) === 'human'`. The coordinator can't tell apart "agent went idle" from "human paused agent" — it just sees `{status: "running"}` afterwards and likely loops. Worth changing the resolved value to `{ reason: 'idle' | 'human_control' | 'exited' }` so the agent can branch on it.

  5. Token file permissions. `.mcp.json` (worktree, `register.ts:1075`), `parallel-code-mcp-*.json` (tmp, `register.ts:1067`), and the per-sub-task config (`coordinator.ts:374`) are all written with default umask = `0644`. On a shared machine, other local users can read the token. `{ mode: 0o600 }` everywhere we write a token-bearing config.

  6. Small things: `controlMap` accepts unknown taskIds (no `tasks.has` check in `setTaskControl`); `coordinatorTaskId: 'api'` is a magic-string sentinel that should be `undefined`; `get_task_diff` truncates at 50 KB without reporting the original size; the `gitIsolation` option mentioned in the PR description isn't in the `create_task` JSON schema in `server.ts`.

Item #1 is the only one that affects whether the test plan currently passes; the rest can ride along with the beta-gating PR. Excited about this — the pilot/co-pilot framing has aged well over the discussion.

brooksc and others added 22 commits May 7, 2026 19:40
Adds HTTP REST endpoints for task management (create, list, get, prompt,
wait, diff, output, signal-done, merge, close) plus an MCP log ring buffer.
Also adds AGENTS.md (project context for AI agents) and rebuild-integration.sh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… diff truncation metadata, gitIsolation schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- wait_for_signal_done redesign: removed taskId parameter; returns
  whichever sub-task completes next with a `remaining` count so the
  coordinator can loop naturally without guessing order
- merge_task / review_and_merge_task: operate in the sub-task's worktree
  so the target branch doesn't need to be checked out in the main repo
- Docker: sub-agents spawned via docker exec into the coordinator's
  container when coordinator runs with Docker mode enabled
- Preamble injection: writes signal_done instructions to the right file
  per agent type (AGENTS.md / GEMINI.md / .agent.md /
  settings.local.json) in the worktree rather than the project root;
  file is cleaned up by close_task
- Notification system: suppress pending notifications during signal waits
  to prevent double-notify; escalate to orphaned notification after 10
  missed autofire attempts; clear stale notifications on coordinator
  deregistration
- get_task_output: append truncation sentinel when scrollback exceeds
  20 000 chars so the coordinator knows it saw partial output
- wait_for_idle: return reason field (idle / exited / human_control)
- Autofire: countdown UI; don't treat previous staged text as user edit
- Persistence: persist mcpConfigPath across app restarts; rewrite config
  on startup with current port/token
- Tests: 63 unit tests in coordinator.test.ts; git mergeWorktreePath test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Coordinators can drive sub-tasks via send_prompt; users can reclaim a
task at any time with Take Control and hand it back with Release Control.
This PR extends that model to the coordinator task itself.

Control bar (TaskPanel)
- Renamed "Pause/Resume coordinator" → "Take Control / Release Control"
  for clearer ownership language
- Extended to coordinator tasks (coordinatorMode) in addition to sub-tasks
  (coordinatedBy): the same bar now appears on the coordinator's own panel
- Auto-mode label: "Auto mode" for coordinator tasks, "Coordinator driving"
  for sub-tasks

Input locking (PromptInput + TerminalView)
- PromptInput textarea is disabled when controlledBy === 'coordinator';
  onKeyDown/onInput guards provide belt-and-suspenders protection
- TerminalView: term.options.disableStdin toggled reactively so xterm
  stops forwarding keystrokes to the PTY (the real input path users take)
- Autofire interval skips ticks (no miss count) when controlledBy is human

Coordinator task panel UX
- Invisible pointer-events overlay gives coordinator task the same
  cursor:not-allowed treatment sub-tasks already had
- Overlay click propagates to maybeShowControlHint, enabling the
  discoverability tooltip (shown ≤3 times, dismissible, "Don't show again")
- PromptInput panel hidden (display:none, minSize getter → 0) in auto mode
  while keeping the component mounted so autofire keeps running

Control state persistence
- controlledBy persisted in PersistedTask; restored on app restart
- Default: 'coordinator' for coordinator/coordinated tasks, undefined otherwise
- setTaskControl skips the MCP_ControlChanged IPC for coordinator tasks
  (backend only knows sub-tasks, not the coordinator task itself)

Bug fixes
- MCP_TaskCreated handler now sets controlledBy:'coordinator' on new
  sub-tasks (was missing, leaving textarea unlocked)
- Closing a coordinator task clears controlledBy on all children so
  their textareas re-enable when the control bar disappears
- setTaskControl calls saveState() so Take Control survives autosave

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- design.md: comprehensive write-up of the coordinator mode feature —
  architecture diagram, Coordinator class internals, MCP tool surface,
  wait_for_signal_done redesign rationale, notification system, control
  handoff, autofire, discoverability hint, settings, security, Docker,
  New Task Dialog changes, and test coverage summary
- TODOS.md (renamed from KNOWN-TODOS.md): easy coordinator test tasks,
  medium known issues (get_task_output truncation, merge_task live run,
  autofire timing edge case, MCP config staleness), hard backend
  hydration issues, and frontend test requirements (items 12–16)
- Removed AGENTS.md from project root (preamble is now written per-agent
  into the worktree by the coordinator, not the project root)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verifies that newly created sub-tasks have controlledBy set to
'coordinator' and coordinatedBy set to the coordinator task ID,
with a regression guard ensuring controlledBy is always defined.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l tick

Extracts the autofire tick decision logic into processAutoFireTick (autofire-tick.ts)
so it can be tested as a pure function without mounting the SolidJS component.

Tests verify:
- controlledBy==='human' returns 'paused' without touching the miss counter
- controlledBy==='coordinator' increments miss count and fires when prompt visible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts disableStdin logic into computeDisableStdin() and adds
tests for coordinator-controlled (true), human-controlled (false),
and undefined controlledBy (false) scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The merge_task tool merges into baseBranch (the coordinator's feature
branch), not necessarily main. Update both the MCP tool description and
coordinator preamble to reflect the correct behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The coordinator preamble was being injected twice — once in NewTaskDialog.tsx
(from src/lib/coordinator-preamble.ts) and again in src/store/tasks.ts
(from src/store/coordinator-preamble.ts), causing coordinators to start with
duplicated and contradictory instructions.

Remove src/lib/coordinator-preamble.ts and its prepend in NewTaskDialog.tsx,
consolidating all rules into the store version as single source of truth.
The store version now includes the BAD/GOOD task-assignment guidance, baseBranch
reminder, max-3-concurrent rule, file-overlap guidance, and the verify-before-assigning
rule from the deleted lib version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_merge_task

Both handlers now append ' [NOT COMMITTED — will be auto-committed on merge]'
to any file entry where committed === false, matching the safety context the
file-object type already carries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In mergeTask(), before delegating to gitMergeTask(), attempt an auto-commit
of any uncommitted changes in the task worktree. If the commit fails and git
status still shows dirty files, throw an error to abort the merge. If nothing
was staged (git commit fails due to "nothing to commit"), swallow silently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tasks with needsReview set now return 'review' from getTaskAttentionState()
and getTaskDotStatus(), making them visible in the sidebar instead of
appearing idle/waiting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only store.taskOrder was scanned, so collapsed coordinated
children disappeared from the strip. Now uses getCoordinatorChildren()
which covers both active and collapsedTaskOrder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On create failure after preamble injection, only newly-created files were
cleaned up (unlinked). Existing files that had the preamble appended were
left modified on disk. Now tracks original content per agent type and
restores it on failure, or unlinks if the file was newly created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a coordinator task is collapsed, child tasks remain locked with no
way to interact, and staged notifications cannot fire because the
coordinator's PromptInput is not mounted. Prevent this broken state by
returning early from collapseTask() when the task has coordinatorMode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eProjectWithTasks

Reorder the close sequence so non-coordinator tasks are closed first, then
coordinators last. Prevents a partial-close state where a coordinator fails
but its children were already removed from the original (now stale) snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add stripPreambleFromBranch() to remove injected <sub-task-mode> content
from AGENTS.md, GEMINI.md, and .agent.md before staging. Call it before
git add -A in the auto-commit step so preamble files are absent or
restored to their original content when the commit runs. Newly created
preamble-only files are deleted; modified files are restored to their
pre-injection content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
brooksc and others added 11 commits May 8, 2026 23:52
…b-task preamble cleanup

- Rewrite coordinator preamble with strict sliding-window pattern, native
  background Agent landing flow, and explicit rules for baseBranch, dirty
  worktree, merge-before-close, scope discipline, and test verification
- Add MAX_CONCURRENT placeholder substituted at task creation time
- Add max concurrent tasks numeric input to NewTaskDialog (default 3)
- Update sub-task preamble to require tests/typecheck before signal_done
- Add TODOS.md items R1-R4 (regressions) and johannesjo#12-johannesjo#19 (new findings)
- Rule 6c now passes coordinator worktree path to landing Agent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only items 7 and 8 remain (known edge cases with no fix yet).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README keyboard shortcuts were reformatted by a sub-agent — not our change
to own. CONTRIBUTING.md was created by a sub-agent but this isn't our repo
to add docs like that.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brooksc brooksc force-pushed the feature/orchestrator-control-v2 branch from 0be08ca to ad6746d Compare May 9, 2026 07:56
@brooksc
Copy link
Copy Markdown
Contributor Author

brooksc commented May 9, 2026

Update — coordinator mode: major second pass, largely self-developed

We've done a substantial second pass on this feature. The headline: most of the work in this update was done by the coordinator itself — we used the tool to fix the tool.

Video demo (recorded mid-session, before max-concurrent UI was added — shows the core flow live)

Design document: design.md — architecture overview, MCP server design, and data flow.


The coordinator ran this PR

To validate the implementation, we gave the coordinator a single prompt:

"Work through TODOS.md in your worktree. Use feature/orchestrator-control-v2 as the baseBranch for all sub-tasks."

From that one prompt, the coordinator:

  • Read TODOS.md and decomposed 13 bugs autonomously — no further input from us
  • Named each sub-agent task descriptively (fix-r1-task-closed-neighbor, fix-r2-persist-signal-done-review, fix-9-backend-task-registry-hydration, etc.)
  • Spawned up to 3 sub-agents in parallel, each in an isolated git worktree on its own branch
  • Managed the sliding-window loop: as each sub-agent called signal_done, the coordinator dispatched a landing agent to diff → merge → close while immediately spawning the next task
  • Sequenced tasks with file dependencies correctly (e.g., backend hydration before controlMap restore) without being told to
  • Merged all 13 branches into feature/orchestrator-control-v2 via squash commits
  • Total wall-clock time: ~54 minutes for 13 bugs

The only human involvement was watching the coordinator work and confirming the final result — no manual coding.


What changed since the initial revision

Control model renamed and extended

  • "Pause/Resume coordinator" → Take Control / Release Control — clearer ownership language
  • The coordinator task itself now shows the same control bar, with autofire skipping ticks while the user has control (prevents spurious 10-miss escalation when you're mid-thought)
  • Discoverability tooltip: clicking inside a coordinator panel while it's in auto mode shows a hint pointing at Take Control (shown max 3 times, dismissable with "Don't show again", count persisted)

Max concurrent tasks UI
Numeric input in the coordinator dialog (1–10, default 3). Injected into the preamble at task creation time. The selected baseBranch is also auto-injected so users don't need to repeat it in the prompt.

Coordinator preamble rewritten from scratch
Iterated through live sessions — including asking the coordinator directly what was confusing. Key rules now encoded:

  • Sliding-window pattern (spawn N, replace immediately on completion — never batch-drain)
  • Background Agent landing (dispatch native Agent for diff→merge→close, don't do it inline)
  • baseBranch must be the project's durable branch, not the coordinator's ephemeral task branch
  • Commit before merge_task — dirty tree fails the merge
  • Scope discipline — only read sources explicitly specified in the prompt
  • Sub-agents run tests/typecheck before calling signal_done

13 bugs fixed (all found during live coordinator sessions, most fixed by sub-agents):

Item Fix
R1 MCP_TaskClosed neighbor selection (idx captured before cleanup removes taskId)
R2 signalDoneReceived/needsReview now persisted across restarts
R3 createTask failure cleanup — no more zombie PTYs/tasks in memory
R4 .claude/settings.local.json preamble stripped before merge
item 9 Backend Coordinator.tasks hydrated on app restart
item 10 Backend controlMap restored on restart (human control respected after relaunch)
item 11 MCP_CoordinatorOrphanedNotification channel split; ack path now marks needsReview
item 12 review_and_merge_task deprecated in preamble; preamble guides get_task_diff → merge_task → close_task
item 13 gitIsolation now rejected at REST (400) instead of silently ignored
item 14 SubTaskStrip: clicking collapsed sub-task now uncollapses it
item 15 Preamble strip preserves intentionally empty tracked instruction files
item 18 Sidebar drag uses visible-order translation to keep coordinator+child blocks contiguous
item 19 Clicking a collapsed coordinator child also uncollapses the coordinator

Tests: 464 passing across 33 test files — this PR adds ~2,057 lines of new test code covering coordinator notification logic, control handoff, PTY idle detection, and git operations.


What's still open

Two known edge cases, intentionally deferred:

  • item 7 — Autofire escalates unnecessarily if the coordinator is mid-tool-call during a countdown. Rare timing window; no clean fix yet.
  • item 8 — If the coordinator process restarts (not the Electron app), MCP URL/token changes and running sub-tasks lose connection. App restart is handled; process restart is not.

Not yet tested: Docker integration — the coordinator skipPermissions propagation and Docker-mode sub-agent spawning have not been exercised end-to-end.


Test plan

  • Create coordinator task — control bar appears with "Take Control"
  • Sub-task panels show auto mode / "You have control" banner
  • Take Control on coordinator → autofire stops counting misses
  • Release Control → autofire resumes
  • Click in coordinator panel while in auto mode → tooltip appears (max 3×, "Don't show again" persists)
  • Kill and relaunch app → coordinator MCP tools still work, human-control sub-tasks stay locked
  • npm run check && npm test passes (464 tests)

🤖 Generated with Claude Code

@johannesjo
Copy link
Copy Markdown
Owner

I rechecked the current head (ad6746d). I’d still hold this before merging, even behind beta, because a few coordinator/MCP lifecycle boundaries are not doing what the PR/test plan says yet.

  1. Sub-task MCP is not limited to signal_done. electron/mcp/server.ts:48-209 exposes one unconditional tool list, including create_task, send_prompt, merge_task, and close_task. Sub-task configs are created with --task-id only (electron/mcp/coordinator.ts:470-476), but execution only role-checks wait_for_signal_done and signal_done (electron/mcp/server.ts:306-365). create_task remains callable at server.ts:219-227. This means a sub-task can still perform coordinator-level lifecycle operations, including spawning more tasks.

  2. The restart path still does not make coordinated tasks reliably usable. There is now startup restore code in src/App.tsx:328-370, but it does not await StartMCPServer, only iterates store.taskOrder, and hydrates children without their restored renderer agent IDs. The IPC handler then invents a fresh agentId (electron/ipc/register.ts:1221-1255), and Coordinator.hydrateTask() stores that random ID with status exited (electron/mcp/coordinator.ts:869-895). After relaunch, MCP calls such as send_prompt/wait_for_idle are not connected to the actual respawned PTY session, so the “kill and relaunch app → coordinator MCP tools still work” test plan item is still not guaranteed.

  3. wait_for_idle still mishandles human takeover, and the MCP result hides the useful reason. waitForIdle() returns { reason: 'human_control' } only if the task is already human-controlled at call time (electron/mcp/coordinator.ts:652-656). If a waiter is already pending, setTaskControl(..., 'human') does not resolve it; resolvers are fired only when control returns to 'coordinator' (coordinator.ts:113-125). Even when the coordinator returns a reason, the HTTP route discards it and replies only with { status } (electron/remote/server.ts:344-348), and the MCP client type also exposes only { status: string } (electron/mcp/client.ts:65-70).

  4. Failed coordinated task creation can leak worktrees and backend state. createTask() creates the backend worktree (electron/mcp/coordinator.ts:323-329) and inserts task/buffer/decoder state (:351-357) before several failure points. The guarded catch later only restores/deletes the injected preamble file (:578-590); it does not call deleteTask, remove this.tasks, clear buffers/decoders, unsubscribe, or delete MCP config. A bad agent command or spawn failure is enough to leave stale coordinator state plus a worktree/branch behind.

  5. signalDoneReceived and needsReview are not persisted. These runtime fields exist (src/store/types.ts:90-91) and are set from MCP events (src/store/tasks.ts:1039-1045), but saveState() omits them for active/collapsed tasks (src/store/persistence.ts:88-114, :123-150), PersistedTask does not include them, and load reconstruction does not restore them (persistence.ts:483-520, :562-602). A relaunch loses the done/review-needed UI state.

brooksc and others added 5 commits May 9, 2026 10:52
1. Sub-task MCP tool filtering: server.ts now returns only signal_done
   in ListTools when running as a sub-task (--task-id set, no
   --coordinator-id). CallTool rejects any other tool with isError.

2. Restart path agentId: App.tsx passes task.agentIds[0] to
   MCP_HydrateCoordinatedTask; register.ts uses it instead of
   crypto.randomUUID(). hydrateTask() now sets up the output callback
   and subscriber so wait_for_idle and idle detection work after restart.

3. wait_for_idle human takeover: setTaskControl('human') now resolves
   pending idleResolvers immediately with { reason: 'human_control' }
   instead of leaving them hanging. HTTP route and client type now
   propagate reason through to the MCP caller.

4. createTask failure cleanup: expanded catch block calls cleanupTask()
   to remove the worktree, kill any spawned agent, and clear all
   in-memory state (tasks, tailBuffers, decoders, subscribers, MCP
   config). Previously only the preamble file was restored.

5. signalDoneReceived/needsReview persistence: added both fields to
   PersistedTask, included in both save blocks (active + collapsed),
   and restored in both load blocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts keeping our coordinator additions:
- git.ts / git.test.ts: kept ours (merge_task resolution fix + tests)
- DiffViewerDialog.tsx: kept ours (adds direct mode support to CommitNavBar Show)
- TaskChangedFilesSection.tsx: kept ours (hasCommitNav checks both worktree and direct)
- TerminalView.tsx: kept ours (computeDisableStdin import)
- NewTaskDialog.tsx: re-applied coordinator mode additions on top of main's base

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gap 1 (Test 2): signal_done appeared in coordinator tool list — coordinator
agents could see a tool they should never call. Extracted tool-list logic into
mcp-tool-list.ts (pure, testable) so selectTools() enforces the role boundary:
sub-tasks get only signal_done, coordinators get everything except signal_done.
Added 6 unit tests in mcp-tool-list.test.ts that would have caught this.

Gap 2 (Test 7): wait_for_signal_done returned { taskId, name, remaining } but
the test plan specifies { taskId, name, status, signalDoneAt, remaining }.
Added status and signalDoneAt (ISO timestamp) to WaitForSignalDoneResult and
both resolve call sites in coordinator.ts. Updated coordinator.test.ts to
match the new shape using toMatchObject.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ol-v2

Resolved conflicts keeping our coordinator fields:
- autosave.ts: kept coordinatorNotificationDelayMs alongside shareDockerAgentAuth
- persistence.ts: kept coordinator fields (delay, mode, hint count) in all three blocks
- SettingsDialog.tsx: kept our full section layout; main's shareDockerAgentAuth
  checkbox was already present at line 623 in our version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Items 9-11 track unit tests missing for: edit-suppresses-autofire,
/tmp config cleanup on close, and coordinator checkbox re-enable.
Each maps to a specific test plan section and names the target test file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@johannesjo
Copy link
Copy Markdown
Owner

I rechecked current head c2727a0 with an additional pass. I'd still hold this before beta because a few lifecycle boundaries are still not reliable:

  1. Two new MCP IPC channels are blocked by preload. MCP_CoordinatorNotificationDropAck and MCP_HydrateCoordinatedTask exist in electron/ipc/channels.ts:156-159 and are used from src/components/PromptInput.tsx:452 / src/App.tsx:358, but electron/preload.cjs:124-143 does not allowlist them. That means autofire drop ack and restart hydration fail before reaching main.

  2. Coordinators are not scoped to their own project/tasks. create_task sends no projectId from the MCP server (electron/mcp/server.ts:75-80), while StartMCPServer overwrites a global default project. Also list_tasks, send_prompt, merge_task, and close_task accept only taskId without checking caller coordinatorTaskId (electron/remote/server.ts:300-325, :420-450). Multiple coordinators can see/control each other's children.

  3. Coordinator MCP is broken if remote access was started first. startRemoteServer() captures opts.coordinator when constructed, but StartMCPServer reuses an existing remoteServer (electron/ipc/register.ts:1027). If the remote server was already running with no coordinator, /api/tasks coordinator routes are absent.

  4. Restart restore is still racy/incomplete. StartMCPServer is fired without awaiting despite the comment saying config is rewritten before agents resume (src/App.tsx:329-349), and hydrateTask() does not restore signalDoneAt/consumed state (electron/mcp/coordinator.ts:902-914). Already-signaled children can be lost to wait_for_signal_done after restart.

  5. Closing a coordinated child from the UI leaves stale backend coordinator state. closeTask() only deregisters when the closed task is the coordinator (src/store/tasks.ts:349-368). Closing a child deletes the renderer task/worktree, but backend Coordinator.tasks can still list/merge/close that stale child.

  6. Docker coordinator paths do not line up. Docker coordinator mode writes mcp-server.cjs / .mcp.json under projectRoot (electron/ipc/register.ts:1059-1119), but the container only mounts the coordinator task cwd (electron/ipc/pty.ts:257-261). For worktree-isolated coordinator tasks, the container cannot see those files. Docker sub-task respawn args also omit -w <subtask worktree> (electron/mcp/coordinator.ts:567-569), unlike the initial backend docker exec.

These are mostly boundary/restore/scoping issues, but they affect the exact scenarios the beta gate is meant to make safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants