Adds supervisor/deploy_cutover.py: stops the old Live process, retires the
health-check-mode candidate, and relaunches the same already-verified
candidate checkout bound to LIVE_PORT/PROD_DB_PATH -- http.server has no
live socket-rebind operation, so "switch to production port/db" is
implemented as stop-then-relaunch-from-the-same-dir rather than an in-place
rebind (functionally equivalent, the only mechanism stdlib actually offers).
Adds LIVE_PORT=8420 to 05 §5.9's MVP-fixed parameter table -- the one
parameter that table was missing (frontend/app.js never needed one, same-
origin relative fetches don't care, but successive deployments need to keep
landing on the same port for a human bookmark to keep working).
"在途 work_node_execution 重启后可恢复" is satisfied by relying on the
existing REQ-06 recovery machinery unchanged -- cutover doesn't add any
special-case logic, a cutover-triggered restart is just another interruption
REQ-06 already survives.
A real (not mocked) test caught a real bug: deploy_supervisor.is_alive()'s
os.kill(pid, 0) check can't distinguish "truly running" from "exited but
never reaped by its own parent" -- a zombie's PID slot answers kill(pid, 0)
successfully until waited on. U-036's own subprocess test never caught this
because that skeleton deletes its own PID file before exiting, sidestepping
is_alive() entirely; deploy_cutover's test is the first one to actually poll
for a stopped process to disappear, and it hung until fixed. Added
reap_if_exited() (real os.waitpid for processes we spawned ourselves, falls
back to is_alive() for pids from a prior session where ChildProcessError
means we're not the real parent) and switched deploy_cutover._stop_process,
deploy_health_check.check_process_alive, and deploy_supervisor.stop() to use
it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>