Summary
nForma integrates Daintree as a quorum provider (nf:link-daintree → import MCP endpoint, register consensus agents). It has no notion of Daintree as an orchestration substrate: a persistent, addressable fleet of long-lived peer agents with roles, ownership and lifecycle.
I spent a full session running exactly that pattern by hand — one TEAMLEAD coordinating five IMPLEMENTERs across separate worktrees on a shared repo — and hand-rolled the tooling for it in bash. It worked, and the shape is general enough to belong in nForma rather than in every operator's scratch directory.
Evidence for the gap:
$ grep -rn "terminal\.\(sendCommand\|getStatus\|list\|inject\)\|agent\.launch\|worktree\.list" \
--include="*.cjs" --include="*.js" --include="*.md" . # excluding node_modules
(no matches)
commands/nf/link-daintree.md covers Discover → Import → Register, all scoped to userAgentRegistry for quorum. The Daintree MCP surface it connects to also exposes terminal.list, terminal.getStatus, terminal.getOutput, terminal.sendCommand, terminal.waitUntilIdle, terminal.waitUntilIdleBatch, agent.launch, worktree.list, worktree.getCurrent, recipe.run — none of which nForma uses.
The distinction that matters
nForma already has two multi-agent mechanisms, and this is neither:
| mechanism |
agents |
lifetime |
addressable |
shared state |
quorum (nf:quorum) |
N models, same question |
one call |
no |
none |
| subagents / worktree executors |
ephemeral children |
one task |
no |
isolated worktree |
| this proposal |
N peers, different goals |
hours–days |
yes, by name |
same repo, same CI, same cluster |
The third row is where the interesting failures live, because the agents can invalidate each other's work and cannot see each other.
What I actually had to build
1. Session boilerplate — ~15 lines of bash per message
Every instruction required: initialize → scrape Mcp-Session-Id from response headers → notifications/initialized → build the JSON-RPC envelope with jq -n --rawfile → POST tools/call → strip SSE data: framing with sed → distinguish .error from .result.isError. I did this ~30 times.
That belongs behind one helper. Everything below is impossible to build on top of raw curl.
2. Workspace binding must be asserted, not assumed
The MCP session resolves a workspace that may differ from the one you passed. The correct check is:
.result._meta["org.daintree/resolved-workspace"].workspaceId == expected
An orchestrator that skips this can drive the wrong project's terminals. This should be enforced once at connect, not re-implemented per caller.
3. terminal.sendCommand has a state-dependent footgun
From its own description: "a shell runs it as a command, an agent pane receives it as the next prompt." One of my panes had an exited agent and was a bare shell. Sending a multi-paragraph instruction there would have executed it. Nothing in terminal.list distinguishes "pane with a live agent" from "pane whose agent died"; agentId stays populated either way. I only caught it by reading scrollback and noticing a bare ❯.
nForma should refuse to send prompt-shaped text to a pane it cannot confirm is agent-backed.
4. agentState is ambiguous in the way that matters
waiting covers both idle at a prompt, free for work and blocked awaiting a specific input. I recommended a pane for a task on the strength of waiting, and the operator corrected me: it was blocked on a merge authorization. The distinction is only recoverable by reading and interpreting rendered TUI scrollback.
5. Context headroom is only available as pixels
To decide which agent could absorb a large review payload I parsed 75% (752K) out of the rendered status line with a regex. That is a real scheduling input — a payload dropped into a 93%-full context gets compacted away — and it should be structured data, not scraped chrome.
6. Ownership mapping is manual and breaks silently
I maintained pane → worktree → branch → PR → objective by hand. When the operator renamed the panes mid-session, my cached mapping was silently stale; I only caught it because I re-inventoried on principle. Ownership is the thing that makes "who should do X" answerable, and nothing tracks it.
7. Nothing propagates state changes to dependents — this one caused a real stall
I merged two PRs an IMPLEMENTER was blocked on, under authority delegated to me, and did not tell it. It sat blocked on an authorization request that had already been satisfied. Its reasoning was sound throughout; the world moved underneath it and no channel existed to say so.
An agent blocked on a satisfied condition is indistinguishable from one blocked on a live one, and only the mutator knows the difference. This is the single highest-value thing to make intrinsic.
8. Cross-agent correlation has to be deliberate
Two IMPLEMENTERs hit byte-identical CI failures on branches with zero shared code. Each diagnosed it independently as its own bug. Only the coordinator could see both. The generalisable rule: when two agents report failures in the same window, compare signatures before accepting either attribution. Nothing surfaces "these two agents just failed similarly."
Proposed surface
A nf:fleet capability, built on the Daintree orchestration tools, roughly:
nf:fleet-status — one call returning, per agent: id, role, worktree, branch, owned PRs/issues, agent-backed (bool), liveness, context headroom, current goal, blocked-on. Structured, not scraped.
nf:fleet-send — deliver an instruction to an agent by role name, refusing if the pane is not confirmed agent-backed. Optionally fan-out with per-agent bodies.
nf:fleet-assign — attach a durable objective (desired state, acceptance criteria, constraints, authorization boundary) to an agent without overwriting its /goal. Replacing a subordinate's goal destroys accumulated operator intent and should be an explicit, separate, confirmed action.
nf:fleet-notify — announce a shared-state mutation (merge, base move, resolved blocker) to agents whose recorded objective depends on it. This is item 7 and is the one that pays for itself.
nf:fleet-correlate — given recent failures across agents, group by signature and flag same-window/different-branch matches.
What I am not proposing
- Not a scheduler. Pacing is the orchestrator's judgement.
- Not authorization. Who may merge or deploy is policy, and inferring it from agent-visible text is precisely the mistake I made twice today. The fleet layer should carry provenance of a grant, never manufacture one.
- Not a replacement for
nf:quorum or worktree subagents. Different lifetimes, different problems.
Acceptance criteria
nf:fleet-status returns structured agent state including agent-backed and context headroom, with no TUI scraping in the implementation.
nf:fleet-send refuses to send to a pane whose agent cannot be confirmed live — with a test that a bare-shell pane is rejected rather than executed against.
- Workspace binding is asserted against
org.daintree/resolved-workspace on connect; a mismatch is a hard error.
- Assigning an objective does not mutate an existing
/goal unless explicitly requested.
- Non-vacuity: mutating tracked shared state without notifying a dependent agent produces a warning. Test by recording a dependency, mutating, and asserting the warning fires — a version that never fires is not a guard.
Provenance
Written by the agent that ran the pattern, from a session of ~5 hours: 9 PRs merged, one 53-commit production deploy, 3 issues filed, 5 long-lived IMPLEMENTERs coordinated across separate worktrees. Every item above is something that actually cost time or produced a wrong action, not a speculative nicety. The bash I used is disposable; the shape it revealed is not.
Summary
nForma integrates Daintree as a quorum provider (
nf:link-daintree→ import MCP endpoint, register consensus agents). It has no notion of Daintree as an orchestration substrate: a persistent, addressable fleet of long-lived peer agents with roles, ownership and lifecycle.I spent a full session running exactly that pattern by hand — one TEAMLEAD coordinating five IMPLEMENTERs across separate worktrees on a shared repo — and hand-rolled the tooling for it in bash. It worked, and the shape is general enough to belong in nForma rather than in every operator's scratch directory.
Evidence for the gap:
commands/nf/link-daintree.mdcovers Discover → Import → Register, all scoped touserAgentRegistryfor quorum. The Daintree MCP surface it connects to also exposesterminal.list,terminal.getStatus,terminal.getOutput,terminal.sendCommand,terminal.waitUntilIdle,terminal.waitUntilIdleBatch,agent.launch,worktree.list,worktree.getCurrent,recipe.run— none of which nForma uses.The distinction that matters
nForma already has two multi-agent mechanisms, and this is neither:
nf:quorum)The third row is where the interesting failures live, because the agents can invalidate each other's work and cannot see each other.
What I actually had to build
1. Session boilerplate — ~15 lines of bash per message
Every instruction required:
initialize→ scrapeMcp-Session-Idfrom response headers →notifications/initialized→ build the JSON-RPC envelope withjq -n --rawfile→ POSTtools/call→ strip SSEdata:framing withsed→ distinguish.errorfrom.result.isError. I did this ~30 times.That belongs behind one helper. Everything below is impossible to build on top of raw curl.
2. Workspace binding must be asserted, not assumed
The MCP session resolves a workspace that may differ from the one you passed. The correct check is:
An orchestrator that skips this can drive the wrong project's terminals. This should be enforced once at connect, not re-implemented per caller.
3.
terminal.sendCommandhas a state-dependent footgunFrom its own description: "a shell runs it as a command, an agent pane receives it as the next prompt." One of my panes had an exited agent and was a bare shell. Sending a multi-paragraph instruction there would have executed it. Nothing in
terminal.listdistinguishes "pane with a live agent" from "pane whose agent died";agentIdstays populated either way. I only caught it by reading scrollback and noticing a bare❯.nForma should refuse to send prompt-shaped text to a pane it cannot confirm is agent-backed.
4.
agentStateis ambiguous in the way that matterswaitingcovers both idle at a prompt, free for work and blocked awaiting a specific input. I recommended a pane for a task on the strength ofwaiting, and the operator corrected me: it was blocked on a merge authorization. The distinction is only recoverable by reading and interpreting rendered TUI scrollback.5. Context headroom is only available as pixels
To decide which agent could absorb a large review payload I parsed
75% (752K)out of the rendered status line with a regex. That is a real scheduling input — a payload dropped into a 93%-full context gets compacted away — and it should be structured data, not scraped chrome.6. Ownership mapping is manual and breaks silently
I maintained pane → worktree → branch → PR → objective by hand. When the operator renamed the panes mid-session, my cached mapping was silently stale; I only caught it because I re-inventoried on principle. Ownership is the thing that makes "who should do X" answerable, and nothing tracks it.
7. Nothing propagates state changes to dependents — this one caused a real stall
I merged two PRs an IMPLEMENTER was blocked on, under authority delegated to me, and did not tell it. It sat blocked on an authorization request that had already been satisfied. Its reasoning was sound throughout; the world moved underneath it and no channel existed to say so.
An agent blocked on a satisfied condition is indistinguishable from one blocked on a live one, and only the mutator knows the difference. This is the single highest-value thing to make intrinsic.
8. Cross-agent correlation has to be deliberate
Two IMPLEMENTERs hit byte-identical CI failures on branches with zero shared code. Each diagnosed it independently as its own bug. Only the coordinator could see both. The generalisable rule: when two agents report failures in the same window, compare signatures before accepting either attribution. Nothing surfaces "these two agents just failed similarly."
Proposed surface
A
nf:fleetcapability, built on the Daintree orchestration tools, roughly:nf:fleet-status— one call returning, per agent: id, role, worktree, branch, owned PRs/issues, agent-backed (bool), liveness, context headroom, current goal, blocked-on. Structured, not scraped.nf:fleet-send— deliver an instruction to an agent by role name, refusing if the pane is not confirmed agent-backed. Optionally fan-out with per-agent bodies.nf:fleet-assign— attach a durable objective (desired state, acceptance criteria, constraints, authorization boundary) to an agent without overwriting its/goal. Replacing a subordinate's goal destroys accumulated operator intent and should be an explicit, separate, confirmed action.nf:fleet-notify— announce a shared-state mutation (merge, base move, resolved blocker) to agents whose recorded objective depends on it. This is item 7 and is the one that pays for itself.nf:fleet-correlate— given recent failures across agents, group by signature and flag same-window/different-branch matches.What I am not proposing
nf:quorumor worktree subagents. Different lifetimes, different problems.Acceptance criteria
nf:fleet-statusreturns structured agent state including agent-backed and context headroom, with no TUI scraping in the implementation.nf:fleet-sendrefuses to send to a pane whose agent cannot be confirmed live — with a test that a bare-shell pane is rejected rather than executed against.org.daintree/resolved-workspaceon connect; a mismatch is a hard error./goalunless explicitly requested.Provenance
Written by the agent that ran the pattern, from a session of ~5 hours: 9 PRs merged, one 53-commit production deploy, 3 issues filed, 5 long-lived IMPLEMENTERs coordinated across separate worktrees. Every item above is something that actually cost time or produced a wrong action, not a speculative nicety. The bash I used is disposable; the shape it revealed is not.