# Plan: Fix the TrapOS boot lifecycle ## Context `startup/servers.lua` is filed as a CraftOS **startup hook** but behaves as an **init process**. CraftOS runs `/startup/*` files *inside* its own shell and expects each to return; `servers.lua` instead blocks forever (`parallel.waitForAny(shellFn, eventLoopFn)`) and then calls `os.shutdown()` (`startup/servers.lua:69`). That `os.shutdown()` is not a feature — it's a guard against falling through to the host CraftOS shell with no eventloop and no servers running (a degraded bare prompt). The installer makes this worse: `install-trapos.lua:229-231` "boots TrapOS in place" via `shell.execute('/startup/servers.lua')`, three shells deep, where the `os.shutdown()` kills the machine instead of letting `shell.setDir(previousDir)` (line 233) run. Goal: keep the (correct, unavoidable) `parallel(shell, eventLoop)` shape, but stop running the orchestrator as a startup-hook-pretending-to-be-init. Make the boot path single and honest, decouple the installer, and turn the implicit shutdown into an explicit policy. ### Decisions (confirmed with user) - **Session-end default = `shutdown`**, with `reboot` and `relaunch` as opt-in policy values. - **Move `motd` into `programs/`** and invoke it from the orchestrator (single `/startup/` file). - **Docs: new ADR-0019** for the boot-lifecycle decision; update living docs (README/AGENTS/periphemu); leave accepted ADRs 0002/0005/0010 as historical record. - **`trapos.shutdown_on_shell_exit`**: clean break — replaced by `trapos.on_session_end`. The old key is undocumented and never persisted (only read in the orchestrator + one test), so no migration shim. (Recorded in ADR-0019.) ## Implementation ### 1. Rename orchestrator → `startup/boot.lua` + explicit session-end policy Rename `startup/servers.lua` → `startup/boot.lua`. Replace the `SHUTDOWN_ON_SHELL_EXIT_SETTING` boolean logic with a named policy: - New setting `trapos.on_session_end`, values `"shutdown"` (default) | `"reboot"` | `"relaunch"`. Any unset/invalid value falls back to `"shutdown"`. - `relaunch`: when the shell exits, re-run it (never drop to bare CraftOS); shutdown/reboot only via explicit command. Implement as a `repeat shell.run("shell") until action ~= "relaunch"` loop inside `shellFn`, re-reading the setting each iteration (so `set ... shutdown` then exit actually powers off). - After `parallel.waitForAny`: if `shellExited`, dispatch `os.reboot()` for `reboot`, else `os.shutdown()` (covers `shutdown`; `relaunch` never reaches here). - Drop the file's self-deprecating TODO comment (`servers.lua:6`). ### 2. Move `motd` into `programs/` and call it from `boot.lua` - Move `startup/motd.lua` → `programs/motd.lua` (drop its TODO comment, lines 3-4). No code change otherwise — it already reads `/trapos/manifest.json` and returns early if absent. - In `boot.lua`, after `init()` (which appends `:/programs` to the path), call `shell.run('/programs/motd.lua')` early — before the "Starting servers..." block — to preserve the current banner-before-servers order. Use the absolute path so it doesn't depend on path-setup ordering. ### 3. Installer: replace "boot in place" with `os.reboot()` In `install-trapos.lua`, replace lines 229-231 (the `fs.exists('/startup/servers.lua')` + `shell.execute(...)` block) with a reboot at the end of the full-install path: ```lua shell.execute('trapos-postinstall'); shell.setDir(previousDir); print(); print('=> Rebooting...'); os.reboot(); ``` This gives one boot path (fresh install == normal power-on), drops the deep nesting, and the installer no longer needs to know the orchestrator's filename. (`--cpm-only` already returns early at line 220, so it won't reboot.) Mirrors the existing `programs/trapos-upgrade.lua:34-37` reboot pattern. ### 4. Upgrade migration: prune stale startup files (self-heal) `api.upgrade` → `api.install(force=true)` (`apis/libccpm.lua:412,432,372`) writes the new file list but **never deletes** files dropped from a package (only `uninstall` deletes, `libccpm.lua:463-466`). So upgraded machines would keep the old `startup/servers.lua` + `startup/motd.lua` alongside the new files → two orchestrators in `/startup/`. Fix in `boot.lua`: at startup (in `init()`), prune known-stale paths, guarded by `fs.exists`: ```lua for _, p in ipairs({ '/startup/servers.lua', '/startup/motd.lua' }) do if fs.exists(p) then fs.delete(p) end end ``` CraftOS runs `/startup/*` alphabetically; `boot.lua` sorts before `servers.lua`, so it deletes the stale orchestrator before it would ever run. Idempotent; converges in one boot. (The general "ccpm should prune removed files on force-install" gap is noted as future work in ADR-0019, not fixed here to keep scope contained.) ### 5. Package metadata - `packages/trapos-boot/ccpm.json`: `files` → `["programs/motd.lua", "startup/boot.lua"]`; bump `version` `0.3.3` → `0.4.0` (file set + behavior changed). - `packages/index.json:5`: bump `trapos-boot` `0.3.3` → `0.4.0` so `ccpm update` advertises it and `ccpm upgrade` detects the new version on existing machines. ### 6. Tests - **`tests/startup-servers.lua` → rename `tests/boot.lua`** (or keep name; rename preferred for clarity). Update `loadfile('/startup/servers.lua', ...)` → `/startup/boot.lua`. Keep the two existing cases retargeted to `trapos.on_session_end` semantics: - default → `shutdown` (existing "shuts down by default"). - add `reboot` case → asserts `calls.rebooted == true`, `calls.shutdown == false`. - add `relaunch` case → make the `settings.get` stub return `"relaunch"` once then `"shutdown"` so the `repeat...until` loop terminates; assert `shellRuns` ran twice and ended in shutdown. The harness already stubs `os.reboot`/`os.shutdown`/`parallel.waitForAny` (lines 21-39). - **`tests/install-trapos.lua`**: the env stub (lines 75-101) does **not** stub `os`, so the new `os.reboot()` would really reboot during the test. Add an `os` stub that records the reboot (e.g. push a `{ program = '__reboot__' }` marker into `calls.executes` or set `calls.rebooted`). Rewrite the "postinstall before startup servers" test (lines 164-174): drop the `/startup/servers.lua` lookup; assert reboot occurs after `trapos-postinstall`. In the `--cpm-only` test (176-184), assert reboot was **not** called. - `tools/trapos-server/test-integration/lua/full-boot.lua:1`: update the comment reference `startup/servers.lua` → `startup/boot.lua` (no functional change — it doesn't load the file). ### 7. Docs - **New `docs/adrs/adr-0019-boot-lifecycle.md`**: orchestrator renamed `boot.lua` and framed as init; installer reboots instead of boot-in-place (single boot path); `trapos.on_session_end` policy (shutdown default / reboot / relaunch) replacing the `shutdown_on_shell_exit` boolean; motd moved to `programs/`; boot.lua self-heals stale startup files. Note ccpm-prune-on-upgrade as future work. Add it to the Records list in `docs/adrs/README.md` (markdown link — lychee validates it). - Update living-doc prose mentions `startup/servers.lua` → `startup/boot.lua`: `README.md:73`, `AGENTS.md:27,36,42`, `docs/periphemu.md:9,26`. - Leave accepted ADRs `0002:36`, `0005:22,41,86`, `0010:57,86` unchanged (historical; the mentions are backticked prose, not links, so `just check`/lychee is unaffected). ## Files to modify - `startup/servers.lua` → **rename** `startup/boot.lua` (policy + prune + motd call) - `startup/motd.lua` → **move** `programs/motd.lua` - `install-trapos.lua` (reboot instead of boot-in-place) - `packages/trapos-boot/ccpm.json`, `packages/index.json` (file list + version bumps) - `tests/startup-servers.lua` → **rename** `tests/boot.lua` (policy cases) - `tests/install-trapos.lua` (os stub + reboot assertions) - `tools/trapos-server/test-integration/lua/full-boot.lua` (comment) - `docs/adrs/adr-0019-boot-lifecycle.md` (**new**), `docs/adrs/README.md` - `README.md`, `AGENTS.md`, `docs/periphemu.md` ## Verification - **Unit tests**: run the suite (`just test` / `programs/runtest.lua`). Confirm the renamed `tests/boot.lua` passes all three session-end cases and `tests/install-trapos.lua` passes with the new reboot assertions. Confirm the test runner discovers the renamed file. - **Markdown lint**: `just check` (lychee `lint-markdown`) — confirms the new ADR-0019 link in `docs/adrs/README.md` resolves and no doc links broke. - **Boot path (CraftOS-PC harness, per ADR-0005)**: launch a clean computer, confirm `boot.lua` prints the motd banner, starts autostart servers, and gives an interactive shell with the eventloop live (e.g. a `net.call`-based program responds). Type `exit` → machine powers off (default). `set trapos.on_session_end reboot` → exit re-enters TrapOS. `set ... relaunch` → exit returns a fresh prompt without powering off. - **Upgrade migration**: on a computer with the old `startup/servers.lua` + `startup/motd.lua` present, drop in the new `boot.lua` and reboot; confirm both stale files are deleted and only `boot.lua` remains in `/startup/`. - **Installer**: run `install-trapos.lua` end-to-end (or via the test harness) and confirm it finishes with `=> Rebooting...` and comes back up into a live TrapOS rather than a nested shell.