diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index f8ebb2521..ce2f2b94b 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -37,6 +37,9 @@ server-side update execution surfaces. 15. `scripts/dev-check.sh` 16. `scripts/toggle-mock.sh` 17. `scripts/clean-mock-alerts.sh` +18. `scripts/dev-launchd-setup.sh` +19. `scripts/dev-launchd-wrapper.sh` +20. `scripts/com.pulse.hot-dev.plist.template` ## Shared Boundaries @@ -51,7 +54,7 @@ server-side update execution surfaces. 2. Add or change release-build metadata injection, release artifact assembly, or governed promotion metadata resolution through `scripts/build-release.sh`, `scripts/release_ldflags.sh`, and `scripts/release_control/resolve_release_promotion.py`, plus the container Dockerfile and governed release workflows that consume those same contracts 3. Add or change shell installer, Windows installer, container-agent installer, or auto-update script behavior through `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, and `scripts/pulse-auto-update.sh` 4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts` -5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `tests/integration/scripts/managed-dev-runtime.mjs`, `package.json`, `frontend-modern/package.json`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, and `scripts/clean-mock-alerts.sh` +5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `tests/integration/scripts/managed-dev-runtime.mjs`, `package.json`, `frontend-modern/package.json`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, and `scripts/com.pulse.hot-dev.plist.template` ## Forbidden Paths @@ -196,6 +199,12 @@ Makefile targets, `scripts/toggle-mock.sh`, and `scripts/clean-mock-alerts.sh` m route through the managed runtime control plane when they are operating on the local dev stack, instead of resurrecting lane-local `hot-dev.sh` or raw Vite process management through separate shell folklore. +That same rule now extends to the macOS auto-start surface. The launchd helper +may not boot a separate legacy foreground runtime beside the managed dev stack: +`scripts/dev-launchd-wrapper.sh`, `scripts/dev-launchd-setup.sh`, and the +generated `com.pulse.hot-dev` LaunchAgent template must supervise the same +managed `hot-dev-bg` control plane, so login-time auto-start, crash restart, +and takeover diagnostics all operate on one runtime model. That shared `scripts/install.sh` boundary must also keep one canonical service argument builder for the runtime flags it persists. Token-bearing install paths, token-file systemd paths, wrapper-script launches, and later service diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 04868cb55..75e8aaae2 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2136,7 +2136,10 @@ "package.json", "scripts/build-release.sh", "scripts/clean-mock-alerts.sh", + "scripts/com.pulse.hot-dev.plist.template", "scripts/dev-check.sh", + "scripts/dev-launchd-setup.sh", + "scripts/dev-launchd-wrapper.sh", "scripts/hot-dev-bg.sh", "scripts/hot-dev.sh", "scripts/install-container-agent.sh", @@ -2241,7 +2244,10 @@ "Makefile", "package.json", "scripts/clean-mock-alerts.sh", + "scripts/com.pulse.hot-dev.plist.template", "scripts/dev-check.sh", + "scripts/dev-launchd-setup.sh", + "scripts/dev-launchd-wrapper.sh", "scripts/hot-dev-bg.sh", "scripts/hot-dev.sh", "scripts/toggle-mock.sh", diff --git a/scripts/dev-launchd-setup.sh b/scripts/dev-launchd-setup.sh index dc1293fe1..e4d944210 100755 --- a/scripts/dev-launchd-setup.sh +++ b/scripts/dev-launchd-setup.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# dev-launchd-setup.sh - Install or uninstall the Pulse dev Launch Agent +# dev-launchd-setup.sh - Install or uninstall the Pulse managed dev LaunchAgent # # Usage: # ./scripts/dev-launchd-setup.sh # Install and start @@ -89,15 +89,16 @@ install() { fi echo "" - log_info "=== Pulse Dev Launch Agent Installed ===" + log_info "=== Pulse Dev LaunchAgent Installed ===" log_info "" - log_info "The dev environment will now auto-start on login and auto-restart on crash." + log_info "The managed dev runtime will now auto-start on login and auto-restart on crash." log_info "" log_info "Useful commands:" log_info " Restart: launchctl kickstart -k ${GUI_DOMAIN}/${LABEL}" log_info " Stop: launchctl kill SIGTERM ${GUI_DOMAIN}/${LABEL}" log_info " Status: launchctl print ${GUI_DOMAIN}/${LABEL}" log_info " Logs: tail -f ${LOG_DIR}/hot-dev.stderr.log" + log_info " Runtime: cd ${ROOT_DIR} && ./scripts/hot-dev-bg.sh status" log_info " Disable: launchctl bootout ${GUI_DOMAIN}/${LABEL}" log_info " Remove: $0 uninstall" } diff --git a/scripts/dev-launchd-wrapper.sh b/scripts/dev-launchd-wrapper.sh index 3ffbecff0..c29a3a81f 100755 --- a/scripts/dev-launchd-wrapper.sh +++ b/scripts/dev-launchd-wrapper.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash -# dev-launchd-wrapper.sh - Wrapper for running hot-dev.sh under launchd +# dev-launchd-wrapper.sh - Wrapper for running hot-dev-bg launchd-session # # launchd doesn't source login profiles, so PATH won't include go, npm, # node, fswatch, etc. Hand off to an interactive login zsh so the user's -# normal shell environment is loaded before exec'ing hot-dev.sh. +# normal shell environment is loaded before exec'ing the managed launchd +# supervisor path. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" export PULSE_DEV_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)" -exec /bin/zsh -ilc 'cd "$PULSE_DEV_ROOT" && exec bash scripts/hot-dev.sh' +exec /bin/zsh -ilc 'cd "$PULSE_DEV_ROOT" && exec bash scripts/hot-dev-bg.sh launchd-session --takeover' diff --git a/scripts/hot-dev-bg.sh b/scripts/hot-dev-bg.sh index e31509996..89ee6020e 100755 --- a/scripts/hot-dev-bg.sh +++ b/scripts/hot-dev-bg.sh @@ -7,6 +7,7 @@ # ./scripts/hot-dev-bg.sh stop # ./scripts/hot-dev-bg.sh restart # ./scripts/hot-dev-bg.sh backend-restart +# ./scripts/hot-dev-bg.sh launchd-session [--takeover] # ./scripts/hot-dev-bg.sh verify [--takeover] # ./scripts/hot-dev-bg.sh status # ./scripts/hot-dev-bg.sh logs @@ -669,6 +670,56 @@ verify_bg() { run_verify_proof_command } +launchd_session_bg() { + local takeover="${1:-false}" + local managed_pid poll_interval + + require_python + + stop_and_exit() { + log "Launchd supervisor received termination signal; stopping managed runtime..." + stop_bg + exit 0 + } + + trap stop_and_exit TERM INT HUP + + if ! is_running; then + log "Managed runtime is not running. Starting supervised session..." + start_bg "${takeover}" + else + managed_pid="$(managed_session_pid)" + if [[ -n "${managed_pid}" ]] && has_unmanaged_listeners "${managed_pid}"; then + if [[ "${takeover}" != "true" ]]; then + fail "Managed runtime has split ownership. Rerun with: ./scripts/hot-dev-bg.sh launchd-session --takeover" + fi + log "Managed runtime has split ownership. Reclaiming ports before launchd supervision..." + stop_bg + start_bg "${takeover}" + elif ! runtime_healthy; then + log "Managed runtime is unhealthy. Restarting it before launchd supervision..." + stop_bg + start_bg "${takeover}" + fi + fi + + managed_pid="$(managed_session_pid)" + [[ -n "${managed_pid}" ]] || fail "Managed runtime did not produce a session pid" + + poll_interval="${HOT_DEV_BG_SUPERVISOR_POLL_INTERVAL:-1}" + log "Supervising managed runtime for launchd (pid: ${managed_pid})" + + while is_running; do + sleep "${poll_interval}" + done + + rm -f "${PID_FILE}" + log "Managed runtime exited unexpectedly under launchd supervision" + touch "${LOG_FILE}" + tail -n 80 "${LOG_FILE}" || true + return 1 +} + usage() { cat < @@ -678,6 +729,7 @@ Commands: stop restart backend-restart + launchd-session [--takeover] verify [--takeover] status logs @@ -689,7 +741,7 @@ parse_takeover_flag() { local flag="${2:-}" case "${command}" in - start|restart|verify) + start|restart|verify|launchd-session) if [[ -z "${flag}" ]]; then printf "false\n" return 0 @@ -731,6 +783,7 @@ main() { start_bg "${takeover}" ;; backend-restart) restart_backend_bg ;; + launchd-session) launchd_session_bg "${takeover}" ;; verify) verify_bg "${takeover}" ;; status) status_bg ;; logs) logs_bg ;; diff --git a/scripts/tests/test-hot-dev-bg.sh b/scripts/tests/test-hot-dev-bg.sh index e61640526..06d00160f 100755 --- a/scripts/tests/test-hot-dev-bg.sh +++ b/scripts/tests/test-hot-dev-bg.sh @@ -8,6 +8,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" HOT_DEV_BG="${ROOT_DIR}/scripts/hot-dev-bg.sh" PACKAGE_JSON="${ROOT_DIR}/package.json" FRONTEND_PACKAGE_JSON="${ROOT_DIR}/frontend-modern/package.json" +DEV_LAUNCHD_WRAPPER="${ROOT_DIR}/scripts/dev-launchd-wrapper.sh" if [[ ! -x "${HOT_DEV_BG}" ]]; then echo "hot-dev-bg.sh not found or not executable at ${HOT_DEV_BG}" >&2 @@ -106,6 +107,7 @@ test_cli_parses_takeover_flag() { printf "start=%s\n" "$(parse_takeover_flag start --takeover)" printf "restart=%s\n" "$(parse_takeover_flag restart --takeover)" printf "verify=%s\n" "$(parse_takeover_flag verify --takeover)" + printf "launchd=%s\n" "$(parse_takeover_flag launchd-session --takeover)" printf "backend_restart=%s\n" "$(parse_takeover_flag backend-restart)" printf "plain=%s\n" "$(parse_takeover_flag start)" if parse_takeover_flag status --takeover >/tmp/hot-dev-bg.invalid 2>&1; then @@ -119,6 +121,7 @@ test_cli_parses_takeover_flag() { assert_contains "takeover parsing enables start" "${output}" "start=true" assert_contains "takeover parsing enables restart" "${output}" "restart=true" assert_contains "takeover parsing enables verify" "${output}" "verify=true" + assert_contains "takeover parsing enables launchd-session" "${output}" "launchd=true" assert_contains "backend restart remains flagless" "${output}" "backend_restart=false" assert_contains "start without flag stays false" "${output}" "plain=false" assert_contains "unexpected status flag is rejected" "${output}" "invalid=rejected" @@ -145,6 +148,37 @@ PY" assert_contains "verify proof defaults password" "${output}" "password=admin" } +test_launchd_session_supervises_managed_runtime() { + local output + output="$( + HOT_DEV_BG_PATH="${HOT_DEV_BG}" \ + bash -lc ' + source "${HOT_DEV_BG_PATH}" + set +e + test_dir="$(mktemp -d)" + trap "rm -rf \"$test_dir\"" EXIT + PID_FILE="${test_dir}/hot-dev-bg.pid" + LOG_FILE="${test_dir}/hot-dev-bg.log" + require_python(){ :; } + start_bg(){ printf "%s\n" "999999" > "${PID_FILE}"; } + launchd_session_bg true + status=$? + printf "status=%s\n" "${status}" + ' + )" + + assert_contains "launchd session starts supervised runtime" "${output}" "Managed runtime is not running. Starting supervised session..." + assert_contains "launchd session announces supervision" "${output}" "Supervising managed runtime for launchd" + assert_contains "launchd session exits nonzero on child crash" "${output}" "status=1" +} + +test_launchd_wrapper_uses_managed_supervisor() { + local output + output="$(sed -n '1,80p' "${DEV_LAUNCHD_WRAPPER}")" + + assert_contains "launchd wrapper uses managed launchd-session" "${output}" "scripts/hot-dev-bg.sh launchd-session --takeover" +} + test_root_package_exposes_managed_runtime_entrypoints() { local output output="$( @@ -292,6 +326,8 @@ test_detects_unmanaged_listeners() { main() { test_cli_parses_takeover_flag test_verify_command_injects_managed_runtime_env + test_launchd_session_supervises_managed_runtime + test_launchd_wrapper_uses_managed_supervisor test_root_package_exposes_managed_runtime_entrypoints test_frontend_package_exposes_managed_runtime_entrypoints test_backend_restart_requires_managed_runtime