Files
roboco/docs/map/foundation-conventions-identity.md

30 KiB

Purpose

The pure, no-IO/no-DB foundation layer for RoboCo's identity model and the architectural-conventions standard schema. identity.py is the single source of truth for roles, teams, the AGENTS roster, role-sets, and slug/role/team lookups consumed by the orchestrator, services, and API. The conventions/ sub-package defines the YAML schema models (.roboco/conventions.yml), the org-default BUILTIN_RULES, and the effective-map merge (auto-derived scan defaults overlaid by the committed file). _validate.py runs cross-table integrity checks at import time (fail-fast: a misconfigured foundation blocks container start). _generators.py deterministically renders lifecycle artifacts (markdown/JSON/prompt fragments) consumed by the CI build-lifecycle-artifacts gate.

Files

Path Role LOC
roboco/foundation/policy/conventions/__init__.py Package facade re-exporting BUILTIN_RULES, ConventionsParseError, ConventionsStandard, CustomRule, DefinitionKind, Module, Rule, RuleLevel, Waiver, effective_map. 33
roboco/foundation/policy/conventions/effective_map.py Pure merge of auto-derived scan defaults with the committed conventions file into one ConventionsStandard (per-field precedence). 65
roboco/foundation/policy/conventions/models.py Pydantic schema for the conventions standard: Module/Rule/CustomRule/Waiver/ConventionsStandard, BUILTIN_RULES, ConventionsParseError, parse_yaml entry point. 131
roboco/foundation/identity.py Single source of truth for Role, Team, CELL_TEAMS, RoleLevel, AgentRow, the AGENTS roster, role-sets (PM/BOARD/DEV/REVIEWER/ALL), ROLE_LEVEL, and slug/role/team lookup helpers. 317
roboco/foundation/_validate.py Import-time identity validators (unique UUIDs/slugs, every real role has an agent, ROLE_LEVEL covers all roles, PM/BOARD roles populated); run_all() entry. 93
roboco/foundation/_generators.py Deterministic renderers of lifecycle intent-verb/status-transition markdown, panel JSON, and per-role prompt fragments from the lifecycle spec. 145

Key Symbols

