# Marina — Claude Code Conventions

This file keeps only what a contributor must follow to make a correct change: build commands, code style, architecture rules, the invariants of each subsystem, and the key-file map. **Deep dives: [docs/architecture/](docs/architecture/README.md)** — design, history, measurements, and per-feature walkthroughs. Every subsystem section below ends with a pointer to its long-form page.

## Change delivery

Use a feature branch and pull request for every repository change. Never push directly to
`main`. A request to commit and push means publishing the feature branch and opening or
updating its PR; it does not authorize merging. Leave the PR for review unless merging is
explicitly requested. Required CI checks must pass on the latest revision before merge;
do not bypass checks, reviews, or branch protection.

## Build & Test
```bash
bun run start          # Start server
bun run test           # Run all backend tests (parallel workers; `test:serial` for one process)
bun run typecheck      # TypeScript strict check (tsconfig.json, then tsconfig.benchmarks.json for benchmarks/)
bun run lint           # Biome lint
bun run format         # Biome auto-format (run before committing)
bun run test:fast      # pre-commit loop: 138 engine-free files, ~10 s parallel (scripts/test-fast.ts --check reports drift; --serial opts out)
bun run test:shard I N # time-balanced shard I of N from test/timing.json (CI runs 3); regenerate timings per docs/guides/testing.md
bun run test:coverage  # full suite + coverage (text + coverage/lcov.info); opt-in, never in PR CI
bun run check:coverage # per-directory line coverage from lcov (--strict gates on MARINA_COVERAGE_MIN_LINES, default 75)
bun run check:versions # every package.json must match the root version
bun run check:overrides # audit root `overrides` (--strict to gate, --offline without registry)
bun run clean          # Reset database and scratch files
cd dashboard && bun run test  # Frontend tests (vitest)
```
Single Bun workspace: one root `bun install` covers `dashboard`, `site`, `marina-desktop`, `examples/usecase-ui`, `examples/coding-agent-demo`, `src/sdk` (extensions/* are excluded on purpose — native deps, own lockfiles). Isolated linker: declare every import in the member's `package.json`; transitive deps are not resolvable. Tests: no real sleeps ≥ 500 ms — poll with `until()` from `test/helpers.ts`; a new empty `catch {}` fails `test/lint-no-empty-catch.test.ts` (opt out with `// allow-empty-catch: <reason>`); `lean-agent-adapter.ts` logs through `Logger`, never `console`. No server code under `src/**` may use `console.*` either (`test/net-logging.test.ts`; excepted: the `src/sdk/` client package, the Logger sinks, `import-process.ts`'s stdout result line, and the boot banner in `src/main.ts`) — log through a module `Logger` with the module's category (`ws`, `mcp`, `telnet`, `db`, `agents`, `model-api`, `dashboard-api`, `main`, … — the full map is in `test/net-logging.test.ts`). `test/docs-contract.test.ts` additionally asserts the POSITIVE contract: every safety gate is in `docs/architecture/civic-substrate.md`, every builtin resolves to `src/engine/commands/` and is documented, migration versions are contiguous, every architecture page is linked, every `MARINA_*` in `config/environment.reference` is read and every `MARINA_*` read in `src/`/`scripts/`/`worlds/`/`benchmarks/` is in the reference or `ENV_VARS_OUTSIDE_SERVER_CATALOG`, `.env.example` ⊆ reference, and `docs/reference/environment.md` is freshly generated — fix the doc or the named allowlist, never the assertion. Coverage is opt-in and non-blocking; bun's own `coverageThreshold` is per-file and unusable as a repo floor, so the floor lives in `scripts/check-coverage.ts` (see docs/guides/testing.md).


WebChat is a composition root: `ChatInputBar` owns drafts/history/login, `ChatMessageList` owns transcript/timeline rendering, `ChatStatusOverlay` owns inspection views, and `chat-state-reducer.ts` maps perceptions to transitions without transport side effects. Preserve the shared command and context paths when changing these boundaries.

`bun run test:mutation` gates cache/admission contracts; `bun run check:model` checks the bounded MCP session specification in `specs/`. Context p99 CI compares the same workload against base/candidate code (see docs/guides/testing.md).

## Code Style
- **Documentation placement**: all research, plans, strategy, competitive analysis, internal audits and qualification reports belong in the private `marina-internal` repository. Public documentation should contain user guides and reference material. Reusable source code, tests and qualification tools can remain public; save their internal reports outside this repository. Archive existing research before replacing its public copy with a documentation pointer; do not rewrite Git history as part of routine documentation cleanup.
- **Formatter**: Biome — line width 100, indent 2 spaces
- **Imports**: alphabetical by path (biome organizeImports)
- **Types**: branded `EntityId`, `RoomId` — cast in tests: `"e_1" as EntityId`
- **Entity properties**: use `KnownProperties` interface — typed optional fields for rank, quest state, benchmarks, etc.
- **Errors**: use `getErrorMessage()` for extraction, `tryLog()` for non-critical DB ops
- **DB table**: `groups_` not `groups` (SQL keyword)
- **Memory tiers** (migration 37): every note has `tier ENUM('fact','reflection','skill','core','process')`. `recall*` helpers default to fact-like tiers; `process` is excluded unless `includeProcess: true`. `[compaction]`-prefixed notes auto-infer to `process`. `createNote` dedups on exact `(entity, note_type, content)` within fact-like tiers through `numeric_notes` so native revisions participate in deduplication. Per-entity `process` cap = `PROCESS_TIER_QUOTA` (500), evicted on write when over cap.
- **FTS5**: add insert/update/delete triggers when creating FTS tables
- **Tests**: use helpers from `test/helpers.ts` (MockConnection, stripAnsi, cleanupDb). `createTestEngine()` in `test/engine-fixture.ts` owns an isolated in-memory world; always await `dispose()`. Use disk storage for WAL/reopen tests. Scope profile, rate-limit bypass and selected environment overrides with `using state = scopeProcessState(...)` from `test/process-state.ts`; restore prior state, not assumed defaults. Keep scopes serial or nested within each worker; an engine fixture does not isolate process-wide state. Property tests support `FC_SEED`, `FC_RUNS`, `FC_PATH`; see docs/guides/testing.md.
- **Dashboard animations**: motion (`motion/react`) — use `<AnimatePresence>` for mount/exit, `motion.*` for declarative anims, `useMotionValue` + `useTransform` for realtime-driven values without React re-renders, `layoutId` for shared element transitions. See `dashboard/src/components/AnimatedNumber.tsx` for the count-up pattern.

## Architecture Rules
- Commands: one file per command in `src/engine/commands/`, register in `src/engine/command-registry.ts` → `registerBuiltinCommands()`
- **Argument conventions** (`src/engine/parse-input.ts`, 2026-09-22): one modifier grammar — `parseModifiers(tokens, spec)` accepts `key:value` (canonical, shown first in usage), `key=value`, `--key value`, `--key=value`; a standalone `--` ends modifier parsing (`splitOnTerminator`). Durations go through `parseDuration` in `format-duration.ts` (`s m h d w mo`; `market` passes `{ minUnit: "h" }` to keep its documented `mo`-months / no-minutes semantics). Subcommand verbs normalize through `canonicalSub(sub, allowed, { noAlias? })` with families `list/ls`, `show/info/view`, `delete/remove/rm` — pass `noAlias` for commands whose bare form takes free text (`note` exempts `rm`/`remove`). `unknownSubcommand(name, sub, usage)` is the shared default-branch reply. Legacy spellings (`--kind X`, `--ttl=30s`, `formation=f -- goal`, `!pN`, `importance 7`, `confidence 0.9 source URL`) all still parse.
- **`code` command layout**: `src/engine/commands/code.ts` (~850 lines) is the entry — `CommandDef`, modal routing, LAYER-0 telnet refusal, no-code-root refusal, the imperative `code.exec` gate, and a `SUBCOMMANDS: Record<string, SubcommandHandler>` dispatch table (83 keys, aliases share handlers, `Object.hasOwn`-guarded); bodies live in `src/engine/commands/code/{shared,profiles,runner,workspace,artifacts,stream,crew,session,driver,spawn,exec,files,sandbox,project,service,task-run,verification,visual}.ts` (strict DAG; `shared` is the root; no `code/*` imports `code.ts`). `runner` (`code workspace runner local|container …`, `MARINA_CODE_CONTAINER_*`) moves a session's finite commands into a container image (`src/coding/container-workspace.ts`: same allowlist/approver/gates, no host fallback); `code test`/`verify` follow the detected project runner (`src/coding/project-detection.ts`). `code.ts` re-exports `codeCommand`, `isLoopbackConnection`, `CodeDeps`. Security ordering stays top-to-bottom in the entry file.
- Migrations: append to `FORWARD_MIGRATIONS` in `src/persistence/schema.ts` (re-exported as `MIGRATIONS` by `database.ts`), never modify existing migrations. Fresh databases install the generated schema-137 baseline atomically; immutable pre-baseline SQL lives in `schema-history.ts` for existing-database upgrades. Verify baseline generation with `bun run schema:check`.
- **Row retention** (`src/engine/retention.ts`, hourly tick phase 2100, `runRetentionPass`): declarative `RETENTION_POLICIES` per table class — add every prunable table there; `chronicle`, `entity_standing`, `memory_resolutions`, `economic_events` are `append-only` and are never pruned (an override cannot re-enable it). `MARINA_RETENTION_OVERRIDES="table=30d,table2=0"` (0 = never). Missing tables/columns are skipped, not errors. → docs/architecture/persistence.md
- **Durable keys (migrations 109, 117, 118)**: ledgers and membership tables are keyed by the durable world account `users.id`, not the transient entity id; `MarinaDB` delegates resolve `durableEntityKey()` on write and project back to the LIVE entity id on read (`liveEntityIdSql`), so callers keep passing entity ids (ids with no account pass through unchanged — tests can still use `"e_1"`). Migration 119 closes the last two (`board_posts.author_id`, `macros.author_id`) — no entity-keyed column is still transient. `approveSubmission`, `deleteNote`, and `deleteUser` (account erasure: deletes the account's standing/competence/witness/membership rows, anonymizes its shared tasks and posts, one audited chronicle event — the one exception to append-only) are transactional. → docs/architecture/persistence.md
- **Command phase**: commands run through a per-entity promise chain (one entity strictly FIFO, entities interleave) under `COMMAND_PHASE_BUDGET_MS` (150 ms); a throw before a handler's own try lands on the tick error path, never as an unhandled rejection. Room command wrappers RETURN the handler's promise. Every ingress (telnet, Discord, Telegram, MCP, WebSocket, dashboard, auto-bootstrap, `look`/`brief` housekeeping, challenge re-runs) goes through `engine.dispatchCommand()` (admission + FIFO; resolves `false` on capacity); `processCommand()` executes NOW and is only for work already inside the entity's slot — `dispatchCommand` detects that slot (AsyncLocalStorage) and runs inline, so macro/`batch` expansion never waits on itself. A rejected async handler is a failure (no quest progress, failed activity/usage, no `command` event). Macros expand in-slot (`src/engine/macro-expansion.ts`): the caller's raw args bind to `$*`/`$@`/`$1`..`$9`/`$$` (no placeholder ⇒ appended to the last command; the body splits on `;` before substitution, never the args); resolution is builtin → room command → room macro (owner `room:<roomId>`) → own → system. A cycle is always refused; depth/expansion caps (`MARINA_MACRO_MAX_DEPTH`/`_EXPANSIONS`, 0 = unlimited) default to 8/100 on shared/public and unlimited under local-ungated or `open`; one rate-limit token per expanded command. Async tick jobs never overlap themselves and, with `engine.trackBackground()` work (evolve trials/replication), are drained by `engine.shutdown()` before the DB closes (bounded by `BACKGROUND_DRAIN_TIMEOUT_MS`). → docs/architecture/persistence.md
- **Migration 118** rekeys `groups_.leader_id` / `tasks.creator_id`; **migration 119** rekeys `board_posts.author_id` / `macros.author_id` (`BOARD_POST_COLUMNS` / `MACRO_COLUMNS` project the live id back; `macro` lookups resolve the durable key) — none left transient. **Migration 120** adds `agent_configs.thinking_level` (NULL = unset) so `agent spawn … thinking:<level>` / `agent config <name> thinking <level>` survive the boot-time respawn. `note conflicts` never pairs a note with its own durable twin; retired records' source excerpts are not served in `[evidence]` (`servableSourceIds`). → docs/architecture/persistence.md
- DB modules: query logic split into `src/persistence/db-notes.ts`, `db-entities.ts`, `db-tasks.ts`, `db-channels.ts`, `db-agents.ts` — MarinaDB delegates to standalone functions
- **Store interfaces**: `src/persistence/interfaces/` has one explicit interface per store domain (`NotesStore`, `TasksStore`, … 53 in all, over the `db-*.ts` modules); `MarinaDB implements MarinaStores` and is a thin facade (constructor, migrations, `durableEntityKey()`, one-line delegates — no inline SQL). Adding a delegate means: query in the module, one-line delegate on the facade, method added to the interface AND its `*_STORE_METHODS` tuple — `ExactKeys` and `test/persistence-interfaces.test.ts` (prototype-reflection drift check) fail until all agree. Consumers may type dependencies as a store slice instead of `MarinaDB`. → docs/architecture/persistence.md
- Command discovery: put `category`, `help`, and structured `usage` on `CommandDef`; `command-manifest.ts` generates help, the agent roster, dashboard forms, and MCP discovery. New world commands need no separate MCP schema. `capabilities { command, syntax, expose: true }` publishes a focused typed tool from those same fields; `invoke` executes any declared form. Both revalidate the live form inside the session command queue.
- MCP modules: `mcp-server.ts` owns transport/auth/lifecycle; `mcp-session.ts` owns the rate-limited execution queue and context delivery; `mcp-world-tools.ts` generates participation tools; `mcp-named-tools.ts` holds frozen compatibility contracts; `mcp-memory-tools.ts` holds the portable service bridge. New commands use CommandDef, not another named schema. Use `runCmd()` / `cmdTool()` for execution. Existing named tools preserve their parameter contracts. Automatic context uses an explicit task or the resident's own saved goal, rebuilds with current access, and never turns an enrichment failure into a failed world action.
- MCP tool categories: bootstrap (login/auth), cognition (think/memory/next/brief/quest), world, coordination, canvas (canvas), building, escape hatch (command/batch), session
- Engine boundaries: `AuthCoordinator` owns login/reconnect, rank restoration and eviction; `CommandPhaseCoordinator` owns modal routing, rank/gate checks, handler execution and usage/quest accounting; `CommandCoordinator` owns admission/FIFO/drain. Hosts are explicit interfaces, not Engine references. `ConnectionManager`, `EventLog`, `BriefManager`, `RoomTickCoordinator` and `TickScheduler` own the other extracted lifecycles.
- Room handlers get `RoomContext`, built-in commands get `CommandContext` (extends with mcp/http/notes/memory/pool)
- `minRank` on `CommandDef` is the permission gate — don't add custom rank checks in handlers
- Command queue: per-entity round-robin processing (fair-share scheduling)
- Tick budget: `RoomTickCoordinator` limits the synchronous room phase to 200ms; pending async handlers never overlap themselves and are drained before database shutdown.
- Non-critical DB operations should be wrapped in `tryLog()` or `tryLogAsync()`

## Civic Substrate — Standing, Rank, and Safety Gates
- `standing` is the only contribution score (`src/agent/standing.ts`; exponential decay, 60-day half-life, floor at 0 — no negative scores), earned ONLY by work someone INDEPENDENT verified (2026-10-14): `recordVerified` / `independentParties` refuse the worker's own account and its spawn lineage (`spawnLineage`: `agent_configs.spawned_by`, then the agent principal's owner; a person and its agents are one party). Live kinds: `task_complete` (creator approval in any mode, value ≤ `TASK_STANDING_MAX` 10, default 1, `task create` refuses more), `code_verified` (independently approved Code Mode attempt whose checks passed), `forecast_resolved` (outside resolution, ≤ 0.5 by skill, 1/day, never an `eval_mode` filing), `experiment_complete` (independent replicator), `assistance_*` (independent requester), `quest_complete`. `RETIRED_STANDING_KINDS` (`chronicled`, `pool_note`, `reflection_recalled`, `crew_*` credits, `helping_act`) are refused by `record()`; their rows stay and decay out — never reset. Credits key on the durable account by name (`accountKey`), and every readout (`standing`, `task standing`, `standing top`, `/who`, `orient`, `brief`) shows `standingOf`. An operator-set rank below 5 is a floor (`users.rank_floor`, migration 167) that progression keeps. Rank 0–4 is derived from standing thresholds 5 / 15 / 40 / 100 and is *descriptive*; above rank 4 standing does NOT auto-promote — capability is gated by per-operation **safety gates** (`src/engine/safety-gates.ts`, 14 gates), each requiring sufficient standing AND an unsupervised `entity_competence` row. Operators bootstrap via `safetyGates.grant(db, entityId, gateId)` from world seeds — no rank shortcut. `applyRankProgression()` is a pure threshold lookup, no metric scoring.
- All enforcement sites call `checkGateForExecution` + `recordGateExecution` (never bare `checkGate` — the site lint test enforces this). `MARINA_AUTONOMY=guarded|earned|open` is env-only by design — never settable in-world; `open` never passes `OPEN_POSTURE_CORE` (`key.manage`, `admin.destructive`, `shell.exec`, `code.exec.unrestricted`, `world.code`), and `open`+public+passwordless is a fatal boot error. Self-witnessing/self-attestation is always refused. `code.exec.unrestricted` is NEVER in `RANK_GATES` — not even a sovereign gets it from rank. Self-improvement is by succession (`src/engine/role-guard.ts`): no one edits/deletes/reloads the role they run on (nor `trait delete` inside it, nor `agent config <self> role`) — refused in every profile; they `role create <role>-v2` (rank 3, no safety gate) and `agent spawn` an improved iteration; changing an EXISTING role/trait takes `role.edit`. The loop (Phases 1–4): `evolve trial` (child/parallel world only, held-out split, Agresti–Caffo interval) → `evolve decide` → `evolve replicate` / `evolve adoption` (both behind `earnedWin`: interval > 0 AND fishing margin) → parent `world adopt` (pending → approve by someone else, `role.edit` for `into:`, rollback) — `src/engine/evolution-trial.ts`, `src/world/adoption.ts`.
- Trust profile (`src/engine/trust-profile.ts`, `MARINA_PROFILE=local|shared|public`): `isLocalProfile()` drives performance defaults; `isLocalUngated()` (local **and** `MARINA_AUTONOMY` not explicitly `guarded`) drives permissions. In-process default before `main.ts` resolves is `shared`, so tests keep exercising gates (opt in with `setTrustProfile("local")` + `resetTrustProfileForTests()`). Never off in any profile: exec-decision audit, cognitive ledger, memory receipts, trusted/unverified labels, untrusted framing, `assertTrustProfileSafe`.
- Imperative gate sites (the router handles `CommandDef.gate`, see next bullet): `agent.spawn`, `code.exec`, `role.edit` (`checkRoleEdit`), `shell.exec` for `run raw` and `connect add … stdio` (`shellExecRefusal` in `commands/run.ts`), and `code.exec.unrestricted` (the headless exec approver — the one allowlisted bare-`checkGate` site). A room command that shadows a builtin runs under its own (empty) definition, never the builtin's `minRank`/gate (`CommandRouter.resolveCommand`). Exec approver (`src/coding/exec-approver.ts`): the `code` allowlist is the default; an `ExecApprover` is attached per-call to `LocalWorkspace.run`, never global; LAYER 0 in `code.ts` (before the `code.exec` gate check) refuses host exec entirely over telnet; every approve/deny writes an `exec_decision` artifact + event.
- `CommandDef.gate` is checked by the router INSTEAD of `minRank` in every posture — the gate is the single authority (`{ minRank: 5, gate: "<id>" }`); inline subcommand rank floors go through `rankFloorRefusal` (`src/engine/rank-floor.ts`: passes under local/`open`, otherwise refuses + raises a challenge; `{ core: true }` for a floor guarding a path into the core — `rank` set, `shell allow|deny` — which `open` does not lift) — never a bare `getRank(…) < N`; `gate grant|revoke|list` passes gates on without escalation. The current command and its armed challenge pass are scoped per execution (`runCommandScope`, AsyncLocalStorage), so nested or interleaved commands never clear an outer approval.
- **Challenges, not walls** (`src/engine/challenges.ts`): a rank/gate refusal or a decision-gate `ask` notifies the requester's owner + operators with a token and returns AT ONCE — nothing ever waits on a person (no tool call, command or tick blocks); `challenge approve <token> [once|always]` re-runs the held input in the requester's slot via a single-use pass (`src/engine/gate-context.ts`, `mode: "approved"`). **People approve (2026-10-14)**: an agent never answers another's hold, whatever its rank — agent = internal or MCP connection, an SDK client declaring `principal: "agent"` (`MarinaClient` default; the `marina` REPL on a TTY is `person`), or an account with an agent config / agent principal; never demote residents, just never let them approve. Approvers must hold the authority themselves (an operator = a person at rank 9, or the owner — the first person up the requester's spawn chain — holding rank + gate); self/child refused; `always` = grant (core gates: operator only). Holds are durable (`challenge_holds`, migration 166: masked summary; the held input only when `maskSensitiveText` leaves it unchanged; a tool hold survives when its call is a world command), restored on boot, expired by a 15 s timer sweep (not lazily), and owed notices (restore, expiry, deny) wait in the row until the requester connects; offline approvers get an `ops inbox` item (`challenge:<token>`, content-free) and a tell when they next connect. Per requester: 5 open per class, 20 in all. `challenge|decision approve|deny` is `communicate` for the decision gate (never held). Dashboard `authorizePrivileged` has no rank shortcut (the gate decides, as at the router) and returns the gate's `reason`. Answers are labels (`challenge_outcomes`, migration 139); `MARINA_CHALLENGE_JUDGE=observe|on` (default observe) scores held commands AND decision-gate tool holds — it auto-approves only classes it has EARNED (≥ 25 approved allow calls, Wilson LB ≥ 0.85, every 10th audited), never core, never `always`/deny. A decision-gate hold tells the agent which question held it, against which goal/role/focus/task, and its options (`explainGateHold`). → docs/architecture/civic-substrate.md
- Flywheel sandbox: Marina owns identity, policy, approvals, Code Mode, artifacts, audit; Flywheel owns isolated execution. Never silently fall back from requested sandbox execution to host execution or imply host/guest file coherence.
→ docs/architecture/civic-substrate.md

## Chronicle — Canonical Record of the Marina
Append-only civic history (`chronicle` table, `src/persistence/db-chronicle.ts`). `event` entries are engine-emitted and immutable; a `correction` supersedes a prior narrative/digest and the original is never touched (`chronicle correct` refuses `event` entries). Write commands are Chronicler-only (`properties.role === "chronicler"` or the entity's agent config role is `chronicler` — i.e. an agent spawned/configured on the `chronicler` role); `chronicle record` requires ≥1 ref. Citation is recognition, not standing (`chronicled` is retired). → docs/architecture/chronicle.md (design note: docs/chronicle.md)

## Worlds and Room Agents
World definitions in `worlds/` export a `WorldDefinition`; `MARINA_WORLD` selects one (default `default`). `WorldDefinition.seed?(db)` runs once on first boot and must be idempotent. Room agents spawn lazily in room `onEnter` via `ctx.spawnRoomAgent()` — never on tick — use model `marina/default` (the local `/v1/chat/completions` endpoint) and authenticate with the auto-generated internal token (no `MARINA_OPEN_API` needed); with no upstream API keys rooms fall back to static entities. Seeded/saved agents respawn on boot when `AGENT_AUTORESPAWN` is unset only under the `local` profile with a usable provider (`src/engine/auto-respawn.ts`); explicit `true`/`false` wins. → docs/architecture/worlds.md

## Agent Cognitive Architecture
Agents are autonomous from birth, not assistants ("You think, therefore you are here"; "Write for the minds that come after you"); they learn the world through quests and exploration, not prompt injection, and every turn either acts in the world or rests deliberately (`memory set rest <why>`). The harness OFFERS loop instruments rather than imposing rules — rest, `channel_sends`, `focus_persistent`, crew-responder `autonomy full` (`src/agent/loop-preferences.ts`), 75 % warnings before per-run caps; the reference monitor blocks policy language only on consequential calls with untrusted context. System and continuation prompts (`src/agent/prompts/lean-system.ts`, `src/agent/lean-agent-adapter.ts`) stay code-driven by design — the role section is the editable surface (traits/roles at rank 3; a role may replace OPERATING LOOP / HOW TO BE / EVERY TURN only under `earned`/`open` or local-ungated — AUTHORITY AND TRUST never; propagate via `agent config <name> role <r>` or `role reload <name>`; `saveTrait`/`saveRole` record history only when the serialized definition changes). Role metadata *informs* — it never controls; PRISM task gating (`filterTraitsByTask`) is conservative: no inferred category or no declared scope ⇒ trait kept, `domains` alone never suppresses, `activation: ["always"]` is never gated out, and no shared seed trait is task-gated. Prompt budget: system prompt ≤ `LEAN_SYSTEM_PROMPT_BYTE_CAP`, continuation prompt uses `continuationPromptBudgetBytes` (6000–16000 bytes by effective window, or `MARINA_CONTINUATION_BUDGET_BYTES`) with mandatory sections first and overflow events re-queued, not dropped; the `marina/default` proxy model reserves `DEFAULT_CLOUD_MAX_TOKENS`, NOT `localOutputBudget`. `InterruptibleWaiter` wakes only the cycle-delay sleep — rate-limit backoffs intentionally stay on the non-wakeable `sleep()`. All cognitive features are platform commands accessible to any agent (internal, MCP, WebSocket, telnet). → docs/architecture/agent-cognition.md
- **pi-agent-core ≥ 0.87 (2026-09-24)**: the system prompt and tool declarations are `system` messages INSIDE the transcript (`agent.state.systemPrompt` is a read-only replay). Change the prompt only via `setSystemPrompt` / `replaceSystemPrompt` (swaps the leading message in place — one base prompt, byte-stable); `createContextManager` never compacts, summarizes or drops `system` messages and returns the original array when unchanged; the continuity journal skips them; the per-prompt turn cap is `finishTurn` (not the removed `shouldStopAfterTurn`).
- **Transcript hygiene (2026-10-01)**:
  - Between prompts, the conversation is capped at 48k estimated tokens and compacted to a third of that. `MARINA_AGENT_CONTEXT_CAP_TOKENS=off` disables the cap.
  - The cap starts after the first run for ordinary residents; bound Code Mode coders also apply it after the first assistant turn within a long task. Fresh requests are never compacted before work starts; the full task remains durably addressable.
  - Reasoning blocks from earlier runs are dropped (`MARINA_DROP_OLD_THINKING_SIGNATURES=off` keeps them). Responses-API messages are skipped.
  - The 0.8 × window trigger still applies whenever it is smaller.
- **Models that reason unasked + output repair (2026-10-03)** (`src/agent/reasoning-control.ts`, `tool-call-probe.ts`, `src/repair/`): only AUTOMATIC output caps get reasoning headroom or grow — explicit caps never grow; the OpenRouter reasoning disable goes only to a probe-verified model (agents; a tool-free classifier such as the memory relevance gate sends it unless an upstream said reasoning is mandatory); a repair shot keeps only values that appear verbatim in the draft (`groundedIn`), and every repaired output is labelled `repaired:parse|shot` (`MARINA_OUTPUT_REPAIR=off` returns raw output). → docs/architecture/agent-cognition.md
- **Thinking + tool ordering (2026-09-22)**: `thinkingLevel` resolves explicit → crew responder `off` → `MARINA_AGENT_THINKING` (default `off`); thinking on ⇒ `temperature`/`top_p` omitted for Anthropic. World-mutating tools are stamped `executionMode: "sequential"`; read-only tools stay parallel (`MARINA_TOOL_EXECUTION`). → docs/architecture/agent-cognition.md
- **Every budget ends in an answer (2026-10-04)**: a loop with a turn, step or time budget reuses `src/agent/budget-terminal.ts` (steer at 75 %, then block tools and ask for the answer) instead of a loop-local cap; a forced answer is labelled `BudgetForced` (traces, run records, `budget_forced` ledger items, migration 157) and never passed off as a free answer. A Code Mode task with `code do budget:<duration>` (the `-p` one-shot always sends one) uses the wall-clock form (`deadlinePhase`): the final request comes a bounded reserve BEFORE the deadline, then only submit/report tools run, and a later submission is submitted with its recorded verification and labelled `budgetForced` (never reported verified unless checks passed). → docs/architecture/orchestration.md

## Memory Architecture — Canonical Records and Compatibility (read before touching memory code)
- **One canonical record history**: fact/reflection/skill memory is stored as versioned records in the durable repository (`src/persistence/db-memory-service.ts`). Numeric commands and `/mem/*` are synchronous adapters (`db-memory-numeric.ts`), never a second writable store. Numeric `notes` rows retain address/owner/pool metadata with empty content; `numeric_notes` is a read-only view of the canonical version. `memory_note_projections.version = NULL` follows current; superseded numeric addresses pin historical versions. Process/core journals and `memory kv` keep their distinct semantics.
- `trusted` is strict everywhere: it never silently falls back. One retrieval surface — `buildUnifiedContext` (`src/memory/unified-context.ts`), five fixed-order tiers within a byte budget; same fixture ⇒ same tiers on every surface (continuation §4, `recall <q> all|evidence`, `GET /mem/context`, MCP `think{action:"context"}`, passthru injection — `test/unified-context.test.ts`). `servableRecord` drops `resolve` losers and historical versions from `[evidence]`.
- **New memory features write canonical records** through `residentMemoryOperation`, `CommandContext.durableMemory.run`, or the extension caller's `durableMemory.run`. `/mem/*`, `note`, and `ctx.notes.add` are deprecated for new integrations; existing numeric IDs, tiers and pool semantics remain supported. Read-only compatibility identity/provenance helpers live in `legacy-projection.ts`; readers must not import the replay worker.
- **Atomic numeric writes**: create/correct/delete/source/verify/link/consolidate commit through the canonical repository in the same SQLite transaction. No write outbox, asynchronous bridge or retry worker remains. A failed canonical write fails and rolls back the numeric operation. Canonical versions use `db-note-storage.ts`; ordinary writers use `db-notes.ts`. Never write content into a bound numeric handle or bypass the canonical repository.
- **Upgrade and access** (migration 138): convert existing fact-like notes, provenance, verdicts, links and pending retirement intents before dropping the historical outbox; failure rolls back the schema and data conversion together. Numeric IDs, canonical IDs/history, owner/pool ACLs and explicit erasure remain intact. Source URLs cannot establish identity. Accountless server namespaces have separate system owners, never automatically claimed by a later human account. `isServiceMemoryNote` fences physical version IDs; numeric handles are owner/pool-scoped. `note delete` retires; durable `forget` erases. New text clears prior verification. Institutional publication is explicit independent content, never a private write binding.
- Continuity contract (`src/agent/durable-memory.ts`): every completed message is journaled before the loop advances; a failed archive aborts compaction (`ContextPersistenceError`) rather than dropping history. `resolve` losers get `valid_time` closed and stay readable — never deleted; every resolution is an append-only audit row; recency ordering breaks ties by write order (`currentWriteOrder`), never by `version` or UUID. Writer standing weights SHARED retrieval only — an entity's own notes are NEVER weighted by its own standing; adopted summaries inherit the LOWEST authority of their inputs. Dispatch debounce stamps are durable (`core_memory` owner `memory:dispatch`, keyed by entity NAME). Retrieval is lexical by default; hybrid/embeddings are OFF and evidence-gated. `MARINA_DB_DURABILITY` defaults to `full`, except `normal` under the `local` trust profile. `memory kv` is the canonical home of core key-value beliefs.
- **One outcome path** (`src/outcomes/`, migration 162): every filed answer lives in `forecast_answers` (`source`, `external_id` unique per owner, `eval_mode`); `resolveForecast` is the ONLY scorer (settle once); each resolution is ONE append-only `outcomes` row per subject (numbers and labels, never question text) with durable per-consumer `outcome_deliveries` (`lessons`, `history`). Delivery never drops: over budget, unarmed or unjudged waits, repeated failure ends `failed` with a reason; a `measure` outcome teaches like any other when its board is named (its source, or an `eval:<board>` ref — leakage rule 2 keeps the lesson out of that board's own measurements), is skipped when it names none, and never writes history. Boards file/resolve via `src/outcomes/filing.ts`; live work (task verdicts, Code Mode checks) via `src/outcomes/live.ts`, with lessons scoped by `workScopeFor` (a person's work never reaches the shared pool). New producers record through `recordResolved`, not `noteOutcome`/history writers of their own.
- **Lessons from work** (`src/learning/work.ts`, `MARINA_LESSONS_FROM_WORK=off|observe|on`, default on with a model provider, else observe): agent tool recoveries/repeat failures/budget blocks, gate holds, argcheck flags, challenge answers, task verdicts/bounces, exec denials and opted-in passthru (`x-marina-learn: on` or the stack's `learn`, bound keys) become signals of tool NAME + mechanical `errorClass` only; the hourly `lessons-from-work` job (phase 3300) turns each recurring pattern into ONE judged `tools`/`code` outcome via `recordOutcome` (≤ 20/flush, spend cap, no verdict ⇒ no write, learned/retired patterns skipped). A person's work (incl. agents it spawned) goes to its OWN `lessons:<domain>` spaces, served only via `RecallOptions.owner`; only owner-less world agents feed the shared pools. Request-driven agent turns (measurement runs included) teach too; `x-marina-eval` passthru requests do too (an `eval:` ref on the signal); `engine.shutdown()` flushes what a short-lived process collected and parks what is still below its floor (core-memory owner `learning:work`, no case text) for the next process to claim.→ docs/architecture/memory.md

## Orchestration Patterns
All 16 patterns are convention-based pool notes discovered via `recall`, not engine constraints (`src/world/templates/orchestration.ts`; activated via `project <name> orchestrate <pattern>`; "NSED" is the legacy name for Deliberation — accepted, never advertised). Crew pools are members-only: every persisted crew's `crew:<name>` pool has a group object of the same id (`memory_pools.group_id`) and the group-pool ACL in `src/memory/access.ts` plus the `gatherRetrievalContext` guard are the fence; dissolving keeps pool and notes scoped to the final roster. Formation mediators post at most one `[formation-mediator]` line per event. → docs/architecture/orchestration.md

## Dashboard, Canvas, and Memory Observability

Published panels are validated A2UI Canvas documents, not another runtime or resource store.
`codingDesk()` composes existing session/task/participant/feed references; operational buttons use
the live command or routing contracts after explicit reader review. Opening, refreshing or closing
a view must not start, stop or select a coding session. Public definitions never copy private source
contents. Resource invalidations carry no contents; reread under the viewer's credential, including
after reconnect. Hidden views suspend reads, access failures hide cached private content, and layout
presets save references/geometry rather than drafts. Browser and terminal views share these contracts.
→ docs/guides/published-panels.md; docs/architecture/dashboard.md

Every mutation at the DB write site emits one `EngineEvent` (`note_created`, `note_link_created`, `feed_event`, `canvas_edge_created`, …) that dashboard stores subscribe to. Canvas intent data lives in the node's `data` JSON as `{ intent: { prompt, status, … } }` — no schema changes. `memory_job` / `memory_service_event` broadcasts carry no content (no task/answer/citations); content is always fetched per-principal over REST, scoped server-side by `memoryObserverScope`; job cancel runs through the REQUESTER's resident binding and refuses the dev-open sentinel. GlassPanel section headers deliberately do NOT use the `title` prop (it is the react-grid-layout drag handle). Dashboard animations: see Code Style. → docs/architecture/dashboard.md (user guide: docs/guides/dashboard.md)
- **Ops surface (2026-09-22)**: `GET /api/ops/overview` (incl. `decisions` — harness decisions in scope, Admin → Ops → Decisions), `POST /api/ops/agents/:name/stop` (`src/net/api/ops-api.ts`, contract `ops-types.ts` re-exported type-only); Admin → Ops tab, header `HealthBadge` + `SpendChip`; `light` theme + `system` choice; every icon-only button needs an `aria-label` (a11y test enforces it); never subscribe a large component reactively to `eventFeed` — use `use-event-feed-bridge.ts`. HTML routes carry `HTML_CSP` (`script-src 'self'` + hashes; `MARINA_DASHBOARD_CSP=off` to drop). → docs/architecture/dashboard.md, security.md

## Schedules
`schedule add <name> every:|daily:|at:|before: lead: -- <command>` (`src/schedules/`, migration 165): an owner's OWN command run later or on a repeat — a general world primitive for people and agents, not a benchmark feature. A run is the owner's command in the owner's FIFO slot (`submitCommand` + `withCommandResponse`): rank, gates, challenges, spend caps and audit apply exactly as typed, so a schedule never grants anything. Owner connected → runs on that entity; a person offline → a quiet in-process attach (`Engine.attachScheduled`, current stored rank, no token, no network surface); an agent not running → `owner-absent` (never acts for a stopped agent). `schedule add` refuses a session holding less than the account's stored rank. Next-run times are wall-clock and durable (catch-up `once|none`; a `before:` run never runs past its deadline; `before:<kind>:<id>` takes a registered deadline reference such as `arena:<round>`, re-read every 10 min); `schedule_runs` is append-only; 2 consecutive failures (or owner-absent) raise alert `schedule:<id>`; readiness `schedules` and `tick-jobs` surface failures. No world ships a paid recurring schedule. → docs/architecture/schedules.md

## Resolvers, Watchers, Calibration, TabH2O
A resolver (`src/resolvers/types.ts`) is pure: reads external state, returns a typed `Sample`, never mutates engine state, never emits events; all five entry points converge on the same `resolve()`. `resolved`/`changed` samples land in tier `fact`; `no-change`/`error` land in `process`. Watch specs are notes in the shared `watches` pool — no new table. Calibration finders register via `registerCalibrationFinder()` (`src/resolvers/calibration.ts`); new prediction methods close their own loops with their own finder. `position` invariants are enforced at the data layer, not via prompt (bankroll > 0; no self-hedge on a ticker; single position ≤ bankroll cap; daily-loss floor on close-realized losses); live trading needs `MARINA_TRADING_ENABLED=true` + venue credentials, paper is the default. → docs/architecture/resolvers.md

## Harness Decisions
Cheap per-step judgement calls (`src/decisions/`): a decision backend answers `noul` / `choice` / `score` questions with numbers only; PURE policies in `policy.ts` turn them into verdicts (gate fails CLOSED, router fails OPEN to the powerful tier, verifier accepts on outage). Backends are pluggable — `decisions-api` (Jev family / OpenJev), `typesafe` (TypeSafe's `/v1/systemone`) or `chat-classifier` (any chat model) — and every reply passes `normalizeAnswers`; HTTP answers go out via `toWireAnswers` in TypeSafe's complete shape so `langchain-typesafe` can point `TYPESAFE_BASE_URL` at Marina (`/v1/systemone` alias). `MARINA_DECISIONS` defaults to `auto` (a chat classifier on Marina's own `/v1` with the world's default model — `marina-self`, never a cheaper stand-in; off with no provider), `MARINA_DECISION_GATE` stays off; set in the env or by an OPERATOR at runtime (`src/decisions/settings.ts`: `admin decisions set …` / Admin → Ops → Decisions → Settings; a person needs rank 5 + `admin.destructive`; an AGENT needs the earned `decisions.configure` gate — never admin.destructive alone, never self-witnessed, passed under `open` — via `decision settings set …`; a boot env var wins and locks the setting; base URLs, paths and API keys are env-only; every change audited). The pi tool gate runs AFTER `mediateToolCall` and only for `egress`/`mutate`/`consequential` risk (a batch sends only its gated parts; `egress` = `web search|fetch` never counts `outsideScope`); it sends the redacted tool call plus the agent's operator-set intent and trust LABELS (`MARINA_DECISION_GATE_CONTEXT`, adds the `unauthorized` question), never the transcript, and can only restrict. `model:route` resolves once at spawn (persisted, never mid-history); `MARINA_DECISION_VERIFY` bounces a weak `task submit` at most once (advisory — approval still decides; under `earned`/`open` posture `on` acts as `observe` and never bounces). Providers declare `calibrated`; a chat-classifier backend gets `UNCALIBRATED_GATE_POLICY` (one cut at 0.5, positives go to the owner, never a fine threshold). `POST /v1/decisions` serves any harness; `agent_decision` events carry the numbers, never arguments. Agents reach for the same primitive themselves — `decision check` (self-verify a draft) / `decision choose` / `decision qualify` (the labeled cases in `src/decisions/decision-cases.json` against the world's own backend only, rate limited) / `decision agreement` (judge opinions in `judge_observations` vs creators' verdicts, per `<kind>:<model>`; `MARINA_DECISION_VERIFY=observe` (default; Code Mode submits too) records without acting; an outage is opinion `none`, never a pass) — advisory only, never blocking or rewarding; cited `note:N`/`task:N`/`chronicle:N` become `grounded` evidence (`src/decisions/evidence.ts`, access = the citer's). → docs/architecture/decisions.md

## Signal discovery
`src/arena/discovery/`: signals are DATA in a fixed language (`signals.ts`), never code. The proposer sees discovery scores only, never holdout (`test/arena-discovery.test.ts` asserts it); a signal is promoted only on a time-split holdout win over the incumbent by `promotionMargin(tried)` without losing on discovery, and every scored attempt is recorded. → docs/architecture/forecasting.md

## Forecasting any question
`src/forecast/` generalises the arena's research pipeline (retrieve → mechanical citation check → one analyst per vendor → Jev judge on verified lines → aggregation) behind `bun run forecast`, the rank-0 `forecast` command (per-entity rate limit — every call costs money) and `POST /v1/forecast` (`forecast_unavailable` only when no model is available; one model runs it degraded).
- Every question takes the full typed pipeline (`typedForecastFor`): a plain probability/number is a yes/no choice or a typed number answered there, keeping the plain shape plus `typed` (`MARINA_FORECAST_PLAIN=legacy` is the old single-round path). Run verification is on by default (`MARINA_FORECAST_VERIFY`), routing observes, and resolved history is kept next to the DB (formation + score, the routing evidence). A caller-chosen formation is validated (`formationEnv`): `verify` turns the checker on, an unknown name is an error.
- General capability only — no benchmark-specific logic in `src/forecast/`. Competitions get thin adapters (`benchmarks/futurex/` …) that never send a submission themselves and record filings in append-only `external_submissions` (migration 152).
- Evidence is frozen at a cutoff (`asOf`, else min(now, `endTime`)); no outcome, lesson or resolved record is visible before it was known (`visibleRecords`; FutureX: end time + `SETTLEMENT_MARGIN_MS`). The answer object is the audit trail — keep every stage in it. Models stay operator-set: env, or an earned `forecast-config` promotion read through `resolveDefault` (`src/forecast/defaults.ts`; env wins, an unusable earned config falls back to the built-in).
- Lessons are ONE system (`src/learning`, judged, retire/supersede): every surface recalls through `forecastLessonsFor`; invalidating a ledger run retires what cites `bench:<id>`; backtests share one knowledge-bound rule (`benchmarks/forecasting/knowledge.ts`).
- Learned settings run by default and replace a default only on a held-out win by the margin (prior shrink and recalibration `earned` with no cold-start weight, routing `on` falling open to the default formation, selection through earned `forecast-config` promotions), so with no history no answer changes; shrinkage moves only toward an INFORMATIVE prior; `MARINA_FORECAST_HISTORY` stores numbers, option-label hashes and formation names, never text.
→ docs/architecture/forecasting.md (user guide: docs/guides/forecasting.md)

## Social Simulation Arena
Marina can be a signed participant in the MIT Social Simulation Arena (`src/arena/`, docs/guides/arena.md). Env-only and OFF by default (`MARINA_ARENA_ENTRANT`, `MARINA_ARENA_KEY_FILE` mode 0600 or refused); filing is an operator act (`bun run arena`, hourly `arena-autopilot` tick job at phase 1500) — the in-world `arena` command is read-only.
- `protocol.ts` is a byte-exact port of the arena's `signed_forecasts.py` (golden vector in `test/arena.test.ts` — never change it without re-deriving it from the arena's code); `score-shapes.ts` must keep matching `test/arena-score-shapes.test.ts`.
- `arena_submissions` (migration 127) is append-only; the autopilot files each round once; an operator may replace it before lock with `--replace`; a transport failure re-sends the SAME signed request within 240 s. Filing traces (migration 154) live in `detail`, separate from the signed request; sign at completion and recheck the lock margin after forecasting.
- No outcome before its release: evaluation runs in lock order, and inputs are only data fetched or published before the lock. A forecaster change is judged on the leaderboard's metric (mean of per-round skill), never total CRPS.
- `arena-filing-watch` (tick job, ~10 min, `src/arena/filing-watch.ts`) only READS `arena_submissions`/`arena_attempts` for rounds locking within 24 h and keeps one `arena-filing:<round>` alert per round in trouble (unfiled, failed or stuck refresh, no near-lock refresh); readiness `arena-filing`. It never files.
- Web research and experiments are shadow-only (`arena_shadow`, migration 128), never backtested against revealed outcomes; plans and selector advice never promote themselves into live routes; budget exhaustion is never a successful fallback.
→ docs/architecture/forecasting.md

## Execution Traces & Adaptive Routing
Every model-API request mints `{runId, traceId, spanId}` (`requestTrace()`), emits `model_request_lifecycle` events, and returns the id in `x-request-id` (the id for `trace show`); agent events carry the optional trace fields on the `EngineEvent` union; `projectTraces` (`src/engine/trace-projection.ts`) is the single span-tree projection for every consumer. `trace_judgments` (migration 71) is append-only. Adaptive routing is opt-in, falls back to least-busy on insufficient evidence, and the routing hot path skips judgment fetches by design. Benchmark evidence (`src/engine/benchmark-evidence.ts`, `MARINA_ROUTE_EVIDENCE=off|observe|on`, default `observe` — records, never changes a route) lets spawn-time `model:route` prefer another of its OWN candidates from ledger evidence (≥ 2 candidates with `MIN_N` items in the role's families — configured, else the role's declared `families`, within the cost budget; traced + direct-model evidence only, never shared). The objective is `MARINA_ROUTE_EVIDENCE_OBJECTIVE`:
  - `lcb` (default): the best Wilson lower bound.
  - `budget`: the best lower bound under `MAX_COST_USD`.
  - `value`: the cheapest candidate not measurably worse than the best (within `TOLERANCE`, and no significant paired McNemar loss, else overlapping intervals).

  Role-level participant evidence is used first, with a fallback to model level. The pick fails open to the router's choice. Inspect with `trace advise evidence <benchmark> [role:<r>]`. → docs/architecture/traces.md

## Security & API
- **Daily spend cap (`src/engine/spend-ledger.ts`, migration 131 `spend_daily`)**: every upstream dollar is recorded ONCE where it leaves Marina — `/v1` passthru `finish("completed")`, an agent turn's OWN provider cost (never the proxy header), the metered decision provider (`providerFromConfig`), `modelComplete` / the web retriever. `MARINA_DAILY_SPEND_CAP_USD` (UTC day; default 50, `0`/`off` = uncapped, junk never lifts it) is one total per world DB: the server and every spending CLI attach the `DB_PATH` ledger (`attachCliSpendLedger` / `attachWorldSpend`); `MARINA_SPEND_SCOPE` caps only narrow it; batch jobs stop through `SpendGuard` (`src/engine/spend-guard.ts`). It refuses `/v1` (429 `spend_cap_reached`), decisions (`spend_cap`), forecasts and priced media jobs, and pauses agents; child worlds get min(`MARINA_CHILD_DAILY_SPEND_CAP_USD` default 50, parent cap). A new spend site must call `recordSpend` + `dailyCapRefusal`. → docs/architecture/security.md
- HTTP endpoints require `MODEL_API_KEYS` / `MEM_API_KEYS` or explicit `MARINA_OPEN_API=true` — dev-only, never enable in production; the `OPEN_API_ENTITY_ID` sentinel is read-only (`refuseOpenApiWrite()`, 403 on asset/canvas writes) EXCEPT under the `local` trust profile, where the loopback dev operator IS the sentinel and Canvas writes stay open (the Playwright canvas suite depends on this). All endpoints are rate-limited; every per-IP limiter goes through `clientIp(req, peer)` (socket peer unless `MARINA_TRUST_PROXY=true`), named limiters live in `HTTP_RATE_LIMITS` (`src/net/http-utils.ts`). Outbound fetches go through `src/net/url-guard.ts` (SSRF: private IPs, IPv6 loopback, link-local, cloud metadata, IPv4-mapped IPv6). Assets: MIME allowlist + magic-byte check, `nosniff`, `Content-Security-Policy: default-src 'none'; sandbox`. MCP transport: bearer required when `MODEL_API_KEYS`/`MARINA_AUTH` is set or the bind is non-loopback; `quoteArg` REJECTS whitespace/control chars in interpolated params (the tokenizer has no quote grammar); the `flywheel` tool routes through `runCmd` so gates apply. Login limits (`MARINA_MAX_LOGINS`, `MARINA_LOGIN_ATTEMPTS_PER_MIN`) are enforced centrally in `engine.login()`/`reconnect()`; internal room/crew agents use the internal token and are exempt.
- `GATEWAY_SECRET` authenticates only the gateway *handshake* — it does **not** fence bridged-channel participation or cross-instance tells; a hard federation boundary also needs `MARINA_AUTH=better-auth` without open login. External auth is OFF by default; when off: no import, no routes, no schema effect; admins by verified email, not name. Dashboard broadcasts strip sensitive data (connectionId, raw command input).
→ docs/architecture/security.md

## Drop-in Compatibility (Passthru)
Compat profiles (`src/net/adapters/compat-profiles.ts`) are self-contained — they only register passthru model-id aliases on `/v1/models` (`MARINA_COMPAT` allow-list or `none`). Anthropic upstream (`src/net/model/model-api.ts`, `src/net/adapters/anthropic-tools.ts`): join EVERY `text` block (`anthropicTextContent`), forward EVERY `system` message in order (`anthropicSystemPrompt`), omit `temperature`/`top_p` for Claude 5 (`omitsSamplingParams`; the family rejects both — native Anthropic bodies pass through untouched); every other upstream drops them for a model that refuses them (`src/net/model/sampling-params.ts`: OpenAI reasoning families, Claude 5 via OpenRouter, any model an upstream refused them for), and Marina's own helpers ask through the same rule, surface upstream 4xx bodies. Unsupported parameters (`json_object`, `n>1`, non-function tools; agents mode refuses `tools`/`n`/`response_format`) → `400 unsupported_parameter` — never a silent drop or provider fallback; every error carries a string `code` (`src/net/model/openai-errors.ts`). A fallback-served reply must not credit the probed provider (`servedBy`) — run `readiness providers` after adding a provider or a model generation. A first-party key's usage/quota limit is a ROUTE limit (`src/net/model/route-health.ts`, shared by `/v1` and the agent loop; never Marina's spend cap): the route is avoided until the reset it named (else a bounded re-probe, persisted), and meanwhile the SAME model is served via OpenRouter when its catalog lists the mapped id (`claude-opus-5-5` → `anthropic/claude-opus-5.5`), labelled `openrouter/…` + `x-marina-route`, priced at OpenRouter — never a different model (`MARINA_PROVIDER_FALLBACK=off` disables); with no such route agents pause `route-limit` naming route and reset. ACP (`src/net/acp-server.ts`): stdout is reserved for the protocol, all logs go to stderr. → docs/architecture/passthru.md
- **Obligations ledger (2026-10-05)** (`src/obligations/`, passthru surface `src/net/model-api/obligations.ts`): per request (`marina/obligations:<model>`, `x-marina-obligations: on|observe`, or the passthru **instrument stack** — `src/net/model-api/stack.ts`, `MARINA_PASSTHRU_STACK=identified|all|off`, default `identified`: a bound-key, name-mapped or local-profile caller gets lessons + ledger + argcheck + learn by default, `x-marina-stack: full|none|<members>` / `marina/full:<model>` choose explicitly, an unrunnable listed member is a 400, the response's `x-marina-stack` says what ran; anonymous/shared callers stay byte-identical; `marina/verify:` composes with the ledger and the check (its checker sees the caller's memory context); the opt-in `ensemble` member / `marina/ensemble:<a>,<b>` drafts a tool-calling turn N times and a checker picks ONE whole draft (never merged; fail open to the agreeing group; `src/net/model-api/ensemble.ts`, never in the default or `full`); passthru `/v1/responses` runs the same stack through `runOpenaiChat` (an instrumented turn is answered un-streamed, then re-emitted as SSE if asked), while the Ollama routes and the agents route refuse it; keyed on `x-marina-session`, else a caller-scoped hash chain over system text, tool schemas and history checkpoints, so same-opening conversations part once they diverge), agents via `MARINA_OBLIGATIONS=off|observe|on` (default on for Code Mode work, observe otherwise; review default `auto`). The reminder rides as a trailing note AFTER the cache breakpoints (`trailingNote` hint), never a mid-conversation system message; on OpenAI-compatible upstreams earlier turns' notes stay where they were sent (`sticky-notes.ts`, caller-scoped), because that cache reuses only a whole earlier prompt; a final reply is nudged at most once per owed obligation and the model's output is never rewritten (judge or retry failure returns the draft). Ledgers are session state, never task content in the world DB. The **argument check** (`src/obligations/argcheck.ts`, surface `src/net/model-api/argcheck.ts`; `marina/argcheck:<model>` / `x-marina-argcheck`, agents `MARINA_ARGCHECK`, default on) checks a write call's values against the conversation (mechanical first, one judgement for a flagged call — or every write under `MARINA_ARGCHECK_TRIGGER=all-writes`, off by default because its replayed false-alarm rate was 13–25 % in-sample and held out; a generic dispatcher call is classified by the tool it runs, `src/obligations/tool-call.ts`, for the ledger too; read vs write is ONE classifier, `src/obligations/tool-effect.ts` — declared hints, then a generic HTTP request tool by the method it uses (`POST` through `api_fetch` is a write), then name/description verbs, ambiguous verbs by the description; unplaced tools are writes in the check's `guard` role, while the ledger's `track` role also takes weak read evidence; rule passages for the judge are opt-in, `MARINA_ARGCHECK_RULE_BYTES`), nudges at most once per call signature, never rewrites arguments, and fails open (`unjudged`). → docs/architecture/passthru.md
- **Responses streaming (2026-09-22)** is incremental on passthru and agents mode; `formatResponseRecord` is the single source for `response.completed` and `GET /v1/responses/:id`. Responses `tools`/`tool_choice` are translated to chat tools upstream (`responses-tools.ts`); non-function tool types → `400 unsupported_parameter`. → docs/architecture/passthru.md

## Key Files
- `src/types.ts` — all core types (includes `KnownProperties`, `RoomContext.brief`, `RoomContext.logEvent`, market event types)
- `src/engine/engine.ts` — composition root, room contexts and world lifecycle; auth/session policy in `auth-coordinator.ts`, command execution policy in `command-phase-coordinator.ts`
- `src/engine/tick-scheduler.ts` — the periodic half of the tick, extracted from `Engine.tickInner()` (same pattern as `ConnectionManager` / `EventLog` / `BriefManager`): declarative `TickJob { name, every, phase, run, critical? }` registered by `registerTickJobs(host, scheduler)` in `src/engine/tick-jobs.ts` (the engine supplies its collaborators as LAZY getters — registration happens in the constructor while `db`/managers may be assigned later) and fired by `runDue(tick)` on `tick % every === phase`. `register()` ENFORCES the distinct-phase invariant (two jobs sharing an interval may not share a phase) that used to be only a comment; `describeTickSchedule()` reports name/every/phase/runs/lastError for operators. The command phase lives in `command-coordinator.ts` (fair queues, per-entity FIFO, budgets and drain); room `onTick` budgets and async drain live in `room-tick-coordinator.ts`; sandbox, DM expiry and briefs stay in `engine.ts`.
- `scripts/monitor.ts` — `bun run monitor`: the OUTSIDE monitor (never imports the engine; installs nothing on the host — no service/scheduler/OS setting; Linux/macOS/Windows alike): reads `/health` (503 `stalled` when the tick loop stops), alerts once per outage via `--desktop`, `MARINA_MONITOR_WEBHOOK`, and pings `MARINA_MONITOR_HEARTBEAT_URL` while healthy (dead-man's switch). → docs/operations.md
- `src/engine/connection-manager.ts` — connection tracking, entity-connection mapping
- `src/engine/event-log.ts` — event storage, trimming, listener notification
- `src/engine/brief-manager.ts` — brief subscription lifecycle
- `src/engine/command-registry.ts` — ordered composition of domain registrations in `src/engine/registrations/`; operator extensions use `src/extensions/loader.ts` and owned router registrations
- `src/engine/readiness.ts` — `computeReadiness(engine)` capability-health report (ok/degraded/off + remediation per capability); backs the rank-0 `readiness` command (aliases `doctor`/`health`). Operator's answer to "what must I spawn/configure for ability X to work?" — see [docs/operations.md](docs/operations.md)
- `src/engine/constants.ts` — named constants for tick intervals, rate limits, scoring weights
- `src/engine/parse-input.ts` — shared command input parsing helpers
- `src/persistence/database.ts` — migrations, MarinaDB class (delegates to modules)
- `src/persistence/db-notes.ts` — note CRUD, FTS5 recall, knowledge graph, importance adjustment, core memory, pools
- `src/persistence/db-entities.ts` — entity persistence, room KV store, activity tracking, events
- `src/persistence/db-tasks.ts` — task CRUD, claims, bundles, standing, projects
- `src/persistence/db-channels.ts` — channel management, boards, groups, global search
- `src/persistence/db-agents.ts` — traits, roles, agent configs, API keys, adapters
- `src/net/mcp/mcp-server.ts` — MCP transport/auth/lifecycle; `mcp-world-tools.ts` registers live `capabilities`/`invoke` and task-aware `context`, with compatibility adapters in `mcp-named-tools.ts` and execution through `mcp-session.ts`
- `src/net/model/model-api.ts` — model-API entry: auth, rate limit, path dispatch + stable re-exports; surfaces live in `src/net/model-api/` behind a strict import DAG rooted at `shared.ts` (`upstream`, `anthropic-bridge`, `openai-responses-bridge`, `routing`, `passthru`, `responses-sse`, `chat-completions`, `responses`, `ollama`, `models`, `obligations`, `argcheck`, `verify`, `learn`) — see docs/architecture/passthru.md → "Source layout"
- `src/net/dashboard-api.ts` — dashboard REST entry: auth gate, per-principal limiter, ordered route dispatch, stable re-exports. Route groups live in `src/net/dashboard-api/*` (`shared` DAG root, `command` pre-auth ingress, `system`, `traces`, `readiness`, `ops`, `memory`, `agents`, `keys`, `world`, `discovery`, `routing`) — dispatch ORDER is load-bearing; see docs/architecture/dashboard.md → "Dashboard API source layout"
- `src/agent/tools/index.ts` — agent tool entry: re-exports only. Tools live in `src/agent/tools/*` (`shared` DAG root, `command`, `world`, `code`, `media`, `memory`, `memory-service`, `memory-assistance`, `think`, `evolution`, `profiles`); `profiles.ts` owns the resident/deferred split, `marina_tool_search`, execution modes and strict schemas. Schema bytes per profile are byte-identical to the pre-split monolith.
- `src/net/api/mem-api.ts` — Memory API REST endpoint with per-agent rate limiting
- `src/net/url-guard.ts` — SSRF protection (private IP, IPv6, cloud metadata blocking)
- `src/agent/agent-runtime.ts` — agent spawning, lifecycle, LLM dispatch
- `src/agent/lean-agent-adapter.ts` — cognitive architecture: cadenced, byte-budgeted continuation prompt, dynamic tick, idle consolidation
- `src/agent/roles.ts` — semantic role composition (traits with capabilities, synergy/tension detection)
- `src/agent/standing.ts` — civic-contribution ledger (decay, credit table, record/getStanding/recomputeAll/recordFromEvent)
- `src/agent/rank-progression.ts` — threshold-derived rank (no metric scoring; pure standing → tier lookup)
- `src/engine/safety-gates.ts` — per-operation competence proofs (SAFETY_GATES registry, checkGate, grant, recordDemonstration)
- `src/persistence/db-standing.ts` — standing ledger SQL (append, decay query, rollup cache)
- `src/persistence/db-competence.ts` — entity_competence SQL (per-gate demonstration tracking)
- `src/agent/social.ts` — relationship tracking, adaptive social response, coordination signals
- `src/world/world-definition.ts` — WorldDefinition interface (includes `seed`)
- `src/world/templates/orchestration.ts` — 16 orchestration pattern templates (5 notes each)
- `worlds/default.ts` — compact intent-first Workbench (the default)
- `worlds/showcase.ts` — full-featured 25-room capability showcase
- `worlds/seed.ts` — shared seed utilities, trait/role definitions with capabilities
- `src/engine/commands/canvas.ts` — canvas CRUD, asset management, layout, intent system, per-entity canvas (`canvas visit`), typed edges (`canvas connect/disconnect/edges`)
- `src/engine/commands/feed.ts` — `feed list --kind X --entity Y --since 30m` querying the `feed_events` timeline
- `src/engine/commands/market.ts` — market discovery, leaderboards, `market forecast <id>` (TabH2O-backed calibration)
- `src/engine/commands/benchmark.ts` — in-world `benchmark` (list/run/sweep/runs/result/leaderboard/frontier/compare/replicates/participants/reference/orchestrations, defaults/challenge/promote, invalidate/revalidate, purge-content-notes). No benchmark path writes an item's question or answer to memory: a run teaches only through ONE judged lesson outcome (`noteBenchmarkRun`): a general rule about configuration or model choice, never item content — `itemLeak` (`src/learning/leak-guard.ts`) rejects item ids, quoted item text and answers before the judge (`test/benchmark-no-content-memory.test.ts`, `test/lesson-leak-guard.test.ts`). The ledger (migrations 146–157, `src/engine/benchmark-ledger.ts`) stores ids only, never case content; its items are never deleted and its validity, regroup and promotion histories are append-only. Defaults change only by earned promotion (`src/engine/benchmark-promotion.ts`): a held-out win with the paired interval above 0 and the gain past `promotionMargin(tried)`, `role.edit`, enough same-configuration replicates checked before the holdout is read, and never by the author of a pooled run. An invalid run (`benchmark invalidate`, or automatic past `MARINA_BENCHMARK_MAX_FALLBACK_RATE`) is skipped by every ranking reader and never frees a promoted slot. Defaults are READ through one path, `resolveDefault` (`src/engine/default-resolution.ts`): env > local slot (`<slot>[:<board>]`) > family slot (`<slot>:family:<f>`, never filled by a per-board promotion) > upstream seed (empty hook) > built-in, each resolution traced (`default_resolved`); they are WRITTEN only through `fileSlotPromotion` (in-world `benchmark promote`, and `forecastbench`/`metaculus select`, which file their selection-split winner on `forecast-config:<board>`). → docs/architecture/benchmarks.md
- `src/engine/benchmark-run-keys.ts` — run keys: `benchmark run … arm:<bare|on|full|members> cap:<usd>` mints a process-local `/v1` key (`mrk_…`, env only, revoked at run end) that adds the arm's `x-marina-stack` and the run's `x-marina-eval` to requests naming none, attributes upstream spend to the run (429 `run_budget_exhausted` at the cap) and trips after 5 consecutive network failures (503 `run_transport_failed`); a stopped run is recorded invalid. No arm and no cap = the internal token, unchanged. → docs/architecture/benchmarks.md
- `src/engine/benchmark-compare-run.ts` — `benchmark compare-run <name> arms:<a>,<b> cap:<usd> [replicates:N]` (default 2): both arms per replicate at once, each arm's runs in replicate group `cmp:<id>:<arm>`, cap split per run; report rebuilt from the ledger (Wilson per arm, cost per success, `model@source`, paired two-stage bootstrap; unpaired model/code flagged). In-world runs now record per-item outcomes (ids and numbers only) and target `{model, stack, source}`.
- `src/engine/benchmark-kit.ts` — `benchmark kit list|doctor|status|<setup> … [dry-run]` launches the reproduction kit (`benchmarks/repro`, `bun run repro`) from inside the world: allowlisted options, plain-token values, argv only (never a shell); `--run-dir`/`--ledger` are set by Marina (`MARINA_BENCHMARK_KIT_DIR`, the world DB); a real run needs rank 4 and `budget:`.
- `src/engine/commands/skill.ts` — `skill compose/store/search/verify/list/share/import` for markdown-with-frontmatter skill packages (Claude-Code-compatible, world-seeded universally).
- `src/engine/commands/knowledge-hygiene.ts` — shared note auditor (`auditKnowledgeNotes`/`renderKnowledgeHygieneReport`). Five finding kinds: `duplicate`, `overlong`, `stale-command` (unknown command/subcommand refs), `unsupported-claim` (empirical-assertion markers with no citation — conservative, so instructional notes don't trip it), and `stale` (untouched past `maxAgeMs`, opt-in). Backs `pool <name> audit`, `skill audit`, and `guide audit`.
- `src/engine/commands/guide.ts` — first-class read-only surface (rank 0) for the platform `guide` pool: `guide` (overview), `guide <topic>`/`guide recall <topic>`, `guide list`, `guide audit` (alias `guide lint`). Authoring stays a world-seed concern / `pool guide add`.
- `src/persistence/db-chronicle.ts` — chronicle SQL (append-only, four kinds, JSON participants/refs, `like` substring filter, recursive supersession-chain query).
- `src/engine/commands/chronicle.ts` — read commands (rank 0) + Chronicler-gated write commands (record / correct / digest / pending). Citations earn no standing.
- `src/net/api/entity-api.ts` — `GET /api/entity/:name/profile` consolidated public endpoint backing `/who/<name>`. Computes achievements on-the-fly; privacy-filtered (excludes connection_id, IP, session tokens, raw input, private notes).
- `dashboard/src/who/` — public per-entity blog/wiki pages. `WhoPage.tsx` (six sections), `Sigil.tsx` (deterministic name-hash identicon), `types.ts` (mirrors EntityProfile contract). Routed in `dashboard/src/main.tsx` via path-based detection.
- `src/engine/benchmark-runner.ts` — 17 registered benchmarks in `BENCHMARKS`: 16 academic (mmlu-pro, truthfulqa, arc-challenge, hellaswag, musr, bbh, gsm8k, math, simple-qa, humaneval, ifeval, frames, aime, gpqa — GPQA-Diamond, hle-verified-gold, hle-verified-gold-mm — its image items sent as `image_url` parts) plus `smoke` (the tracked 15-item prompt A/B set, `benchmarks/smoke-eval.json`, harness adapter `checks` shared with `scripts/eval-prompt.ts`). The runner passes the harness `--endpoint` + the internal token (env, not argv); an all-error run is `failed`, never a 0% score. Voice-friendly multi-word resolution via `resolveMultiWordName`.
- `benchmarks/reference-scores.ts` — frontier-model reference scores seeded for leaderboard comparison.
- `src/net/clients/tabh2o-client.ts` — H2O.ai TabH2O HTTP wrapper, SSRF-guarded, graceful degradation
- `src/net/feed-publisher.ts` — engine → feed canvas bridge (calibration moved to `src/resolvers/calibration.ts`)
- `src/resolvers/` — resolver primitive (types, registry, built-in `resolving` + `echoing` kinds), calibration finder registry, watch specs, cadence parser, sample writer
- `src/engine/commands/probe.ts` — invoke a resolver, persist a Sample, auto-link to active watch
- `src/engine/commands/watch.ts` — declarative observation requests (create/list/due/retire), specs stored in the `watches` pool
- `src/engine/commands/position.ts` — Kelly-sized Kalshi/Polymarket positions, no-self-hedge invariant, auto-spawned resolving watch on open
- `src/net/api/probe-api.ts` — `/api/probe` HTTP route (GET lists kinds, POST invokes)
- `src/routing/service.ts`, `src/persistence/db-routing.ts`, `src/net/dashboard-api/routing.ts` — generic participant sessions, output replay and delivery receipts; native conversation sends reuse channel history and ChannelManager. Private output never goes into the public activity feed. CLI: `scripts/route.ts`; opt-in native agent supervisor: `scripts/supervise.ts`, `src/routing/supervisor.ts` and `agent-adapters.ts` (Claude SDK, Codex app-server, pi RPC; local journal in `db-routing-runner.ts`). Runtime controls use existing `code.exec`/`agent.spawn` gates; generic envelopes never execute as controls. SDK: `src/sdk/routing-client.ts`; UI: Workspace → Streams. See [participant routing](docs/guides/participant-routing.md).
- `src/sdk/client.ts` — `MarinaClient` / `MarinaAgent` external SDK; `tellAndAwait()` synchronous-reply primitive
- `src/agent/execution-trace.ts` — `AgentExecutionTracer`, `traceParentFromPerception`, `unambiguousTraceParent` (causal trace propagation into agent events)
- `src/engine/commands/trace.ts` — rank-0 `trace` command (show/find/stats/compare/dataset/advise/choose/eval/judge/judgments/otel)
- `src/engine/trace-projection.ts` / `trace-analytics.ts` / `trace-dataset.ts` / `trace-evaluation.ts` / `trace-routing-advice.ts` / `otlp-trace-export.ts` — span-tree projection, descriptive analytics, eval-json datasets, judgment evaluation, weight-free routing advice, OTLP pull export
- `src/engine/entity-name.ts` — `sanitizeEntityName` (single source for login name normalization; Code Mode identity comparisons normalize with it)
- `src/net/acp-server.ts` — Agent Client Protocol (ACP) stdio JSON-RPC server for editor clients (Zed, JetBrains, VS Code, Neovim); launched via `bun run scripts/acp.ts <name>`
- `src/engine/gateway-runtime.ts` — federation: structured JSON relay with protocol versioning
- `scripts/connect.ts` — CLI binary (`marina` command): REPL, one-shot (-c), pipe modes. One-shot lingers until output has been quiet for 2.5 s but only starts counting once the command produced SOME output (capped by `--wait`), so slow commands such as `readiness providers` or a model-backed `ask` are not cut off by their initial silence
- `test/helpers.ts` — shared test utilities

## Environment Variables

The canonical reference is **[`config/environment.reference`](config/environment.reference)** — the complete annotated server catalog, updated alongside code. `environmentCatalog()` (`src/config/environment.ts`) parses it for the dashboard settings editor (`src/net/dashboard-api/keys.ts`), `scripts/generate-environment-reference.ts` renders `docs/reference/environment.md` from it (`bun run docs:api`; CI checks with `--check`), and `test/docs-contract.test.ts` holds both directions. Tags in a block: `@protected` (dashboard never writes it; unions with the hard-coded `PROTECTED_ENV_KEYS`), `@internal` (hidden from the editor), `@restart` (never reported as hot-reloaded). `.env.example` is only a short commented starter and must stay a subset. Script/CI knobs live in `docs/guides/testing.md` / `release-qualification.md`, SDK example knobs in `src/sdk/examples/.env.example`. A new server variable goes in the reference in the same change. The handful below is the *load-bearing subset for understanding world shape*; if anything below conflicts with the reference, the reference wins.

- `MARINA_WORLD` — which world definition to load (`default` / `showcase` / `commons` / `research` / `markets` / `prediction-lab` / `craft` / `evolve` / `personal` / `demos` / `deep-research` / `red-team` / `due-diligence` / `data-investigation` / `empty`). Forecasting, `arena` and `market`/`position` are global commands in every world; never switch worlds (and split the `DB_PATH` ledger) for arena work.
- `MARINA_COMPAT` — comma-separated compat-profile allow-list, or `none` to disable. Default: all registered profiles (e.g. the `assistant` alias) surface in `/v1/models`.
- `MARINA_NAME` — instance name, scopes tour dismissal + seen flags per-world.
- `MARINA_ROOM_AGENTS` — `false` suppresses all room-agent auto-spawn. Use for cost-controlled demos or explicit-only populations.
- `MARINA_TRADING_ENABLED` — `true` allows live Kalshi/Polymarket order placement; paper trading is the default. Live needs `KALSHI_API_KEY` / `KALSHI_API_SECRET` (Polymarket still gated on EIP-712 signing).
- `MODEL_API_KEYS` — comma-separated Bearer tokens for `/v1/*` and Ollama-compat routes (or `MARINA_OPEN_API=true` for dev). Without either, model API rejects.
- `MEM_API_KEYS` — comma-separated `secret:agent` pairs for the Managed Memory REST endpoint at `/mem`.
- `GATEWAY_SECRET` — pre-shared key for peer Marina federation.
- `STANDING_HALF_LIFE_DAYS` — tunable, default 60. Standing decay shorter = faster reputation churn.
- `TABH2O_API_KEY` / `TABH2O_ENDPOINT` — H2O.ai's hosted tabular foundation model; endpoint override for self-hosted deployments.
