page-serve: a URL a human can open, with a stop

#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 up registered 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

ScriptPromiseHow it keeps it
serve.shThe printed URL has no port and answers 200, or the exit code says otherwiserefuses 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.shA row that says "up" is up right nowre-measures pid liveness, portless list, and HTTP per row; never trusts the state file
stop.sh"stopped" means the route is really gonereports 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.