Name Kind File:Line Responsibility
Role StrEnum roboco/foundation/identity.py:22 The 12 canonical agent roles (developer, qa, documenter, cell_pm, main_pm, product_owner, head_marketing, auditor, pr_reviewer, prompter, secretary, ceo, system).
Team StrEnum roboco/foundation/identity.py:38 Team enum (backend, frontend, ux_ui, board, main_pm, fullstack, marketing-legacy, system).
CELL_TEAMS frozenset[Team] roboco/foundation/identity.py:54 The three delivery cells {BACKEND, FRONTEND, UX_UI} — distinct from the full Team enum; single source of truth for 'the cells' subset.
RoleLevel IntEnum roboco/foundation/identity.py:57 Hierarchical authority levels (-1 SYSTEM, 0 INTAKE, 1 DEV, 2 QA, 3 DOCUMENTER, 4 CELL_PM, 5 MAIN_PM, 6 BOARD, 7 AUDITOR, 8 CEO) for 'X or above' checks.
AgentRow dataclass(frozen=True) roboco/foundation/identity.py:70 Identity record for one agent: slug, role, team, uuid, is_human (True only for ceo).
_u function roboco/foundation/identity.py:81 Shorthand UUID literal constructor for the AGENTS table.
AGENTS dict[str,AgentRow] roboco/foundation/identity.py:86 The full agent roster: system sentinel, CEO, 3 cells (5 members each), main-pm, product-owner, head-marketing, auditor, intake-1, secretary-1, pr-reviewer-1, and 3 per-cell pr-reviewers.
PM_ROLES frozenset[Role] roboco/foundation/identity.py:251 {CELL_PM, MAIN_PM} — roles allowed to plan/delegate/merge.
BOARD_ROLES frozenset[Role] roboco/foundation/identity.py:252 {PRODUCT_OWNER, HEAD_MARKETING, AUDITOR} — board reviewers; intake/secretary/pr-reviewer deliberately excluded.
DEV_ROLES frozenset[Role] roboco/foundation/identity.py:255 {DEVELOPER}.
REVIEWER_ROLES frozenset[Role] roboco/foundation/identity.py:256 {PR_REVIEWER}.
ALL_ROLES frozenset[Role] roboco/foundation/identity.py:257 frozenset(Role) — every role value.
ROLE_LEVEL dict[Role,RoleLevel] roboco/foundation/identity.py:262 Per-role authority level; PR_REVIEWER=QA, PROMPTER=INTAKE, SECRETARY=BOARD, AUDITOR above MAIN_PM, CEO ceiling.
agent_for_slug function roboco/foundation/identity.py:279 Return AgentRow for slug; raise KeyError on unknown (lists known slugs in message).
slugs_for_role function roboco/foundation/identity.py:286 frozenset of slugs whose agent has a given role.
slugs_for_team function roboco/foundation/identity.py:291 frozenset of slugs whose agent is on a given team.
role_for_slug function roboco/foundation/identity.py:296 Shorthand role lookup; raises KeyError on unknown slug.
role_for_slug_or_none function roboco/foundation/identity.py:301 Safe variant returning None for unknown/stale slug so dispatcher skip-guards don't crash the tick (added in 5bb13c84).
_HUMAN_ONLY_ROLES frozenset[Role] roboco/foundation/identity.py:315 {CEO, PROMPTER, SECRETARY} — the three human-driven roles that are never containers; single source of truth for the human-only set consumed by is_human_only_role and is_spawnable_agent_slug.
is_human_only_role function roboco/foundation/identity.py:320 True for the CEO / prompter / secretary human-driven roles; None (unresolvable slug) returns False. Prefer over bare role in (CEO, ...) checks to avoid the stale-None blind spot.
is_spawnable_agent_slug function roboco/foundation/identity.py:331 True only when slug resolves to a known non-human agent role; False for an unresolvable/stale slug or a human-only role. Dispatcher may spawn a slug only when this returns True.
role_for_uuid_or_none function roboco/foundation/identity.py:347 UUID→Role lookup over the AGENTS table; returns None for a non-seeded/stale UUID. Used by creation-time PM-vs-code guard where assigned_to is carried as UUID not slug.
team_for_slug function roboco/foundation/identity.py:365 Shorthand team lookup; raises KeyError on unknown slug.
RuleLevel Literal roboco/foundation/policy/conventions/models.py:18 'warn' / 'block' — gate severity for a convention rule.
DefinitionKind Literal roboco/foundation/policy/conventions/models.py:19 model / route / helper / business_logic / component / other — what a tree-sitter-classified definition is.
ConventionsParseError class(ValueError) roboco/foundation/policy/conventions/models.py:24 Raised when .roboco/conventions.yml is malformed or fails schema validation; carries .reason.
BUILTIN_RULES dict[str,RuleLevel] roboco/foundation/policy/conventions/models.py:38 Org-default hygiene rules seeded into every project: no_lint_suppressions=block, no_inline_comments=warn. Placement rules are NOT here (derived per-project).
_Base BaseModel roboco/foundation/policy/conventions/models.py:44 Shared pydantic config: extra='ignore' for forward-compat with unknown YAML keys.
Module BaseModel roboco/foundation/policy/conventions/models.py:50 One module boundary: path prefix, purpose, forbidden definition kinds.
Rule BaseModel roboco/foundation/policy/conventions/models.py:58 A toggleable named rule and the level it fires at.
CustomRule BaseModel roboco/foundation/policy/conventions/models.py:65 Project-specific regex rule: id, pattern, message, level, optional language scope.
Waiver BaseModel roboco/foundation/policy/conventions/models.py:75 Accountable escape hatch: a (path, rule, reason) the gate must not flag.
ConventionsStandard BaseModel roboco/foundation/policy/conventions/models.py:83 Parsed standard (raw file or merged effective map): version, languages, modules, rules, custom, waivers.
ConventionsStandard._name_rules_from_keys field_validator roboco/foundation/policy/conventions/models.py:93 Inject mapping key as each rule's name; accepts both {name: {level:...}} YAML and pre-built Rule objects.
ConventionsStandard.parse_yaml classmethod roboco/foundation/policy/conventions/models.py:112 Single entry point from raw file text to validated ConventionsStandard; raises ConventionsParseError on malformed YAML / non-mapping top level / ValidationError.
_merge_rules function roboco/foundation/policy/conventions/effective_map.py:22 Merge rules: BUILTIN_RULES < derived < file (per key).
_merge_modules function roboco/foundation/policy/conventions/effective_map.py:34 Merge modules: derived, file modules override by path, new paths appended in file order.
_union_languages function roboco/foundation/policy/conventions/effective_map.py:43 Union languages preserving derived order then file-only extras.
effective_map function roboco/foundation/policy/conventions/effective_map.py:53 Produce the merged ConventionsStandard: derived defaults overlaid by the committed file; version/custom/waivers come from the curated side (file if present else derived).
_SENTINEL_ROLES frozenset[Role] roboco/foundation/_validate.py:23 {Role.SYSTEM} — excluded from 'every real role has an agent' check.
IdentityValidationError class(RuntimeError) roboco/foundation/_validate.py:26 Raised at import time when identity tables are inconsistent.
_check_unique_uuids function roboco/foundation/_validate.py:30 Fail if any two AgentRows share a UUID.
_check_unique_slugs function roboco/foundation/_validate.py:39 Belt-and-suspenders duplicate-slug check (dict already guarantees uniqueness).
_check_every_real_role_has_agent function roboco/foundation/_validate.py:47 Every Role except SYSTEM must have at least one agent in AGENTS.
_check_role_level_covers_all_roles function roboco/foundation/_validate.py:56 ROLE_LEVEL must have an entry for every Role.
_check_pm_roles_have_agents function roboco/foundation/_validate.py:63 Each role in PM_ROLES must have at least one agent.
_check_board_roles_have_agents function roboco/foundation/_validate.py:71 Each role in BOARD_ROLES must have at least one agent.
_VALIDATORS tuple roboco/foundation/_validate.py:79 Ordered tuple of the six identity validators run by run_all.
run_all function roboco/foundation/_validate.py:89 Run every identity validator in order; first failure raises IdentityValidationError.
_composes_line function roboco/foundation/_generators.py:25 Render the 'Composes' line for one intent verb.
_intent_verb_section function roboco/foundation/_generators.py:31 Render a single intent-verb markdown section (name, description, allowed roles, composes, side effects, preconditions).
render_intent_verbs_md function roboco/foundation/_generators.py:51 Render the full intent-verbs markdown doc, one section per verb sorted by name.
_transition_roles_str function roboco/foundation/_generators.py:59 Render the roles column for a status transition ('any' if no constraint).
_transition_sort_key function roboco/foundation/_generators.py:65 Sort key (source, target, action) for deterministic transition ordering.
render_status_transitions_md function roboco/foundation/_generators.py:69 Render the status-transitions markdown table.
_intent_to_panel_dict function roboco/foundation/_generators.py:85 Serialize one IntentSpec to the panel-UI dict shape.
_transition_to_panel_dict function roboco/foundation/_generators.py:96 Serialize one StatusTransition to the panel-UI dict shape.
render_panel_json function roboco/foundation/_generators.py:106 Deterministic JSON dump (intents + transitions + claim_rules) for the panel lifecycle visualizer.
render_agent_prompt_fragment function roboco/foundation/_generators.py:125 Per-role prompt fragment listing the verbs the agent can call, injected into the system prompt so agent perception matches the gateway surface.

