Status: DRAFT — grilled 2026-07-25 · Author: design session 2026-07-25 Goal: let a user open a session in which multiple models from different backends work one goal in coordinated, specialized roles — an orchestrator that plans and routes, a searcher, a reasoner, an architect, and a panel of reviewers — as a native, identity-governed control, not a bolted-on script.
Goal. At session-create time the user toggles Collaborative and binds a model+backend to each role (orchestrator, internet-search, reasoning, architecture, review — review may be many). When the user states a goal, the orchestrator decomposes it, delegates to the role-agents on their assigned backends, they coordinate through shared structured state, and the work converges — with the user watching, interrupting, and approving throughout.
Non-goals.
- Not a rewrite.
This is a specialization of three primitives codeoid already has — the multi-provider meta-harness (
docs/multi-provider-meta-harness.md), the conductor/fleet dispatch subsystem (docs/conductor-design.md,src/daemon/dispatch.ts), and the pack/pipeline SDLC engine (docs/sdlc-pipeline.md,src/daemon/pipeline/). - Not a second orchestration system. Collaborative sessions unify into packs/pipeline (§8), not alongside them — one engine, two topologies (sequential phases vs. parallel role fan-out).
- Not identity-optional. Every role-agent is a ZeroID identity delegated from the orchestrator, which is delegated from the owner — one verifiable tree, one revocation root (§6).
- Not a message-relay orchestrator. We deliberately reject the "orchestrator context is the only shared medium" model (the ceiling every current open-source attempt hits) in favor of a daemon-owned blackboard (§4).
- Substrate = multi-session fleet, not single-session role-swap.
Each role runs as its own
Sessionwith its own(provider, model), its own store row, and its own delegated ZeroID identity. The rejected alternative — one bound session whose backend is swapped per role viaswitchProvider— is lossy (it resets the model and mints a fresh backing id,session.tsswitchProvider), is inherently sequential, assumes oneprovidercolumn per session (store.tssessions.provider), and cannot run reviewers in parallel or keep them independent. - Unify into packs/pipeline.
A collaborative session is a pack: roles gain a
(provider, model, policy)binding, phases gain enforcedreads/writes, and a newpanelphase kind fans a phase out across backends. A pure collaborative session is a degenerate one-goal pack; an SDLC pack gets a cross-backendpanelreview phase for free. - Coordination = orchestrator-LLM-driven dispatch over a daemon-owned typed-artifact blackboard.
The orchestrator plans dynamically and delegates via tool calls (choreography lives in editable skill prose, not compiled control flow), but role-agents hand off through structured artifacts in the daemon, not through the orchestrator's context (§4).
v1 is lean: orchestrator-driven dispatch (the existing
dispatch.tsqueue) + the blackboard as a typed-handoff store + one parallel primitive (the review panel). The full "scheduler over a dependency graph" (auto-firing a role the moment its inputs exist) is the end-state (P4), not v1 — in v1 the reservedreads/writesare enforced access scoping, and become scheduling edges in P4 (§8). - Review = a breadth panel. "All models take a stab at review" means N different-backend reviewers, each reading the same input artifact, each writing independent findings, merged by a synthesis step — with an optional cross-critique (debate) round before synthesis (§7).
Grill outcomes (2026-07-25) — additional decisions locked:
- Orchestrator = Claude for v1; tools built as a mountable MCP server.
The fleet + blackboard tool surface is built as a mountable MCP server (stdio/registry) rather than the in-process Claude-SDK object (
createSdkMcpServer,src/daemon/fleet.ts), so letting gemini/openai/codex orchestrate later (they already parsemcp__*) is a mount-config change, not a rewrite. Worker roles run on any backend in v1; any-backend orchestrator is tracked in #245. - Blackboard schema = fixed-core + scoped
extra. A first-class typed set (spec,research,adr,task-list,diff,findings) — versioned, UI-rendered, per-role read/write enforced — plus anextra/<key>namespace for ad-hoc artifacts that is still per-role scoped (no "unenforced field is false security" hole). - Role-child lifetime = per-goal; cross-goal continuity via the blackboard. A role-child lives for one goal/run (survives the implement↔review fix-loop, keeps its worktree/branch), and is torn down at goal completion. Continuity across goals rides durable blackboard artifacts, not live children — preserving the conductor's low-credentials-at-rest discipline.
- Cost/concurrency guards ship in v1 (lightweight). A per-session live-worker cap (closing omnigent's cross-turn concurrency gap), a per-collaboration cost roll-up surfaced at the R3 approve-time, and reuse of the existing per-child tool budget + failure-limit auto-block. Hard token ceilings and Cedar/Shield governance stay P6.
- Create UX = a first-class Collaborative toggle that compiles to an ephemeral one-goal pack.
The create dialog leads with a Collaborative toggle + role→backend pickers and compiles to a one-goal pack + orchestrator under the hood; pack vocabulary stays hidden for this path.
The
/pipelinepack-selector remains the surface for pre-authored SDLC packs. - Synthesis + independence defaults.
The orchestrator merges the panel's findings by default (the human always sees disagreement; a dedicated judge role is a pack option).
Reviewers read
diff+speconly — never implementer reasoning — enforced at thecanUseToolfence.
A role is a { backend, model, contract, capabilityPolicy } binding, selected per delegation by a purpose tag.
The five named roles (orchestrator / search / reasoning / architecture / review) are a default profile, stored as a list so that adding a role — "security-reviewer", "test-author", "docs-writer" — is a config change, never a code change.
This is the single most important lesson from the omnigent prior art (§9): do not hard-code the role taxonomy.
One worker spec can serve several purposes (implement / review / explore), chosen per dispatch — so the number of distinct backend processes is O(distinct backends configured), not O(roles).
The precedent already exists in codeoid.
ConductorSchema { provider, model } (src/config.ts) is exactly a role→backend binding for a single privileged role; a collaborative session generalizes it to a list.
Roles as capability envelopes also already exist in the pack system — roleSchema { name, write, network, envelope, exceptions } (src/daemon/pipeline/pack.ts) — but they carry no model or provider today.
The one addition to the schema is an optional (provider, model) on a role, plus the enforcement that makes a role's envelope real (§6).
| Default role | purpose | typical backend fit | capability policy |
|---|---|---|---|
| orchestrator | plan, delegate, schedule, synthesize | strongest reasoning + tool-use (Claude today, for fleet MCP) | no file/exec write; session:dispatch only |
| search | web research → research artifact |
any backend with WebSearch/WebFetch |
read + web; no repo write |
| reasoning | bulk logical work, implementation → diff |
strong coder | write (in a worktree), execute |
| architecture | decompose, decide structure → adr |
strong long-context planner | read + write adr; no repo write |
| review (×N) | independent critique → findings |
different backend from the implementer | read diff+spec, no write (enforced) |
Why the obvious design has a ceiling. The default multi-agent pattern (and every open-source attempt surveyed, including omnigent) makes the orchestrator's own conversation the only shared medium: every handoff is the orchestrator re-serializing text into a worker's prompt, and every result flows back through the orchestrator. That makes the coordinator the bottleneck — its context bloats with each round, handoffs are lossy text, and parallelism is capped by the orchestrator's serial turn.
Codeoid can do structurally better, because it owns three things a message-relay harness does not:
- Canonical history (
src/daemon/providers/canonical.ts) — a provider-neutralCanonicalTurn[]with full structured tool inputs and outputs, convertible into any backend's native format. Handoffs can be structured, not re-narrated prose. - A daemon-owned durable memory + retrieval engine (
src/daemon/memory/engine.ts) with hybrid (vector + FTS + recency + path-overlap) recall and saliency compression. Shared state can live in the daemon, addressable by query, not held in a chat. - A ZeroID identity per agent — so every contribution to shared state is attributable, scope-attenuated, and auditable.
The model: a goal-scoped, typed-artifact blackboard; the orchestrator is a scheduler over a dependency graph, not a relay.
- The daemon owns a goal workspace: a small set of typed, versioned artifacts —
spec,research,adr,task-list,diff,findings— each stamped with the producing ZeroID identity and a version. - Role-agents read their inputs and write their outputs as artifacts directly, via blackboard tools that close over the goal workspace (mirroring how
buildFleetMcpServercloses over the session population,src/daemon/fleet.ts, and howbuildMemoryMcpServeris bound to a session). The searcher writesresearch; the architect readsresearch+specand writesadr; the reasoner readsadr+specand writesdiff; each reviewer readsdiffand writes afindingsentry. No handoff is ever re-serialized through the orchestrator's context. - The orchestrator holds an index of artifact states, not the artifacts themselves — the conductor's founding principle ("holds an index, not transcripts",
docs/conductor-design.md§2) applied inside a single goal. It observes which artifacts exist and at what version, decides what is ready to run (inputs present), delegates, and advances.
Why this is strictly better than a message relay:
- The orchestrator never becomes the context bottleneck — it carries an index; artifacts live in the daemon.
- Handoffs are structured and lossless (canonical artifacts), not truncated re-narrated text.
- Parallelism falls out of the dependency graph — any role whose inputs are ready runs — instead of being throttled by the orchestrator's serial turn or a crude per-turn dispatch cap.
- Every contribution is identity-stamped, auditable, and resumable: the graph is durable, so a daemon restart resumes a collaboration mid-flight (the same guarantee
dispatch.tsgives workers via its SQLite work queue). - Independence becomes an enforced policy, not a convention.
A reviewer identity may read
diff+specbut notimplementer-reasoning; the reviewer is unbiased because it cannot see the author's reasoning, enforced at the tool fence — not because a prompt asked it not to peek.
v1 scope (from the 2026-07-25 grill).
In v1 the orchestrator drives dispatch explicitly (the existing dispatch.ts queue) and the blackboard is the typed-handoff store; the only parallel primitive is the review panel (§7).
The deterministic "fire a role the moment its inputs exist" scheduler — where reads/writes are live dependency edges — is the end-state (P4, §8), because most SDLC flow is sequential and the structured-handoff win does not require it.
In v1, reads/writes are enforced access scoping; in P4 they also become scheduling edges.
This is codeoid's intended direction, not an invention.
PhaseDef already reserves reads/writes fields, documented as "Reserved metadata (not yet consumed)" (src/daemon/pipeline/interface.ts).
The blackboard is the convergence point of three existing investments — canonical history, the memory engine, and the reserved reads/writes — which is why it is the right "best-in-class" bet rather than an over-engineered detour.
The one hard constraint (a codeoid principle, not an objection).
The same reserved-field comment warns that "an unenforced field is false security."
So artifact read/write scoping must be enforced at the canUseTool fence (the gate that already backs role tool-deny, session.ts roleDeniesTool), never left advisory.
Lighting up reads/writes means enforcing them — Claude-hard today, and advisory-with-a-logged-warning on backends whose tools don't all route through the gate, exactly as the pipeline already handles cross-backend role enforcement (docs/pipeline-run.md, roleEnforcement).
Additions, no architectural change:
| Addition | What it is | Mirrors existing |
|---|---|---|
| Collaboration profile on session-create | An optional collaboration object on SessionCreateMsg binding each role to { providerId, model } + policy; validated fail-closed against the provider registry. |
providerId/pack/packRole on session.create (packages/protocol/src/types.ts) |
| Orchestrator role | A conductor-shaped Session (role: "conductor"-class) whose fleet MCP surface gains role-aware delegation; holds no repo-write tools by ZeroID scope. v1: Claude only (mounts the fleet MCP); any-backend orchestrator tracked in #245. |
#createConductor + buildFleetMcpServer (src/daemon/session-manager.ts, src/daemon/fleet.ts) |
| Role-bound child sessions | One disposable role: "worker" Session per active role, each with its own providerId+model+initialMode and a delegated leaf identity. |
spawnWorker (src/daemon/session-manager.ts), generalized to carry provider/model |
| Goal blackboard | A daemon-owned, goal-scoped typed-artifact store (fixed-core + scoped extra) + a mountable MCP tool surface (stdio/registry, not the in-process Claude-SDK object — so non-Claude orchestrators are a later mount, #245), read/write-scoped per role. |
buildMemoryMcpServer binding + the dispatch_tasks/dispatch_events tables (src/daemon/store.ts) |
panel phase kind + dispatch barrier |
A phase that fans out to N role-children on distinct backends and joins on all of them, then synthesizes. | the reserved "panel" kind in the phase-kind union (src/daemon/pipeline/interface.ts) + Dispatcher (src/daemon/dispatch.ts) |
The collaborative session is a flagship Highflame dogfood: many models, many backends, one verifiable authority tree.
- Delegation tree.
owner → orchestrator → role-child → (sub-agent). The orchestrator is an owner-delegated agent holdingsession:read+session:dispatchonly (CONDUCTOR_SCOPES,src/daemon/agent-identity.ts) — nevertools:write/tools:execute— so nothing it delegates can mutate a repo unless the owner sanctioned that child. Each role-child istokens.delegate-d from the orchestrator (delegation_depthincrements; theactchain is verifiable), and cascading revocation kills the whole collaboration by deactivating one identity. - Shape → scope, cryptographically.
Reuse the existing worker scope profiles (
WORKER_SCOPE_PROFILES,agent-identity.ts): areviewrole maps to thescoutshape (read + execute, notools:write), areasoningrole toship(write in a worktree). A reviewer that "must not edit" is a leaf identity that cannot mint a write scope — read-only by construction, not by prompt. - Blackboard read/write scoping.
Extends the same fence: a role's artifact
reads/writesare enforced incanUseTool(§4), so independence and least-privilege are structural. - Human approval gate (R3) is preserved.
Send-class dispatch and any egress ride the existing hard, mode-independent approval flow (
#shouldAutoApprovereturns false for fleet-send tools,session.ts;FLEET_SEND_TOOL_NAMESkept offallowedTools,fleet.ts). A collaborative session cannot silently spawn or act; the owner sees the full delegation input. - Per-backend capability matrix + verification bench.
Formalize the ad-hoc
roleEnforcement(providerId)classification into a declared matrix — per backend: hard tool-deny? sub-agents? interrupt? warm resume? model family? approval mechanism? — verified by a bench that probes real behavior (borrowed from omnigent's harness-capability matrix, §9). This is what lets the role-picker UI honestly tell the user "reviewer read-only is guaranteed on Claude, advisory on gemini/codex" instead of failing silently, and lets the orchestrator query capabilities instead of branching on backend name.
Two review shapes exist in the wild, with different plumbing and different value; the user's requirement ("all models take a stab") is the breadth panel. We build that, and keep the correctness gate as a composable option.
Breadth panel (the default for the review role).
- When
difflands, the orchestrator fans out to N reviewers on distinct backends (apanelphase, §8). - Each reviewer reads only
diff+spec+the acceptance contract (independence enforced at the fence, §4/§6) and writes an independentfindingsartifact:{ blocking[], non-blocking[], suggestions[] }with file:line evidence. - The dispatch barrier joins on all reviewers (the one genuinely new dispatch primitive — today dispatch is per-task event injection, not an N-way join).
- A synthesis step merges the findings, de-duplicates, and surfaces the merged verdict to the user — the orchestrator by default; a dedicated
judgerole is a pack option. Following omnigent, there is no silent auto-vote — a model or the human synthesizes, and disagreement is shown, not hidden. - Optionally, a debate round relays each reviewer's findings to the others for one pass of cross-critique before synthesis (borrowed from omnigent's
/debate), for goals where surfacing disagreement is worth the extra turn.
Correctness gate (composable).
For coding packs, a blocking command gate (tests/lint/typecheck) runs before any reviewer is involved — cheap deterministic checks first, expensive cross-model review only on green — and each blocking finding becomes a fix-task routed back to the same implementer child (reusing its worktree), looping until clean.
The human merges; the collaboration never merges (docs/conductor-design.md R-decisions; feedback_user_owns_releases).
The pipeline is a conductor over a live session that drives sequential phases with a human Approve/Revise/Reject boundary between each (docs/pipeline-run.md).
A collaborative session is the parallel sibling: one orchestrator fanning concurrent role-children at one goal.
They share one engine:
- Role→backend binding in the pack.
Add optional
(provider, model)toroleSchema(src/daemon/pipeline/pack.ts), so a pack role carries its backend.resolvePhaseActivation(src/daemon/pipeline/pack-service.ts) returns it alongside the constitution/envelope it already resolves. - Wire the dormant per-phase override.
PhaseDef.provider/modelare plumbed from pack YAML all the way torunPhaseOnSessionbut dropped there (src/daemon/session-manager.ts, "a run drives ONE session with one provider"). For the fleet substrate this stops being a contradiction: a phase's provider/model selects the child it runs on, not a mutation of one bound session. - Enforced
reads/writes. Light up the reserved fields (§4) as the blackboard's artifact dependency edges, enforced at the fence. - The
panelphase kind. Implement the reserved"panel"kind (src/daemon/pipeline/interface.ts, onlynoop+skillare registered today) as: fan out to the phase's role-set across backends → dispatch barrier → synthesis (§7).
Net: /pipeline with an SDLC pack stays a governed sequential run, but its review phase can be a cross-backend panel; a pure collaborative session is a pack with one goal, an orchestrator, and a fan-out phase.
No second wire API, no second create dialog, no duplicated governance.
Reuse the pattern pipeline-run.md already established: the run is a session plus a goal and a config, so it extends the existing create-session dialog rather than a bespoke panel.
- A first-class Collaborative toggle reveals per-role pickers and compiles to an ephemeral one-goal pack + orchestrator under the hood — pack vocabulary stays hidden for this path.
The
/pipelinepack-selector remains the surface for pre-authored SDLC packs. - Each picker lists backends from
AuthOkMsg.providers(packages/protocol/src/types.ts, "first entry = default") and models frommodels.list(per-provider), gated by the capability matrix (§6) — a backend that can't hard-deny is flagged, not hidden, for thereviewrole. The orchestrator picker is Claude-only in v1 (#245); the worker pickers are open. - Submit compiles to a pack and creates the run; the collaboration config persists as a JSON column (
#addColumnIfMissing,src/daemon/store.ts) and is stamped into transcript meta, so it survives a daemon restart the wayproviderId/rolealready do.
Daemon-wide default role→backend mappings belong in the settings manifest (src/daemon/settings/manifest.ts, which already exposes conductor.provider/conductor.model); the per-session config is a create-request concern, not a manifest knob.
Reused as-is — canonical history + converters (providers/canonical.ts); the durable dispatch queue + crash-safe worker lifecycle + digests (dispatch.ts, store.ts); ZeroID delegated identities + scope-intersection safety + cascading revocation (agent-identity.ts); the R3 hard approval gate (session.ts); pack governance + phase/gate/halt state machine (pipeline/); applyPhaseActivation for live role reconfiguration (session.ts); the memory engine for artifact-adjacent recall (memory/engine.ts); the create path (session-manager.ts #create).
New to build —
- Role→backend binding in
roleSchema+ collaboration config onSessionCreateMsg(+ Zod, fail-closed validation). provider/modelplumbed throughspawnWorker/DispatchTaskRow(+ a provider-aware model-id validation path — todayresolveModelId/set_modelare Claude-only,src/daemon/models.ts).- The goal blackboard: fixed-core typed-artifact store (schema + tables) + scoped
extra+ a mountable MCP tool surface (stdio/registry, not the Claude-SDK in-process object, #245), read/write-scoped per role at thecanUseToolfence. - The dispatch barrier/join over a dispatch group (today: per-task event injection).
- The
panelphase kind + the synthesis/merge step. - The per-backend capability matrix + verification bench.
- Create-time collaboration UI (toggle → ephemeral one-goal pack) + persistence column.
- v1 guards: a per-session live-worker cap + a per-collaboration cost roll-up surfaced at approve-time (reusing per-turn metrics + the per-child budget/failure auto-block).
Vertical slices; each ends in a working, shippable daemon.
- P0 — Role→backend on child sessions.
Generalize
spawnWorker/DispatchTaskRowto carry(provider, model); add a provider-aware model-id validation path. Exit: a dispatched worker runs on a chosen non-default backend;conductor-session-style tests prove per-child provider. - P1 — Collaboration config + create path.
collaborationonSessionCreateMsg+ Zod + fail-closed provider validation + persistence column; the toggle compiles to an ephemeral one-goal pack; a collaborative session spawns its role-children (per-goal lifetime) on their bound backends. Exit: create a collaborative session from the CLI; children come up on the right backends with the right leaf scopes and are torn down at goal completion. - P2 — Goal blackboard.
Fixed-core typed-artifact store (+ scoped
extra) + tables + the mountable MCP tool surface (stdio/registry, #245); enforcereads/writesas access scoping at thecanUseToolfence (Claude-hard, advisory-logged elsewhere). Exit: searcher→architect→reasoner hand off through artifacts; the orchestrator holds only an index; a restart resumes mid-goal. - P3 — Panel + barrier + synthesis + v1 guards.
The dispatch barrier over a dispatch group; the
panelphase kind; orchestrator synthesis (judge role optional); optional debate round. Plus the lightweight guards: a per-session live-worker cap and a per-collaboration cost roll-up surfaced at approve-time (reusing per-turn metrics + the existing per-child budget/failure auto-block). Exit: N cross-backend reviewers run in parallel on onediff, join, and a merged verdict surfaces; reviewers provably cannot read author reasoning or write files; a runaway fan-out is capped and its cost is visible before approval. - P4 — Unify with packs/pipeline (+ dependency-graph scheduling).
(provider, model)onroleSchema; wire the dormant per-phase override onto child selection;reads/writesgraduate from access scoping to live scheduling edges (auto-fire a role when its inputs exist); an SDLC pack'sreviewphase becomes a panel. Exit:/pipelineruns a pack whose review phase is a cross-backend panel; a pure collaborative session is a one-goal pack; ready roles fire without explicit orchestrator dispatch. - P5 — Front doors + capability matrix. The extended create dialog (web + Telegram + CLI); the declared per-backend capability matrix + verification bench driving the role-picker. Exit: toggle Collaborative, assign models to roles, watch the graph run live, approve dispatches — from web and Telegram.
- P6 — Governance (later).
Cedar policy per identity/
delegation_depth; Shield on egress; per-collaboration token budget + loop cap.
Omnigent (Databricks) is the closest shipped meta-harness with multi-model collaboration; its polly (orchestrator + cross-vendor review) and debby (parallel panel + debate) exemplars are directly relevant.
Borrow:
- Role =
{backend, model, prompt, policy}+ per-dispatchpurpose; do not hard-code role enums (§3). - Delegation as a tool call driven by one orchestrator prompt; choreography in editable skill prose, not compiled pipeline code.
- Async inbox + auto-wake (codeoid already has this via event-driven digests); carry over the hard-won "act in the same turn you announce" rule to avoid dropped-turn deadlock.
- Runner-side enforced bounds independent of prompt discipline: per-turn fan-out caps, a purpose guard, blast-radius shell heuristics (as a safety net, not a boundary).
- The declarative per-backend capability matrix + verification bench — the single most reusable engineering idea for a multi-backend harness (§6).
- Two review shapes with different plumbing; pick per goal (§7).
Where codeoid is structurally better:
- Blackboard, not message relay (§4) — omnigent's orchestrator re-serializes text between isolated workers and is the context bottleneck; codeoid hands off structured canonical artifacts through the daemon.
- Verifiable identity — omnigent has no per-agent identity; codeoid puts the whole collaboration on one ZeroID delegation tree with one revocation root and full audit.
- Enforced envelopes — omnigent's read-only reviewer is a policy heuristic; codeoid's is a leaf identity that cannot mint a write scope.
- Resumable collaboration — omnigent's coordination lives in a turn; codeoid's lives in a durable graph + work queue that survives restart.
Watch the footguns omnigent hit: no cross-turn concurrency accounting (add a live-worker cap, not just a per-turn cap), uneven cancellation across backends (surface it, don't assume a killed worker is gone), and context re-serialization cost (the blackboard is our answer).
The adversarial grill resolved every original open question:
- Blackboard schema → fixed-core + scoped
extra(§2, §4). - Orchestrator backend → Claude for v1, tools built as a mountable MCP server; any-backend orchestrator tracked in #245.
- Concurrency ceiling → v1 live-worker cap + approve-time cost roll-up; hard ceilings + Cedar/Shield stay P6.
- Synthesis authority → orchestrator by default; a
judgerole is a pack option. - Independence vs. context → reviewers read
diff+speconly, never implementer reasoning, enforced at thecanUseToolfence. - Degenerate-pack ergonomics → a first-class Collaborative toggle that compiles to an ephemeral one-goal pack (§9).
- Cost visibility → per-collaboration roll-up surfaced at the R3 approve-time.
Deferred to implementation / later phases (not design branches):
- The exact typed schema of each core artifact (
spec/research/adr/task-list/diff/findings) — settled in P2. - The
reads/writes→ scheduling-edge semantics and cycle/deadlock handling — settled in P4 when they graduate from access scoping. - The concrete contents of the per-backend capability matrix + the verification bench — settled in P5.
- Whether a debate round is on by default — a P3 pack-config knob.