---
name: dev-workflow
description: Development workflow for everything-dev projects using bos dev, bos start, and the Module Federation runtime. Use when starting dev servers, debugging hot reload, or understanding the service-descriptor architecture.
metadata:
  sources: "packages/everything-dev/src/service-descriptor.ts,packages/everything-dev/src/orchestrator.ts,packages/everything-dev/src/dev-logs.ts,packages/everything-dev/src/dev-session.ts,packages/everything-dev/src/process-registry.ts,packages/everything-dev/src/app.ts,packages/everything-dev/src/infra/preflight.ts,packages/everything-dev/src/dev-program.ts"
---

> **Config form:** the authored config is `bos.app.ts` (canonical, preferred when both exist). A legacy `bos.config.json` is still supported for older children. Where this doc says `bos.config.json` for the *local authored file*, read "the authored config". The published artifact on FastKV keeps the key name `bos.config.json`.

# everything-dev Development Workflow

## Starting Development

```bash
# Typical: start development (host mode auto-detected)
bos dev

# Isolate work
bos dev --api remote     # UI only
bos dev --ui remote      # API only
bos dev                  # Full local (rarely needed)

# Pin individual service ports (explicitly-passed flags are pinned; unset services derive from the base)
bos dev --port 3100 --api-port 3101 --ui-port 3103 --auth-port 3102 --plugin-port-start 3110
```

### Port persistence (ADR 0012)

Port allocation is atomic block allocation: the layout derives deterministically from one base
(`--port N` → api N+1, auth N+2, ui N+3, plugins N+10+), and the whole block is validated before
anything spawns. Only explicitly-passed port flags persist to `.bos/infra-state.json` under
`devPorts` — never drift. If the preferred block is occupied, the session takes the next block
(+100) with a prominent notice naming the holders (live sibling sessions via registry claims;
foreign processes via an lsof ownership probe), and restarts re-try the preferred base.
Explicitly-passed flags are pinned: an occupied explicit port fails loudly instead of silently
moving. `bos kill` escalates SIGTERM → 5s → SIGKILL, reaps orphaned children of dead sessions,
and verifies ports actually freed; new sessions adopt-and-reap orphaned children from dead
same-project sessions at boot. Test-spawned stacks never persist ports (`BOS_NO_PERSIST_PORTS=1`,
plus `NODE_ENV=test` / `BOS_TEST=1` are honored).

### Port budgets (forward-compat for `--workspaces`)

The allocator accepts an optional `portBudget: { min, max }`. When set,
`pickAvailablePort` skips candidates outside the budget and throws `PortAllocationError` if no
free port is found inside it. The standalone `bos dev` path does not pass a budget today; a future
`bos dev --workspaces` orchestrator will slice disjoint budgets per child project.

## Port Assignments

| Service | Default | URL |
|---------|---------|-----|
| host | 3000 | http://localhost:3000 |
| api | 3001 | http://localhost:3001 |
| auth | 3002 | http://localhost:3002 |
| ui | 3003 | http://localhost:3003 |
| plugins | 3010+ | http://localhost:3010+ (incremental — one per plugin in config order) |

## Service-Descriptor Architecture

The orchestrator builds a `ServiceDescriptorMap` from the resolved config. Each descriptor defines:
- `key` — service identifier (host, ui, api, auth, plugin:*)
- `source` — `"local"` or `"remote"` (determines if process is spawned or URL is probed)
- `port` / `defaultPort` — TCP port for local services
- `readinessPath` — HTTP path for readiness probes (e.g., `/health`, `/remoteEntry.js`)
- `readyPatterns` / `errorPatterns` — Regexes matched against stdout/stderr

The orchestrator:
1. Spawns local services via `pnpm run dev` in each package directory
2. Probes remote services via HTTP GET to their readiness path
3. Tracks process state: pending → starting → ready → error
4. Writes logs to `.bos/logs/{service}.log`

## Hot Reload

- **UI changes**: Rsbuild HMR — instant at :3003, no rebuild
- **API changes**: Rspack HMR — instant at :3001, no rebuild
- **Auth / Plugin changes**: No HMR — require full restart (`bos kill && bos dev`)
- **SSR (`bos dev --ssr`)**: source-composed SSR in the host dev process — manifests read from disk; restart not required
- **Config changes**: Require host restart (`bos kill && bos dev`)

## Contract Sync & Type Generation

Plugin types are auto-generated from the authored config via `bos types gen`:

```bash
bos types gen   # Regenerate ui/src/lib/api-types.gen.ts and api/src/lib/plugins-types.gen.ts
```

**When it auto-runs:**
- `pnpm run typecheck`
- `bos dev` startup
- `bos build`, `bos deploy`, `bos publish`
- `bos pluginAdd` / `bos pluginRemove`

**How plugin types are resolved:**
1. `local:plugins/<name>` → reads `src/contract.ts` directly from disk
2. Remote URL → fetches contract types from the deployed plugin's manifest
3. Missing local path with no URL → skipped with a warning

**Source of truth:** the authored config (`bos.app.ts`). If a plugin is listed there, its routes appear on `ApiContract`. If removed, TypeScript catches stale usage.

**After hand-editing the authored config:** Run `bos types gen` or restart `bos dev` to pick up changes.

## Runtime Config Loading

