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_delegation are supervision registry tools, and an agent only receives them if its
capabilities.tools declares them (toolNamesFor treats 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).
Two halves of supervision that never check each other
Supervision is split across two scopes, correctly:
agent_supervision(migration 0060) mapssupervisor_instance_id→subordinate_instance_id. You wire two of your instances together.delegate_goal,list_subordinates,subordinate_status,check_delegationaresupervisionregistry tools, and an agent only receives them if itscapabilities.toolsdeclares them (toolNamesFortreats a declared list as authoritative).Exactly one agent declares them:
coder-lead(migrations0063,0067).Nothing joins the halves:
The UI offers the wiring to everyone.
TeamworkSection.tsxrenders "Agents this onesupervises" 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, onesupervisor 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=okfor a write-gated tool; here the supervision editorand 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 whenits tool set contains no
supervisionconnector 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
TeamworkSectionon the same fact, from the resolvedcapabilitiesthat/v1/instances/my/instancesalready attaches to each instance. Connections andDeliveries 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).