Twins
Persona twins are a program capability, not a fifth product. Every item on this page is
designed and carries the release in which it is designed to ship: R3 twins for
program personas, R4 stage gates, R5 organisation twins. Names are fixed in PRODUCT-CONTRACT.md
section 11.4; the design is PRODUCT-PLAN.md Part 7. Baselines verified in source today: 81 permissions
(EXPECTED_PERMISSION_COUNT), persona version 2, 157 routes (EXPECTED_ROUTE_COUNT in route_matrix.py and
api/scripts/designed-routes.json, including the two public site routes), 53 tables after migration 0003_public, tone map 56. Every
pin below is re-pinned in the API, the web app, this site, the contract and CLAIMS-REGISTER.md in one commit
on the day a release ships; until then the numbers here are targets.
Tables
| Table | Release | Purpose | Key columns |
|---|---|---|---|
twin_definitions | R3 | one twin per persona, person and program | organization_id, program_id (nullable for R5), user_id, persona_key, agent_key (console), status, duties jsonb, tool_allowlist_hash, expiry_hours (72), digest_time, batch_times, budget_tool_calls_month, budget_usd_month (nullable), paused_by, paused_reason, retired_at; unique (program_id, user_id, persona_key); partial unique (program_id, user_id) while active |
twin_triggers | R3 | a duty’s schedule, event, hand-off or mention trigger | twin_id, duty_key, kind, event_action (constrained to TRIGGERABLE_ACTIONS), spec jsonb, min_interval_seconds (3600), priority, standing_plan_json, standing_plan_hash, plan_interrupt_id, enabled, last_fired_at, fire_count |
twin_schedules | R3 | next run per trigger | twin_id, trigger_id, rrule, next_run_at, last_run_at, locked_at |
inbox_items | R3 | transactional pointers to what waits for a person | user_id, organization_id, program_id, kind, source_type, source_id, interrupt_id, twin_id, trigger_id, priority, batch_key, due_at, reminded_at, status, snoozed_until, delegated_from_user_id, feedback, done_by, done_at |
handoff_requests | R3 | the only channel between twins, created sent on approval | from_user_id, from_twin_id, to_persona_key, to_user_id, kind, linked_type, linked_id, message_md (redacted), status, thread_id, decided_by, decided_at, reason; unique partial index on (linked_type, linked_id, to_persona_key, kind) while sent or accepted |
delegations | R3 | one-hop routing of write approvals | user_id, delegate_user_id (must differ), program_id (nullable), starts_at, ends_at (at most 30 days), reason, renewal_count, revoked_at, status |
stage_gates | R4 | the only writer of programs.current_stage_id | program_id, stage_id, version, status, required_records jsonb, brief_artifact_id, brief_version, opened_at, ready_by, ready_at, review_signature_id, close_approval_id, close_signature_id, closed_at, next_stage_id, supersedes_id; unique (program_id, stage_id, version) |
review_audit_reviews | R4 | QA’s monthly oversight summary, counts and ids only | program_id, period_start, period_end, summary jsonb, artifact_id, accepted_by, accepted_at |
Table counts: 53 today (50 after 0002_console plus the three public site tables of 0003_public: public_inquiries,
public_rate_events, site_event_counts); 59 after the six R3 tables (contract 11.4), one more if audit_chain (0003_chain)
ships inside R3 as table-only; 61 after the two R4 tables. Migration names as designed: 0004_twins and
0005_stage_gates, or the next free revision at build time.
Columns added in R3: agent_runs + twin_id, trigger_id, initiated_by, priority, target_id;
run_interrupts + detached; tool_calls + basis_hash; notifications + deliver_after; messages +
mentions; audit_events + batch_id, twin_id, trigger_id, delegated_from_user_id (never together with
signature_id); users.settings + quiet_hours, digest_time, timezone, inbox_shortcuts,
illustrative; programs.settings + twin_defaults, escalation_days; threads.schedule migrated into
paused twins, then dropped. R4: programs.settings + gate_critical_milestone_tag.
Permission keys
Grammar product.resource.action, closed verbs, appended after program.agent_memory.manage in catalogue
order. Counts: 81 today, 87 at R3, 91 at R4, 92 at R5.
| Key | Release | Description | Held by (persona) |
|---|---|---|---|
program.twins.read | R3 | Read every twin of the program and its activity, for oversight | program_owner, program_lead, quality_reviewer |
program.twins.read_own | R3 | Read your own twins, duties and schedules | every program persona |
program.twins.use | R3 | Start, pause, resume, configure or retire your own twin and approve its standing plans | every program persona but viewer |
program.twins.manage | R3 | Pause or retire any twin and set program twin defaults, with a reason | program_owner, program_lead |
program.handoffs.read | R3 | Read hand-off requests | every program persona |
program.handoffs.manage | R3 | Send, accept, decline, complete or withdraw hand-off requests | every program persona but viewer |
review.stage_reviews.approve | R4 | Sign the quality review of a stage gate | quality_reviewer, program_lead, program_owner |
progress.stages.read | R4 | Read stage gates | every program persona |
progress.stages.manage | R4 | Open a stage gate and mark it ready | program_owner, program_lead, manufacturing_specialist |
progress.stages.approve | R4 | Close a stage gate with a signature | program_owner, program_lead |
org.twins.use | R5 | Run an organisation twin | org_owner, org_admin |
Group counts: R3 org 13, program 27, evidence 13, plans 14, review 10, progress 10 (sync line 87 strings (13 org, 74 program)); R4 review 11, progress 13 (91 strings (13 org, 78 program)); R5 org 14. No dormant
key is created: org.twins.use ships with its routes in R5. program.settings.update is re-described in R4
as “Update program settings” because the gate becomes the only writer of the stage.
Personas per version
| Persona | v2 (today) | v3 (R3) | v4 (R4) | Change |
|---|---|---|---|---|
program_owner | 68 | 74 | 78 | absorbs every new key |
program_lead | 67 | 73 | 77 | absorbs every new key |
scientist | 47 | 49 | 50 | gains read_own, use, handoffs.read, handoffs.manage; loses evidence.hypotheses.approve and plans.plans.approve; R4 gains progress.stages.read |
quality_reviewer | 38 | 43 | 45 | gains program.twins.read (oversight), read_own, use, both hand-off keys; R4 gains progress.stages.read and review.stage_reviews.approve |
manufacturing_specialist | 34 | 38 | 40 | gains read_own, use, both hand-off keys; R4 gains progress.stages.read and progress.stages.manage |
viewer | 27 | 29 | 30 | gains read_own and handoffs.read only; has no twin |
TWIN_OVERSIGHT (program.twins.read) is subtracted from scientist, manufacturing specialist and viewer the
way THREAD_OVERSIGHT is today; program.handoffs.read is absorbed by the program-scope reads. Organisation
personas are unchanged until R5 (org_owner and org_admin + 1).
Kinds
| Enumeration | Release | Values added | Count |
|---|---|---|---|
AGENT_RUN_KINDS | R3 | twin_cycle, twin_handoff | 11 -> 13 |
AGENT_RUN_KINDS | R4 | stage_readiness | 13 -> 14 |
agent_runs.initiated_by | R3 | schedule, event, handoff, user (null for non-twin runs) | new |
INTERRUPT_KINDS | R3 | batch_approval (at most 10 items, explicit selection, per-item permission and audit) | 4 -> 5 |
inbox_items.kind | R3 | decision, signature, question, handoff, mention, information | new |
inbox_items.kind | R4 | stage | + 1 |
inbox_items.status | R3 | open, snoozed, done, dismissed, expired | new |
inbox_items.feedback | R3 | none, helpful, not_useful | new |
twin_definitions.status | R3 | active, paused, retired | new |
twin_definitions.paused_reason | R3 | user, manage, budget, high_decline_rate, permission_lost, membership_lost, user_inactive, migrated | new |
twin_triggers.kind | R3 | schedule, event, handoff, mention | new |
twin_triggers.priority | R3 | now, digest | new |
handoff_requests.kind | R3 | review, data, decision, information | new |
handoff_requests.status | R3 | sent, accepted, declined, done, withdrawn (no unreachable proposed state) | new |
delegations.status | R3 | active, expired, revoked | new |
NOTIFICATION_KINDS | R3 | handoff_received, digest, twin_paused, mention, decision_reminder (e-mailed: handoff_received, digest) | 7 -> 12 |
NOTIFICATION_KINDS | R4 | stage_ready | 12 -> 13 |
ARTIFACT_KINDS | R3 | persona_brief (server-written, no model call) | 10 -> 11 |
ARTIFACT_KINDS | R4 | readiness_brief | 11 -> 12 |
stage_gates.status | R4 | open, ready, reviewed, closed | new |
SIGNATURE_TARGET_TYPES, APPROVAL_TARGET_TYPES | R4 | stage_gate | 3 -> 4, 5 -> 6 |
| Tone map | R3 | enabled (pass); sent (review); done, snoozed, revoked (neutral): 12 pass, 16 review, 7 fail, 4 info, 22 neutral | 56 -> 61 |
| Tone map | R4 | reviewed (pass) | 61 -> 62 |
Tools
All pass the forbidden-verb test of the catalogue; every propose_* handler computes basis_hash.
| Tool | Release | Kind | Permission exercised |
|---|---|---|---|
console.propose_handoff | R3 | write (never in PLANNING_TOOLS) | program.handoffs.manage |
console.list_handoffs | R3 | read | program.handoffs.read |
console.get_twin | R3 | read | program.twins.read_own |
console.list_decisions_waiting | R3 | read | program.twins.read_own |
plans.propose_plan_change | R3 | write | plans.plans.manage |
console.get_stage_gate | R4 | read | progress.stages.read |
progress.list_stage_gates | R4 | read | progress.stages.read |
Seeded capabilities (R3): console.twin_literature_watch, console.twin_handoff, review.twin_check_triage,
each with a context of use, limitations and a risk class.
Worker jobs, triggers and registries
| Job | Release | Interval | Does |
|---|---|---|---|
twin_schedule | R3 | 60 s | claims due twin_schedules, enqueues twin_cycle with initiated_by = schedule, honours backpressure and the cadence floor |
twin_events_reconcile | R3 | 3600 s | compares the audit entry_seq watermark per trigger and enqueues anything missed or marked by backpressure |
twin_digest | R3 | 60 s | writes the persona_brief artifact, then the digest notification with deliver_after, in that order |
inbox_expire | R3 | 3600 s | expires items (“Expired, nothing written”, the run stays succeeded), un-snoozes, raises decision_reminder at most daily per item |
twin_evaluate | R3 | 86400 s | proposals filed, approved unchanged, edited, declined by reason, refused, expired; not_useful rate per trigger |
stage_readiness_refresh | R4 | 3600 s | recomputes required_records for open gates only |
TRIGGERABLE_ACTIONS (closed): connector_object.update, hypothesis.decision, sop_version.update,
milestone.update, handoff.accept, experiment.update, approval.create. PAUSE_ACTIONS:
role_assignment.delete, user.deactivate. AUDIT_ACTIONS registers every action name write_audit
accepts; the post-flush hook runs in a savepoint so a hook failure never fails the record write, coalesces per
trigger and interval, and inserts twin_cycle runs with on_conflict_do_nothing. New audited mutations:
inbox.snooze, inbox.dismiss, inbox.feedback, twin.create, twin.pause, twin.resume,
twin.retire, twin.duty_enable, twin.duty_disable, twin.self_pause, twin.enqueue,
handoff.send, handoff.accept, handoff.decline, handoff.complete, handoff.withdraw,
delegation.create, delegation.renew, delegation.expire, delegation.revoke, mention.create; R4
stage_gate.open, stage_gate.ready, stage_gate.reopen, stage_gate.review, stage_gate.close,
stage_gate.supersede.
Routes
P is /programs/{program_id}. Counts: 157 after the site wave, 189 at R3 (+32), 196 at R4 (+7),
198 at R5 (+2). Router order: inbox after me_settings; twins, handoffs, board, metrics after
progress; the export in audit_export.
| Group | Release | Routes |
|---|---|---|
| Inbox and brief (9) | R3 | GET /me/inbox; GET /me/inbox/{item_id}; POST /me/inbox/{item_id}/resolve (the path the inbox, the e-mail link and the phone use; never requires reading the private thread); POST .../snooze; POST .../feedback; GET /me/brief; GET, POST /me/delegations; DELETE /me/delegations/{delegation_id} |
| Twins (13) | R3 | GET, POST P/twins; GET, PATCH P/twins/{twin_id}; POST .../pause, .../resume, .../run-now; GET, POST P/twins/{twin_id}/triggers; PATCH, DELETE .../triggers/{trigger_id}; GET, PUT .../schedule |
| Hand-offs (7) | R3 | GET, POST P/handoffs; GET P/handoffs/{handoff_id}; POST .../accept, .../decline, .../complete, .../withdraw, each scoped by party |
| Board, metrics, export (3) | R3 | GET P/board; GET P/metrics/decisions (per-persona aggregates under program.audit.read; by_user[] only under program.twins.read); GET /orgs/current/twins/export (org.audit.read, metadata only, never tool_calls.arguments) |
| Stage gates (7) | R4 | GET, POST P/progress/stage-gates; GET .../{gate_id}; POST .../ready; POST .../review (signature); POST .../close (signature and approval); GET .../brief |
| Organisation twins (2) | R5 | GET, POST /orgs/current/twins |
Error codes added: R3 stale_proposal, handoff_exists, self_handoff, twin_paused,
twin_active_exists, duty_unavailable, delegation_invalid, batch_item_forbidden, bound_user_only,
baseline_missing; R4 gate_incomplete, gate_stage_restricted, reviewer_role_required, same_signer,
stage_via_gate.
Fixed phrases
Console copy, quoted by the web app, the docs and the deck and never paraphrased; angle-bracket parts are filled at render time.
| Phrase | Release | Where |
|---|---|---|
Decisions waiting for you | R3 | inbox heading, program home |
Your twin proposes. You decide. | R3 | twin setup, twin panel |
Your twin never approves, signs or sends. | R3 | twin setup, twin panel |
Paused. Nothing runs until you resume. | R3 | twin panel when paused |
Nothing waiting. Your twin's next pass is at <time>. | R3 | inbox empty state |
Expired, nothing written | R3 | expired proposal card |
Changed since proposed | R3 | proposal refused with stale_proposal |
Filing paused: <N> decisions waiting | R3 | brief under backpressure |
Let my twin look at this | R3 | mention item; the only way a mention starts a run |
Share this thread to mention colleagues. | R3 | composer in a private thread |
Declining needs a reason; it is stored as evaluation data. | R3 | decline dialog |
Proposed by <Name>'s twin (agent); decided by <Name> | R3 | record footer |
Routed under delegation from <Name>; decided by <Name> in their own right. | R3 | delegated decision |
Agent-generated draft, unsigned. | today | every twin-written artifact (existing phrase) |
Viewers have no twin. Nothing waits for you. | R3 | viewer home |
Every decision stays human and recorded. Timing claims wait for measured data. | today | interim copy on site, docs and deck |
<N> of <M> required records present | R4 | stage gate card |
Stage gate reviewed by <Name> (reviewed) and closed by <Name> (approved) | R4 | closed gate footer |
Twin run footers read “Started by schedule, event, hand-off or you under Name’s standing plan (approved date, plan hash prefix)” and never “Requested by”. The word is “agent-generated”; nothing is described as generated by a model or a vendor.
Source: PRODUCT-CONTRACT.md section 11.4; PRODUCT-PLAN.md Part 7 sections 5, 6 and 7 (7.1 to 7.5); api/app/auth/permissions.py (EXPECTED_PERMISSION_COUNT = 81); api/app/auth/personas.py (PERSONA_VERSION = 2); api/tests/route_matrix.py (EXPECTED_ROUTE_COUNT = 157); api/scripts/designed-routes.json (157); api/app/tools/catalogue.py (FORBIDDEN_TOOL_VERBS); api/app/db/models/signatures.py; api/app/db/models/progress.py; docs/lib/content.ts (tone map 56)