Data Flow

identity.py is loaded eagerly when roboco.foundation is imported (foundation/__init__.py line 10-26 re-exports its symbols; line 48-50 imports and invokes _validate.run_all()). At import, _validate.run_all() iterates _VALIDATORS, each reading identity.AGENTS / Role / ROLE_LEVEL / PM_ROLES / BOARD_ROLES and raising IdentityValidationError on any inconsistency — so a misconfigured foundation aborts container start before any service runs. At runtime, the orchestrator (runtime/orchestrator.py: line 2186, 10327, 10504, 10931), services (permissions, secretary, prompter, release_readiness, self_heal_engine, ci_watch_engine, dep_update_engine, release_manager_engine, gateway/choreographer), API deps/routes, models/base, and seeds/initial_data import Role/Team/AGENTS/role-sets/lookups from here. Adding an agent edits only identity.py; the validators then enforce roster integrity at next import. The conventions sub-package is imported by roboco/services/conventions.py (ConventionsService) and roboco/api/routes/project.py; raw .roboco/conventions.yml text flows through ConventionsStandard.parse_yaml (models.py:112) -> ConventionsStandard, then ConventionsService pairs it with a scan-derived standard and calls effective_map(effective_map.py:53) to produce the merged standard consumed by the validator CLI (roboco/conventions/) and the ambient/baseline prompt injection. _generators.py is offline: scripts/build_lifecycle_artifacts.py imports it and writes rendered markdown/JSON/per-role prompt fragments to disk; the CI gate make lifecycle && git diff --exit-code asserts the on-disk artifacts match the current lifecycle spec (which itself imports Role/Team from identity). No function in this slice performs IO or DB access — all are pure transforms over in-memory tables/specs.

