#3899, milestone 166. An agent renders a page; this hands it over at https://<name>.localhost/, records it, and can take it down. It replaces a hand-typed python3 -m http.server that outlived its session.
Flip the preconditions
The cycle, as it actually ran (2026-09-07, first verified run)
$ serve.sh docs/playgrounds/roadmap-2026-09-07.html --name ps-test --no-screenshot
url=https://ps-test.localhost/roadmap-2026-09-07.html
name=ps-test
port=54301
pid=38277
http=200
stop=.../scripts/stop.sh ps-test
serve rc=0$ serve.sh docs/playgrounds/roadmap-2026-09-07.html --name ps-test --no-screenshot # again
pid=38277# same pid: idempotent per name + root$ status.sh
ps-test upregistered http=200 https://ps-test.localhost/roadmap-2026-09-07.html
$ stop.sh ps-test
alias ps-test.localhost removed
server pid 38277 stopped
stopped=ps-test
$ status.sh
page-serve: nothing served
portless list | grep -c ps-test → 0
What each script promises
Script
Promise
How it keeps it
serve.sh
The printed URL has no port and answers 200, or the exit code says otherwise
refuses on a dead 443 service (exit 2) instead of falling back to a :port; curls the URL after registering and exits 4 if it is not 200
status.sh
A row that says "up" is up right now
re-measures pid liveness, portless list, and HTTP per row; never trusts the state file
stop.sh
"stopped" means the route is really gone
reports each step's own rc; an alias removal that failed is printed, not swallowed
Two things this PR also did
The skill was an island by the activation-channels gate (reachable only by typing it), so glyph and visualize-plan now route their page output through it, which is also the first step of #3900. And the count anchors moved 106 to 107 in five places (manifest description, CLAUDE.md, the structure test's user-invocable constant, and three skill docs that state the total), all caught by the gates, none by hand.