Version. 0.5 — supersedes v0_4 (sibling, per the never-overwrite rule). Q5 RULED and SHIPPED. Changes: §11 census closed and its output promoted to §13 (the seed roster, written as data per the ruling); §3 item 2 restated around the roster module; new §14 (the correction to this CR's own record, both statements preserved); §2 carries the standard_agents.py citation the Operator directed; §5 restated. Sections 4, 6–10 and 12 held stable.
Date. 2026-08-20
Status. Open. No item is blocked. Item 1's migration is now writable — the census that gated it is closed. Items 10 and Q5 are shipped to engine main.
Runtime direction: no conflict. Unchanged from v0_4, and Q5 strengthens O5: two non-agents left the agent set, so "which agents are running right now" is closer to counting agents. The roster-as-data requirement (§13) serves O5 and O10 on the same reasoning R1 gave for SANCTIONED_SYSTEM_WRITERS.
Rulings cited. R1, R2; the 2026-08-20 rulings on Q1–Q4 (v0_1) and Q5 (this version); the natural-key + SEED ruling (v0_4); the DR-01 amendment (boundaries-record d829a2a); DR-01 as amended, DR-02, DR-05, DR-07, DR-13, DR-16, DR-18, DR-19.
Q5 — yes to both. …0731 and …0051 take kind="system". Neither goes on SANCTIONED_SYSTEM_WRITERS — per R1 that allowlist is an authority grant, and neither actor asks a commit-path gate for human authority.
…0731 (scheduler): same case as …0010. Substrate machinery must not appear in get_live_sessions.
…0051 (CME dispatcher): same ruling, one condition. DR-19 names fail-closed money paths as not subject to pace, so the same evidence bar as …0010 applied — full suite, ruff, mypy against the unchanged baseline — with a halt-and-report obligation if any gate's decision changed for this actor. It did not, and the check was run anyway. Evidence in §11.
…004c stays agent. The CR that activates it decides.
agents/standard_agents.py had this ruling before it was made. Its module docstring, written long before the Boundaries arc:
> "the durable identity that survives a re-instantiation is the display name + purpose, not the row id."
Item 1 keys on purpose because the engine already treated purpose as the stable role identifier. StandardAgent carries display_name (operator-facing, plain language) and purpose (a stable programmatic tag — "shaping", "rendering"). The natural-key ruling did not introduce a concept; it gave an existing one a row and a constraint.
The rest of §2 is carried forward from v0_4 unchanged: contributors rows with kind='agent' are the existing per-engagement roster (ensure_standard_agents, CR-2026-223); ck_contributors_token_presence already forbids agent bearer tokens, giving O2 a schema precedent; process-local uuid4 identity is the render path only (render_specialist_bootstrap.py:80, :294); RoomSpendContext carries actor_kind: str with no actor id.
agent_identities, role-keyed and seeded. Class AgentIdentityRow. Columns: id (UUID PK), role (the natural key — StandardAgent.purpose is its existing source), display_name (operator-facing, editable, explicitly not the key), status (active · frozen · retired, append-only per DR-05), minted_at, minted_by. No instruction_version, no config version, no engagement_id, no token, key, secret, capability, or scope column. The absence of a config version is the ruling's point: an instruction edit must not disturb identity.
Blast radius, now recorded on the Boundaries side as well — boundaries-record/arc/briefs/loomworks-boundaries-note-freeze-blast-radius-v0_1.md (a462ca0): per-role platform-wide identity means freezing the shaping agent freezes shaping on every engagement at once. Carried to the Step 5 report and the demo one-pager so it is said in the demo rather than discovered during it.
NATIVE_AGENT_ROSTER (§13), status='active'. The runtime has no INSERT path into agent_identities at all — not resolve-or-mint, not upsert, not get-or-create. Resolution is by role. Absent role → fail closed: no session, and a boundary-record-form verdict naming the missing role as the deciding condition (O6). Adding a role is a migration — a human act with a review, which is the human-only-widening posture (O10, DR-02).
Clamp/revoke per R2's re-scoping of DR-16: status carries the two-lane shape — tightening instant and machine-permitted, widening human-only with confirm-step. W1 lands the column and constraint; W2 lands the switch. No code imported from stele-agentic-id.
render_specialist_bootstrap.py:80 and :294 stop calling uuid.uuid4(). The seeded roster loads once at process startup into a frozen role → UUID map, so ensure_render_dispatch_agent_for_engagement keeps its synchronous signature and both call sites in engagement/render.py are untouched. A frozen cache is sound only because the ruling forbids runtime minting — that is the concrete way SEED is cheaper than the resolve-or-mint it replaced.Items 4–9 and 11–12: unchanged from v0_3/v0_4.
Item 10 — SHIPPED. …0010 → kind="system". Engine work 39733d9, merge d275df6.
Unchanged from v0_4. §4 out of scope. §6 Q4 (own-key path → CR-2026-228, before demo assembly). §7 the released naming fence. §8 acceptance. §9 the plural-chokepoint carry-forward. §10 the substitution rule. §12 the unique constraint — UNIQUE (role), single-column, with the contributors precedent proving it necessary.
The +2 days quoted in v0_4 for the census is spent, and it came in. The census is closed (§11), its output is §13, and item 1's migration is unblocked.
Running total: 3–4 days of margin consumed of 20. Unchanged from v0_4 — Q5 cost hours, not days, because the two reclassifications followed a precedent already built and tested at …0010.
The date holds and is no longer comfortable. Standing trip-wire, restated: anything that threatens the date is raised the day it appears (Step 4 item 4), not absorbed into the margin.
Reclassified to kind="system" (Q5, shipped at engine 0c1bf80, merge 5c0eeea):
…0731 _DEFAULT_SCHEDULER_ACTOR (api/app.py) — cadence firings on a clock.…0051 CME_COMPANION_AUTHORITY_ACTOR (credit/grant_decision_dispatcher.py) — its own comment recorded that kind="agent" was chosen for want of a kind that fit; W7 supplied one.
instruction_version dropped from both — it pins an agent's instruction set and neither actor has one.
DR-19 evidence for …0051, run because it sits on a credit grant-decision path:
| Gate | as agent | as system | changed |
|---|---|---|---|
| required_tier_for_kind | agent | agent | no |
| has_human_commit_authority | False | False | no |
The mechanism, not just the result: the spend gate maps kinds through _AGENT_KINDS, which is NON_HUMAN_ACTOR_KINDS = {agent, companion, system} — system was already in the agent tier the day W7 landed, so no re-tiering was possible. has_human_commit_authority refused agent as non-human and refuses an un-enumerated system: same refusal, same reason.
That reasoning is now a standing test, not a one-time check. test_cme_actor_reclassification_changes_no_gate_decision pins both gates for this actor, so a future edit to either kind-set trips a test rather than silently re-tiering a money path.
Suite: 3838 passed → 3839 passed, 68 skipped, 0 failed. The +1 is exactly the DR-19 test added above; no pre-existing test changed outcome. Ruff clean. mypy 536 errors, all covered by the 336-entry baseline; the baseline file is untouched.
One pre-existing test changed its expected value — reported, not absorbed. test_cme_companion_authority_actor_constructs pinned the kind as a literal (== "agent") rather than by reference to the constant, so unlike the …0010 assertions it did not follow the change. Updated deliberately, with the reason recorded in the test body. This is worth carrying: a contract test that pins a literal is a test that will not follow a ruling, and the two reclassifications differed only in that.
Staying agent: …004c RECONCILIATION_SPECIALIST_ACTOR — unused at runtime, reserved for a future generalised dispatch path where it would be a genuine specialist. The CR that activates it decides.
Per the ruling: the census output is the seed roster, and it is written as data, not as rows scattered through a migration body — on the same reasoning R1 gave for SANCTIONED_SYSTEM_WRITERS. A roster written as data can one day become a governed object.
Shape. A module-level frozen structure beside the identity model — one entry per native agent, each carrying role (the natural key), display_name, and the deterministic id the migration seeds. Deliberately data, not conditionals and not INSERT statements, so that the questions a governed object must answer — what is on this roster, who put it there, when did it change — have one place to be asked.
Roster contents at close of census: the two roles already declared in STANDARD_AGENTS (shaping, rendering), the render dispatch agent, the render specialist role, and the seed-induction agent. Exact ids and the final role list are fixed when item 1 is written — this CR fixes the shape and the source, not the literals, because a roster is data that must be reviewable as a unit and inventing its values in prose would be the scattering the ruling forbids.
Migration-stability note, so the shape does not create a trap. Migrations must stay valid against the code of their own era, so the migration does not import the roster module at apply time; it carries the roster as a single data literal at the top of its own file, and a test asserts the seeded rows match NATIVE_AGENT_ROSTER exactly. Data in both places, one assertion binding them — rather than a migration that silently drifts from the module, or a module the migration cannot safely import in five years.
Adding an entry is therefore three deliberate acts — the roster module, a migration, and a passing binding test. That is the human-only widening posture (O10) expressed as friction rather than as a rule nobody reads.
Per Operator direction: preserve both, do not smooth.
What v0_1, v0_2, and v0_3 said, in §2 of each:
> "There is no agents table… agent identity is a fresh UUID in a module-level dict."
What is true:
> That was true of the render path and false as a general claim. render_specialist_bootstrap.py:80 and :294 do mint uuid.uuid4(), and _DISPATCH_AGENT_REGISTRY is a module dict — so the statement held exactly where it was first looked. But a durable, per-engagement, enumerable agent roster already existed: contributors rows with kind='agent', created from STANDARD_AGENTS at instantiation and resolved idempotently by ensure_standard_agents (CR-2026-223). There is no table named agents; there was never an absence of persisted agent rows.
Both statements stand in the record. v0_1–v0_3 are siblings and are not edited; the correction lives here and in v0_4 §2.
Why it is worth preserving rather than tidying. The original claim came from reading the render path first and generalising from it — and it was the claim that made Q1 look like a question about whether identity should exist at all, rather than about which of two existing identity schemes should be the permanent one. The corrected reading is what let the natural-key ruling land on purpose, a tag that was already there. The error and its correction are the reason the design improved; smoothing it would hide the mechanism and leave only the outcome.