> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SPEC-118: prism_persona_ratify

> Committed persona contract ratification and registry reconciliation

# SPEC-118 v0.1 — `prism_persona_ratify`

**Status:** draft for Candi governance gate\
**Author:** Texi, architecture\
**Created:** 2026-05-14\
**Owner:** Texi architecture; Candi governance gate; Donna engineering implementation\
**Related:** SPEC-105, SPEC-106, SPEC-107, SPEC-111, task #57, postmortem `904392ee`, signal `7b7f6081`

## 1. Purpose

Prism needs a named, audited path that reconciles an existing runtime persona row
against its committed `.agent/personas/<identity>.json` contract.

Today, implicit-bootstrap personas can become real behavioral participants before
their registry row reflects the committed contract. `prism_persona_create` is
create-only and fails for an existing identity. `prism_start` intentionally does
not read committed persona files. SPEC-106's `prism_persona_promote` exists as a
backend service and `/personas/promote` HTTP route, but it is not exposed as an
MCP verb and is flip-only: it records a path and flips `implicit_bootstrap=false`
without validating/reconciling the committed contract, binding row, description,
authority fields, or stale-registry state.

`prism_persona_ratify` closes that gap.

## 2. Decision

Add a dedicated `prism_persona_ratify` verb.

Do not make `prism_persona_create` an upsert. Do not make `prism_start`
mutate persona registry state from filesystem contracts. Ratification is a
governance state transition, so it needs its own verb, preconditions, audit
event, idempotency semantics, and fail-closed drift behavior.

## 3. Definitions

**Committed persona contract:** The JSON file at
`.agent/personas/<identity>.json`, committed to the project repository and
validating against `.agent/schemas/persona.schema.json`.

**Binding row:** The project binding at
`.agent/projects/<pid>/bindings.json` that maps `identity` to a `role`,
`launch`, and `persona_ref`.

**Registry row:** The backend `personas` table row for the PID-scoped persona.
The authoritative join key is `persona_id`, not the mutable identity string.

**Contract-active, registry-stale state:** A persona may have a committed
contract with behavioral authority already in force while the backend registry
still shows stale metadata. Status surfaces MUST represent this explicitly as:

```json theme={null}
{
  "contract_active": true,
  "registry_ratification_stale": true
}
```

until `prism_persona_ratify` succeeds.

## 4. Non-Goals

`prism_persona_ratify` does not:

* create a new persona row
* create a new binding row
* mint authority from an uncommitted file
* ratify governance content without recorded approval evidence
* auto-run during `prism_start`
* change launch mode outside the committed binding row
* bypass Donna/Candi/Texi/Andora/Samantha gates
* deploy, launch, or restart the persona session

## 5. Verb Contract

### 5.1 MCP Verb

```text theme={null}
prism_persona_ratify
```

Inputs:

```json theme={null}
{
  "pid": "PID-PGR01",
  "identity": "Carla",
  "persona_id": "45c51226-f316-42ce-9957-a4aa5e52c0ea",
  "contract_path": ".agent/personas/carla.json",
  "binding_path": ".agent/projects/PID-PGR01/bindings.json",
  "expected_contract_hash": "sha256:...",
  "expected_commit": "afdbd40...",
  "caller_identity": "Donna",
  "caller_session": "271a96a5-d494-48f5-bbf6-dca13e810720",
  "authorization_basis": "accepted_contract",
  "approval_evidence_refs": [
    "signal:7cd334fe-4816-477c-ad92-b11ffea9f7b5",
    "pr:400"
  ],
  "ratified_by": "Donna",
  "reason": "Ratify committed Carla Cursor HITL contract",
  "dry_run": true,
  "confirmation_token": null
}
```

Required:

* `pid`
* `caller_identity`
* `authorization_basis`
* `approval_evidence_refs`
* `ratified_by`
* `reason`
* either `persona_id` or `identity`

Defaults:

* `contract_path` defaults to `.agent/personas/<lowercase identity>.json`
* `binding_path` defaults to `.agent/projects/<pid>/bindings.json`
* `dry_run` defaults to `true`

Live mutation requires `dry_run=false` plus a confirmation token or an
equivalent authorized operator/session gate. The audit MUST distinguish the
actor invoking the mutation (`caller_identity`, `caller_session`) from the
authority that approved it (`authorization_basis`, `approval_evidence_refs`,
`ratified_by`).

### 5.2 Response