Mermaid

graph TD
    subgraph identity["roboco/foundation/identity.py (single source of truth)"]
        Role["Role (StrEnum, 12)"]
        Team["Team (StrEnum)"]
        RoleLevel["RoleLevel (IntEnum)"]
        AgentRow["AgentRow dataclass"]
        AGENTS["AGENTS dict[slug→AgentRow]"]
        RoleSets["PM/BOARD/DEV/REVIEWER/ALL_ROLES\nROLE_LEVEL\nCELL_TEAMS"]
        Lookups["agent_for_slug / slugs_for_role\nslugs_for_team / role_for_slug\nrole_for_slug_or_none / team_for_slug"]
    end
    subgraph validate["_validate.py (import-time)"]
        RunAll["run_all()"]
        Checks["_check_unique_uuids / _check_unique_slugs\n_check_every_real_role_has_agent\n_check_role_level_covers_all_roles\n_check_pm_roles_have_agents\n_check_board_roles_have_agents"]
    end
    subgraph conv["policy/conventions/ (pure schema)"]
        Models["models.py\nConventionsStandard.parse_yaml\nBUILTIN_RULES\nModule/Rule/CustomRule/Waiver"]
        Eff["effective_map.py\neffective_map(derived, file)"]
    end
    subgraph gen["_generators.py (offline render)"]
        Render["render_intent_verbs_md\nrender_status_transitions_md\nrender_panel_json\nrender_agent_prompt_fragment"]
    end

    foundation["roboco/foundation/__init__.py\neager import + run_all()"]
    foundation --> identity
    foundation --> RunAll
    RunAll --> Checks
    Checks --> AGENTS
    Checks --> Role
    Checks --> RoleLevel
    Checks --> RoleSets

    AGENTS --> Lookups
    Lookups -->|runtime| Orchestrator["runtime/orchestrator.py"]
    Lookups -->|runtime| Services["services/* + gateway/choreographer"]
    Lookups -->|runtime| Api["api/deps + routes"]
    Role --> Lifecycle["foundation/policy/lifecycle.py"]
    Lifecycle --> Render
    Render -->|CI gate| Artifacts["on-disk markdown/JSON"]

    ConvYml[".roboco/conventions.yml text"] --> Models
    Scan["scan-derived standard"] --> Eff
    Models --> Eff
    Eff -->|merged effective map| ConvService["services/conventions.py + roboco/conventions/ CLI"]

    classDef pure fill:#e8f5e9,stroke:#2e7d32;
    class identity,validate,conv,gen pure;

