ruvnet/ruflo · error
Cannot compose capabilities with different scopes: "${cap1.s
Error message
Cannot compose capabilities with different scopes: "${cap1.scope}" vs "${cap2.scope}" What it means
CapabilityAuthority.compose() intersects two capabilities (actions intersected, constraints unioned, tighter expiry), which is only meaningful when both describe the same target. It throws when `cap1.scope !== cap2.scope` because intersecting capabilities over different scopes (e.g. 'global' vs 'workspace') has no well-defined result. Scope must match exactly before the resource check even runs.
Source
Thrown at v3/@claude-flow/guidance/src/capabilities.ts:451
}
return chain;
}
/**
* Compose two capabilities via intersection.
*
* - Actions = intersection of both action sets
* - Constraints = union of both constraint sets
* - Expiry = the tighter (earlier) of the two
* - Delegatable = true only if both are delegatable
* - Scope and resource must match; throws if they differ
*
* @throws Error if scope or resource do not match
*/
compose(cap1: Capability, cap2: Capability): Capability {
if (cap1.scope !== cap2.scope) {
throw new Error(
`Cannot compose capabilities with different scopes: "${cap1.scope}" vs "${cap2.scope}"`
);
}
if (cap1.resource !== cap2.resource) {
throw new Error(
`Cannot compose capabilities with different resources: "${cap1.resource}" vs "${cap2.resource}"`
);
}
// Actions: intersection
const actionSet1 = new Set(cap1.actions);
const intersectedActions = cap2.actions.filter(a => actionSet1.has(a));
// Constraints: union
const combinedConstraints = [...cap1.constraints, ...cap2.constraints];
// Expiry: tightest
let expiresAt: number | null = null;View on GitHub (pinned to fa13ee4ad6)
Solutions
- Only compose capabilities minted with the identical scope string
- Re-grant one of the two under the other's scope, then compose
- Filter capability pools by `cap.scope === targetScope` before composing
- Centralize scope names as constants so naming cannot drift
Example fix
// before const merged = authority.compose(globalCap, workspaceCap); // scopes differ → throws // after const pairs = caps.filter(c => c.scope === 'workspace'); const merged = pairs.length === 2 ? authority.compose(pairs[0], pairs[1]) : mintFresh();
Defensive patterns
Strategy: validation
Validate before calling
if (cap1.scope !== cap2.scope) {
// different scopes — skip compose or re-grant one side
} Type guard
const sameScope = (a: Capability, b: Capability): boolean => a.scope === b.scope;
Prevention
- Filter capability pools by scope before composing
- Define scope names as shared constants
- Never compose capabilities minted for different tenants/environments
When it happens
Trigger: compose(capA with scope 'global', capB with scope 'workspace'); composing capabilities minted for different environments or tenants; generic helper code that pairs any two capabilities from a pool without filtering by scope.
Common situations: Capabilities issued per-namespace/per-tenant being mixed in a shared pool; refactors that change the scope naming scheme ('ws' vs 'workspace') so older stored capabilities no longer match; multi-environment configs mixing staging and production caps.
Related errors
- Cannot compose capabilities with different resources: "${cap
- Capability ${capability.id} is not delegatable
- Cannot delegate revoked capability ${capability.id}
- Cannot delegate expired capability ${capability.id}
- Issue ${input.issueId} is not claimed by ${input.claimantId}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/816ed6e7708b858b.
Report an issue: GitHub.