```json theme={null}
{
  "dry_run": true,
  "pid": "PID-PGR01",
  "persona_id": "45c51226-f316-42ce-9957-a4aa5e52c0ea",
  "identity": "Carla",
  "contract_path": ".agent/personas/carla.json",
  "binding_path": ".agent/projects/PID-PGR01/bindings.json",
  "contract_hash": "sha256:...",
  "source_commit": "afdbd40...",
  "registry_ratification_stale": true,
  "idempotent_noop": false,
  "changes": {
    "implicit_bootstrap": {"from": true, "to": false},
    "canonical_role": {"from": "project_persona", "to": "coordination"},
    "launch_mode": {"from": "triggered", "to": "triggered"},
    "description": {"from": "old text", "to": "Cursor HITL Interface"}
  },
  "warnings": [],
  "audit_event_id": "uuid-or-null-for-dry-run"
}
```

The response MUST expose all planned changes during dry run. Live response MUST
include the final postcondition state and audit id.

## 6. Validation

Ratification MUST fail closed unless all checks pass.

### 6.1 Registry Target

Resolve the target by `persona_id` when provided. `identity` is only a fallback
lookup path and MUST resolve within the requested `pid`.

Validation:

* registry row exists
* row `tenant_id` matches caller tenant
* row `pid` matches input `pid`
* row is not archived or deduped away
* if both `persona_id` and `identity` are provided, both identify the same row

### 6.2 Contract File

The first-slice trust path is MCP-shim supplied content:

1. The MCP shim runs on the editor host and therefore has access to the working
   tree containing `.agent/personas/*.json` and `.agent/projects/<pid>/bindings.json`.
2. The shim reads the contract and binding files, canonicalizes their JSON,
   computes SHA-256 hashes, verifies the files are committed and the relevant
   paths are clean at `source_commit`, then sends content + hashes + commit to
   the backend.
3. The backend re-validates JSON schema, recomputes hashes from the supplied
   content, verifies `expected_*` inputs, and persists only the backend-validated
   canonical content/hash metadata.

Backend GitHub blob fetch by SHA is an allowed future hardening path, not the
v0.1 default. A backend that cannot verify supplied content/hash/commit MUST
return `contract_source_unverified`.

Validation:

* contract file exists at `contract_path`
* file is valid JSON
* file validates against `.agent/schemas/persona.schema.json`
* `pid` equals requested `pid`
* `persona_id` equals registry row id
* `identity_binding.registry_identity` equals registry identity unless an
  explicit rename operation is in flight
* `identity_binding.dedupe_of` is null or points to a valid same-PID canonical
  persona per SPEC-106
* `identity_binding.implicit_bootstrap` is `false`

The backend must treat file paths as audit labels, not proof. Content, canonical
hash, and commit/source evidence are the proof inputs.

### 6.3 Binding File

Validation:

* binding file exists at `binding_path`
* binding JSON validates against `.agent/schemas/bindings.schema.json`
* exactly one row matches the persona identity
* `persona_ref` equals contract `persona_id`
* binding `role` matches `role:<contract.canonical_role>`
* binding `launch` matches `contract.identity_binding.launch_mode`
* no other binding row in the PID points to the same `persona_ref`

### 6.4 Authorization

`prism_persona_ratify` is restricted to governance/persona authorities:

* operator
* Candi, when acting within governance/persona-contract scope
* Donna, when implementing an accepted Candi/Texi contract
* an explicit future admin role with persona-ratify permission

Ordinary agents cannot ratify themselves. A persona MAY request ratification,
but the mutation requires an authorized caller.

Live ratification requires one of two approval paths:

1. **Accepted contract path:** governance and architecture gates have accepted
   the committed contract shape. Example: Candi governance gate plus Texi
   architecture ownership, recorded by PR, signal, SPEC status, or audit ref.
2. **Explicit operator authorization / override:** Frank/operator explicitly
   authorizes ratification. Operator approval may satisfy the governance approval
   requirement, but the override MUST be recorded in `approval_evidence_refs`.

This preserves Humans Steer / operator authority while preventing non-operator
agents from bypassing Candi/Texi gates by relaying an unverifiable authority
claim.

### 6.5 Hash and Commit Checks

The verb computes a canonical contract hash from the committed contract content.

If `expected_contract_hash` is provided and does not match, return
`contract_hash_mismatch`.

If `expected_commit` is provided and the contract/binding are not read from that
commit or a descendant accepted by policy, return `contract_commit_mismatch`.

## 7. Effects

Ratification reconciles the registry to already-committed authority. It does not
mint new scope.

On `dry_run=false`, the operation updates the registry row from the committed
contract and binding:

* `implicit_bootstrap=false`
* registry identity/display label, unless rename is separately governed
* canonical role
* specialization
* assignment
* surface preference
* capabilities
* launch mode
* active/archive status fields
* description/display summary
* persona contract/schema versions
* persona path
* binding path
* contract hash
* source commit
* ratified\_at
* ratified\_by
* ratification audit id
* `registry_ratification_stale=false`

### 7.1 Registry Storage

Implement SPEC-118 with an additive migration. The current `personas` table does
not have enough columns to represent the ratified contract. Add:

