Skip to Content

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

TableReleasePurposeKey columns
twin_definitionsR3one twin per persona, person and programorganization_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_triggersR3a duty’s schedule, event, hand-off or mention triggertwin_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_schedulesR3next run per triggertwin_id, trigger_id, rrule, next_run_at, last_run_at, locked_at
inbox_itemsR3transactional pointers to what waits for a personuser_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_requestsR3the only channel between twins, created sent on approvalfrom_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
delegationsR3one-hop routing of write approvalsuser_id, delegate_user_id (must differ), program_id (nullable), starts_at, ends_at (at most 30 days), reason, renewal_count, revoked_at, status
stage_gatesR4the only writer of programs.current_stage_idprogram_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_reviewsR4QA’s monthly oversight summary, counts and ids onlyprogram_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.

KeyReleaseDescriptionHeld by (persona)
program.twins.readR3Read every twin of the program and its activity, for oversightprogram_owner, program_lead, quality_reviewer
program.twins.read_ownR3Read your own twins, duties and schedulesevery program persona
program.twins.useR3Start, pause, resume, configure or retire your own twin and approve its standing plansevery program persona but viewer
program.twins.manageR3Pause or retire any twin and set program twin defaults, with a reasonprogram_owner, program_lead
program.handoffs.readR3Read hand-off requestsevery program persona
program.handoffs.manageR3Send, accept, decline, complete or withdraw hand-off requestsevery program persona but viewer
review.stage_reviews.approveR4Sign the quality review of a stage gatequality_reviewer, program_lead, program_owner
progress.stages.readR4Read stage gatesevery program persona
progress.stages.manageR4Open a stage gate and mark it readyprogram_owner, program_lead, manufacturing_specialist
progress.stages.approveR4Close a stage gate with a signatureprogram_owner, program_lead
org.twins.useR5Run an organisation twinorg_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

Personav2 (today)v3 (R3)v4 (R4)Change
program_owner687478absorbs every new key
program_lead677377absorbs every new key
scientist474950gains read_own, use, handoffs.read, handoffs.manage; loses evidence.hypotheses.approve and plans.plans.approve; R4 gains progress.stages.read
quality_reviewer384345gains program.twins.read (oversight), read_own, use, both hand-off keys; R4 gains progress.stages.read and review.stage_reviews.approve
manufacturing_specialist343840gains read_own, use, both hand-off keys; R4 gains progress.stages.read and progress.stages.manage
viewer272930gains 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

EnumerationReleaseValues addedCount
AGENT_RUN_KINDSR3twin_cycle, twin_handoff11 -> 13
AGENT_RUN_KINDSR4stage_readiness13 -> 14
agent_runs.initiated_byR3schedule, event, handoff, user (null for non-twin runs)new
INTERRUPT_KINDSR3batch_approval (at most 10 items, explicit selection, per-item permission and audit)4 -> 5
inbox_items.kindR3decision, signature, question, handoff, mention, informationnew
inbox_items.kindR4stage+ 1
inbox_items.statusR3open, snoozed, done, dismissed, expirednew
inbox_items.feedbackR3none, helpful, not_usefulnew
twin_definitions.statusR3active, paused, retirednew
twin_definitions.paused_reasonR3user, manage, budget, high_decline_rate, permission_lost, membership_lost, user_inactive, migratednew
twin_triggers.kindR3schedule, event, handoff, mentionnew
twin_triggers.priorityR3now, digestnew
handoff_requests.kindR3review, data, decision, informationnew
handoff_requests.statusR3sent, accepted, declined, done, withdrawn (no unreachable proposed state)new
delegations.statusR3active, expired, revokednew
NOTIFICATION_KINDSR3handoff_received, digest, twin_paused, mention, decision_reminder (e-mailed: handoff_received, digest)7 -> 12
NOTIFICATION_KINDSR4stage_ready12 -> 13
ARTIFACT_KINDSR3persona_brief (server-written, no model call)10 -> 11
ARTIFACT_KINDSR4readiness_brief11 -> 12
stage_gates.statusR4open, ready, reviewed, closednew
SIGNATURE_TARGET_TYPES, APPROVAL_TARGET_TYPESR4stage_gate3 -> 4, 5 -> 6
Tone mapR3enabled (pass); sent (review); done, snoozed, revoked (neutral): 12 pass, 16 review, 7 fail, 4 info, 22 neutral56 -> 61
Tone mapR4reviewed (pass)61 -> 62

Tools

All pass the forbidden-verb test of the catalogue; every propose_* handler computes basis_hash.

ToolReleaseKindPermission exercised
console.propose_handoffR3write (never in PLANNING_TOOLS)program.handoffs.manage
console.list_handoffsR3readprogram.handoffs.read
console.get_twinR3readprogram.twins.read_own
console.list_decisions_waitingR3readprogram.twins.read_own
plans.propose_plan_changeR3writeplans.plans.manage
console.get_stage_gateR4readprogress.stages.read
progress.list_stage_gatesR4readprogress.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

JobReleaseIntervalDoes
twin_scheduleR360 sclaims due twin_schedules, enqueues twin_cycle with initiated_by = schedule, honours backpressure and the cadence floor
twin_events_reconcileR33600 scompares the audit entry_seq watermark per trigger and enqueues anything missed or marked by backpressure
twin_digestR360 swrites the persona_brief artifact, then the digest notification with deliver_after, in that order
inbox_expireR33600 sexpires items (“Expired, nothing written”, the run stays succeeded), un-snoozes, raises decision_reminder at most daily per item
twin_evaluateR386400 sproposals filed, approved unchanged, edited, declined by reason, refused, expired; not_useful rate per trigger
stage_readiness_refreshR43600 srecomputes 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.

GroupReleaseRoutes
Inbox and brief (9)R3GET /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)R3GET, 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)R3GET, POST P/handoffs; GET P/handoffs/{handoff_id}; POST .../accept, .../decline, .../complete, .../withdraw, each scoped by party
Board, metrics, export (3)R3GET 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)R4GET, POST P/progress/stage-gates; GET .../{gate_id}; POST .../ready; POST .../review (signature); POST .../close (signature and approval); GET .../brief
Organisation twins (2)R5GET, 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.

PhraseReleaseWhere
Decisions waiting for youR3inbox heading, program home
Your twin proposes. You decide.R3twin setup, twin panel
Your twin never approves, signs or sends.R3twin setup, twin panel
Paused. Nothing runs until you resume.R3twin panel when paused
Nothing waiting. Your twin's next pass is at <time>.R3inbox empty state
Expired, nothing writtenR3expired proposal card
Changed since proposedR3proposal refused with stale_proposal
Filing paused: <N> decisions waitingR3brief under backpressure
Let my twin look at thisR3mention item; the only way a mention starts a run
Share this thread to mention colleagues.R3composer in a private thread
Declining needs a reason; it is stored as evaluation data.R3decline dialog
Proposed by <Name>'s twin (agent); decided by <Name>R3record footer
Routed under delegation from <Name>; decided by <Name> in their own right.R3delegated decision
Agent-generated draft, unsigned.todayevery twin-written artifact (existing phrase)
Viewers have no twin. Nothing waits for you.R3viewer home
Every decision stays human and recorded. Timing claims wait for measured data.todayinterim copy on site, docs and deck
<N> of <M> required records presentR4stage gate card
Stage gate reviewed by <Name> (reviewed) and closed by <Name> (approved)R4closed 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)