How to run, develop, and deploy Bobbit. For project architecture and concepts see README.md. For agent-facing context (repo layout, key abstractions, common tasks) see AGENTS.md.
Bobbit has three runtime modes: production, dev, and dev with harness. The difference is how server-side TypeScript is compiled and how the UI is served.
npm run build # compile server + bundle UI
npm start # serve everything from dist/The gateway serves the bundled UI from dist/ui/ as static files. Everything runs from a single process (plus agent child processes). No hot reload — you must rebuild and restart to pick up changes.
npm run build:server # required once before first run
npm run dev # gateway + vite dev serverRuns two processes concurrently:
- Gateway (
node dist/server/cli.js --cwd . --no-ui) on port 3001 — handles REST API, WebSocket, and agent subprocesses. - Vite on port 5173 — serves the UI with hot module replacement. Proxies
/apiand/wsto the gateway.
UI changes (src/ui/, src/app/) hot-reload instantly in the browser. Server changes (src/server/) require manually rebuilding (npm run build:server) and restarting the gateway.
npm run dev:harnessSame two-process setup, but the gateway is wrapped in a restart harness (src/server/harness.ts). The harness:
- Watches a sentinel file at
.bobbit/state/gateway-restart - On signal: kills the server, waits for the port to free, runs
npm run build:server, relaunches - Auto-restarts on unexpected crashes
- Sessions survive restarts (persisted to
.bobbit/state/sessions.json)
To trigger a restart:
npm run restart-serverThis touches the sentinel file. The harness picks it up within ~500ms (polled on Windows, fs.watch elsewhere) and begins the restart cycle.
| What you changed | What to do |
|---|---|
src/ui/** or src/app/** |
Nothing — Vite hot-reloads automatically |
src/server/** |
Run npm run restart-server (if using harness) or manually npm run build:server + restart |
package.json (new dependency) |
npm install, then restart server |
vite.config.ts |
Restart Vite (kill and re-run npm run dev:harness) |
tsconfig.*.json |
Restart server; may need to restart Vite for web config changes |
.bobbit/config/system-prompt.md |
Restart server (the path is resolved at startup and passed to agents) |
Rule of thumb: UI is hot. Server is compiled. If you touched anything under src/server/, you need a rebuild + restart.
If you are an AI agent running inside a Bobbit session and you are modifying Bobbit itself:
Edit files under src/ui/ or src/app/. Vite picks up the changes and hot-reloads the browser. The user sees updates within seconds. No restart, no build command.
After editing files under src/server/:
npm run restart-serverThis signals the harness to rebuild and restart the server. Your current session will survive — the harness persists session metadata to disk, and on relaunch the server restores all sessions from .bobbit/state/sessions.json.
Do not skip this step. The gateway runs from compiled JavaScript in dist/server/. Your TypeScript edits under src/server/ have no effect until the server is rebuilt.
After npm run restart-server, watch for the harness output:
[harness] ======== RESTART TRIGGERED ========
[harness] Waiting for port 3001 to be free...
[harness] Building server...
[harness] Build complete.
[harness] Launching server (port 3001)...
If the build fails, the harness logs the error and attempts to launch the old build anyway. Fix the compilation error and run npm run restart-server again.
To check both server and UI types without emitting or restarting:
npm run checkThis runs tsc --noEmit against both tsconfig.server.json and tsconfig.web.json. Useful to catch errors before triggering a restart.
New files under src/server/ are automatically picked up by the next npm run build:server (triggered by the harness). No extra configuration needed — the TypeScript config includes all .ts files under src/server/.
New UI files need to be imported somewhere in the dependency graph (from src/app/main.ts or an existing component). Vite handles the rest.
dist/
├── server/ # tsc output from src/server/ (Node16 ESM)
│ ├── cli.js # gateway entry point
│ ├── harness.js # dev server harness
│ └── agent/ # session manager, RPC bridge, stores
└── ui/ # vite bundle from src/ui/ + src/app/
├── index.html # SPA entry
└── assets/ # JS, CSS, fonts
Two independent build pipelines:
- Server:
tsc -p tsconfig.server.json→dist/server/. Plain TypeScript compilation, no bundling. - UI:
vite build→dist/ui/. Bundles, minifies, tree-shakes. In dev mode, Vite serves directly from source with HMR.
Bobbit is designed for remote access over a NordVPN mesh network. The user runs the server on a dev machine and connects from other devices (laptop, tablet) via a mesh IP or a custom domain.
Browser (ProArt / phone / etc.)
│
│ https://yourname.dedyn.io:5173 ← user-facing URL
│
▼
Vite dev server (:5173) ← serves UI with HMR, HTTPS using gateway cert
│ proxy /api/* ──────────────► Gateway (:3001) ← REST API + agent management
│ proxy /ws/* ──────────────► Gateway (:3001) ← WebSocket (session streaming)
│
└─ HMR websocket (:5173) ← Vite's own hot-reload channel (same port)
In production mode (npm start), there is no Vite — the gateway serves the bundled UI directly on port 3001.
Both the gateway and Vite auto-detect the NordLynx (NordVPN mesh) interface IP and bind to it.
- Gateway: exits with an error if NordLynx isn't found, unless you pass
--host <addr> - Vite: falls back to
localhostwith a warning, or usesVITE_HOSTenv var
The detected mesh IP (e.g. <mesh-ip>) is what other mesh devices use to reach the server.
On startup, the gateway updates a deSEC (dedyn.io) DNS A record so that yourname.dedyn.io points to the current mesh IP. Config lives at .bobbit/state/desec.json:
{ "domain": "yourname.dedyn.io", "token": "<deSEC API token>" }This means the user can always access https://yourname.dedyn.io:5173 (dev) or https://yourname.dedyn.io:3001 (prod) without memorizing mesh IPs, even when the IP changes across NordVPN reconnects.
Important: The deSEC update is skipped for loopback addresses (127.0.0.1, ::1, localhost) to prevent E2E tests or local-only runs from clobbering the DNS record. If DNS points to 127.0.0.1, a prior server start with --host 127.0.0.1 likely caused it — restart the server normally (without --host) to push the correct mesh IP.
TLS is on by default. The server generates certificates on first run and stores them at:
| File | Purpose |
|---|---|
.bobbit/state/tls/cert.pem |
Server certificate (covers the host IP + localhost) |
.bobbit/state/tls/key.pem |
Server private key |
.bobbit/state/tls/ca.crt |
Local CA certificate (install on other devices to trust) |
.bobbit/state/tls/ca.key |
Local CA private key |
The cert is generated via mkcert (npm package) signed by the local CA, with fallback to openssl self-signed. Vite reuses the same cert/key for its HTTPS server (vite.config.ts reads them from disk).
To trust the cert on a remote device, install .bobbit/state/tls/ca.crt as a trusted CA.
If the cert doesn't cover the current host (e.g. the mesh IP changed), it is regenerated automatically on next startup.
| Symptom | Likely cause | Fix |
|---|---|---|
ERR_CONNECTION_REFUSED on :5173 |
Vite not running, or not bound to mesh IP | Check npm run dev:harness output; verify NordVPN is connected |
ERR_CONNECTION_REFUSED on :3001 |
Gateway not running | Same as above |
| WebSocket connects but session fails | Browser has wrong gateway URL in localStorage |
Open DevTools console: localStorage.getItem("gw-url") — should match the gateway's actual address. Fix with localStorage.setItem("gw-url", "<correct URL>") and reload |
DNS resolves to 127.0.0.1 |
A prior --host 127.0.0.1 run (e.g. E2E tests) pushed loopback to deSEC |
Restart the server normally — it will push the mesh IP to deSEC. Flush DNS on the client device if cached |
| Vite HMR WebSocket error in console | Normal when accessing via domain/mesh IP — Vite's HMR can't always connect back | Harmless. Vite falls back to polling. The "Direct websocket connection fallback" message confirms this |
ERR_CERT_AUTHORITY_INVALID |
Remote device doesn't trust the local CA | Install .bobbit/state/tls/ca.crt on the device, or click through the browser warning |
# Terminal 1: gateway on localhost
node dist/server/cli.js --host localhost --port 3001 --cwd . --no-ui --no-tls
# Terminal 2: vite on localhost
GATEWAY_NO_TLS=1 VITE_HOST=localhost npx viteOr use the E2E test config which does this automatically:
npx playwright test --config playwright-e2e.config.tsnpm test # All tests (unit + E2E)
npm run test:unit # Unit tests — Node test runner + Playwright file:// fixtures
npm run test:e2e # E2E tests — API (in-process) + browser (spawned gateway)E2E tests use playwright-e2e.config.ts which defines two projects:
api(4 workers): API-only tests import fromin-process-harness.js— the gateway runs in the same Node process, eliminating child process spawn overhead. Covers HTTP/WS API tests, CRUD, agent protocol.browser(2 workers): Browser and process-level tests import fromgateway-harness.js— spawns a real gateway child process. Used for UI E2E tests (tests/e2e/ui/), MCP integration, and tests needing process-level behavior (port allocation, auth bypass).
See AGENTS.md for harness selection guidance and test recipes.
When you list git branch in a Bobbit-managed repo you'll see several namespaces:
| Prefix | Owner | Purpose |
|---|---|---|
pool/_pool-<id> |
Worktree pool | Pre-built worktrees waiting to be claimed by a session or goal. Renamed atomically on claim. (Pre-Phase 3 these used session/_pool-*; both prefixes are recognised on startup for back-compat.) |
session/<id8> |
Live regular session | A session worktree, named immediately on pool claim (no first-prompt rename). Cleaned up on session archive. See internals.md — Session worktrees and design/remove-session-worktree-rename.md. |
goal/<slug>-<id> |
Live goal | Spans every component repo in multi-repo projects. |
goal/<goalId8>/<role>-<short4> |
Team-member agent | Per-role worktree under a live goal. Created on team_spawn, cleaned up on goal archive (alongside the goal branch) or agent dismiss. Legacy goal-goal-<slug>-<id>-<role>-<short> from before the pithier-te rename is recognised by the same cleanup path. |
staff-<name>-<id> |
Staff agent worktree | Long-lived when the staff uses a worktree; rebased onto the primary branch/base ref on each wake. No-worktree staff have no staff branch. |
The boot sweeper (worktree-sweeper.ts) reconciles these against persisted state on every server start — orphaned pool entries and renamed-but-unpersisted worktrees are cleaned up automatically. See internals.md — Session worktrees for the full lifecycle.
Base ref for new worktrees. New worktrees (session, goal, staff, pool) branch off the project's configured base_ref when set, otherwise off the remote primary (origin/master/origin/main). The same value drives the {{baseBranch}} workflow variable and the aheadOfPrimary/behindPrimary git-status comparator. Some worktrees also use it as @{u} for local status, but Bobbit-owned branch publication never relies on upstream tracking: it pushes explicit destination refspecs such as <branch>:refs/heads/<branch> or HEAD:refs/heads/<branch>. {{master}} keeps resolving to the project primary independently. Team-member worktrees branch off the goal branch by hierarchical design and are not affected. Full semantics, validation rules, and error inventory: design/base-ref.md.
- README.md — Architecture overview, quick start, CLI flags
- REST API — Full REST API reference
- Security Model — Auth, TLS, and security details
- Networking — Bind addresses, TLS, deSEC, QR codes
- Bundle profile workflow — Diagnose UI bundle-size regressions; budget guard at
tests/bundle-size.test.ts - AGENTS.md — Agent context: repo layout, key concepts, common tasks, debugging tips