Cortex — Investigation Lifecycle
This spec adds lifecycle semantics and implementation guidance to the Investigation (InsightRoot) object: status semantics and transitions, entry-context requirements per mode, evidence lifecycle within an investigation, edition creation and sealing, accountability enforcement points, and session management. The Investigation, Block, Edition, and Event schemas are authoritative in component.cortex.decision-evidence.
What an Investigation Is
An Investigation IS a git-like repository of evidence-gathering activity; anchored to a subject entity (subject_ref required); the container for all blocks, events, editions, and tasks related to a decision; tracked via an append-only DAG event ledger; the parent object for editions (sealed decisions).
An Investigation IS NOT a workflow engine (no automatic state progression), a decision (editions are decisions), a signal, or deletable (investigations are append-only records; they can only be archived).
Cortex stores additional fields via additionalProperties (see component.cortex.decision-evidence#investigation.fields): linked_signal_ids, rationales, narrative (executive_summary, methodology, etc.), and ai_usage (token rollup auto-accumulated from events).
Investigation Lifecycle States
DRAFT ──review──> IN_REVIEW ──approve──> APPROVED ──publish──> PUBLISHED
│ │ │
│ └──reject──> DRAFT (re-opened) │
└───────────────── archive ──────────────────────────────── ARCHIVED
| Status | Meaning | Typical Duration |
|---|---|---|
draft |
Active — evidence being gathered, narrative written | Hours to days |
in_review |
Edition submitted for review — awaiting reviewer | Hours |
approved |
Reviewer approved — ready for attestation or publication | Minutes to hours |
published |
Complete, decision sealed and attested | Terminal (permanent record) |
archived |
Closed (completed, superseded, or abandoned) | Terminal |
Transition Rules
| From | Valid Targets | Who | Gate |
|---|---|---|---|
draft |
in_review, archived |
Author | Edition must exist for in_review |
in_review |
approved, draft, archived |
Reviewer | Review decision |
approved |
published, in_review, archived |
Attester or author | Attestation for published |
published |
archived |
Admin/system | Superseded by new investigation |
archived |
— (terminal) | — | — |
Implementation gap (#REQ.transition-validation): set_insight_status() does not validate transitions today (see §status). Intended:
VALID_INSIGHT_TRANSITIONS = {
"draft": {"in_review", "archived"},
"in_review": {"approved", "draft", "archived"},
"approved": {"published", "in_review", "archived"},
"published": {"archived"},
"archived": set(), # terminal
}
if new_status not in VALID_INSIGHT_TRANSITIONS.get(old_status, set()):
return 409 INVALID_INVESTIGATION_TRANSITION
Status Drivers
Investigation status is driven by edition and signal lifecycle, not by direct user action on the status field.
| Action | Status Effect |
|---|---|
| User creates investigation | → draft |
User submits edition for review (review_requested) |
→ in_review |
Reviewer approves edition (review_closed, approved) |
→ approved |
Reviewer rejects edition (review_closed, rejected) |
→ draft (re-opened) |
Attester attests edition (attested) |
May → published |
| All linked signals resolved | May → archived |
Key principle: Tasks do NOT change investigation status. Tasks support decisions; they don't define them (see component.cortex.task-and-effect#investigation). Auto-advance must NOT be automatic — it is a suggestion or one-click UI action; automatic state changes violate "AI assists, humans decide."
Closure
An investigation reaches terminal state when all linked signals are resolved or dismissed, at least one edition is attested (or a no_action decision), and no open tasks remain. Closure is NOT automatic — it requires explicit human action.
Entry Modes
The four entry modes are defined in component.cortex.decision-evidence#investigation.entry-modes. Trigger and signal requirements:
| Mode | Required Trigger Type | Requires Signal? |
|---|---|---|
signal_driven |
signal |
Yes |
curiosity_driven |
home, direct, api |
No |
task_driven |
task |
No |
decision_driven |
decision |
No |
Entry Context
Every investigation has a required entry_context (schema in component.cortex.decision-evidence#investigation.entry-context). Before creation, Cortex checks the accountability pack via check_entry_mode(pack, mode) → 403 ACCOUNTABILITY_ENTRY_MODE_DENIED. Entry modes are controlled per role:
| Role | Allowed Entry Modes |
|---|---|
| RM | signal_driven, curiosity_driven |
| Risk Manager | signal_driven, curiosity_driven, task_driven |
| Treasury | signal_driven, curiosity_driven |
| Auditor | none (read-only) |
INVARIANT: Every investigation MUST have a subject_ref with type and id. For signal-driven investigations the subject is copied from the signal's subject; for curiosity-driven the user specifies it.
Event Ledger
Events form a DAG via parent_event_id, with branch heads in heads (schema and append-only invariant in component.cortex.decision-evidence#event). All events are created via _append_event(), which generates a DES-compliant evt_ id, sets parent_event_id to the current branch head, writes to ES, and returns the event_id for head advancement. The main branch head always points to the latest event. Actor types (user, agent, system) and their legality are defined in component.cortex.decision-evidence#event.actor-legality.
Evidence Lifecycle Within an Investigation
Block lifecycle (transient → curated → frozen) is defined in component.cortex.decision-evidence#block.lifecycle. Within an investigation:
Auto-Journal
When an investigation is active (via start_investigation()), every MCP tool call that produces a block automatically creates the block with insight_id set to the active investigation, appends a block_created event, and starts the block as transient (user must explicitly pin to advance to curated). State lives in the Redis session as investigation_context.insight_id and the in-process ContextVar session_context.
Pinning
Pinning is a deliberate human action advancing a block to curated: pin_block(insight_id, block_id, rationale=...). It requires a rationale, validates the forward-only lifecycle transition, updates lifecycle_stage to curated, sets the block's insight_id, and appends block_pinned.
Freezing
Freezing happens at edition creation. When create_edition() is called: all blocks associated with the investigation (pinned + auto-journaled) are gathered; each non-frozen block is frozen (materialization_mode: frozen, lifecycle_stage: frozen); captured_at is set; result_hash (SHA-256) is computed from structural content; the block is written back; the evidence manifest is assembled with {block_id, title, digest, mode: frozen}. INVARIANT: a frozen block's content can NEVER change; result_hash is proof.
Edition Creation and Sealing
An investigation produces one or more editions; each is a sealed snapshot. Edition status lifecycle and content-hash rules are in component.cortex.decision-evidence#edition.
Creation Flow
create_edition(insight_id, decision_outcome, executive_summary, decision_statement, ...) steps: accountability pack validation (minimum evidence, decision type, decision template); gather all evidence blocks (pinned + auto-journaled by insight_id); freeze all blocks, compute digests → evidence_manifest; build narrative_snapshot; create EditionSnapshot in ES; backfill pinned_block_ids with auto-journaled blocks; append edition_created.
Freezing the Edition
freeze_edition_for_attestation(edition_id): verify all evidence blocks frozen; accountability validation (decision type, require rationale); compute content_hash = SHA256({insight_id, edition_number, evidence_manifest, narrative_snapshot, decision_metadata}); set frozen_at and frozen_by; append revision_committed.
Attestation
attest_edition(edition_id, attestation_type): verify the edition has a content_hash (must be frozen first); enforce separation of duties (attester ≠ author); accountability validation (attester role); build attestation record with content_hash_attested, confirmations, signature; set status to attested; append attested. INVARIANT: only actors of type user may attest; agents MUST NOT.
Accountability Pack Enforcement Points
The accountability pack is checked at multiple points (constraints in component.cortex.decision-evidence#accountability, pack schema in component.cortex.pack-reference#accountability):
| Action | Check | Error Code |
|---|---|---|
| Create investigation | check_entry_mode(pack, mode) |
ACCOUNTABILITY_ENTRY_MODE_DENIED |
| Create edition | check_minimum_evidence(pack, evidence_count) |
ACCOUNTABILITY_EVIDENCE_INSUFFICIENT |
| Create edition | check_decision_template(pack, template_id) |
ACCOUNTABILITY_TEMPLATE_NOT_ALLOWED |
| Freeze edition | check_decision_type(pack, decision_type) |
ACCOUNTABILITY_DECISION_TYPE_DENIED |
| Freeze edition | check_require_rationale(pack) |
ACCOUNTABILITY_RATIONALE_REQUIRED |
| Attest edition | check_attester_role(pack, roles) |
ACCOUNTABILITY_ATTESTER_ROLE_DENIED |
Fail-Closed Pattern
If a profile references an accountability pack but it can't be loaded → BLOCK (ACCOUNTABILITY_PACK_NOT_FOUND). If the pack exists, enforcement is mandatory. If no profile is configured → ALLOW (no accountability enforcement) — this enables development/testing without packs.
TheBank Enforcement Matrix
| Role | Entry Modes | Min Evidence | Decision Types | Attester? |
|---|---|---|---|---|
| RM | signal_driven, curiosity_driven | 1 (default) | action, no_action, deferred | No (routes to Risk) |
| Risk | signal_driven, curiosity_driven, task_driven | 2 | action, no_action, deferred, escalation | Yes |
| Treasury | signal_driven, curiosity_driven | 1 (default) | action, no_action, deferred | No (routes to Risk) |
| Auditor | none (read-only) | — | — | No |
Session Management
Investigation Session Binding
start_investigation() binds the investigation to the current MCP session: the Redis session store (store.update(sid, {"investigation_context": {"insight_id": insight_id}}), persists across requests) and the in-process ContextVar session_context (for same-request tool calls).
Auto-Journal Activation
With an active session, every subsequent tool call that creates a block checks session_context for investigation_context.insight_id, sets block.insight_id, and appends block_created — a complete audit trail without manual logging.
Idempotent Guard
start_investigation() has an idempotent guard: a session check (if an investigation is already active in the session, return it) and ES dedup (if a non-archived investigation already exists for the same trigger signal, return it). Both are bypassed with force_new=True.
Signal Linkage
Signals and investigations maintain bidirectional references: signal linked_insight_ids[] and investigation linked_signal_ids[].
Auto-link on creation: in start_investigation(), when trigger.type == "signal", _auto_link_signal() reads the signal and adds insight_id to linked_insight_ids; reads the insight and adds signal_id to linked_signal_ids; appends signal_linked with auto_linked: true; advances the insight head; writes back.
Manual link: link_signal(insight_id, signal_id, rationale) lets users link additional signals to an existing investigation — useful when a second signal fires for the same subject, an analyst discovers a related signal from a different policy, or cross-subject signals share a common root cause.
Implementation Status
What exists today: create_insight() (full entry_context validation); create_insight_from_signal() (auto-links); start_investigation() (session binding + idempotent guard + ES dedup); get_insight(), list_insights() (with dedup), count_insights(), get_insight_events() (filterable); set_insight_status() (NO transition validation); pin_block() (forward-only validation); update_insight_text() (5 narrative fields); link_signal() (bidirectional); create_edition(), freeze_edition_for_attestation() (computes content_hash), attest_edition() (separation of duties); event ledger DAG; actor types; auto-journal; accountability enforcement at all 6 points.
What's missing (priority order):
| ID | Gap | Priority |
|---|---|---|
| I1 | Investigation status transition validation | P1 — correctness |
| I2 | Auto-status suggestion from edition events (attested → published) | P2 — UX |
| I3 | Closure validation (all signals resolved, tasks complete) | P2 — governance |
| I4 | prompt_submitted event type (user chat messages during investigation) |
P2 — audit trail |
| I5 | Branching support (multiple investigation threads) | P4 |
| I6 | Investigation merge (combine parallel branch findings) | P4 |
| I7 | Investigation handoff (transfer to another user/role) | P3 — production |
I4: the Beacon sidebar shows prompt_submitted events but Cortex doesn't capture them; every user chat message during an investigation should be a ledger event for complete auditability.
Depends on: component.cortex.decision-evidence, component.cortex.pack-reference
Realizes: product.block, product.edition, product.investigation