CLAUDE.md · diff

git:20261026.bf68f40 to git:20261026.f840eb5

1 added, 1 removed. Audit A to A.

# 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. `main` is protected by a ruleset with a
merge queue: `gh pr merge <n> --squash` queues the PR, CI reruns on it merged onto current
`main`, and it lands only when that passes. A workflow that a required check comes from must
also run on `merge_group`.
## 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, tsconfig.benchmarks.json for benchmarks/, tsconfig.scripts.json for scripts/ and worlds/)
bun run lint # Biome lint
bun run format # Biome auto-format (run before committing)
bun run test:fast # pre-commit loop: 137 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)
```
**Bun version**: Bun ≥ the version in `.bun-version` (currently 1.4.3; `engines.bun` and `packageManager` in `package.json` must agree, held by `test/bun-version.test.ts`). An older Bun fails tests in ways that look like Marina bugs (empty proxied responses, missing image handling; before 1.4.3 a `Bun.spawnSync` in a long-lived `--parallel` worker could spin until its timeout, or forever, and lose the child's output — the 15 s git-fixture timeouts and hung CI shards), so `test/preload.ts` refuses to run below it and the server and `marina` warn at startup (`bunVersionRefusal()` in `src/version.ts`). Check `bun --version` and which `bun` is first on PATH before chasing a local-only test failure; bump the pin in all three places together.
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**: research, plans, strategy, competitive analysis, internal audits and qualification reports are kept outside this repository; public documentation is 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; the lookup finds the text through `idx_notes_content_prefix` (migration 177) and maps it back to its handles, so a write never reads the entity's history. Note searches never join `numeric_notes` to `notes_fts` directly (SQLite re-runs the MATCH per note): use `MATCHED_NOTES_CTE` with `MATCHED_FROM` / `MATCHED_FROM_SCOPE` (`src/persistence/fts.ts`; `lexicalFrom` picks the smaller side). `test/note-paths-equivalence.test.ts` holds both to the original queries and plans. 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()`
+ - Commands: one file per command in `src/engine/commands/`, register in `src/engine/command-registry.ts` → `registerBuiltinCommands()`; a handler that refuses (bad usage, no such target, not permitted, wrong state) replies with `refuseCommand(ctx, entity, message)` (`src/engine/command-response.ts`) so usage and activity record a failure — a read that finds nothing stays `ctx.send` (`unknownSubcommand`, `rankFloorRefusal` and a refused gate check already declare it; a probe that only narrows a command runs in `withoutRefusal`)
- **Argument conventions** (`src/engine/parse-input.ts`): 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.
- **Command families** (`src/engine/command-families.ts`): `memory` is the one memory command — `memory note|recall|reflect|recap|debrief|context …` is forwarded to the member builtin before resolution (`CommandRouter.forwardFamily`; same def, gates, fence row, quests, logged input). Bare member names are announced aliases (one `(alias: …)` line per session per alias); help and rosters list members under the head (`CommandCatalogEntry.familyOf`); text classifiers read a family form as its member (`unfoldFamily`). Internal producers (MCP named tools, `memory-platform`) send family forms; `pool`/`skill`/`orient`/`bookmark` stay distinct.
- **`code` command layout**: `src/engine/commands/code.ts` (~900 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 (88 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,review-request}.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-179 baseline atomically (an empty database replayed through history, data conversions included; `MIGRATION_HOOKS` in `schema.ts` keeps folded hooks); 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. `memory_requests` is pruned ONLY for continuity-journal receipts (`CONTINUITY_RECEIPT_WHERE`, 7 d: per-process session keys never resent); every other idempotency receipt is kept. → 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`, … 56 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` — every usage form is `{ syntax, effect }` with `effect` `read|write|delete|execute` (`read` only when nothing changes: it becomes MCP `readOnlyHint`; `test/tool-policy-effects.test.ts` enforces it); `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.
- **Feature status** (`src/config/feature-status.ts` → `docs/reference/feature-status.md`): every major feature is core / siloed / experimental / demo / deprecated. A command that exists only for an experimental feature lists itself in `commands` and wraps its help in `experimentalHelp()` (`[experimental]` prefix); `test/docs-contract.test.ts` holds the page, the registry and the help text together. Update the entry when a feature gains (or loses) production use. **Capability posture** (`src/config/capability-posture.ts` → generated `docs/reference/capability-posture.md`, rank-0 `posture`, readiness `capability-posture`): every non-core feature and every `MARINA_*` setting documented as `off`/`observe`/`false` is `measure` (evidence source + `decideBy` date) or `by-design` (reason); a new off-by-default setting fails `test/capability-posture.test.ts` until it has one. The dates are development deadlines; informational on installs: the readiness line is always `optional` (never an ops alert or a degraded header).
- 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()`
- **Statistics** live in `src/stats/` (Wilson, McNemar, seeded PRNGs, every bootstrap); core and harnesses import it directly. A new interval or bootstrap reuses it; `test/stats-equivalence.test.ts` holds each deduped harness copy bit-identical to its frozen original (`test/fixtures/stats-legacy.ts` — never edit it to make a test pass).
## 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 ANOTHER ACCOUNT verified. **Contributors earn standing**: for standing every account is its own party (`standingVerifierRefusal` in `recordVerified`): self-verification (same name or same durable account) is refused; a verifier outside the worker's spawn lineage (`independentParties`; `spawnLineage`: `agent_configs.spawned_by`, then the agent principal's owner) credits; a PERSON inside the lineage (owner approving its agent's work) credits; an AGENT inside the lineage (sibling, child, or judging its owner) credits only when GROUNDED — today only `code_verified` (checks passed); kinds with no grounding signal keep the lineage rule for agent verifiers. Caps unchanged. One chronicle `event` records the change (`chronicleCivicRuleChanges`, `src/agent/civic-rules.ts`, idempotent by its ref). Live kinds: `task_complete` (creator approval in any mode, value ≤ `TASK_STANDING_MAX` 10, default 1, `task create` refuses more), `code_verified` (Code Mode attempt whose checks passed, approved by another account — sibling reviewers included), `forecast_resolved` (outside resolution, or a resolver passing the party rule; ≤ 0.5 by skill, 1/day per account, never an `eval_mode` filing; paid to the filer and every producer the filing names — the self-declared producer list stays one lineage once per answer), `experiment_complete` (replicator passing the party rule), `assistance_*` (a person requester credits any other principal, its own agents included; any other requester only outside its party, `isPersonPrincipal` / `principalParty`), `quest_complete`. `RETIRED_STANDING_KINDS` (`chronicled`, `pool_note`, `reflection_recalled`, `crew_*` credits, `helping_act`) are refused by `record()`; their rows stay as history but count toward no standing by default (`MARINA_STANDING_RETIRED=exclude`; `decay` = the old fade-out; `standing` shows them as "retired credit") — never deleted or reset. Credits key on the durable account by name (`accountKey`), and every readout (`standing`, `task standing`, `standing top`, `/who`, `orient`, `brief`) shows `standingOf`. Agents SEE it (`src/agent/standing-summary.ts`): every new ledger row tells the online earner `standing +N <kind> <ref> → total (rank step)` (`MarinaDB.onStandingCredit`, after commit); `standing summary` / `orient` / `brief` / the resident `[Standing]` line show live standing, last live credits and retired credit apart, plus an earn line built from `liveStandingKinds()` (never hand-written). 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`, 15 gates); solo use needs an unsupervised `entity_competence` row. **Approvals are the proof**: an EARNABLE gate has a low standing floor (code.exec 3; agent.spawn/role.edit/world.lineage 10; agent.run 20; decisions.configure/adapter/connect/gateway 30 — placed on the verified-work equilibrium ≈ 86.6 × points/day) and becomes solo after `demoThreshold` held runs a person approved `once` from OUTSIDE the requester's lineage (`approvalDemonstrates`, which keeps `independentParties`; an owner's approval runs the action but never builds its agent's gate — the `gate grant` rule; the judge never counts; one person's agents can lift each other's standing to a gate's floor, never to solo use). OPERATOR-GRANT gates (`GateDef.operatorGrant` = exactly `OPEN_POSTURE_CORE`) have no floor and no demonstrations: refusals say "Operator grant", an operator may approve one held run that builds nothing. The witness ladder is folded into challenges: no supervision windows; `witness` stays as compatibility pointers, `witness attest` reviews the `earned` posture's pending runs (an operator outside the lineage). Operators bootstrap via `safetyGates.grant(db, entityId, gateId)` from world seeds — no rank shortcut; living worlds keep stored ranks, grants and demonstrated rows. `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|sovereign` is env-only by design — never settable in-world; `open` never passes `OPEN_POSTURE_CORE`; `sovereign` is the explicit all-gates-off switch (feeds `isLocalUngated()` in any profile, core included; non-loopback needs `MARINA_ALLOW_INSECURE_PUBLIC=true`) (`key.manage`, `admin.destructive`, `shell.exec`, `code.exec.unrestricted`, `world.code`, `trading.live` — the last also NOT lifted by local-ungated, `GateDef.holdsUnderLocalProfile`: only `sovereign` or an explicit grant), 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 — and is operator-grant only (no approval mints it; the headless approver's refusal says so). 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`, or `sovereign`) drives permissions; `isImplicitLocalUngated()` (ungated but not `sovereign`) keeps an agent's host commands held. 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`), `code.exec.unrestricted` (the headless exec approver — the one allowlisted bare-`checkGate` site), and `trading.live` in `position open|confirm|close` when the order would reach a live venue. 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>" }`); the router's `minRank` for ungated commands AND inline subcommand rank floors go through the one rule `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` (deliberate exceptions, not floors: `schedule add` comparing the session's rank to the account's, `code exec-mode`'s loopback-sovereign eligibility, `run`'s no-database fallback, and `brief`'s display hint); read forms of a ranked/gated command declare `CommandDef.readOnlyForm` (`world list`, `position list`, `admin decisions`) and skip both rank and gate; `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**: 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 and takes an operator (anyone else's `always` is once); a `once` approval from outside the lineage is a demonstration. 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. Unanswered expiries keep ONE rolling `challenge:unanswered` inbox item (warning, attention kind `approval`) open until an operator resolves it; `challenge summary [days]` is the content-free readout; `MARINA_OPS_NOTIFY` (`src/engine/ops-notify.ts`, default off: http(s) webhook or `cmd:<program>`, no shell) pushes `challenge.opened` / `challenge.unanswered` notices out of the world, never the held input. `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 — `earnedJudge` in `src/decisions/earned-judge.ts` is the ONE earned rule for every judge, read through the ONE judge ledger — `judge_ledger`, migration 179, `judgeEarned`/`recordJudgeOpinion` in `src/decisions/judges.ts`, per surface and judge; `MARINA_JUDGE=off|observe|on` sets every surface, a surface's own variable wins; `judge stats` is the readout), 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
- **The as-of fence** (`src/engine/evidence-fence.ts`): an evidence cutoff on the per-execution `runCommandScope` frame (read once at entry by `commandFenceFor`; nested frames inherit and never loosen it). A `model_request` routed with `evidenceCutoff` (`x-marina-asof`) fences its responder until it settles; a fenced execution relays the fence to every agent it delivers to (`ConnectionManager.sendToEntity`). Under a cutoff the command phase consults `EVIDENCE_FENCE_POLICY`: `fenced` handlers bound themselves via `currentEvidenceCutoff()` (web on date-strict providers / Wayback, `data asOf`, `recall`/`context`/`context_preview` as of the cutoff — revised notes at their version known by then, durable tiers by their own known-at time (persisted on the row, migration 172, backfilled by the `memory-known-at-backfill` tick job; retention keeps a dating event until it is copied), selected and ranked on the as-of text only (`fencedNoteRecall` / `rankAsOf` — never current text, scores, vocabulary or links), undated items withheld (`src/memory/as-of.ts`), `lessons` `visibleAt`, `triage`), `allow` reads nothing dated, everything else — nested `forecast`, unbounded outbound tools, knowledge reads without as-of, unlisted commands, room commands — is refused (fail closed). A new command must take a row (`test/evidence-fence.test.ts`). → docs/architecture/civic-substrate.md
- Sandbox providers: Marina owns identity, policy, approvals, Code Mode, artifacts, audit; a provider owns isolated execution. Every provider implements `SandboxProvider` (`src/coding/sandbox-provider.ts`: `id`, `label`, declared `capabilities`) and registers in the engine's `SandboxProviders` (Flywheel today); a session's target is `local` or a provider id, `WorkspaceGateway` dispatches by id, and `code sandbox providers|use <id>` / `provider:<id>` select among several. Never silently fall back from requested sandbox execution to host execution, claim a capability the provider does not declare, or imply host/guest file coherence.
- **Local agent host-command hold**: under the IMPLICIT local profile (`isImplicitLocalUngated()`: local, autonomy neither `guarded` nor `sovereign`) the loopback person is ungated, but an AGENT's `shell.exec` / `code.exec.unrestricted` (`LOCAL_AGENT_HELD_GATES`) is held as a challenge for a person (`checkGateForExecution`; agent = `principalOf()` in `gate-context.ts`, fed by the challenge host's `principalKind`, fail-closed to agent); `{ core: true }` rank floors hold an agent too, Code Mode's `selectExecApprover` defaults an agent to `prompt`. `sovereign` lifts it — the ONE all-gates-off switch, agents included (`test/gates-authority.test.ts`).
- **Gate grants are never seeded for agents, and never reset on boot**: under `local` only a loopback PERSON is made sovereign (`AuthCoordinator.isAgentLogin`: internal/MCP connection, `principal: "agent"`, or an agent config/principal ⇒ not). Old databases keep their 999 rows; `gate audit` (read) lists them by holder, `gate reset <entity|agents> [gate|all]` is a dry run and `… apply` (operator) revokes sentinel rows only, chronicled per holder (`src/engine/gate-audit.ts`).
→ 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
## Worlds and Room Agents
- **Private rooms are opt-in (2026-10-21)** (`src/engine/room-access.ts`, migration 176 `room_access`): every room is public unless its owner (the room's first builder, else an operator) runs `build access <room> private`; only `build access|invite|uninvite` writes the table (a test scans for other writers). Entry checks go through `roomEntryRefusal` in `move`, `goto` (room or person) and resident enrolment; owner, invitees, their spawn lineage and operators pass; the start room cannot go private; occupants stay. Speech needs no extra fence (room-scoped already).
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 the `room` model role, `marina/cheap` by 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
- **Model tiers** (`src/config/model-tiers.ts`): code that picks a model for work that names none asks `roleModel(role)` / `roleModelFromEnv(role)`, never a literal. Cheap roles (`resident`, `room`, `crew`, `helper`) get the live alias `marina/cheap`; strong roles (`judge`, `outcome-judge`, `lessons-judge`, `decisions`, `arbiter`) get `MARINA_MODEL_TIER_STRONG`, else `marina/default` — anything that grades or overrules is never on the cheap tier by default. A role's older specific variable wins, then `role:<role>=` in `MARINA_AGENT_MODELS` (and a resident's explicit default), then a promoted `role-model:<role>` slot (read through `resolveDefault`; an agent spawned for work role `<r>` reads `role-model:<r>` first; a model this installation cannot call falls through, traced), then the tier. That slot is earned only on shared LIVE work (`src/engine/live-promotion.ts`: time-split holdout, `differenceInterval` above 0, `promotionMargin(tried)`, no discovery loss, ≥ 20 per arm per split); the 6-hourly `live-promotion` job raises one ops-inbox proposal and never files; a PERSON holding `role.edit` files it with `benchmark promote role-model:<role> <model>` (never an agent, never on their own work). `/v1` rewrites `marina/cheap` / `marina/strong` (and bare `cheap` / `strong` from a resident's loopback) per request (`tierRequestModel`); the `marina` model itself is unchanged. A cheap-tier request with no output limit gets `cheapTierOutputCap` (`MARINA_MODEL_TIER_CHEAP_MAX_OUTPUT_TOKENS`, default 16384, `src/net/model/tier-output-cap.ts`) — never over a caller's own `max_tokens`, never on an explicit id or the strong tier; a length stop under it is traced `outputCapHit`. `admin models` is the readout.
## Agent Cognitive Architecture
Agents are participants, not assistants: they run continuously and act on their own goals ("You think, therefore you are here"; write for whoever comes next); 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: the system prompt frame has two tiers by model context window — `lean` (≤ 32k tokens: `LEAN_SYSTEM_PROMPT_BYTE_CAP` 5,800 B, `minimal` tools unless the profile is explicit) and `full` (`FULL_SYSTEM_PROMPT_BYTE_CAP` 9,000 B); `marina_code`/`think` are on demand (resident while a coding task is bound); the roster never repeats a command TOOL ROUTING or MEMORY names; 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`. Perception attention (keep/drop, priority, owed reply, wake band) is decided only by `assessPerception` in `src/agent/attention.ts` — pure; the adapter performs the side effects, and `test/fixtures/attention-corpus.json` pins the behaviour. `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**: 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**:
- 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.
- Compaction is incremental: earlier summaries are kept verbatim (never re-summarized) and every summary carries a `Pointers:` line (URLs, typed ids, paths).
- **Cache-aware idle (`src/agent/prompt-cache.ts`)**: a prompt after the provider's cache TTL (`MARINA_PROMPT_CACHE_TTL`, 5 min) is cold; a cold history ≥ target + 8k is compacted first (`MARINA_AGENT_COLD_COMPACT`, on). Direct Anthropic requests mark tools + system `ttl: 1h` (`MARINA_PROMPT_CACHE_STATIC_TTL`, on); keep-alive pings are opt-in (`MARINA_AGENT_CACHE_KEEPALIVE`, off). Keep timestamps, counters and anything room-specific out of the system prompt and tool schemas (the `This room:` line rides per turn — `getRoomCommandsLine`; the prompt is byte-identical across rooms).
- **Models that reason unasked + output repair** (`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**: `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**: 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. `DurableResidentMemory` checkpoint-only saves reuse the remembered checkpoint (the server CAS guards it, a `version_conflict` re-reads once); a journal entry or archive is ONE `capture_checkpoint` request — parts, manifest and checkpoint in one transaction whose expected version is checked before anything is stored, so nothing is captured after a forget (oversized ones take the read-first path) and skip a save that changes only `timestamp`; the adapter's preference reads back off 50 → 400 cycles while unchanged. Dead memory-assistance jobs (deadline passed, or worker account with no running agent/connection past a 15 min grace and no live lease) go to state `expired` hourly with one `assistance.expired.<reason>` event each — never deleted; `ops assistance [expire|restore <job>]` (`src/engine/assistance-expiry.ts`). `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.
- **Memory over media** (`src/engine/media/asset-memory.ts`, `MARINA_MEDIA_MEMORY=off|text|vision`, default text, test preload off): asset text (caption/alt/tags/prompt, `pdftotext`, `metadata.transcript|ocr|caption_suggestion` written ONLY via `setAssetText`) becomes `asset-text` records in the asset OWNER's resident space (subject `asset:<id>`, untrusted, never a shared pool, never request images or derivatives); edits revise, removals/deletions retire (delete trigger queue, migration 169); the pass is incremental (durable `(updated_at,id)` watermark over `idx_assets_updated`, state in `core_memory` owner `media:memory`; any `updated_at` bump re-indexes), and every read rechecks `mayReadAsset` (`asset-memory-access.ts`). `asset:<id>` is an evidence ref. Vision = one call per image through the model proxy (spend once, cap respected), never over a person's caption.
- **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 or unarmed waits; no model provider, the daily spend cap, or a TRANSIENT writer/judge failure (`src/learning/unavailable.ts`: 429/5xx/timeout/transport/spend cap) waits with no attempt spent and pauses the rest of that pass; only a repeating no-verdict (no rule written, a refusal, a malformed judge reply — `invalid_response`) spends attempts and ends `failed` with a reason naming the kind; each pass re-opens a bounded number of `failed` rows whose reason was a transient cause (`reopenTransientFailures`); forecasts, arena rounds and `research:ask` teach only in aggregate (`AGGREGATE_MIN_OUTCOMES` of one scope/source/family/board wait as `pending` `aggregating:` rows, then ONE family-scoped lesson; a group of ≥ `AGGREGATE_STALE_MIN_OUTCOMES` whose oldest waited `AGGREGATE_STALE_MS` is learned anyway with its true N; a pass claims a group atomically (`claimed:` reason) before its model call, released if no lesson is written); each delivery pass first backfills missing delivery rows per `consumersFor` (bounded, idempotent, never touching an existing row); history names its `formation` from a `method:formation:<f>` ref; a candidate with no rule is never judged or stored, the judge's bars are the same for every engine (`leak_free` enforced only for measurement; only-`consistent` failures are stored contested with a resolve case), and recall serves only trusted lessons with a rule (never-judged `unverified` records stay stored, unserved); 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. Residents' own calls (a market room's `predict`, `conduct track`) file through `fileResidentForecast` on their Sample (a newer call settles the open one `superseded`); they teach `lessons` and earn standing but never write forecast history (`RESIDENT_FORECAST_SOURCES`).
- **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
The formation catalogue (`src/orchestration/formations/catalogue.ts`) is the ONE list of formation names, aliases, descriptions, fit, roles and protocols, run four ways: a crew (`crew create … formation:<f>`), a project, one `pattern run` task, a room's turn rule; every surface resolves names through `canonicalFormationName` and help lists catalogue names only (`nsed` → `deliberation`, `custom` → `freeform` are accepted, never advertised). As projects, all 16 patterns are convention-based pool notes discovered via `recall`, not engine constraints (`src/world/templates/orchestration.ts` generates each template's opening note from the catalogue entry; activated via `project <name> orchestrate <pattern>`). 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. A room may run a formation too (`RoomModule.formation`, `build formation [room] debate|chorus|verification|off`, `src/coordination/room-formations.ts`): after each `say` one line in the room names the next actor with `@name`, so attention owes and wakes it; a round ends on its own closing move — no timers, no caps. → docs/architecture/orchestration.md
- **The formation catalogue** (`src/orchestration/formations/`): ONE entry per formation — its one description (`PATTERN_FIT.why` reads it), its Score templates (typed forecasts, free-text `pattern run`, arena numeric) with abstract slots resolved at run time (`model:`/`role:`/`marina:`/pipeline), pure combiners per answer type, and the protocol phrases its crew brief and project template must keep. A new formation or surface goes through the catalogue, never a fifth copy. The catalogue is held by BEHAVIOUR, not frozen bytes: `test/formation-behaviour.test.ts` (determinism given the same replies, step order, robustness to failed/junk replies, combiner math, the arena move cap) and `test/formation-catalogue.test.ts` (drift between the catalogue and every surface). Prompts and answers may change; a change meant to improve answers is measured (benchmarks, resolved outcomes), not pinned. Arena: only `ensemble`/`delphi` run from the catalogue; the other ten keep their own code, held byte-identical to `test/fixtures/arena-formations-legacy.ts` by `test/arena-formations-legacy.test.ts` until they move — never edit that fixture to make it pass.
- **The Arbiter** (`src/orchestration/arbiter.ts`): adjudication only where answers disagree, never always-on. Contested = free signals (run agreement by answer type, verifier/critic dissent, judge spread), capped (`MARINA_ARBITER_CAP`). Verdict keep / overrule / abstain with citations per answer item and source/reasoning quality. Overruling a majority needs grounded citations from ≥ 2 independent registrable domains AND confidence ≥ 0.7, else it is downgraded and the majority stands. Earned trust = overrule precision from resolved forecast history (`ResolvedRecord.arbiter`, booleans only). Surfaces: `MARINA_FORECAST_ARBITER=off|observe|on|replace` (a small hook in `typed.ts`, adapter `src/forecast/arbiter.ts`), `MARINA_VERIFY_ARBITER` / `x-marina-arbiter` (final text answers only; tool-call drafts unchanged), the seeded `arbiter` role and the read-only `arbiter` command. Every `off` is byte-identical, prompts included. Replay with no model calls: `benchmarks/transfer/arbiter-replay.ts`.
## 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**: `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, and every live order (`position open|confirm|close`) passes the operator-grant `trading.live` gate — without a grant it is held for a person's approval (never an agent's; `open` and the local profile don't lift it, `sovereign` does); a market order is costed at 99¢ for the cap; the daily-loss floor sums world-wide realized losses against the opener's floor. → 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` (the arena crew too — it keeps no lesson notes of its own); 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.
- `src/quant/` holds the shared numeric helpers: normal primitives, `fitSeries` (persistence/EWMA/blend/trend/damped/combo/auto with a rolling-origin sd) and `bucketProbabilities` (normal or quantiles → interval probabilities). They are pure and model-free, and agents reach them through rank-0 `quant fit` / `quant buckets`. The arena baselines (`forecastScalar`, poll-level, `erf`, `normalQuantile`) run on them; `test/quant-arena-equivalence.test.ts` must stay bit-identical against the frozen `test/fixtures/arena-baseline-legacy.ts` (never edit the fixture to make it pass). `MARINA_FORECAST_QUANT` (`src/forecast/quant.ts`, default `off`) wires them into typed forecasts. `off` must stay byte-identical, prompts included, and `observe` only adds `answer.quant`. The `quantitative-estimates` skill ships built in (`skill builtin`), with no seeding.
- Shadow challengers (`src/forecast/shadow.ts`, `MARINA_FORECAST_SHADOW`, default `off`) run ONE `explore` card beside a sampled FILED live forecast, filed `eval_mode = live` under `marina:shadow` (tags `shadow`/`method:`/`card:`/`model:`/`family:`/`q:`|`pair:`) and settled with the live answer inside `resolveForecast` (FutureX: filed on standing-answer adoption, settled at `futurex learn`). They teach lessons but never write history (no correlated second record). They never change the live answer, never run for a measurement filing, are capped per UTC day, and a past cutoff gets backtest isolation. Arena routing reads the same outcome rows (`MARINA_ARENA_ROUTE_OUTCOMES`) without counting a round twice; `forecast routes` is the read-only view of both.
- Intake triage (`src/forecast/triage.ts`, `MARINA_FORECAST_TRIAGE`, default `off`) classifies lookup / anchor / list / forecast. CODE derives the class from facts and NOW = the cutoff (never the wall clock in a backtest, never a model's verdict); a figure published after the cutoff is never a lookup (`test/forecast-triage.test.ts` holds both). The only hook is `forecastFormed` → `forecastTriaged` when the deps carry `triage`; `off` stays byte-identical and `observe` only adds `answer.triage`. A lookup's read-off answers only on mechanically checked verified evidence, else the full panel runs. Agents reach it via rank-0 `triage` and the built-in `intake-triage` skill.
- **Residents as forecasters** (`MARINA_FORECAST_RESIDENTS=off|on`, default off — every `off` path byte-identical): `ResidentPart` (`src/forecast/resident-part.ts`) is a `ModelPart` answered by a resident — `marina:<crew>` via `routeToChannel`, `role:<r>` by standing, pinned — in-process in the server (`src/net/model-api/resident-dispatch.ts`), else `marina:` over HTTP with `x-marina-asof`/`x-marina-deadline-ms`, reading `x-marina-respondent`. `forecast <q> crew:|role: pattern:ensemble|delphi|tournament|skeptic` (and the API's `pattern`/`crew`/`role`) is a task (value 0) run as a Score (`src/forecast/resident-forecast.ts`: the formation's catalogue template — step 0 the pipeline's research, K resident steps (a slot may also be `model:<id>`, `assigneePart`), pure-code combine; `WorkBudget` + `SpendGuard` per forecast) and files each resident's run as its own answer tagged `q:`/`method:`/`model:`/`resident:` (no eval mode when live — any eval mode never earns). With resident parts a past cutoff (always, when measuring) runs under the as-of fence; a measured part refuses without it.
→ 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; a stale anchor on ANY open round); readiness `arena-filing`. It never files.
- Anchor guard (`src/arena/anchor.ts`, `MARINA_ARENA_ANCHOR_GUARD`): before signing, a forecast's anchor is compared with the arena's latest published value (site data persistence, rounding-tolerant, per cell); stale → re-read inputs once, then re-anchor and record attempt `stale_anchor`. `submit due --replace` refiles every open round whose accepted filing is stale, whatever its lock.
- The proposal judge (`proposal-judge.ts`, delphi/symbiosis with verified lines) can only LOWER trust (× mean judged support) and re-weight the median; it fails OPEN to the unjudged aggregation on any judge error and records `forecast.judge`.
- 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 `on` — applies the pick when eligible, else the router's choice stands; `observe` only records) 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`), `src/model` library calls / the web retriever. Every `src/model` call (`modelComplete`, `modelTurns`, one core `modelCall`) names a purpose (`src/model/purpose.ts`): recorded under the purpose's source plus a `purpose:<name>` row in `spend_scope_daily` (a breakdown, never a cap; `MARINA_SPEND_SCOPE` may not use the prefix); a loopback `marina/*` call is left to the passthru (never counted twice); transient failures retry (`MARINA_MODEL_RETRIES`, default 1); each call emits `model_request_lifecycle` with `routeKind: "direct"`. Model calls import `src/model`; research retrieval imports `src/research`. `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_LEDGER=<path>` makes every process naming that file share ONE total and cap (`shared-spend-ledger.ts`; world DBs still recorded; children become scope `child:<name>`); `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`). The main listener validates `Host` (`src/net/host-allowlist.ts`): loopback bind or `local` profile ⇒ loopback names + `MARINA_ALLOWED_HOSTS` only (421), and same-origin counts only for an allowed Host; `/mcp` keeps `MARINA_MCP_ALLOWED_HOSTS`. 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* and is REQUIRED for inbound federation on `shared`/`public` (`gatewayIngressRefusal`: no secret ⇒ every `gateway_auth`/`Gateway_` login refused, boot log + readiness `federation-inbound` name the fix; `local` accepts peers without one) — 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. Boot exposure checks (`src/net/bind-safety.ts`): inside a container a wildcard bind counts as loopback only when `MARINA_PUBLISHED_HOST` (compose passes `${MARINA_BIND_IP:-127.0.0.1}`) is loopback — never for the trust profile; `docker-compose.yml` must not set `MARINA_ALLOW_INSECURE_PUBLIC=true` (`test/bind-safety.test.ts`). 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** (`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 it raised too many false alarms on replayed traffic; 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** 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` — for SHORT jobs only: a job with `every >= DURABLE_MIN_EVERY` (600 ticks) is durable on wall clock (last run start persisted in `core_memory` owner `tick:schedule`; after a boot an overdue or never-run job is due within `DURABLE_SPREAD` 300 ticks at an offset scaled from its phase, a recent one waits the remainder, then `tick + every`), because the tick counter restarts at 0 on every boot. `register()` ENFORCES the distinct-phase invariant (two jobs sharing an interval may not share a phase); `describeTickSchedule()` reports name/every/phase/runs/lastError/durable/lastRunAt/nextDueTick for operators. → docs/architecture/persistence.md 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)
- `src/orchestration/formations/` — THE formation catalogue: one entry per formation (summary, Score templates for typed / text / numeric surfaces, crew-only reason), the template grammar (`formationScore`, `knockout`), pure combiners (`combine-typed.ts`, `combine-numeric.ts`) and `runFormationScore`
- `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 (an imported bundle's active `upstream_default_seeds` row, only for a slot with no local row) > 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>`; `role-model:<role>` slots take a live-evidence candidate instead of a run, see Model tiers). → 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. A crew target always gets a key; a bare-model control with no arm and no cap keeps the internal token. → 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 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/quant.ts` — rank-0 `quant fit` (series → mean/sd/quantiles) and `quant buckets` (distribution → range probabilities, most likely range) over `src/quant/`; `calc` stays the arithmetic engine. Skill: `seeds/skills/quantitative-estimates.md`.
- `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-registry.ts` — the ONE benchmark registry (21 entries: smoke, the academic sets incl. aime-2025, gpqa, hle-verified-gold[-mm], and the phase-B narrativeqa, mt-bench, retention). The harness (`benchmarks/registry.ts` adds loaders), the in-world `BENCHMARKS` (`src/engine/benchmark-runner.ts`) and the web UI (`benchmarkCatalog()`) derive from it; never add a second list (`test/benchmark-registry.test.ts`). The runner passes the harness `--endpoint` and its key in env, not argv; the harness, `benchmark fetch` downloader and kit run under the server's own Bun with `--no-env-file` in `MARINA_ROOT` (`bunScriptArgv`, `src/runtime-paths.ts`) — never `bun` from PATH or the checkout `.env`, so a fresh install from any cwd works (`test/benchmark-fresh-install.test.ts`); an all-error run is `failed`, never a 0% score. Voice-friendly multi-word resolution via `resolveMultiWordName`.
- `src/engine/benchmark-target.ts` + `benchmark-judge.ts` — in-world `benchmark run` measures RESIDENTS: `crew:<name>`, or with no target the world's one serving crew (refused when none or several, never a silent bare-model run); `model:<id>` is a bare-model control (`targetKind: "model"`, `control: true`). Crew runs always get a run key and file as `crew` targets with trace-resolved participants. A judge-scored run is never graded by its target: `judge:` / `--judge-model`, else `MARINA_BENCHMARK_JUDGE_MODEL`, else `marina/default`; same-model judging only with `self-judge:on` / `--self-judge`, recorded (ledger judge `[self-judge]`). → docs/architecture/benchmarks.md
- `src/engine/benchmark-reference-scores.ts` (re-exported by `benchmarks/reference-scores.ts`) — frontier-model reference scores seeded for leaderboard comparison.
- `benchmarks/run-version.ts` + `benchmarks/transfer/versions.ts` — test the right versions: a new result writer stamps its output through `stamped()` (`marina: {commit, dirty, describe}`; ledger runs keep it in `config_json.marina`, outside both hashes). `eval:transfer` counts a suite only when both arms share one clean commit (or match a declared `versions` comparison); mixed/unstamped/dirty → `version-mismatch`/`version-unknown`, reported, never counted; `--allow-unversioned` is the labelled escape hatch. → benchmarks/README.md#transfer-report
- `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`). The server can run it in-process (`src/routing/world-supervisor.ts`, `engine.worldSupervisor`, command `supervisor start|stop|status`): a PERSON starts it (agents refused; `agent.spawn`; no telnet) in a configured code root, it needs supervised residents, holds a supervision credential the server issues to that person (renew/revoke as the CLI), calls its own routing API over loopback, gives native agents an allowlisted env (no server keys), stops before the listener drains, and resumes on boot (`MARINA_SUPERVISOR_AUTOSTART=resume|off|person:<name>`) until an explicit stop. Runtime controls use existing `code.exec`/`agent.spawn` gates; generic envelopes never execute as controls. `MARINA_SUPERVISED_RESIDENTS` (default on; `off` restores the earlier routing; a supervisor whose server offers no supervision endpoint falls back; readiness `supervised-residents`; `src/routing/residents.ts`, migration 173): each launched agent is its own world account (`agent_configs.runtime='supervised'`, `spawned_by` = the launching person, ≤ 24 h workload credential renewed/revoked by the supervisor); the child holds no credential — `MARINA_RELAY_SOCKET` (`src/routing/resident-relay.ts`, 0600 socket in a 0700 dir) runs its `marina connect -c` commands on the resident's held connection (a sandbox that refuses sockets uses the relay dir's `spool/` request/answer files; the Codex adapter grants that dir alone as a writable root, network stays off); never auto-respawned; `agent stop` sends a stop control. The supervisor runs on its own routing-only supervision credential (`src/routing/supervision-credential.ts`, audience `marina:routing`, a person only, ≤ 24 h, renewed at 80 %, revoked on stop), so the person's next login never cuts it off; it is accepted only under `/api/routing/` (never a world session or approver) and cannot send controls, messages or channel posts. The attention bridge (`src/routing/attention-bridge.ts`, migration 174 `attention_wakes`) runs each resident's perceptions through `assessPerception` and wakes/steers its native session with a `[World Events]` digest (Codex `turn/steer`, pi `steer`/`follow_up`, Claude next-turn); a blocked session is never woken; a woken turn's final reply is relayed to what it owed (`re #<id>`/`say`/`channel send`, with the message as `cause` — a reply with a cause is never owed), `relay:explicit` via `agent attention-mode` turns it off. Attention judges (`src/agent/attention-judge.ts`, `MARINA_ATTENTION_JUDGE=off|observe|on`, default observe): wake value, converge (depth ≥ 3), review sycophancy and review value via `src/decisions`; each opinion is a judged outcome `judged:<subject>` (kind `request`, evidence only) checked against a mechanical outcome (the woken turn did work — the supervisor reports it; no person stepped in within 15 min; the person's close) so `judgeAgreement`/`earnedJudge` measure each; `on` lets only an earned judge advise (attention counters, a `code review <id>` line), never block/close/approve; `off` makes no calls. A WebSocket command's `cause` is accepted only for a ref delivered to that entity (`src/engine/delivered-messages.ts`). 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/marina.ts` — the `marina` bin (dispatcher; shebang `bun --no-env-file`). Settings come from `scripts/cli-env.ts`: shell > instance `.env` (`~/.marina/projects/<slug>/` for a folder session, cwd for `start`) > `~/.marina/.env` > checkout `.env`; the project's `.env` is never read, and an explicit `bun --env-file` replaces them all. `scripts/setup-cli.ts` links the bin into the first of `~/.bun/bin`/`~/.local/bin` already on PATH.
- `scripts/connect.ts` — `marina connect`: 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
- `scripts/connect-repl.ts` + `scripts/term-*.ts` — the interactive `marina connect` terminal.
- `term-caps`: NO_COLOR / FORCE_COLOR / `--color`, colour depth, Unicode.
- `term-theme`: palettes and tokens; the only place raw colours live.
- `term-sanitize`: every server byte passes `sanitizeServerText`, which keeps vetted SGR only and drops OSC, cursor moves and hidden text.
- `term-render`: `⎿` / `●` / `◆` gutters and Markdown.
- `term-repl`: the bottom input area, collapse and history.
- `term-status` / `term-prompt` / `term-panels`: live dashboard data with the session's own bearer token, starship-style `$segment` formats and `/panels`.
- Pipe and one-shot output to a non-TTY stays plain and byte-identical to the old formatter.
- `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.