Logical Tree

foundation-conventions-identity
├── roboco/foundation/identity.py  (single source of truth)
│   ├── Role (StrEnum: 12 roles incl. system sentinel)
│   ├── Team (StrEnum: backend/frontend/ux_ui/board/main_pm/fullstack/marketing/system)
│   ├── CELL_TEAMS (frozenset {BACKEND, FRONTEND, UX_UI})
│   ├── RoleLevel (IntEnum -1..8)
│   ├── AgentRow (frozen dataclass: slug, role, team, uuid, is_human)
│   ├── AGENTS (dict: 27 rows = system + ceo + 25 AI agents)
│   │   ├── system sentinel (zero UUID)
│   │   ├── ceo (human)
│   │   ├── backend cell (be-dev-1/2, be-qa, be-pm, be-doc, be-pr-reviewer)
│   │   ├── frontend cell (fe-dev-1/2, fe-qa, fe-pm, fe-doc, fe-pr-reviewer)
│   │   ├── ux_ui cell (ux-dev-1/2, ux-qa, ux-pm, ux-doc, ux-pr-reviewer)
│   │   └── board/main_pm (main-pm, product-owner, head-marketing, auditor, intake-1, secretary-1, pr-reviewer-1)
│   ├── Role-sets: PM_ROLES, BOARD_ROLES, DEV_ROLES, REVIEWER_ROLES, ALL_ROLES
│   ├── ROLE_LEVEL (dict Role→RoleLevel)
│   ├── Lookups: agent_for_slug, slugs_for_role, slugs_for_team, role_for_slug, role_for_slug_or_none, team_for_slug
│   ├── _HUMAN_ONLY_ROLES (frozenset: CEO, PROMPTER, SECRETARY)
│   └── Spawn guards: is_human_only_role, is_spawnable_agent_slug, role_for_uuid_or_none
├── roboco/foundation/_validate.py  (import-time integrity)
│   ├── _SENTINEL_ROLES = {SYSTEM}
│   ├── IdentityValidationError
│   ├── _check_unique_uuids
│   ├── _check_unique_slugs
│   ├── _check_every_real_role_has_agent
│   ├── _check_role_level_covers_all_roles
│   ├── _check_pm_roles_have_agents
│   ├── _check_board_roles_have_agents
│   ├── _VALIDATORS (ordered tuple)
│   └── run_all()
├── roboco/foundation/_generators.py  (offline deterministic render)
│   ├── _composes_line / _intent_verb_section
│   ├── render_intent_verbs_md
│   ├── _transition_roles_str / _transition_sort_key
│   ├── render_status_transitions_md
│   ├── _intent_to_panel_dict / _transition_to_panel_dict
│   ├── render_panel_json
│   └── render_agent_prompt_fragment
└── roboco/foundation/policy/conventions/  (pure schema + merge)
    ├── __init__.py  (re-exports)
    ├── models.py
    │   ├── RuleLevel / DefinitionKind (Literals)
    │   ├── ConventionsParseError
    │   ├── BUILTIN_RULES (no_lint_suppressions=block, no_inline_comments=warn)
    │   ├── _Base (extra=ignore)
    │   ├── Module / Rule / CustomRule / Waiver
    │   └── ConventionsStandard (+ _name_rules_from_keys validator, + parse_yaml classmethod)
    └── effective_map.py
        ├── _merge_rules (BUILTIN < derived < file)
        ├── _merge_modules (derived, file overrides by path)
        ├── _union_languages (derived order then file extras)
        └── effective_map (merged ConventionsStandard)

