Serve Workspace
Run serve.py (the FastAPI generation + map server that mounts a workspace at / and
project /assets) correctly from agent context. The launcher tools/serve-bg.py owns the
process logic; this skill is the decision layer + the API contract.
Which command (decision table)
| Goal | Command | Notes |
|---|---|---|
| Start a server for a task (default) | mise run serve:bg -- [--workspace P] [--lan] [--port N] |
Non-blocking; returns when ready, prints URLs, records PID. Use this in any tool call. |
| Stop it | mise run serve:stop (or serve:bg -- --stop) |
Kills by PID file, cross-platform. |
| Is it up? | python tools/serve-bg.py --status |
Reports PID + port state. |
| Restart (replace) | mise run serve:restart -- [...] |
--force: stops the existing one, starts fresh. |
| Interactive human session | mise run serve / serve:lan |
Blocking — never in an agent tool call. |
| Live-reload during code edits | mise run serve:dev |
Blocking; auto-reloads on tools/ changes. |
The non-blocking rule (#1 trap)
serve, serve:lan, and raw uvicorn.run block forever — calling them in a shell
tool wastes the whole call on a timeout. For any agent-initiated server, use serve:bg.
It spawns detached (Windows creationflags, Unix setsid) per the Windows steering — do
NOT hand-roll Start-Process -RedirectStandard*/-NoNewWindow (those block; see
project-conventions/references/windows.md).
Only ONE server at a time (enforced)
serve:bg refuses to start a second server:
- If a background server WE started is already alive (tracked via
.scratch/serve-bg.pid, on any port) → refused. Use--stop, or--forceto replace it. - If the target
--portis held by a different process (a foregroundserve, a stray listener) → refused; it never clobbers something it doesn't own. Stop that process or pick another--port.
To guarantee a clean single instance: mise run serve:restart -- [...] (force-replace) or
serve:stop then serve:bg.
LAN exposure
--lan binds 0.0.0.0 and prints http://192.168.x.x:PORT — reachable from other
devices. First LAN launch on Windows may need a Firewall inbound allow for the port.
Default to --lan when launching for a human to view. When the user asks to see/launch
the site, start with --lan and hand them the LAN address (http://192.168.x.x:PORT) as
the primary link — NOT 127.0.0.1. The user is typically on a different device than the
server. Only give the 127.0.0.1 link for agent-internal validation (headless checks, curl),
never as the primary URL for a person. Always give the full clickable URL (per AGENTS.md).
Ports
Default 8787. Use a distinct --port (e.g. 8799) for throwaway validation so you don't
clobber a running dev server — same reasoning as check-map-edges.py's dedicated 8791.
Status API contract (per-user overlay, #258)
Status is NOT in the committed graph — it lives in a gitignored .user/status-overlay.json
keyed by ULID node id, joined at runtime.
| Endpoint | Does |
|---|---|
GET /api/map/{domain} |
Topics with overlay-joined status + derived available_topics / next_suggestion. |
GET /api/map/{domain}/{slug}/status |
Single topic status (absent overlay = not-started). |
POST /api/map/{domain}/{slug}/status body {"status":"complete|in-progress|not-started"} |
Writes the .user/ overlay ONLY. Returns {ok:true,...}. |
PowerShell gotcha: inline curl -d '{"status":"complete"}' gets mangled. Use a file:
Set-Content .scratch/body.json '{"status":"complete"}' -NoNewline -Encoding ascii
curl.exe -s -X POST http://127.0.0.1:8799/api/map/oidc-rust/oidc-auth-flows/status \
-H "Content-Type: application/json" --data "@.scratch/body.json"
Verify the write landed only in the overlay: git check-ignore <ws>/.user/status-overlay.json
returns the path (ignored), and git status shows no committed status change.
Known gap — the "Mark complete" button (#264)
The lesson mark-complete button POSTs to /api/map/**null**/{slug}/status (404) because
data-domain isn't wired into lesson pages yet (tracked in #264). Until #264 lands,
validate the status endpoints with curl (explicit domain), not the browser button.
Browser-driven testing
For clicking through pages (mark-complete round-trip, component checks), dispatch the
browser specialist (see browse-and-verify) against a server you started with
serve:bg — don't load Playwright in the default agent.
Cleanup discipline
Always serve:stop when done. The .user/ overlay is gitignored and disposable (delete =
reset progress). Remove .scratch/serve-bg.{log,pid} if they linger.