Repository navigation
[bug] Supervision can be wired on an agent that cannot delegate — the picker offers it and the route answers 201 #354
Description
Two halves of supervision that never check each other
Supervision is split across two scopes, correctly:
- The graph is instance-level —
agent_supervision(migration 0060) maps
supervisor_instance_id→subordinate_instance_id. You wire two of your instances together. - The ability is agent-level —
delegate_goal,list_subordinates,subordinate_status,
check_delegationaresupervisionregistry tools, and an agent only receives them if its
capabilities.toolsdeclares them (toolNamesFortreats a declared list as authoritative).
Exactly one agent declares them: coder-lead (migrations 0063, 0067).
Nothing joins the halves:
The UI offers the wiring to everyone. TeamworkSection.tsx renders "Agents this one
supervises" with a live picker on every instance's Settings tab. It is mounted unconditionally from
SettingsTab.tsx:1080. On this account that is 26 instances, 25 of which cannot delegate.
The route agrees with the UI, not with the runtime. POST /v1/instances/:id/supervision
(routes/tools.ts:679-696) checks ownership and then the graph rules — no self-edge, no cycle, one
supervisor per subordinate, depth ≤ 4, fan-out ≤ 12. Every one of those is a real check and they are
right to be there. But it never asks whether the supervisor's agent can delegate at all. It returns
201 and writes the row.
What that produces
A user picks a subordinate on, say, Repo Chat. The picker accepts it, the row appears under "Agents
this one supervises", the API said 201. Then they tell the agent to delegate, and it cannot — the tool
was never in its allowlist, so it does not exist from the model's point of view. The agent's best
available behaviour is to explain that it can't, which reads as the agent being broken rather than the
configuration being impossible.
This is the same shape as #351: a surface reporting a capability the runtime will refuse. There
the tool listing said allowed=true, reason=ok for a write-gated tool; here the supervision editor
and its route both say yes to an edge that can never be used.
Fix
Route — in POST /:id/supervision, resolve the supervisor instance's capabilities and 400 when
its tool set contains no supervision connector tool. The message should name the reason
("this agent cannot delegate — its capabilities declare no supervision tools"), consistent with the
other wiring-time rejections, which are deliberately loud because the human is present. That
sentence is already in migration 0060's header and it applies here.
Console — gate the Supervision half of TeamworkSection on the same fact, from the resolved
capabilities that /v1/instances/my/instances already attaches to each instance. Connections and
Deliveries stay ungated: any agent can emit a fact, and any agent can be a target.
Note the two halves must agree in both directions — a picker that offers nothing is fine, a
picker that offers something the route rejects is not, and neither is a route that accepts what the
picker would have hidden.
Out of scope, but worth naming
Whether the subordinate needs anything declared. It does not today, and probably shouldn't — being
delegated to is not a capability, it is just being an instance. Recording that here so the next person
doesn't add a symmetric check by reflex.
Related: #351 (same class), #183 (the graph), #185 (subordinate executes with its own tools/consent).
Activity
serge-ivo commentedon Aug 7, 2026
Sequence for the settings-scope set filed together (#352, #353, #354, #355): #353 → this → #355 → #352.
This one is independent of the other three — it touches the supervision route and
TeamworkSection, not the connector blocks — so it can land in any order. It is placed second only because it is small and verified.Verification recorded, since the load-bearing claim was worth checking twice: the risk was that an agent declaring no
capabilities.toolsmight receive supervision tools by default, which would make "25 of 26 cannot delegate" false. It does not.toolNamesFor(agent-do-tools.ts:183) falls back toFULL(:111) = BASE + KB_READ + KB_WRITE + FILES + COLLECTIONS + APPLY + CODING — no connector tools, so no supervision tools. Checked against live data too:delegate_goalappears in exactly one of 26 subscribed instances (coder-lead).serge-ivo commentedon Aug 7, 2026
Scoping note, verified while filing #358: the route fix covers MCP for free.
create_supervisioninworkers/mcp/src/instance-tools/composition.ts:72is a thin proxy — it callsauthedCall("/v1/instances/:id/supervision", …, {method:"POST"})rather than reachinglib/supervision.tsor D1 directly.So the two-part fix in the body is complete as written: the validation in
POST /:id/supervisioncloses the API and MCP together, and theTeamworkSectiongate is purely so the picker stops offering what the route now rejects.Its
dry_runtext will also want updating — it currently enumerates the rejections ("supervision loop, depth or fan-out limit, second supervisor") and would need "or the supervisor declares no supervision tools" to stay accurate.fix(api,console): supervision can only be wired where it can be used (#…
docs(api): the tool listing's published schema says what the fields m…
serge-ivo commentedon Aug 7, 2026
Landed in
3229460.Claims checked, all held.
POST /v1/instances/:id/supervisionchecked ownership and then only the graph rules — no capability check anywhere in the path — andTeamworkSectionwas mounted unconditionally fromSettingsTab, picker and all.Route.
delegationDenial(await capabilitiesForInstance(...))→ 400 "This agent cannot delegate — its capabilities declare no supervision tools…", placed with the cycle / tower / fan-out / two-managers rejections and for the reason they state: invisible at run time until it has already cost money, and the human is present now. Same argumentcreate_connectionmakes for validating its filter at create time.Both halves read the same fact from the same source, so they cannot drift into disagreeing — which is the failure mode you named. The route asks the registry (
getConnector("supervision").tools); the console asksGET /v1/instances/:id/tools, which carries each tool's registryconnector. A fifth supervision tool or a rename needs no edit in either. No copied list of tool names exists in this change.Console. Supervision is offered only to an agent that can delegate; Connections and Deliveries stay ungated, per the ticket. Two judgement calls worth naming:
allowed: an owner who switched the delegation tools off has paused the wiring, not made it impossible.Failure-read direction. A capabilities lookup that comes back empty ALLOWS. Ownership is proven before that line runs, so a null is a failed read rather than evidence — refusing on it would turn a D1 blip into "your agent cannot delegate", which is this ticket's own bug pointed the other way. The runtime gate is untouched either way.
Left alone as you asked: the SUBORDINATE needs nothing declared, and
create_supervision's MCP description now says so explicitly so nobody adds the symmetric check by reflex.CI green.
fix(api): a trigger action is offered only where the agent can run it (…
fix(api,console): a connection cannot silently name a pipeline that i…
fix(api): the catalog claims lint runs where the copy actually changes (
test(api): drive both refusal paths, and document what the two now re…