Appendix A - 4. Role-typed dispatch

The Structure of Role-typed dispatch — its shape at a glance:

The role is a small closed enum. Each value maps to one policy tuple (model, isolation, gates), and downstream enforcers refuse a call whose declared role doesn't match.

flowchart LR
  Dispatch[/--role r/] --> Enum{{Role enum}}
  Enum -->|maps to| Tuple["Policy tuple<br/>model · isolation · gates"]
  Tuple --> Enforcer{Enforcer at gated op}
  Enforcer -->|role matches| Run([Operation proceeds])
  Enforcer -->|role mismatch| Deny([Refused])

Accessible description: a dispatch declares a role drawn from a closed enum; the enum maps to one policy tuple of model, isolation, and gates; an enforcer at each gated operation lets the call proceed when the role matches and refuses it when it does not.

Projected from the catalogue entry context-and-dispatch / Role-typed dispatch.

On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns

Intent

Intent — Dispatch every agent under a typed role (sonnet-active / opus / lint-runner / commit-slave) that determines its LLM, isolation mode, permissions, and which gates apply, so those choices are policy-by-type, not a per-dispatch judgment call.

Motivation

Every dispatch bundles several correlated decisions: which LLM (Opus for architecture, Sonnet for mechanical work), whether the agent runs in an isolated worktree or on main, and which compute gates it must respect. Made ad hoc, these decisions drift apart from the work: a Sonnet gets pointed at an architecture change whose incorrect output would cause an incident; a commit agent runs in a worktree when it needs to be on main for the hook to fire; a resource-hungry agent runs the aggregate lint it should never trigger. The failure recurs on every dispatch and is invisible until the wrong-tier output ships or the gate is bypassed.

Applicability

Structure

The Structure diagram appears at the top of this page.

Sample Code

A role is a typed enum bound to one policy tuple. Declaring the role once binds the whole bundle; an enforcer at each gated operation reads the declared role and refuses a mismatch, so the correlated choices cannot silently diverge.

from dataclasses import dataclass
from enum import Enum

class Role(Enum):
    WORKER = "worker"       # mechanical, isolated
    ARCHITECT = "architect" # design/RCA, isolated
    LINT_RUNNER = "lint"    # may run the aggregate sweep
    COMMITTER = "committer" # runs on the main line so the commit hook fires

@dataclass(frozen=True)
class Policy:
    model: str
    isolated: bool
    may_run_aggregate_lint: bool

POLICY = {
    Role.WORKER:      Policy("small", isolated=True,  may_run_aggregate_lint=False),
    Role.ARCHITECT:   Policy("large", isolated=True,  may_run_aggregate_lint=False),
    Role.LINT_RUNNER: Policy("small", isolated=True,  may_run_aggregate_lint=True),
    Role.COMMITTER:   Policy("small", isolated=False, may_run_aggregate_lint=False),
}

def enforce_aggregate_lint(role: Role) -> None:
    if not POLICY[role].may_run_aggregate_lint:      # the gate refuses the wrong role outright
        raise SystemExit(f"role {role.value} may not run the aggregate lint")

Consequences

Example use within DocAble

Contents
© James C. Davis, 2026–present