* `canonical_role TEXT NULL`
* `assignment TEXT NULL`
* `surface_preference TEXT NULL`
* `capabilities JSONB NOT NULL DEFAULT '[]'::jsonb`
* `launch_mode TEXT NULL`
* `contract_path TEXT NULL`
* `binding_path TEXT NULL`
* `contract_hash TEXT NULL`
* `binding_hash TEXT NULL`
* `source_commit TEXT NULL`
* `contract_snapshot JSONB NULL`
* `binding_snapshot JSONB NULL`
* `ratified_at TIMESTAMPTZ NULL`
* `ratified_by TEXT NULL`
* `authorization_basis TEXT NULL`
* `approval_evidence_refs JSONB NOT NULL DEFAULT '[]'::jsonb`
* `registry_ratification_stale BOOLEAN NOT NULL DEFAULT false`

Use `description`, `surface_hint`, `specialization`, `implicit_bootstrap`,
`persona_class`, and existing status/archive fields where they already exist.
`contract_snapshot` stores the canonical validated persona JSON used for
ratification so future drift checks have a stable comparison even if the
repository branch moves.

Authority-bearing fields are synchronized from the committed contract only after
either an accepted Candi/Texi contract path or explicit operator
authorization has been recorded. The verb is not an authority authoring path; it
is a registry reconciliation path.

Description reconciliation is in scope. Misleading persona-row descriptions are
the same class of stale registry state as `implicit_bootstrap`; they MUST NOT
require undocumented manual backend repair.

## 8. Idempotency and Drift

If the registry already matches the same contract hash, binding hash, and source
commit, `prism_persona_ratify` returns success with `idempotent_noop=true`.

If the row is explicit but differs from the committed contract, the verb fails
closed with `contract_drift` and returns a diff. It MUST NOT silently overwrite
an explicit registry row whose current state appears to come from a different
contract.

If the binding is missing, points to another persona, or disagrees with the
contract role/launch, fail with `binding_mismatch`.

If identity/ref/hash/commit disagree, fail with a specific mismatch code.

## 9. Audit

Every live ratification writes an append-only audit event.

Audit payload:

```json theme={null}
{
  "audit_kind": "persona_ratify",
  "pid": "PID-PGR01",
  "persona_id": "45c51226-f316-42ce-9957-a4aa5e52c0ea",
  "identity": "Carla",
  "caller_identity": "Donna",
  "caller_session": "271a96a5-d494-48f5-bbf6-dca13e810720",
  "authorization_basis": "accepted_contract",
  "approval_evidence_refs": [
    "signal:7cd334fe-4816-477c-ad92-b11ffea9f7b5",
    "pr:400"
  ],
  "ratified_by": "Donna",
  "reason": "Ratify committed Carla Cursor HITL contract",
  "contract_path": ".agent/personas/carla.json",
  "binding_path": ".agent/projects/PID-PGR01/bindings.json",
  "contract_hash": "sha256:...",
  "binding_hash": "sha256:...",
  "source_commit": "afdbd40...",
  "changed_fields": ["implicit_bootstrap", "canonical_role", "description"],
  "previous_state_ref": "hash-or-json-pointer",
  "new_state_ref": "hash-or-json-pointer"
}
```

Until Prism has a dedicated persona-audit table, the implementation may append
this event to the persona history/audit structure already used by SPEC-106, but
it must remain queryable by persona id and audit kind.

For v0.1, append the ratification audit entry to `personas.rename_history` with
`kind="ratify"` and the payload above. The name is legacy, but the column exists
today and already acts as the persona append-only history surface. A future
schema may split `persona_audit_events` out; that must preserve these entries.

`operator_id` is not a generic caller field. It may appear inside
`approval_evidence_refs` or an approval-evidence object when the operator is the
authority basis, but the mutation actor still records as `caller_identity` /
`caller_session`.

## 10. Status Surfaces

Status surfaces MUST distinguish contract state from registry state.

Examples:

```json theme={null}
{
  "identity": "Carla",
  "persona_id": "45c51226-f316-42ce-9957-a4aa5e52c0ea",
  "contract_active": true,
  "registry_ratification_stale": true,
  "implicit_bootstrap": true,
  "contract_hash": "sha256:...",
  "registry_contract_hash": null,
  "stale_fields": ["implicit_bootstrap", "description"]
}
```

After ratification:

```json theme={null}
{
  "identity": "Carla",
  "persona_id": "45c51226-f316-42ce-9957-a4aa5e52c0ea",
  "contract_active": true,
  "registry_ratification_stale": false,
  "implicit_bootstrap": false,
  "contract_hash": "sha256:...",
  "registry_contract_hash": "sha256:..."
}
```

First-slice implementation MUST expose these fields in the verb dry-run/live
response and SHOULD expose them in `prism_whois`. Boot packets and dashboard
contract-state views SHOULD follow once the registry fields exist. Until those
surfaces land, the verb response is the minimum required status surface for the
reconciliation state.

