I once built a multi-agent system where every agent had a name, a tone, and a backstory, and not one of them knew anything the others didn’t. Each persona had a character. None of it changed what any agent could actually do, because capability does not come from a costume. It comes from which files are in the context window when the agent runs.
That sounds obvious written down. It was not obvious while I was building it. The persona abstraction is seductive because it maps onto how we think about people: you hire a “PM,” a “reviewer,” a “researcher,” and you expect different behavior from each. So you build the same thing for agents. You write a system prompt that says “you are a meticulous code reviewer who values correctness above all,” and you feel like you did something. You did not. You told a stateless model to role-play. The model will happily play the part and still answer from whatever happens to be in its context. If all you gave it was a persona, that context holds nothing relevant to the actual task.
The tell: knowing-because-named
Here is the test I use now. Ask: why does this agent know X? If the honest answer is “because I named it the X-specialist,” it does not know X. If the answer is “because the files that contain X were loaded into its context for this task,” it knows X.
A persona is a claim about identity. A scope is a claim about contents. Only one of those is checkable, and only the checkable one is real. When I designed the PM engine for my agent system, I made this the load-bearing decision: the unit of context is a scope, not a persona. A scope is a deterministic file set derived from the task/area/epic hierarchy. Give me a task id, I resolve it to a stable bundle of files. Give me an area, I resolve it to the layered set of docs that define that area. The agent that runs against that bundle “knows” exactly what is in the bundle, no more, no less, and I can list it.
This is not a metaphor for permissions. It is structurally the same thing as a Unix file mode or an OAuth scope: a concrete, enumerable grant. “You are a senior architect” is vibes. “Your context is L0 session manifest + L2 epic doc + L3 ticket spec for task-1234” is a grant. You can diff two grants. You cannot diff two backstories. In code the whole interface is one function:
// The entire scope abstraction is one signature.// Everything (briefing, PM, oracle) sits on top of this.
resolve(ref: ScopeRef): FileBundle// ScopeRef = { kind: "task" | "area" | "epic", id: string }// FileBundle = ordered, deduped, deterministic list of file paths
resolve({ kind: "task", id: "task-1234" })// =>[ "sessions/task-1234/manifest.md", // L0 what is running "epics/checkout-rework/epic.md", // L2 why it exists "tickets/task-1234/spec.md", // L3 what to build]// Same ref in, same bundle out, for every consumer.// "Why does this agent know X?" is answered by grepping this list,// not by reading a system prompt.One resolver, three consumers
The thing that made this practical was already half-built before I understood it. The agent dashboard I had was loading layered scopes purely for display. It walked from L0 (session manifests) up through L5 (harness docs) so it could render what was going on across the fleet. The scopes existed. They just stopped at the screen. Nothing was loading them into the agents that got spawned.
So the move was small: extract a thin scope resolver that maps any scope reference to a stable file bundle, and let multiple policy consumers sit on top of it. The resolver does one job. It does not decide what to do with the files. It hands back the deterministic set, and the consumer applies its own policy. Three of them, all on the same resolver, none of them bleeding into each other:
- Briefing. Given a scope, produce a human-readable summary of state. Read-only. It loads the bundle and tells you what is happening.
- PM. Given a scope, produce a decision: one recommended next action, a blocked-on-me queue, and the exact worker handoff payload it would dispatch if authorized. Same bundle, different policy on top.
- Oracle. Given a scope and a question, answer from the loaded files. A context query, not an action engine.
Three different behaviors, one shared notion of “what files are in play for this scope.” The briefing consumer cannot accidentally see something the oracle was scoped away from, because they are pulling the same deterministic bundle through the same resolver. There is no per-consumer file-loading code to drift. If a fourth consumer shows up tomorrow, it does not re-implement scope resolution. It asks the resolver and gets the same bundle everyone else gets.
Compare that to the persona world, where each “role” carries its own ad-hoc idea of what it should look at, baked into a prose system prompt that nobody can statically inspect. You cannot guarantee two personas see a consistent slice of reality. With one resolver you get that for free.
Why this makes multi-tenancy almost free
The payoff I did not expect: a tenant is just a set of scopes.
If your context primitive is a persona, multi-tenancy is a nightmare. Every persona has to be taught about tenant boundaries, and you are one leaky prompt away from agent-for-tenant-A reasoning over tenant-B’s files. The boundary lives in fragile instructions.
If your context primitive is a scope, the tenant boundary is the scope boundary, and it is already enforced by the resolver. Onboarding a tenant means defining which scopes belong to it. The same briefing, PM, and oracle consumers run unchanged. They never knew about tenants and never needed to, because they only ever see the bundle the resolver hands them, and the resolver only hands them scopes inside the tenant’s set. The isolation is a property of the file-set math, not of how well I worded a warning.
The trap, and how to dodge it
There is a sequencing lesson tangled up in this. When I planned the phases of the system, the architecturally clean first deliverable was the scope resolver itself, the reusable primitive everything else needs. A codex review pass flagged that this was backwards from a trust standpoint. The pain I actually felt was “my agents do not make progress on my backlog,” which is the PM consumer’s job, not the resolver’s elegance. So the first visible deliverable became PM in propose-only mode against a real backlog slice: it does the full reasoning, shows me one recommended action and the handoff it would dispatch, and does not act until I have seen enough correct proposals to flip execution on. The resolver still had to exist underneath. But the thing you ship first should be the thing the user feels, not the cleanest proof of the plumbing.
Notice that propose-only is itself a scope story. The agent’s recommendation is only as good as the files it loaded for that task. When a proposal is wrong, the fix is almost never “tune the persona.” It is “the scope was incomplete, the right file was not in the bundle.” One of those you can grep, diff, and fix. The other you can only argue with.
Related
- Capability Gates Beat Risk Labels in Autonomous Agent Design: the permission model that pairs with scope-based context loading
- Writing an AI_CONTEXT.md So Your Assistant Stops Rediscovering Your Codebase: making scope explicit in a file so every session starts with the same enumerable context
- My AI agent’s memory was per-folder, so my four clones never learned from each other: a context-isolation failure with the same root cause, scope not tracked explicitly
- /add-dir Doesn’t Reach Across Your Skills: The IDE-vs-CLI Claude Code Gotcha: a concrete scope-boundary surprise in Claude Code’s context loading
- Teaching the AI to Find the Real Source File: .cursor/rules as a Compiled-Asset Map: encoding scope knowledge so the agent resolves it deterministically