Dependencies

  • Internal: roboco.foundation.identity (re-exported by foundation/__init__.py and consumed by ~30 modules: orchestrator, services/permissions, services/secretary, services/prompter, services/release_readiness, services/self_heal_engine, services/ci_watch_engine, services/dep_update_engine, services/release_manager_engine, services/gateway/choreographer/_impl, services/gateway/content_actions, api/deps, api/routes/orchestrator, api/routes/pitch, api/routes/v1/_role_dep, api/schemas/product, api/schemas/pitch(?), models/base, models/product, models/pitch, seeds/initial_data, agents_config, foundation/policy/lifecycle, foundation/policy/communications, foundation/policy/journaling, foundation/policy/task_completeness, foundation/policy/content/models, enforcement/journal_perms, services/learning, services/pitch, services/product), roboco.foundation._validate (run_all invoked at foundation package import), roboco.foundation._generators (consumed by scripts/build_lifecycle_artifacts.py; reads roboco.foundation.policy.lifecycle._INTENT_VERBS/_STATUS_TRANSITIONS/CLAIM_RULES/IntentSpec/Role/StatusTransition), roboco.foundation.policy.conventions.{models,effective_map} (consumed by roboco/services/conventions.py ConventionsService, roboco/api/routes/project.py, and the roboco/conventions/ validator CLI)
  • External: pydantic (BaseModel, ConfigDict, Field, ValidationError, field_validator), yaml (PyYAML safe_load / YAMLError), dataclasses (dataclass frozen=True), enum (IntEnum, StrEnum), uuid.UUID, collections.Counter, typing.Any/Literal, json (render_panel_json)

Entry Points

Name File Trigger
foundation package import → run_all() roboco/foundation/__init__.py import roboco.foundation (or any submodule) — line 48-50 imports and invokes _validate.run_all() at module load; first failing validator raises IdentityValidationError and blocks orchestrator start
identity lookups (agent_for_slug / role_for_slug / role_for_slug_or_none / slugs_for_role / slugs_for_team / team_for_slug) roboco/foundation/identity.py called throughout the dispatcher tick, claim/plan/escalate flows, notification routing, permissions checks, and seed bootstrap whenever a slug/role/team must be resolved
ConventionsStandard.parse_yaml(text) roboco/foundation/policy/conventions/models.py called by ConventionsService and the validator CLI when raw .roboco/conventions.yml text is loaded from the read clone
effective_map(derived, file) roboco/foundation/policy/conventions/effective_map.py called by ConventionsService to merge the scan-derived standard with the committed file into the effective map consumed by validator + ambient/baseline injection
render_intent_verbs_md / render_status_transitions_md / render_panel_json / render_agent_prompt_fragment roboco/foundation/_generators.py invoked by scripts/build_lifecycle_artifacts.py under the CI gate make lifecycle && git diff --exit-code to keep on-disk lifecycle artifacts byte-identical to the spec

Config Flags

  • ROBOCO_CONVENTIONS_ENABLED (default-off) — gates the surrounding ConventionsService + validator gate enforcement (i_am_done / pr_pass block + ambient/baseline injection). The schema/effective_map in this slice are pure and always importable, but the gate that consumes them is flag-gated. No flag lives inside the slice itself.
  • ROBOCO_ORG_MEMORY_ENABLED / ROBOCO_RELEASE_MANAGER_ENABLED / etc. — do NOT touch this slice; mentioned only to confirm they do not gate identity or conventions-schema behavior.