## 11. Relationship to Existing Verbs

### `prism_persona_create`

Remains create-only. If a matching row exists, it returns an existing-identity
error or a pointer to `prism_persona_ratify`; it MUST NOT upsert.

### `prism_start`

Remains bootstrap/session lifecycle. It may report stale registry state, but it
MUST NOT reconcile persona registry rows from files.

### `prism_persona_promote`

`prism_persona_promote` exists today as a backend service and HTTP route, not as
an MCP verb. It is a flip-only path and is superseded for committed-contract
reconciliation. It may remain as a compatibility alias only if it calls the
ratify service with the same validation and audit rules. A flip-only
implementation is insufficient for SPEC-118 acceptance.

Donna may use the existing promote endpoint as an explicitly interim operational
repair for a single persona row before SPEC-118 ships, but that must be labeled
interim-only and does not satisfy SPEC-118 ratification.

### `.agent/scripts/validate.py`

The validator remains a read-only file/config check. It MAY gain a
`ratify-ready` mode that emits the exact `prism_persona_ratify` input payload,
but it MUST NOT mutate backend state.

## 12. Carla Walkthrough

1. Carla is runtime registered as a Cursor agent and has committed file
   `.agent/personas/carla.json`.
2. `.agent/projects/PID-PGR01/bindings.json` contains:

```json theme={null}
{
  "identity": "Carla",
  "role": "role:coordination",
  "launch": "triggered",
  "persona_ref": "45c51226-f316-42ce-9957-a4aa5e52c0ea"
}
```

3. Until ratification, status surfaces may show:
   `contract_active=true`, `registry_ratification_stale=true`.
4. Authorized caller runs dry run:

```text theme={null}
prism_persona_ratify(
  pid="PID-PGR01",
  persona_id="45c51226-f316-42ce-9957-a4aa5e52c0ea",
  contract_path=".agent/personas/carla.json",
  reason="Ratify Carla Cursor HITL contract",
  dry_run=true
)
```

5. Dry run returns exact changes.
6. Authorized caller runs live ratification with confirmation.
7. Registry row now reflects Carla's committed coordination / `cursor_hitl`
   contract, `implicit_bootstrap=false`, and stale flag false.
8. Donna signals Carla to re-bootstrap so the runtime session observes the
   ratified registry state.

## 13. Acceptance Criteria

1. `prism_persona_ratify` exists as a named backend service and MCP verb, or as
   a backend service plus admin command in the first slice with the same
   semantics.
2. `prism_persona_create` remains create-only.
3. `prism_start` does not mutate persona registry state from files.
4. Dry run returns a full diff and writes no registry changes.
5. Live ratification requires authorized caller plus confirmation and either an
   accepted Candi/Texi contract path or explicit operator authorization /
   override recorded as approval evidence.
6. Ratification validates registry row, contract file, binding row, hash, and
   commit/source evidence.
7. Same-hash rerun is an idempotent no-op.
8. Identity/persona\_ref/hash/binding mismatches fail closed with specific error
   codes.
9. Registry metadata, description/display, authority fields, and
   `implicit_bootstrap` reconcile to the committed contract and binding.
10. Status surfaces can represent `contract_active=true` plus
    `registry_ratification_stale=true` until reconciliation succeeds.
11. A live ratification writes an append-only audit event with changed fields,
    source refs, caller identity/session, authorization basis,
    approval-evidence refs, and `ratified_by`.
12. Carla's `persona_ref=45c51226-f316-42ce-9957-a4aa5e52c0ea` can be ratified
    without creating a parallel persona row.
13. Description/display reconciliation is implemented by `prism_persona_ratify`
    or by an explicitly named sibling path; it is not left as manual backend
    repair.
14. First slice exposes stale/ratified state in the verb response and targets
    `prism_whois` as the first follow-on status surface.
15. First slice uses the MCP-shim trust path: shim reads committed clean files,
    passes canonical content/hash/commit, and backend revalidates schema/hash
    before mutating.
16. Additive migration persists contract/binding hash, source commit, snapshots,
    ratified metadata, approval evidence, and registry stale state.
17. v0.1 audit entries are appended to `personas.rename_history` with
    `kind="ratify"` unless a dedicated persona-audit table lands in the same
    slice.

## 14. Open Implementation Notes

* GitHub blob fetch by SHA is a future hardening option for Cloud/server-side
  ratification, but the v0.1 implementation target is MCP-shim supplied
  content/hash/commit because the backend container has no editor-host checkout.
* If the current description/display field is separate from persona contract
  metadata, reconcile it in this verb or define the sibling update path in the
  implementation plan before shipping.
* After all active rows are explicit and every binding has `persona_ref`, the
  validator warning for missing persona refs can escalate to error per SPEC-106.
