Personas ground on teammates via injected cross-surface resolution — never by minting team atoms into knowledge/
0066-persona-grounding-cross-surface-team
- Reversibility
- two-way door
DEC-0066 — Personas ground on teammates via cross-surface resolution, not by minting team atoms
Reversibility: two-way door. The change is an additive, optional injected resolver plus an additive surface field on PersonaNeighbor (defaulting to 'atom', so no existing reader breaks). No atom ids are minted; nothing in knowledge/ changes; no schema migration. Remove the resolver and neighbours revert to unresolved — exactly the pre-decision behaviour. (Contrast Option 1, which would mint immutable role/agent atom ids — permanent addresses you can only deprecate, never cleanly remove.)
Context — the verified gap
A persona is compiled (compilePersona, packages/runtime/src/persona.ts) by traversing a role's OKF-graph neighbourhood (DEC-0053 Invariant 7, real-edge traversal). But a role's edges also reach its teammates (the expert subagents) and the capabilities it wields (skills) — and those live as build-side operational files (.claude/agents/<id>.md, .claude/skills/<id>/SKILL.md), not as knowledge/ atoms. So an ADR's relates_to: [..., log-auditor] reaches log-auditor, but the persona compiler — which loads only knowledge/ atoms — can't resolve it.
Verified this session against the real, full KB: the forward-deployed-engineer persona grounds on 88/89 reachable targets; the one unresolved target was log-auditor (an agent). Structurally, a persona could never ground on any teammate. This is not link rot — pnpm kb:check is clean because scripts/kb-check.mjs already registers agent/skill stems as known nodes (collectStemIds, ~L162–179) so edges to them aren't flagged. It is a persona-grounding-fidelity gap, and it bites worst exactly where it matters: the fde role's whole job is routing to teammates.
(Distinct from DEC-0053's 41-load-error caveat, which was a reversibility-enum schema issue resolved by DEC-0017 — that class is 0 now, verified. This decision is only the structurally-ungroundable team class.)
The client-vs-Dossier distinction (the constraint that decides it)
This looks like a knowledge-model problem but is really a sovereignty boundary problem. knowledge/ is the client's owned, portable institutional memory (DEC-0001). Dossier's log-auditor/principal-architect/forward-deployed-engineer are Dossier's build-side fleet (DEC-0005). Modelling them as atoms in knowledge/ would make them indistinguishable from real org roles on this dogfood repo, and would invite a client tenant's knowledge/ to carry Dossier's subagents — backwards. A client's personas ground on the client's roles (their own org), never on Dossier's fleet. Whatever we build must be correct for an arbitrary client tenant.
Options considered
- Mint the team as
role/agentatoms inknowledge/. Rejected. Duplicates the description the.claudefile already owns (drift; violates single-source-of-truth —knowledge/modelprinciple "never copy, reference"); puts Dossier's build-side fleet into the client-memory surface (sovereignty); mis-types build machinery as org concepts; doesn't generalize to client tenants. - Teach
compilePersonaan injected cross-surface resolver that reads the agent/skill operational files as ephemeral grounding nodes. Chosen. - Leave it. Rejected. Defensible for arbitrary dangling targets, but not when the single unresolved class is "your own team," for a role defined by teamwork.
Decision
compilePersona gains an optional, injected resolveCrossSurface(id): CrossSurfaceNode | undefined. For a reachable neighbour with no loaded atom, the compiler consults the resolver; a returned node becomes a resolved grounding neighbour tagged with its surface provenance (agent/skill). PersonaNeighbor gains a surface field (atom default) so the floor stays auditable — you can always see which grounding is the client's own memory vs. a referenced build-side teammate. Cross-surface grounding counts toward the floor (a teammate is real grounding).
Purity is preserved as a seam. compilePersona stays fs-free (its zero-credit unit tests depend on that); the resolver is injected. The impure implementation, buildCrossSurfaceResolver(claudeDir) (packages/runtime/src/cross-surface.ts), reads .claude/agents/*.md (id = stem) + .claude/skills/*/SKILL.md (id = dir) once into a sync id→node map, pulling each file's own name/description frontmatter — read, never copied. The AgentSdkBoardWorker supplies one by default (built from tenant.tenantRoot/.claude); tests inject a map.
Generalises to clients for free: a client tenant with no .claude/agents resolves nothing — byte-identical to the pre-decision behaviour. Dossier's own repo gets its team grounded.
SSOT / drift
No drift by construction: the team's SSOT remains .claude/agents/<id>.md and .claude/skills/<id>/SKILL.md; the compiler reads it, never mints an atom. This extends the same "referenceable across surfaces" treatment kb:check already took (known-node handling) from "don't flag as rot" to "resolve as grounding," closing the seam consistently across both tools.
Verification (built + proven, no fabricated status)
compilePersona+buildCrossSurfaceResolver+ the worker wiring implemented;@dossier/runtimetypecheck clean; 135 tests pass (1 live-skip), including 3 new cross-surface cases (resolver resolves a teammate, tagssurface, counts toward floor; a loaded atom is unaffected and the resolver is never consulted for it).scripts/agency-phase0-dogfood.mjsre-run (exit 0): theforward-deployed-engineerpersona now grounds 89/89 — zero dangling targets (was 88/89 withlog-auditorunresolved). The dogfood preflight was also decoupled to compile against the full real KB (not the 6-dir drain seed) so the grounding proof reflects ground truth.pnpm kb:checkclean.
Consequences
- Personas can ground on the teammates a role routes to — the "grounded in the OKF graph" claim is now true for the team too, without polluting
knowledge/. surfacemakes every grounding's provenance auditable (client-atom vs. build-side teammate).- The
knowledge/modelconvention — agent/skill operational files are cross-surface nodes, referenceable + groundable, never minted intoknowledge/— is owned by the Principal Knowledge-Format Architect to record inknowledge/model/index.md; this ADR is its decision record meanwhile. - One small duplication remains acceptable: the stem-collection logic exists in both
scripts/kb-check.mjsandcross-surface.ts(a readdir of two dirs); factoring them into one shared helper is an optional follow-up, not a blocker.