Gotchas

  • Import-time side effect: import roboco.foundation triggers _validate.run_all() which raises IdentityValidationError on any roster inconsistency — the orchestrator container will refuse to start. This is intentional fail-fast but means editing identity.py incorrectly has a blast radius across the whole app.
  • role_for_slug raises KeyError on unknown slugs; role_for_slug_or_none returns None instead. The two coexist and callers must pick the right one. Dispatcher skip-guards MUST use the _or_none variant or a stale assignee/notification-target slug crashes the whole tick (this was the 5bb13c84 fix).
  • role_for_slug_or_none returns None for a stale slug, and None in (Role.CEO, Role.PROMPTER, Role.SECRETARY) is False — so the human-only-role skip silently DOES NOT fire for a stale slug. By design (proceed rather than crash), but a stale slug that referred to a human role would not be blocked at this guard.
  • ROLE_LEVEL maps PR_REVIEWER to QA (peer to QA in authority), SECRETARY to BOARD, and PROMPTER to INTAKE (0, lowest real-agent). Auditor (7) sits ABOVE MAIN_PM (5) because it reads everywhere. 'X or above' checks must respect this non-obvious ordering.
  • BOARD_ROLES deliberately EXCLUDES intake-1 (PROMPTER), secretary-1 (SECRETARY), and pr-reviewer-1 / per-cell pr-reviewers (PR_REVIEWER) even though all are Team.BOARD — they are not board reviewers. Code that assumes 'board team == board reviewer role' is wrong.
  • Team.MARKETING is legacy — kept for seed-data parity, no agent declares it. Team.SYSTEM only for the system sentinel. Team.FULLSTACK declared but no agent in AGENTS uses it.
  • AGENTS is a module-level mutable dict. _check_unique_slugs comment notes dict uniqueness is guaranteed; the check exists only to catch accidental mutation. Mutating AGENTS at runtime would bypass validators (they ran once at import).
  • ConventionsStandard._name_rules_from_keys is a mode='before' field_validator: it accepts both the YAML shape {name: {level: warn}} and pre-built Rule objects. Passing a dict of plain strings will NOT be rewritten and will surface as a ValidationError wrapped in ConventionsParseError.
  • effective_map precedence: rules = BUILTIN < derived < file (per key); modules = derived with file override-by-path and new paths appended; version/custom/waivers = file's when a file is present ELSE derived (file is the curated replacement — an empty-file waivers list wins over derived's). derived normally has empty waivers/custom, so this is safe, but a derived scan that ever produces waivers would be shadowed by an empty file.
  • _generators output is deterministic and CI-gated by git diff --exit-code; any spec change must be regenerated or CI fails. Two calls with the same spec produce the same bytes (sort_keys=True / sorted transitions).
  • _validate_lifecycle.py lives in a SEPARATE module (not _validate.py) specifically to avoid an import cycle, because lifecycle.py imports from foundation at module load. Do not move lifecycle validators into _validate.py.
  • CELL_TEAMS is a frozenset {BACKEND, FRONTEND, UX_UI} and is DISTINCT from the Team enum — 'the cells' subset. Code that iterates Team to find cells is wrong; use CELL_TEAMS.

Drift from CLAUDE.md

  • CLAUDE.md describes '25 AI agents + 1 human CEO' — AGENTS has 27 entries minus system sentinel minus ceo = 25 AI agents + 1 human CEO. Consistent (the 3 per-cell pr-reviewers bring the AI count to 25). No drift.
  • CLAUDE.md verb-surface table lists pr_reviewer verbs (claim_pr_review, post_pr_review, claim_gate_review, pr_pass, pr_fail) and notes the per-cell gate reviewers — identity.py models this exactly with pr-reviewer-1 (global/root) plus be/fe/ux-pr-reviewer (per-cell, team-scoped). No drift.
  • CLAUDE.md states the conventions schema lives in roboco/foundation/policy/conventions/ (pure) and that BUILTIN_RULES are language-agnostic hygiene rules while placement rules are derived per-project — models.py:38 comment and effective_map.py confirm this exactly. No drift.
  • CLAUDE.md does not mention role_for_slug_or_none (added 5bb13c84) — but it is an internal helper, not a user-facing surface, so silence is expected rather than drift.