The host reads `BOS_RUNTIME_CONFIG` at startup (resolved from the authored config by the CLI). `ConfigService` is an immutable Effect Layer — every service is built from that one snapshot.

**Override for testing**: Set `BOS_RUNTIME_CONFIG` env var to a JSON string or file path to bypass config loading from disk. Useful for testing with different configs without modifying the authored config.

On page refresh:
1. Browser re-fetches HTML shell from host
2. Host injects current config into `window.__RUNTIME_CONFIG__`
3. Module Federation container re-initializes from fresh `remoteEntry.js`

This means a new deployment requires a host restart to pick up new URLs.

### Resolved Config (`.bos/bos.resolved-config.json`)

`bos dev` and `bos build` write the fully-merged config to `.bos/bos.resolved-config.json` (gitignored). This file includes `_resolved` metadata with env, timestamp, and extends chain.

**The authored config is NOT modified during dev.** Only `bos publish --deploy`, `bos plugin publish/add/remove`, and `bos sync` write to it (`bos.app.ts` for TS-form projects; legacy JSON children keep their `bos.config.json`).

Build configs (rsbuild/rspack) read from `.bos/bos.resolved-config.json` first, falling back to the authored config. This allows slim child configs with `extends` to work correctly — the merged parent+child config is what the build sees.

## Debugging

```bash
bos ps                    # List running processes + ports
bos status                # Check remote health
bos info                  # Show current configuration
ls .bos/logs/             # Available log files
cat .bos/logs/api.log     # API process logs
```

### Port Conflicts

If a port is already in use, the local service fails to start. Check what's on the port:

```bash
lsof -i :3003             # Check what is using port 3003
```

Kill the conflicting process and retry, or stop the existing `bos dev` session first.

### Stale PID registry

If `bos kill` doesn't run cleanly (e.g., terminal was closed), stale entries can linger in the
global registry at `~/.cache/everything-dev/pids.json`. `bos ps` and `bos kill` always prune
ESRCH (dead) PIDs on read, so stale entries don't block restart — but you can wipe the file
manually if you want a clean slate:

```bash
rm ~/.cache/everything-dev/pids.json   # Clear global registry
bos dev                                # Start fresh
```

### API not responding

1. `bos ps` — is API running?
2. `.bos/logs/api.log` — startup errors?
3. `curl http://localhost:3001/remoteEntry.js` — is the entry accessible?

### UI not loading

1. Check browser console for Module Federation errors
2. The authored config — is `app.ui.development` correct?
3. Clear browser cache and hard reload (Cmd+Shift+R)

### Module Federation errors

- Verify shared dependency versions match across package.json files
- Clear browser cache (Cmd+Shift+R)
- Check the config URLs (authored config / FastKV) are accessible

## Production Mode

```bash
bos start --no-interactive                    # All remotes, production URLs
bos start --env staging --no-interactive      # Staging environment
bos start --account foo.near --domain bar.com # Load specific config
```

## Process Management

```bash
bos ps                       # List tracked development processes (cwd's entries, with role/ports)
bos kill                     # SIGTERM processes owned by cwd
bos kill --all               # SIGTERM across all config directories
bos kill --signal SIGKILL    # Force kill (cwd)
bos kill --config-dir /path  # Target a specific project
```

The registry lives at `~/.cache/everything-dev/pids.json`, keyed by `pid`. Each entry stores
`configDir`, `role` (`standalone` today; `workspace-parent`/`workspace-child` reserved for the
planned `bos dev --workspaces` orchestrator), `ports`, optional `budget`, `startedAt`, and
`description`. `bos dev` registers on startup and unregisters on graceful shutdown; the
`BOS_WORKSPACE_CHILD=1` env var suppresses standalone registration so a future parent
orchestrator owns registry entries.

Process tracking uses `~/.cache/everything-dev/pids.json` (global, atomic writes).

## The tasks you will actually be given

**"Start dev on my own ports."** `bos dev --port 3100` (api 3101, auth 3102, ui 3103, plugins 3110+ derive from the base). Explicit `--*-port` flags are pinned and persisted; derived ones are not.

**"My change didn't show up."** Match the change type to the reload path: UI and API hot-reload instantly; auth/plugins/config changes need `bos kill && bos dev`; stale generated types need `pnpm run typecheck`.

**"API is down."** `bos ps` → read `.bos/logs/api.log` → `curl http://localhost:3001/remoteEntry.js` — in that order.

## What comes back when it fails

| word | do |
|---|---|
| `Infra preflight failed: …` | the DB preflight probes localhost ports and auto-runs `docker compose up -d --wait` when every failure is a down local service — if it still fails, start Postgres (`pnpm run dev:postgres`) and read the failure messages |
| `PortAllocationError` on an explicit port | the pinned port is occupied and allocation fails loudly — free it or pick another; it never silently moves an explicit choice |
| `+100` port-shift notice at startup | expected drift: the preferred block was busy, holders are named in the notice, nothing is persisted — restarts re-try the preferred base |
| service stuck in `starting` | probe its readiness path directly (e.g. `curl http://localhost:3003/remoteEntry.js`) and read `.bos/logs/<service>.log` |
| stale registry entries blocking restart | `bos ps` / `bos kill` prune dead PIDs on read; to wipe manually: `rm ~/.cache/everything-dev/pids.json` |
