run-marina · git:20261030.ef35195 · 2026-10-30 · sha256 91b2cb99fc16c263

run-marina git:20261030.ef35195A

Immutable. This exact content never changes and is served at /api/v1/blob/91b2cb99fc16c263.

---
name: run-marina
description: Build, run, and drive the Marina server on an isolated port. Use when asked to start Marina, run a world command against a live instance, open or screenshot the dashboard, hit the dashboard API, smoke-test a change end to end, or run engine code in-process without a server.
---

Marina is a Bun server (WebSocket/HTTP on one port, MCP on +1, log viewer on +2) with a
built React dashboard. Drive it through `.claude/skills/run-marina/driver.mjs`: it boots an
**isolated** instance (port 3400, its own SQLite DB under `$TMPDIR/marina-run-3400`, no
provider keys), runs world commands over the WebSocket, logs into the dashboard with headless
Chromium (Playwright, no xvfb), screenshots it, and reads the dashboard REST API.

All paths are relative to the repo root.

## Prerequisites

Bun ≥ 1.4.4 (`.bun-version`; older Bun fails in ways that look like Marina bugs), Node (the
driver runs under `node`), and the Playwright Chromium that `playwright-core` expects in
`~/.cache/ms-playwright`. Check which build it wants:

```bash
bun --version
node node_modules/playwright-core/cli.js install --dry-run chromium
```

If the install location it names is empty, run the same command without `--dry-run` (it
downloads Chrome for Testing into `~/.cache/ms-playwright`).

## Setup and build

```bash
bun install                       # one root install covers every workspace member
cd dashboard && bun run build     # Vite → dist/dashboard (git-ignored; the server serves a "not built" page without it)
```

If a server is already serving `dist/dashboard`, build elsewhere and swap it in afterwards
(`bun run build -- --outDir <dir>`): Vite empties the output directory first.

## Run (agent path) — the driver

```bash
node .claude/skills/run-marina/driver.mjs smoke
```

`smoke` boots the instance (or reuses it), runs `look` over the WebSocket, logs into the
dashboard as `driver`, types `look` in Web Chat, screenshots `<run dir>/smoke.png`, and reads
`/api/ops/overview` with the session token. It prints one `ok`/`FAIL` line per check and
exits non-zero on failure. The verbs it is built from:

```bash
D=.claude/skills/run-marina/driver.mjs
node $D up                                   # boot; waits for /health; prints URLs, pid, log path
node $D status                               # /health + `marina status --port 3400`
node $D cmd "note add driver smoke test note" --wait 2   # one world command, printed, done
node $D cmd "recall smoke" --wait 2
node $D ui --cmd "look" --text 300           # dashboard login + command + screenshot (+ body text); --out <file.png>
node $D cmd readiness                        # the full capability report (what is off and why)
node $D api /api/ops/overview --limit 500    # GET with the cached session token (logs in first if none)
node $D down                                 # SIGTERM by pid (add --clean to delete the run dir and DB)
```

Knobs (env): `MARINA_RUN_PORT` (3400), `MARINA_RUN_DIR`, `MARINA_RUN_NAME` (`driver`),
`MARINA_RUN_ENV_FILE` — an env file with provider keys when the test needs agents or
model-backed commands; by default the server starts with `--no-env-file` and a minimal
environment, so nothing from the checkout `.env` leaks in.

Screenshots and the server log land in the run dir (`$TMPDIR/marina-run-3400` unless
`MARINA_RUN_DIR` is set); `up` prints the log path, and a failed `up` prints its tail.

## Direct invocation (no server)

Most PRs touch `src/engine`, `src/persistence` or a command; those need no listener. The
test fixture boots a whole engine in-process with an in-memory DB:

```bash
bun --no-env-file .claude/skills/run-marina/engine-probe.ts look "help memory"
```

It logs in as `probe`, runs each argument through `engine.processCommand`, and prints what the
connection received (`connection.messages`, ANSI stripped). Copy the pattern into a one-off
script for a specific change; the same fixture (`test/engine-fixture.ts`, always `await
dispose()`) backs the test suite. Note it runs the fixture's plain Test Room, not the seeded
Workbench world — use `up` + `cmd` for anything that depends on world seeds.

## Run (human path)

`bun run start` reads the checkout `.env` and listens on 3300–3302 (plus telnet when
`TELNET_PORT` is set) until Ctrl-C. If a Marina is already running from that checkout, this
collides with it; the driver's 3400 does not.

## Test

```bash
bun run test:fast                 # engine-free files, ~10 s
bun test test/arena-freshness.test.ts   # one file
```

## Gotchas

- **Ports follow `WS_PORT`.** MCP = `WS_PORT+1`, log viewer = `WS_PORT+2`; a second instance
  needs only `WS_PORT`. A developer's own Marina usually holds the defaults (3300–3302, and
  a telnet port) — never reuse 3300, and stop instances by pid, never `pkill -f bun`, which
  would also kill that server.
- **The checkout `.env` must not be read** by a test instance: it may set ports that collide
  with a running server, provider keys, and spend scopes. The driver launches with
  `bun --no-env-file` and an explicit minimal environment.
- **Dashboard REST is 401 until a session exists.** The local model-API key the server
  writes next to the DB is *not* a dashboard session token (it answers "Invalid or expired
  session token"). The token is minted on Web Chat login and kept in
  `localStorage.marina_chat_token`; the driver copies it to `<run dir>/token` after `ui`/`smoke`
  and `api` uses it. It dies with the process (`down` deletes it).
- **`marina status --port` shows readiness only with a session** (`MARINA_TOKEN`); without
  one it prints health and says so. `node $D cmd readiness` gives the full report.
- **No provider key ⇒ no agents.** `status` shows "LLM provider configuration: off"; `agent
  spawn` is refused and `/v1` answers 503. That is the intended default for a smoke run; pass
  `MARINA_RUN_ENV_FILE=<file with ANTHROPIC_API_KEY=…>` to test anything model-backed.
- **`connect -c` lingers** until output has been quiet for 2.5 s after the first output,
  capped by `--wait`; model-backed commands (`ask`) need a longer `--wait`.
- **Headless Chromium runs without xvfb** (`playwright-core` 1.64.1, `--disable-gpu`). The
  first dashboard load logs a burst of 401 console errors before login — expected, not a bug.
- **Login selectors:** name box placeholder `Enter your name...`, button `Connect`, then the
  command box placeholder `Type a command...`. The driver waits on the latter to know login
  finished.
- **`.gitignore` ignores `.claude/*` but re-includes `.claude/skills/`** — keep it that way;
  git cannot re-include a file under an ignored directory, so a plain `.claude/` rule would
  drop this skill from the repo.

## Troubleshooting

- `up` says `already up` but it is not your instance — something else answers
  `127.0.0.1:3400/health`; set `MARINA_RUN_PORT=3410` (every listener follows it).
- `Invalid or expired session token` from `api` after a restart — the token file outlived the
  process: `rm <run dir>/token` (or `down` next time), then `api` logs in again.
- `Activity unavailable` in the dashboard screenshot — not logged in; the name box was still
  showing. Re-run `ui`; if it persists, check `dist/dashboard` is the current build.