Changes Since Baseline

SHA Subject Impact
5bb13c84 [F031] identity: role_for_slug_or_none so defensive skip-guards don't crash the dispatcher tick on stale slugs Added role_for_slug_or_none(slug) returning None instead of raising KeyError on unknown/stale slugs; orchestrator call sites (lines 2186, 10327, 10504, 10931) switched from role_for_slug to the safe variant so a stale assignee or notification-target slug no longer crashes the dispatcher tick. Purely additive — role_for_slug and all other lookups unchanged. conventions/, _validate.py, _generators.py untouched since baseline.

Post-snapshot updates (since 2026-06-29): 15effce0 (Chore: 141 Gaps fill-in, PR#283, 2026-06-29) — role_for_slug_or_none arrived in this merge (the function attributed to 5bb13c84 in the snapshot history was squashed/rebased here); no other changes to the files in this slice. 536bbb64 (Chore/all/logical gaps sweep, PR#286, 2026-06-30) — four new symbols added to identity.py: _HUMAN_ONLY_ROLES frozenset (line 315), is_human_only_role(role) (line 320), is_spawnable_agent_slug(slug) (line 331), role_for_uuid_or_none(agent_uuid) (line 347); also updated role_for_slug_or_none docstring to reference the new helpers and the #49 guard-hole. team_for_slug shifted from line 314 to 365 due to the insertions. The stale-slug-bypasses-human-guard regression risk is resolved by is_spawnable_agent_slug. _validate.py, _generators.py, and policy/conventions/ files were not touched by either commit.

Regression Risks

Title File:Line Claim Severity
Stale human-role slug silently bypasses human-only skip guard RESOLVED in 536bbb64 roboco/foundation/identity.py:331 After 5bb13c84, a bare role_for_slug_or_none(slug) in (CEO, PROMPTER, SECRETARY) treated a stale None as "not human" and proceeded to spawn. Fixed in 536bbb64: is_spawnable_agent_slug(slug) returns False for any stale/None slug (not just non-human-resolved ones), and all dispatch-skip call sites were updated to use it. The _HUMAN_ONLY_ROLES frozenset is the single authority consumed by both is_human_only_role and is_spawnable_agent_slug. medium
role_for_slug_or_none introduces a None-handling contract callers must honour roboco/foundation/identity.py:301 The new function returns Role / None. Any future caller that does role_for_slug_or_none(slug).value or passes the result into a function typed Role (not Role/None) will hit AttributeError / TypeError on a stale slug. mypy may not catch call sites that annotate the local as Role. The docstring documents the None-as-'not human-only' convention but nothing enforces it. low
Import-time validators raise on any future AGENTS edit that drops a role's last agent roboco/foundation/_validate.py:47 _check_every_real_role_has_agent / _check_pm_roles_have_agents / _check_board_roles_have_agents run at every import. Removing the last agent of a role (e.g. deleting the only secretary row) will raise IdentityValidationError and block container start. Not a regression from 5bb13c84 itself, but a landmine for any future edit to identity.py: the validators are unforgiving and give only a generic message. low

Health

This slice is a pure, dependency-light foundation layer with no IO/DB and strong fail-fast integrity (six import-time validators abort container start on any roster inconsistency). The single change since baseline (5bb13c84) is purely additive — a new safe-lookup helper alongside the existing raising one — and does not alter any pre-existing symbol or behavior, so regression surface is small. The main latent risk is contractual: role_for_slug_or_none returns None and relies on every caller treating None as "not human-only / proceed", a convention enforced only by docstring and review, not by the type checker at use sites. The conventions sub-package is a clean schema + three-field merge with documented per-field precedence and a permissive (extra=ignore) base model for forward-compat. _generators is deterministic and CI-gated by byte-equality. Overall integrity is high; the slice is well-factored and the only watch item is keeping the role_for_slug vs role_for_slug_or_none discipline intact as new dispatcher call sites are added.