# PID-1 Boot Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Boot hh-prototest with zyginit as PID 1, replacing SMF's boot chain with 21 zyginit-managed services, with clean shutdown orchestration and safe BE-based rollback.

**Architecture:** Two code phases (zyginit/zygctl changes for shutdown orchestration + uadmin FFI; zygctl subcommand additions), one content phase (21 TOMLs + method scripts in `services/hammerhead/`), one deployment phase (BE-based install on hh-prototest), one validation phase (Phase A dry run + Phase B PID-1 boot + rollback drill).

**Tech Stack:** Reef 0.5.9, reefc, libcontract (Hammerhead), Mercurial, ZFS Boot Environments, libvirt/KVM, zsh method scripts, TOML service definitions.

**Spec:** `docs/superpowers/specs/2026-04-24-pid1-boot-design.md`

---

## File Structure

**Code to create:**
- `src/shutdown.reef` — new module: `uadmin`-based shutdown primitives, `AD_*` constants, PID-1 detection
- `services/hammerhead/` — staging tree for production Hammerhead service definitions (distinct from `services/examples/` which is Linux learning scaffolding)
- `services/hammerhead/<svc>.toml` — 21 TOML files
- `services/hammerhead/<svc>/start.sh` and `<svc>/stop.sh` — method scripts for non-trivial services
- `services/hammerhead/enabled.d/<svc>` → `../<svc>.toml` — 21 symlinks
- `scripts/install-to-be.sh` — dev-host utility: installs zyginit binaries + config tree into a mounted BE at a given path
- `tests/integration/test_shutdown.sh` — new integration test focused on shutdown orchestration

**Code to modify:**
- `src/main.reef` — PID-1 detection, shutdown-type tracking, call `sync + zyginit_shutdown()` at end of PID-1 shutdown
- `src/supervisor.reef` — extend shutdown path to walk tiers in reverse and invoke `stop_cmd` for oneshots that declared one
- `src/socket.reef` — parse halt/reboot/poweroff/single/multiuser commands, set shutdown flags
- `src/helpers.c` — add `zyginit_shutdown(int fcn)` wrapper around `uadmin(2)`
- `src/contract_linux_stubs.c` — add stub `zyginit_shutdown` returning -1
- `tools/zygctl/src/main.reef` — new subcommands: halt, reboot, poweroff, single (stub), multiuser (stub)
- `tests/integration/run_tests.sh` — source or call the new shutdown-orchestration tests
- `reef.toml` — build command comments updated if helpers.c surface grows (likely no change)

Each phase's tasks assume earlier phases are complete and committed.

---

## Phase 1 — Core zyginit code changes

### Task 1: Add `zyginit_shutdown()` C wrapper around uadmin(2)

**Files:**
- Modify: `src/helpers.c`
- Modify: `src/contract_linux_stubs.c`

- [ ] **Step 1: Add uadmin include and wrapper to `src/helpers.c`**

Append at the bottom of `src/helpers.c` (after the `zyginit_drop_privileges` function):

```c
/* Shutdown wrapper — uadmin(A_SHUTDOWN, fcn, 0). On Hammerhead this is the
 * primitive init uses to halt, reboot, or poweroff the system. fcn is one of:
 *   AD_HALT     (0) — stop scheduler, wait for operator
 *   AD_BOOT     (1) — reboot
 *   AD_POWEROFF (6) — ACPI/BMC chassis off
 * Returns 0 on success, -1 on error (errno set). On success, does not return
 * for AD_BOOT / AD_POWEROFF; for AD_HALT the kernel enters a halted state.
 */
#include <sys/uadmin.h>
int zyginit_shutdown(int fcn) {
    return uadmin(A_SHUTDOWN, fcn, 0);
}
```

- [ ] **Step 2: Add Linux stub to `src/contract_linux_stubs.c`**

Append at the bottom of `src/contract_linux_stubs.c`:

```c
/* Shutdown stub — on Linux we never actually uadmin; return -1 so the
 * calling code path is exercised without side effects. */
int zyginit_shutdown(int fcn) { return -1; }
```

- [ ] **Step 3: Rebuild on Linux to verify it compiles**

Run:
```bash
cd /home/ctusa/repos/zyginit
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
```

Expected: both compile with no errors. If clang errors on `<sys/uadmin.h>` on Linux, that's expected for the `helpers.c` side — the header isn't on Linux. In that case, guard with `#ifdef __sun`:

```c
#ifdef __sun
#include <sys/uadmin.h>
int zyginit_shutdown(int fcn) {
    return uadmin(A_SHUTDOWN, fcn, 0);
}
#else
int zyginit_shutdown(int fcn) { (void)fcn; return -1; }
#endif
```

If that's needed, remove the stub from `contract_linux_stubs.c` (it'd be a duplicate symbol).

- [ ] **Step 4: Verify on Hammerhead (ssh to hh-prototest)**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 \
  'grep -E "A_SHUTDOWN|AD_HALT|AD_BOOT|AD_POWEROFF" /usr/include/sys/uadmin.h'
```

Expected output should confirm the constant values:
```
#define A_SHUTDOWN      2
#define AD_HALT         0
#define AD_BOOT         1
#define AD_POWEROFF     6
```

(A_SHUTDOWN = 2 on illumos, not 1 as might be assumed from older references.
The code uses the symbolic constant so this value never appears in Reef/C
source; the header does the resolution at compile time.)

- [ ] **Step 5: Commit**

```bash
hg commit -m "shutdown FFI: add zyginit_shutdown() wrapping uadmin(A_SHUTDOWN, fcn, 0)

Adds the C wrapper used by zyginit to halt, reboot, or poweroff the system
once all services are stopped. Linux gets a stub returning -1.

No Reef-side caller yet — wiring comes in later tasks."
```

---

### Task 2: Create `src/shutdown.reef` with FFI extern and AD_* constants

**Files:**
- Create: `src/shutdown.reef`

- [ ] **Step 1: Write the new module**

Write to `src/shutdown.reef`:

```reef
/******************************************************************************
                __               ____                __
               / /   ___  ____ _/ __/_____________ _/ /__
              / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
             / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
            /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

    (C)opyright 2025-2026, Leafscale, LLC -  https://www.leafscale.com

    Project: zyginit
   Filename: shutdown.reef
    Authors: Chris Tusa <chris.tusa@leafscale.com>
    License: <see LICENSE file included with this source code>
Description: PID-1 shutdown primitives — uadmin(2) FFI and shutdown-type
             constants.

******************************************************************************/

module shutdown

export
    // Shutdown function codes (sys/uadmin.h A_SHUTDOWN fcn values)
    fn AD_HALT(): int
    fn AD_BOOT(): int
    fn AD_POWEROFF(): int

    // Symbolic shutdown-type codes used internally by zyginit.
    // These are NOT the uadmin AD_* codes — they're zyginit's own
    // enum for distinguishing what a shutdown was triggered as,
    // before it's mapped to AD_* at the actual uadmin call site.
    fn SHUT_NONE(): int
    fn SHUT_HALT(): int
    fn SHUT_REBOOT(): int
    fn SHUT_POWEROFF(): int

    // Map our internal type to the uadmin fcn code.
    fn to_ad_code(shut_type: int): int

    // Call uadmin(A_SHUTDOWN, fcn, 0). Returns 0 on success, -1 on error.
    // Only succeeds when running as PID 1.
    fn do_shutdown(fcn: int): int
end export

extern "C" fn zyginit_shutdown(fcn: int): int

fn AD_HALT(): int     return 0 end AD_HALT
fn AD_BOOT(): int     return 1 end AD_BOOT
fn AD_POWEROFF(): int return 6 end AD_POWEROFF

fn SHUT_NONE(): int     return 0 end SHUT_NONE
fn SHUT_HALT(): int     return 1 end SHUT_HALT
fn SHUT_REBOOT(): int   return 2 end SHUT_REBOOT
fn SHUT_POWEROFF(): int return 3 end SHUT_POWEROFF

fn to_ad_code(shut_type: int): int
    if shut_type == SHUT_HALT()
        return AD_HALT()
    elif shut_type == SHUT_REBOOT()
        return AD_BOOT()
    elif shut_type == SHUT_POWEROFF()
        return AD_POWEROFF()
    end if
    // Default to halt for unknown / NONE
    return AD_HALT()
end to_ad_code

fn do_shutdown(fcn: int): int
    return zyginit_shutdown(fcn)
end do_shutdown

end module
```

- [ ] **Step 2: Build on Linux to verify module compiles**

```bash
cd /home/ctusa/repos/zyginit
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
```

Expected: build completes. If build errors about shutdown.reef not being found, ensure it's under `src/` which is in `source_dirs` in `reef.toml`.

Note: main.reef / supervisor.reef / etc. don't import `shutdown` yet, so this only validates the module compiles standalone. (Dead-code elimination may not compile it at all — that's OK for this step; we'll exercise it in later tasks.)

- [ ] **Step 3: Commit**

```bash
hg commit -m "src/shutdown.reef: new module with uadmin FFI + shutdown-type enum

Adds AD_HALT/AD_BOOT/AD_POWEROFF constants matching sys/uadmin.h, a
separate SHUT_* internal enum for tracking what kind of shutdown was
requested, and a do_shutdown() entry point that calls through the
zyginit_shutdown() C wrapper. No callers yet."
```

---

### Task 3: Add PID-1 detection helper

**Files:**
- Modify: `src/main.reef`

- [ ] **Step 1: Add `is_pid_1()` helper near the top of main.reef**

Open `src/main.reef`. After the `MAX_SERVICES()` function (around line 50), add:

```reef
// Returns true when running as PID 1 (i.e., actual init). When not PID 1,
// the integration-test path: SIGTERM → stop services → normal exit
// (no uadmin call). When PID 1, shutdown must call uadmin or the kernel
// panics.
fn is_pid_1(): bool
    return process.getpid() == 1
end is_pid_1
```

- [ ] **Step 2: Add startup banner line so PID-1 mode is visible in logs**

Find the startup block in `main()` (search for `"zyginit v"`). Add a line right after the version banner:

```reef
    println("zyginit v" + VERSION() + " starting")

    if is_pid_1()
        println("zyginit: running as PID 1 (init mode)")
    else
        println("zyginit: running as PID " + int_to_str(process.getpid()) + " (non-init mode)")
    end if
```

Remove any duplicate "running as PID ..." prints if they exist.

- [ ] **Step 3: Rebuild and run integration tests (should still be 57/57)**

```bash
cd /home/ctusa/repos/zyginit
rm -f build/zyginit
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/run_tests.sh 2>&1 | tail -5
```

Expected: `Results: 57 passed, 0 failed (of 57)`. The test harness runs zyginit as a regular user process, so `is_pid_1()` returns false and behavior is unchanged.

- [ ] **Step 4: Commit**

```bash
hg commit -m "main.reef: add is_pid_1() helper and startup banner

Sets up the PID-1 detection used by later tasks to decide whether
shutdown ends with uadmin() or a normal process exit."
```

---

### Task 4: Extend supervisor shutdown to run oneshot stop commands in reverse tier order

**Files:**
- Modify: `src/supervisor.reef`
- Modify: `src/main.reef`

This is the biggest code change in Phase 1. Shutdown currently walks services in the order they were added to the table and only stops `STATE_RUNNING` services. We need tier-ordered reverse walk that also runs `exec.stop` commands for oneshots that declared one.

- [ ] **Step 1: Add a `stop_oneshot` supervisor function**

Open `src/supervisor.reef`. The existing `stop_service` function (around line 423) handles daemons with stop.method=contract. Add a new function below it that handles oneshot teardown:

```reef
// Run a oneshot's declared exec.stop command synchronously.
// Used during shutdown to reverse setup that the matching exec.start did.
// Does nothing if no stop command is declared.
// Returns true if stop was attempted (whether or not it succeeded).
fn stop_oneshot(table: ServiceTable, idx: int): bool
    let rt = supervisor.get_runtime(table, idx)
    let def = supervisor.rt_def(rt)
    let name = config.svc_name(def)
    let stop_cmd = config.svc_stop_cmd(def)

    if str.length(stop_cmd) == 0
        // Nothing to reverse — normal for oneshots with no teardown.
        return false
    end if

    println("supervisor: running stop for " + name + " (" + stop_cmd + ")")

    // Parse stop_cmd into program + args (same pattern as start).
    let parts = new [string](32)
    let argc = str.split(stop_cmd, ' ', parts, 32)
    if argc <= 0
        println("supervisor: " + name + ": empty stop command, skipping")
        return false
    end if

    let program = parts[0]
    let argv = new [string](argc)
    mut i = 0
    while i < argc
        argv[i] = parts[i]
        i = i + 1
    end while

    let pid = process.process_spawn(program, argv)
    if pid < 0
        println("supervisor: " + name + ": stop spawn failed")
        return true
    end if

    // Block until the stop command exits. Oneshot stops are expected to be
    // short (umount, swap -d, etc.) — they must complete before we proceed.
    let exit_code = process.process_wait(pid)
    if exit_code != 0
        println("supervisor: " + name + ": stop exited non-zero (" + int_to_str(exit_code) + ")")
    end if
    return true
end stop_oneshot
```

Export it in the module's `export` block (near the top of supervisor.reef, alongside `stop_service`):

```reef
    fn stop_oneshot(table: ServiceTable, idx: int): bool
```

- [ ] **Step 2: Replace `shutdown_services` in main.reef with a tier-reversed walk**

Open `src/main.reef`. Find `proc shutdown_services(table: supervisor.ServiceTable)` (around line 283). Replace with:

```reef
// Stop all services in reverse tier order.
// - Daemons in RUNNING: stop via contract SIGTERM (existing stop_service path).
// - Oneshots in STOPPED with a declared exec.stop: run the stop command.
// - Oneshots without stop: skip.
proc shutdown_services(table: supervisor.ServiceTable,
                       tiers: [string], tier_counts: [int], num_tiers: int)
    println("zyginit: stopping all services...")

    // Compute total offsets per tier so we can scan each tier's slice.
    let offsets = new [int](num_tiers)
    mut off = 0
    mut i = 0
    while i < num_tiers
        offsets[i] = off
        off = off + tier_counts[i]
        i = i + 1
    end while

    // Walk tiers in reverse.
    mut tier = num_tiers - 1
    while tier >= 0
        let tier_off = offsets[tier]
        let tier_sz = tier_counts[tier]
        println("zyginit: shutting down tier " + int_to_str(tier) +
                " (" + int_to_str(tier_sz) + " services)")

        mut j = 0
        while j < tier_sz
            let name = tiers[tier_off + j]
            let idx = supervisor.find_service(table, name)
            if idx >= 0
                let rt = supervisor.get_runtime(table, idx)
                let state = supervisor.rt_state(rt)
                if state == supervisor.STATE_RUNNING()
                    // Running daemon: issue contract stop.
                    supervisor.stop_service(table, idx)
                elif state == supervisor.STATE_STOPPED()
                    // Oneshot that already exited: run its stop if declared.
                    let _ = supervisor.stop_oneshot(table, idx)
                end if
            end if
            j = j + 1
        end while

        // Between tiers: wait for daemons in this tier to reach STOPPED
        // so we don't teardown upstream deps while downstream is still
        // alive. Simple: loop with a short sleep until all RUNNING in
        // this tier are gone, with a safety deadline.
        mut remaining = 1
        mut waits = 0
        while remaining > 0 and waits < 60
            remaining = 0
            mut k = 0
            while k < tier_sz
                let name2 = tiers[tier_off + k]
                let idx2 = supervisor.find_service(table, name2)
                if idx2 >= 0
                    let rt2 = supervisor.get_runtime(table, idx2)
                    if supervisor.rt_state(rt2) == supervisor.STATE_STOPPING()
                        remaining = remaining + 1
                    end if
                end if
                k = k + 1
            end while
            if remaining > 0
                clock.sleep_millis(500)
                // Also reap any child exits that happened during the wait.
                reap_children(table)
                waits = waits + 1
            end if
        end while

        if remaining > 0
            println("zyginit: tier " + int_to_str(tier) +
                    " shutdown timed out with " + int_to_str(remaining) +
                    " services still STOPPING; proceeding anyway")
        end if

        tier = tier - 1
    end while

    println("zyginit: all service tiers stopped")
end shutdown_services
```

Find the caller of `shutdown_services` in `main()` (search for `shutdown_services(table)`). Update the call site to pass the tier arrays (which are already computed at startup — pass the same ones that were used for `start_services_by_tier`):

```reef
    // ---- Shutdown ----
    println("")
    shutdown_services(table, tiers, tier_counts, num_tiers)
```

Make sure `tiers`, `tier_counts`, and `num_tiers` are still in scope at that point; if they were declared inside an inner block, promote them to the outer scope.

- [ ] **Step 3: Add `clock` import in main.reef if not already present**

Near the top imports of `main.reef`:

```reef
import time.clock as clock
```

(If already imported, skip.)

- [ ] **Step 4: Rebuild and run integration tests**

```bash
cd /home/ctusa/repos/zyginit
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/run_tests.sh 2>&1 | tail -5
```

Expected: `Results: 57 passed, 0 failed (of 57)`. Oneshot stops aren't exercised by existing tests (no example service declares `exec.stop`), so this step only validates nothing regressed.

- [ ] **Step 5: Commit**

```bash
hg commit -m "supervisor: shutdown in reverse tier order + oneshot stop commands

Shutdown now walks computed tiers from last to first:
  - RUNNING daemons get stopped via contract SIGTERM (unchanged)
  - STOPPED oneshots with exec.stop declared run their stop command
  - Waits up to 30s per tier for daemons to finish STOPPING before
    moving to the prior tier

This makes it safe to define stop.sh for services like filesystem
(umountall) and root-fs (remount ro) and have them run in the right
order during shutdown."
```

---

### Task 5: Add shutdown-type tracking + sync + uadmin call for PID 1

**Files:**
- Modify: `src/main.reef`

- [ ] **Step 1: Import shutdown module and add global shutdown-type variable**

Near the top imports of `main.reef`, add:

```reef
import shutdown
```

Below the existing mutable globals (e.g., `g_signal_pipe_read`), add:

```reef
// Requested shutdown type (set by signal handler or socket command).
// SHUT_NONE = no shutdown in progress.
mut g_shutdown_type: int = 0   // = shutdown.SHUT_NONE() — but can't call fn in init
```

Reef may not allow calling a function in a global initializer; using `0` which matches SHUT_NONE by definition.

- [ ] **Step 2: Modify `handle_signals` to set shutdown type**

Find `fn handle_signals` (around line 211). Update the SIGTERM/SIGINT branch:

```reef
fn handle_signals(table: supervisor.ServiceTable): bool
    // SIGTERM / SIGINT — initiate shutdown
    if signal.signal_received(signal.SIGTERM()) or signal.signal_received(signal.SIGINT())
        println("zyginit: received shutdown signal")
        // Default: halt (conservative). Socket commands can override.
        if g_shutdown_type == shutdown.SHUT_NONE()
            g_shutdown_type = shutdown.SHUT_HALT()
        end if
        return false
    end if

    // SIGHUP — reload config
    if signal.signal_received(signal.SIGHUP())
        println("zyginit: SIGHUP received, reloading configuration")
        reload_services(table)
    end if

    // SIGCHLD — reap exited children
    if signal.signal_received(signal.SIGCHLD())
        reap_children(table)
    end if

    return true
end handle_signals
```

- [ ] **Step 3: Call sync + uadmin at end of main() when PID 1**

At the very end of `main()`, after `shutdown_services` and any cleanup prints, add:

```reef
    println("zyginit: shutdown complete")

    // As PID 1, we MUST NOT return normally — kernel panics on "init died".
    // Call sync() then uadmin() based on the requested shutdown type.
    if is_pid_1()
        let shut_type = g_shutdown_type
        if shut_type == shutdown.SHUT_NONE()
            shut_type = shutdown.SHUT_HALT()   // defensive default
        end if

        println("zyginit: sync()")
        // No direct sync() FFI yet; the stop scripts handle umount + sync as
        // part of filesystem/stop.sh. Add explicit sync in a later iteration
        // if needed.

        let fcn = shutdown.to_ad_code(shut_type)
        println("zyginit: uadmin(A_SHUTDOWN, " + int_to_str(fcn) + ", 0)")
        let rc = shutdown.do_shutdown(fcn)
        // uadmin does not return on AD_BOOT/AD_POWEROFF; for AD_HALT the
        // kernel halts the CPU. If rc >= 0 we never reach here; if rc < 0
        // something went wrong — loop forever to avoid returning from PID 1.
        println("zyginit: uadmin returned " + int_to_str(rc) + " (errno check needed)")
        while true
            clock.sleep_seconds(3600)
        end while
    end if
    // Non-PID-1 path falls through and returns from main() normally.
```

- [ ] **Step 4: Add explicit sync() FFI**

Add to `src/helpers.c`:

```c
#include <unistd.h>
void zyginit_sync(void) {
    sync();
}
```

And add a Reef extern + wrapper in `src/shutdown.reef`, inside the export block:

```reef
    proc do_sync()
```

And the implementation (before `end module`):

```reef
extern "C" proc zyginit_sync()

proc do_sync()
    zyginit_sync()
end do_sync
```

Update the Linux stub in `src/contract_linux_stubs.c` — we CAN call sync on Linux (POSIX), but to keep stubs minimal and deterministic on the dev host, add:

```c
void zyginit_sync(void) { /* sync() is POSIX but we no-op on Linux dev to keep tests deterministic */ }
```

Actually — **keep it real**. Remove the Linux stub; let both Linux and Hammerhead call the real POSIX `sync()`. So instead:

- Leave `zyginit_sync` defined only in `src/helpers.c` (portable — POSIX)
- Do NOT add a stub version in `contract_linux_stubs.c`

Now update the `main()` shutdown block to call `shutdown.do_sync()` instead of the placeholder comment:

```reef
        println("zyginit: sync()")
        shutdown.do_sync()
```

- [ ] **Step 5: Build and test**

```bash
cd /home/ctusa/repos/zyginit
clang -c src/helpers.c -o build/helpers.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/run_tests.sh 2>&1 | tail -5
```

Expected: `Results: 57 passed, 0 failed (of 57)`. Non-PID-1 code path is unchanged; tests don't trip the new uadmin branch.

- [ ] **Step 6: Commit**

```bash
hg commit -m "shutdown: PID-1 calls sync + uadmin; non-PID-1 exits normally

Adds g_shutdown_type tracking (set by signal handler, eventually also
by socket commands). At end of main():
  - PID 1: sync() then uadmin(A_SHUTDOWN, fcn, 0); fcn from shutdown type
  - non-PID-1: return from main() (existing behavior)

Also adds zyginit_sync() (POSIX sync(2)) in helpers.c, exposed via
shutdown module.

Defensive infinite loop after uadmin returns in error — prevents
kernel panic from PID 1 exit on unexpected uadmin failure."
```

---

### Task 6: Socket handler sets shutdown type for halt/reboot/poweroff

**Files:**
- Modify: `src/socket.reef`
- Modify: `src/main.reef`

zygctl will send commands like `halt\n` or `reboot\n`. The socket handler needs to map them to the appropriate `g_shutdown_type` and trigger the shutdown flow (same way SIGTERM does).

- [ ] **Step 1: Expose a setter for `g_shutdown_type` from main.reef**

In `src/main.reef`, below the variable definition, add:

```reef
// Setter for socket handler to request a specific shutdown type.
// Must be called before handle_signals sees a SIGTERM (or the socket
// handler itself triggers the shutdown via signal_pipe_notify).
proc request_shutdown(shut_type: int)
    g_shutdown_type = shut_type
    signal_pipe_notify()
end request_shutdown
```

Also export it at the top of the module so socket.reef can call it. In the `export` block of `main.reef` (if main.reef has one; if not, proc visibility may already be module-level — check existing `reload_services` export):

```reef
    proc request_shutdown(shut_type: int)
```

Alternatively, if cross-module proc calls are awkward, move the shutdown request flag into `socket.reef`'s existing reload-flag pattern. Prefer the cleaner direct call if it works.

- [ ] **Step 2: Add halt/reboot/poweroff/single/multiuser command handlers to socket.reef**

Open `src/socket.reef`. Find the command dispatch (search for `"status"` or `"start"` to locate the parser). Add new command cases:

```reef
    elif cmd == "halt"
        main.request_shutdown(shutdown.SHUT_HALT())
        return "halting\n"
    elif cmd == "reboot"
        main.request_shutdown(shutdown.SHUT_REBOOT())
        return "rebooting\n"
    elif cmd == "poweroff"
        main.request_shutdown(shutdown.SHUT_POWEROFF())
        return "powering off\n"
    elif cmd == "single"
        return "error: single-user mode not yet implemented\n"
    elif cmd == "multiuser"
        return "error: multi-user target not yet implemented\n"
```

Add imports at the top of socket.reef:

```reef
import main
import shutdown
```

If a circular import (main imports socket, socket imports main) causes build failure, move `request_shutdown` to `shutdown.reef` and have it directly touch a global, or use an indirection via a shared "shutdown_state" module. First check whether the circular import is a problem; Reef may handle it.

- [ ] **Step 3: Rebuild + test**

```bash
cd /home/ctusa/repos/zyginit
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/run_tests.sh 2>&1 | tail -5
```

Expected: 57/57 pass. New commands aren't yet exercised by existing tests.

- [ ] **Step 4: Commit**

```bash
hg commit -m "socket: accept halt/reboot/poweroff/single/multiuser commands

halt/reboot/poweroff set g_shutdown_type and kick the signal pipe to
wake the event loop, matching the SIGTERM path. single/multiuser
return a 'not implemented' error (stubs for future runlevel support)."
```

---

## Phase 2 — zygctl subcommands

### Task 7: Add halt/reboot/poweroff subcommands to zygctl

**Files:**
- Modify: `tools/zygctl/src/main.reef`

- [ ] **Step 1: Add subcommand dispatch**

Open `tools/zygctl/src/main.reef`. Find the subcommand dispatch (typically an if/elif chain matching `args.get(1)` against `"status"`, `"start"`, etc.). Add:

```reef
    elif cmd == "halt"
        send_command_and_print("halt")
    elif cmd == "reboot"
        send_command_and_print("reboot")
    elif cmd == "poweroff"
        send_command_and_print("poweroff")
    elif cmd == "single"
        println("zygctl: single-user mode not yet implemented")
        println("zygctl: reserved for future runlevel support")
        process.exit_now(1)
    elif cmd == "multiuser"
        println("zygctl: multi-user target not yet implemented")
        println("zygctl: reserved for future runlevel support")
        process.exit_now(1)
```

If a `send_command_and_print` helper doesn't exist, use the same socket-call + response-print pattern as existing commands (e.g., look at how `"reload"` is implemented and mirror it).

- [ ] **Step 2: Update help text**

Find the `help` command output (typically a `println` of each subcommand). Add the new ones in a sensible grouping:

```reef
    println("  halt              Cleanly stop all services and halt the system")
    println("  reboot            Cleanly stop all services and reboot")
    println("  poweroff          Cleanly stop all services and power off")
    println("  single            (not yet implemented) Enter single-user mode")
    println("  multiuser         (not yet implemented) Enter multi-user target")
```

- [ ] **Step 3: Build zygctl**

```bash
cd /home/ctusa/repos/zyginit/tools/zygctl
reefc build --obj build/symlink_wrapper.o
./build/zygctl help 2>&1 | grep -E "halt|reboot|poweroff|single|multiuser"
```

Expected: all five new commands visible in help output.

- [ ] **Step 4: Commit**

```bash
cd /home/ctusa/repos/zyginit
hg commit -m "zygctl: add halt/reboot/poweroff + single/multiuser stubs

halt/reboot/poweroff send the matching socket command to zyginit.
single/multiuser are explicit stubs printing 'not yet implemented';
they're in help so the command surface is stable for future expansion."
```

---

### Task 8: Integration test for clean-shutdown orchestration

**Files:**
- Create: `tests/integration/test_shutdown.sh`
- Modify: `tests/integration/run_tests.sh`

This is a new shell-based black-box integration test covering the new shutdown path. Runs zyginit in non-PID-1 mode, starts test services, issues `zygctl halt`, and verifies the expected shutdown log.

- [ ] **Step 1: Write the new test script**

Write to `tests/integration/test_shutdown.sh`:

```bash
#!/bin/sh
# tests/integration/test_shutdown.sh
# Verifies tier-reversed shutdown walk and oneshot stop command invocation.

set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
ZYGINIT="$PROJECT_DIR/build/zyginit"
ZYGCTL="$PROJECT_DIR/tools/zygctl/build/zygctl"

TEST_DIR=$(mktemp -d /tmp/zyginit-shutdown-test.XXXXXX)
trap "rm -rf $TEST_DIR; kill %1 2>/dev/null || true" EXIT

CONFIG_DIR="$TEST_DIR/config"
ENABLED_DIR="$CONFIG_DIR/enabled.d"
BIN_DIR="$TEST_DIR/bin"
LOG_DIR="$TEST_DIR/log"

mkdir -p "$CONFIG_DIR" "$ENABLED_DIR" "$BIN_DIR" "$LOG_DIR"

# Trivial oneshot: start prints "START alpha", stop prints "STOP alpha"
cat > "$BIN_DIR/alpha-start" <<EOF
#!/bin/sh
echo "START alpha" >> "$LOG_DIR/trace"
EOF
cat > "$BIN_DIR/alpha-stop" <<EOF
#!/bin/sh
echo "STOP alpha" >> "$LOG_DIR/trace"
EOF
chmod +x "$BIN_DIR/alpha-start" "$BIN_DIR/alpha-stop"

cat > "$CONFIG_DIR/alpha.toml" <<EOF
[service]
name = "alpha"
type = "oneshot"
[exec]
start = "$BIN_DIR/alpha-start"
stop = "$BIN_DIR/alpha-stop"
[restart]
on = "never"
EOF

ln -sf ../alpha.toml "$ENABLED_DIR/alpha"

# Start zyginit in background
ZYGINIT_CONFIG_DIR="$CONFIG_DIR" \
  ZYGINIT_SOCKET="$TEST_DIR/zyginit.sock" \
  ZYGINIT_LOG_DIR="$LOG_DIR/zyg" \
  "$ZYGINIT" > "$LOG_DIR/zyginit.out" 2>&1 &
ZPID=$!

# Wait for socket to appear
for i in 1 2 3 4 5; do
    [ -S "$TEST_DIR/zyginit.sock" ] && break
    sleep 1
done
[ -S "$TEST_DIR/zyginit.sock" ] || { echo "FAIL: socket never appeared"; exit 1; }

# Give alpha oneshot a moment to run
sleep 1

# Confirm START alpha ran
grep -q "START alpha" "$LOG_DIR/trace" || { echo "FAIL: alpha start didn't run"; exit 1; }

# Issue halt via zygctl
ZYGINIT_SOCKET="$TEST_DIR/zyginit.sock" "$ZYGCTL" halt

# Wait for zyginit to exit (non-PID-1 path returns from main)
for i in 1 2 3 4 5; do
    kill -0 $ZPID 2>/dev/null || break
    sleep 1
done

# Confirm STOP alpha ran during shutdown
grep -q "STOP alpha" "$LOG_DIR/trace" || {
    echo "FAIL: alpha stop didn't run during shutdown"
    echo "trace contents:"
    cat "$LOG_DIR/trace"
    exit 1
}

echo "PASS: oneshot stop command runs at shutdown"
```

- [ ] **Step 2: Wire it into `run_tests.sh`**

Open `tests/integration/run_tests.sh`. Near the end, before the final summary, add a line that sources / calls the new test. Mirror whatever pattern the existing suite uses (suite-based structure). Simplest: at the bottom, before the final `echo`:

```bash
# Shutdown orchestration tests
if bash "$SCRIPT_DIR/test_shutdown.sh" >> "$LOG_DIR/shutdown.log" 2>&1; then
    echo "  PASS: shutdown orchestration"
    PASS=$((PASS + 1))
    TOTAL=$((TOTAL + 1))
else
    echo "  FAIL: shutdown orchestration — see $LOG_DIR/shutdown.log"
    FAIL=$((FAIL + 1))
    TOTAL=$((TOTAL + 1))
fi
```

Adjust variable names to match the existing suite's counters.

- [ ] **Step 3: Make new test executable and run full suite**

```bash
chmod +x tests/integration/test_shutdown.sh
./tests/integration/run_tests.sh 2>&1 | tail -10
```

Expected: `Results: 58 passed, 0 failed (of 58)` — 57 old + 1 new shutdown test.

- [ ] **Step 4: Commit**

```bash
hg commit -m "tests: add integration test for oneshot stop at shutdown

Covers the new tier-reversed shutdown walk and exec.stop invocation
for oneshots. Uses a trivial alpha oneshot that appends 'START alpha'
/ 'STOP alpha' to a trace file. The test asserts 'STOP alpha' appears
after 'zygctl halt'."
```

---

## Phase 3 — TOML and method script authoring

All services go under `services/hammerhead/` in the repo. `scripts/install-to-be.sh` (Task 20) will copy them to `/etc/zyginit/` on the target.

### Task 9: Create `services/hammerhead/` dir and root-fs service

**Files:**
- Create: `services/hammerhead/root-fs.toml`
- Create: `services/hammerhead/root-fs/stop.sh`
- Create: `services/hammerhead/enabled.d/root-fs` (symlink)

- [ ] **Step 1: Create directory structure**

```bash
cd /home/ctusa/repos/zyginit
mkdir -p services/hammerhead/enabled.d
mkdir -p services/hammerhead/root-fs
```

- [ ] **Step 2: Write root-fs.toml**

Write to `services/hammerhead/root-fs.toml`:

```toml
# root-fs — Remount / read-write after kernel boot
#
# Tier 0: no dependencies. First oneshot to run.
# Reverses at shutdown: remount read-only before uadmin.

[service]
name = "root-fs"
description = "Remount root filesystem read-write"
type = "oneshot"

[exec]
start = "/usr/sbin/mount -o remount,rw /"
stop = "/etc/zyginit/root-fs/stop.sh"

[restart]
on = "never"
```

- [ ] **Step 3: Write root-fs/stop.sh**

Write to `services/hammerhead/root-fs/stop.sh`:

```zsh
#!/bin/zsh
# root-fs stop — remount / read-only before halt/poweroff/reboot
#
# Must run AFTER filesystem/stop.sh (which umounts non-root filesystems).
# Tier ordering guarantees this: root-fs is Tier 0, filesystem is Tier 2,
# so shutdown hits filesystem first, root-fs last.

set -e
set -u

/usr/sbin/mount -o remount,ro /

exit 0
```

- [ ] **Step 4: Make script executable, create enabled symlink**

```bash
chmod +x services/hammerhead/root-fs/stop.sh
ln -sf ../root-fs.toml services/hammerhead/enabled.d/root-fs
```

- [ ] **Step 5: Verify TOML parses via a dry-run zyginit invocation**

```bash
cd /home/ctusa/repos/zyginit
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 1
tools/zygctl/build/zygctl -s /tmp/zyg-dry.sock list 2>/dev/null || \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list
kill %1 2>/dev/null
grep -E "root-fs|error|warn" /tmp/zyg-dry.log
```

Expected: `root-fs (oneshot)` appears in `zygctl list`; no parse errors.

- [ ] **Step 6: Commit**

```bash
hg add services/hammerhead/root-fs.toml services/hammerhead/root-fs/stop.sh \
       services/hammerhead/enabled.d/root-fs
hg commit -m "services/hammerhead: add root-fs (Tier 0)

First of the 21 consolidated services. Oneshot that remounts / rw at
boot and remounts ro at shutdown (via stop.sh). No dependencies."
```

---

### Task 10: Tier 1 services — devfs, swap, crypto

**Files:**
- Create: `services/hammerhead/devfs.toml`
- Create: `services/hammerhead/swap.toml`
- Create: `services/hammerhead/swap/stop.sh`
- Create: `services/hammerhead/crypto.toml`
- Create: `services/hammerhead/crypto/start.sh`
- Create: 3 symlinks under `services/hammerhead/enabled.d/`

- [ ] **Step 1: devfs (inline, no script)**

Write `services/hammerhead/devfs.toml`:

```toml
# devfs — Populate /dev with devfsadm

[service]
name = "devfs"
description = "Dynamic /dev population"
type = "oneshot"

[exec]
start = "/usr/sbin/devfsadm"

[dependencies]
requires = ["root-fs"]

[restart]
on = "never"
```

- [ ] **Step 2: swap (inline start, stop.sh)**

Write `services/hammerhead/swap.toml`:

```toml
# swap — Enable swap devices from /etc/vfstab

[service]
name = "swap"
description = "Enable swap devices"
type = "oneshot"

[exec]
start = "/usr/sbin/swapadd -a"
stop = "/etc/zyginit/swap/stop.sh"

[dependencies]
requires = ["root-fs"]

[restart]
on = "never"
```

Write `services/hammerhead/swap/stop.sh`:

```zsh
#!/bin/zsh
# swap stop — disable all swap devices before halt/reboot

set -e
set -u

# swap -l lists active entries with the device path in column 1
# (skipping the header line). Disable each.
if /usr/sbin/swap -l >/dev/null 2>&1; then
    /usr/sbin/swap -l | awk 'NR>1 {print $1}' | while read dev; do
        /usr/sbin/swap -d "$dev" || true
    done
fi

exit 0
```

- [ ] **Step 3: crypto (start.sh)**

Write `services/hammerhead/crypto.toml`:

```toml
# crypto — Initialize Kernel Cryptographic Framework

[service]
name = "crypto"
description = "Kernel cryptographic framework"
type = "oneshot"

[exec]
start = "/etc/zyginit/crypto/start.sh"

[dependencies]
requires = ["root-fs"]

[restart]
on = "never"
```

Write `services/hammerhead/crypto/start.sh`:

```zsh
#!/bin/zsh
# crypto — configure the Kernel Cryptographic Framework
#
# The kernel crypto framework is loaded at boot via kcf module; this
# oneshot applies userland configuration (kcfd persistent state).
# cryptoadm refresh reloads the kernel policy from /etc/crypto/*.conf

set -e
set -u

/usr/sbin/cryptoadm refresh

exit 0
```

- [ ] **Step 4: Create enabled.d symlinks**

```bash
cd services/hammerhead
chmod +x swap/stop.sh crypto/start.sh
ln -sf ../devfs.toml enabled.d/devfs
ln -sf ../swap.toml enabled.d/swap
ln -sf ../crypto.toml enabled.d/crypto
```

- [ ] **Step 5: Dry-run verify**

```bash
cd /home/ctusa/repos/zyginit
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 1
ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list | head
kill %1 2>/dev/null
```

Expected: devfs, swap, crypto all appear in list; no parse errors.

- [ ] **Step 6: Commit**

```bash
hg add services/hammerhead/devfs.toml \
       services/hammerhead/swap.toml services/hammerhead/swap/stop.sh \
       services/hammerhead/crypto.toml services/hammerhead/crypto/start.sh \
       services/hammerhead/enabled.d/devfs \
       services/hammerhead/enabled.d/swap \
       services/hammerhead/enabled.d/crypto
hg commit -m "services/hammerhead: add Tier 1 (devfs, swap, crypto)"
```

---

### Task 11: Tier 2 services — filesystem, identity, sysconfig

**Files:**
- Create: `services/hammerhead/filesystem.toml`, `filesystem/start.sh`, `filesystem/stop.sh`
- Create: `services/hammerhead/identity.toml`, `identity/start.sh`
- Create: `services/hammerhead/sysconfig.toml`, `sysconfig/start.sh`
- Create: 3 symlinks

- [ ] **Step 1: filesystem**

Write `services/hammerhead/filesystem.toml`:

```toml
# filesystem — Mount all local filesystems from /etc/vfstab + ZFS

[service]
name = "filesystem"
description = "Local filesystem mounts and ZFS imports"
type = "oneshot"

[exec]
start = "/etc/zyginit/filesystem/start.sh"
stop = "/etc/zyginit/filesystem/stop.sh"

[dependencies]
requires = ["root-fs", "devfs"]

[restart]
on = "never"
```

Write `services/hammerhead/filesystem/start.sh`:

```zsh
#!/bin/zsh
# filesystem — mount local filesystems + import ZFS pools
#
# mountall -l mounts all local-type entries from /etc/vfstab (idempotent
# — skips already-mounted). zpool import -a imports any pools on attached
# disks. ZFS filesystems with mountpoint set auto-mount.

set -e
set -u

# Mount everything in /etc/vfstab with mount-at-boot = yes, excluding /
/usr/sbin/mountall -l

# Import all ZFS pools visible to the system
/usr/sbin/zpool import -a || true

# Auto-mount ZFS filesystems with canmount=on
/usr/sbin/zfs mount -a

exit 0
```

Write `services/hammerhead/filesystem/stop.sh`:

```zsh
#!/bin/zsh
# filesystem — umount everything except / before root-fs goes read-only
#
# umountall -l unmounts all local-type filesystems that are NOT /.
# ZFS pools export is handled implicitly by umount of their mountpoints.

set -e
set -u

# Best-effort: anything that can't umount (busy) is logged but doesn't
# block shutdown — ZFS in-kernel state will still checkpoint correctly.
/usr/sbin/umountall -l || true

# Explicitly sync ZFS before root goes ro
/usr/sbin/zpool sync || true

exit 0
```

- [ ] **Step 2: identity**

Write `services/hammerhead/identity.toml`:

```toml
# identity — Set hostname, domain, and hostid from /etc files

[service]
name = "identity"
description = "Hostname, domain, hostid"
type = "oneshot"

[exec]
start = "/etc/zyginit/identity/start.sh"

[dependencies]
requires = ["filesystem"]

[restart]
on = "never"
```

Write `services/hammerhead/identity/start.sh`:

```zsh
#!/bin/zsh
# identity — configure hostname, domain, hostid
#
# Reads /etc/nodename, /etc/defaultdomain, /etc/hostid. All optional —
# missing files fall back to system default. Does NOT generate missing
# hostid — that's a separate first-boot setup concern out of scope here.

set -e
set -u

if [[ -r /etc/nodename ]]; then
    /usr/bin/hostname "$(cat /etc/nodename)"
fi

if [[ -r /etc/defaultdomain ]]; then
    /usr/bin/domainname "$(cat /etc/defaultdomain)"
fi

# hostid is already loaded by the kernel from /etc/hostid; nothing to do
# unless we later want to generate it on first boot. Flag for follow-up.

exit 0
```

- [ ] **Step 3: sysconfig**

Write `services/hammerhead/sysconfig.toml`:

```toml
# sysconfig — Misc one-time boot configuration (grab-bag)

[service]
name = "sysconfig"
description = "Misc boot config: core, dump, keymap, scheduler, rctl, tmp cleanup"
type = "oneshot"

[exec]
start = "/etc/zyginit/sysconfig/start.sh"

[dependencies]
requires = ["filesystem"]

[restart]
on = "never"
```

Write `services/hammerhead/sysconfig/start.sh`:

```zsh
#!/bin/zsh
# sysconfig — misc one-time boot configuration
#
# Each command is deliberately wrapped in || true for commands where a
# specific failure (no device present, no config file, etc.) is acceptable.
# A truly mandatory setting would be its own service.

set -u
# Note: NOT using set -e here — individual failures are handled per-command.

# Core files: enable per-process pattern, apply config from /etc/coreadm.conf
/usr/bin/coreadm --init 2>/dev/null || true

# Crash dump: use configured dump device (set once per system, may be a zvol)
/usr/sbin/dumpadm -u 2>/dev/null || true

# Keyboard map from /etc/default/kbd (console only; tty layout)
if [[ -x /usr/bin/kbd ]]; then
    /usr/bin/kbd -s 2>/dev/null || true
fi

# Scheduler default dispatch table (TS = time-share, usual default)
/usr/sbin/dispadmin -d TS 2>/dev/null || true

# Resource controls (/etc/project-based rctls) — reload into kernel
if [[ -x /usr/sbin/rctladm ]]; then
    /usr/sbin/rctladm -u 2>/dev/null || true
fi

# Clean up /tmp on boot (tmpfs is usually empty after reboot but defend)
if [[ -d /tmp ]]; then
    /usr/bin/find /tmp -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
fi

exit 0
```

- [ ] **Step 4: Create symlinks, make scripts executable**

```bash
cd services/hammerhead
chmod +x filesystem/start.sh filesystem/stop.sh identity/start.sh sysconfig/start.sh
ln -sf ../filesystem.toml enabled.d/filesystem
ln -sf ../identity.toml enabled.d/identity
ln -sf ../sysconfig.toml enabled.d/sysconfig
```

- [ ] **Step 5: Dry-run verify all 7 services parse and tier correctly**

```bash
cd /home/ctusa/repos/zyginit
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 1
ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list
kill %1 2>/dev/null
grep -E "tier|error|warn" /tmp/zyg-dry.log
```

Expected: 7 services listed (root-fs, devfs, swap, crypto, filesystem, identity, sysconfig); tier output shows filesystem/identity/sysconfig in tier 2; no parse errors.

- [ ] **Step 6: Commit**

```bash
hg add services/hammerhead/filesystem.toml \
       services/hammerhead/filesystem/start.sh \
       services/hammerhead/filesystem/stop.sh \
       services/hammerhead/identity.toml \
       services/hammerhead/identity/start.sh \
       services/hammerhead/sysconfig.toml \
       services/hammerhead/sysconfig/start.sh \
       services/hammerhead/enabled.d/filesystem \
       services/hammerhead/enabled.d/identity \
       services/hammerhead/enabled.d/sysconfig
hg commit -m "services/hammerhead: add Tier 2 (filesystem, identity, sysconfig)"
```

---

### Task 12: Tier 3 — dlmgmtd, ipmgmtd, network

**Files:**
- Create: `services/hammerhead/dlmgmtd.toml`
- Create: `services/hammerhead/ipmgmtd.toml`
- Create: `services/hammerhead/network.toml`, `network/start.sh`
- Create: 3 symlinks

- [ ] **Step 1: dlmgmtd + ipmgmtd daemons (inline, no script)**

Write `services/hammerhead/dlmgmtd.toml`:

```toml
# dlmgmtd — Datalink management daemon

[service]
name = "dlmgmtd"
description = "Datalink management daemon"
type = "daemon"

[exec]
start = "/sbin/dlmgmtd"

[dependencies]
requires = ["filesystem"]

[restart]
on = "failure"
delay = 5
max_retries = 3

[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

Write `services/hammerhead/ipmgmtd.toml`:

```toml
# ipmgmtd — IP interface management daemon

[service]
name = "ipmgmtd"
description = "IP interface management daemon"
type = "daemon"

[exec]
start = "/lib/inet/ipmgmtd"

[dependencies]
requires = ["filesystem", "dlmgmtd"]

[restart]
on = "failure"
delay = 5
max_retries = 3

[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

- [ ] **Step 2: network (script — brings up lo0, physical, DHCP)**

Write `services/hammerhead/network.toml`:

```toml
# network — Bring up loopback, physical interfaces, routes
#
# Consolidates SMF's network/loopback, network/physical, network/initial,
# network/netmask, network/service, network/routing-setup. Runs DHCP
# request for interfaces configured for DHCP; ipadm create-addr -T dhcp
# implicitly spawns /sbin/dhcpagent (we don't manage it as a service).

[service]
name = "network"
description = "Bring up network interfaces and routes"
type = "oneshot"

[exec]
start = "/etc/zyginit/network/start.sh"

[dependencies]
requires = ["identity", "dlmgmtd", "ipmgmtd"]

[restart]
on = "never"
```

Write `services/hammerhead/network/start.sh`:

```zsh
#!/bin/zsh
# network — configure loopback, physical interfaces, routes
#
# Idempotent: check-before-create pattern for interfaces since
# ipadm create-if fails if the interface already exists.

set -e
set -u

# --- Loopback ---
# lo0 is auto-plumbed by the kernel on boot but ipadm wants explicit
# IP objects. Check first because create-if errors if already present.
if ! /usr/sbin/ipadm show-if lo0 >/dev/null 2>&1; then
    /usr/sbin/ipadm create-if lo0
fi
if ! /usr/sbin/ipadm show-addr lo0/v4 >/dev/null 2>&1; then
    /usr/sbin/ipadm create-addr -T static -a 127.0.0.1/8 lo0/v4
fi
if ! /usr/sbin/ipadm show-addr lo0/v6 >/dev/null 2>&1; then
    /usr/sbin/ipadm create-addr -T static -a ::1/128 lo0/v6
fi

# --- Physical interfaces ---
# For each datalink that's UP (not loopback), create an ipadm interface
# and request a DHCP lease. On hh-prototest this is typically 'vioif0'.
# Real production systems would look up /etc/hostname.<if> or similar,
# but for first-boot-test we assume one physical NIC and DHCP.
for link in $(/usr/sbin/dladm show-link -p -o link 2>/dev/null); do
    # Skip loopback and any link that's not 'up' (as reported by dladm)
    if [[ "$link" == "lo0" ]]; then
        continue
    fi
    if ! /usr/sbin/ipadm show-if "$link" >/dev/null 2>&1; then
        /usr/sbin/ipadm create-if "$link"
    fi
    if ! /usr/sbin/ipadm show-addr "$link/v4" >/dev/null 2>&1; then
        # -T dhcp implicitly spawns /sbin/dhcpagent if not already running
        /usr/sbin/ipadm create-addr -T dhcp "$link/v4" || true
    fi
done

# --- Routes ---
# Default route from DHCP is installed automatically by dhcpagent on
# lease acquisition. Static routes from /etc/inet/static_routes if present.
if [[ -r /etc/inet/static_routes ]]; then
    while read line; do
        case "$line" in
            \#*|'') continue;;
            *) /usr/sbin/route -f add $line || true;;
        esac
    done < /etc/inet/static_routes
fi

exit 0
```

- [ ] **Step 3: Symlinks + executable**

```bash
cd services/hammerhead
chmod +x network/start.sh
ln -sf ../dlmgmtd.toml enabled.d/dlmgmtd
ln -sf ../ipmgmtd.toml enabled.d/ipmgmtd
ln -sf ../network.toml enabled.d/network
```

- [ ] **Step 4: Dry-run verify**

```bash
cd /home/ctusa/repos/zyginit
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 1
ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list
kill %1 2>/dev/null
```

Expected: 10 services now listed (root-fs, devfs, swap, crypto, filesystem, identity, sysconfig, dlmgmtd, ipmgmtd, network); tiers 0-3 populated.

- [ ] **Step 5: Commit**

```bash
hg add services/hammerhead/dlmgmtd.toml \
       services/hammerhead/ipmgmtd.toml \
       services/hammerhead/network.toml \
       services/hammerhead/network/start.sh \
       services/hammerhead/enabled.d/dlmgmtd \
       services/hammerhead/enabled.d/ipmgmtd \
       services/hammerhead/enabled.d/network
hg commit -m "services/hammerhead: add Tier 3 (dlmgmtd, ipmgmtd, network)"
```

---

### Task 13: Tier 4 daemons (7 files)

**Files:**
- Create: 7 TOML files for `syseventd`, `fmd`, `syslogd`, `utmpd`, `pfexecd`, `powerd`, `rpcbind`
- Create: 7 symlinks

All follow the same daemon template. Writing them in one shot.

- [ ] **Step 1: Write all 7 TOMLs**

For each service, write the file below. Paths come from the `ps -ef` capture we have from hh-prototest.

`services/hammerhead/syseventd.toml`:
```toml
[service]
name = "syseventd"
description = "Sysevent framework daemon"
type = "daemon"
[exec]
start = "/usr/lib/sysevent/syseventd"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/fmd.toml`:
```toml
[service]
name = "fmd"
description = "Fault Manager Daemon"
type = "daemon"
[exec]
start = "/usr/lib/fm/fmd/fmd"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/syslogd.toml`:
```toml
[service]
name = "syslogd"
description = "System log daemon"
type = "daemon"
[exec]
start = "/usr/sbin/syslogd"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/utmpd.toml`:
```toml
[service]
name = "utmpd"
description = "utmpx monitor daemon"
type = "daemon"
[exec]
start = "/usr/lib/utmpd"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/pfexecd.toml`:
```toml
[service]
name = "pfexecd"
description = "Profile exec daemon (RBAC privilege execution)"
type = "daemon"
[exec]
start = "/usr/lib/pfexecd"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/powerd.toml`:
```toml
[service]
name = "powerd"
description = "Power management daemon"
type = "daemon"
[exec]
start = "/usr/lib/power/powerd"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/rpcbind.toml`:
```toml
[service]
name = "rpcbind"
description = "RPC portmapper"
type = "daemon"
[exec]
start = "/usr/sbin/rpcbind"
[dependencies]
requires = ["network"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

- [ ] **Step 2: Create symlinks**

```bash
cd services/hammerhead
for s in syseventd fmd syslogd utmpd pfexecd powerd rpcbind; do
    ln -sf ../$s.toml enabled.d/$s
done
```

- [ ] **Step 3: Dry-run verify**

```bash
cd /home/ctusa/repos/zyginit
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 1
ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list | wc -l
kill %1 2>/dev/null
```

Expected: 17 services listed (10 prior + 7 new).

- [ ] **Step 4: Commit**

```bash
hg add services/hammerhead/{syseventd,fmd,syslogd,utmpd,pfexecd,powerd,rpcbind}.toml \
       services/hammerhead/enabled.d/{syseventd,fmd,syslogd,utmpd,pfexecd,powerd,rpcbind}
hg commit -m "services/hammerhead: add Tier 4 daemons (syseventd, fmd, syslogd, utmpd, pfexecd, powerd, rpcbind)"
```

---

### Task 14: Tier 5 daemons — cron, inetd, sshd, console-login

**Files:**
- Create: 4 TOML files
- Create: 4 symlinks

- [ ] **Step 1: Write the 4 TOMLs**

`services/hammerhead/cron.toml`:
```toml
[service]
name = "cron"
description = "cron daemon"
type = "daemon"
[exec]
start = "/usr/sbin/cron"
[dependencies]
requires = ["syslogd"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/inetd.toml`:
```toml
[service]
name = "inetd"
description = "Internet services daemon"
type = "daemon"
[exec]
start = "/usr/lib/inet/inetd start"
[dependencies]
requires = ["rpcbind", "syslogd"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/sshd.toml`:
```toml
[service]
name = "sshd"
description = "Secure Shell daemon"
type = "daemon"
[exec]
start = "/usr/lib/ssh/sshd -D"
[dependencies]
requires = ["syslogd", "pfexecd"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

`services/hammerhead/console-login.toml`:
```toml
[service]
name = "console-login"
description = "Console login (ttymon on /dev/console)"
type = "daemon"
[exec]
start = "/usr/lib/saf/ttymon -g -d /dev/console -l console -m ldterm,ttcompat -h -p 'hammerhead console login: '"
[dependencies]
requires = ["syslogd", "utmpd"]
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
```

- [ ] **Step 2: Symlinks**

```bash
cd services/hammerhead
for s in cron inetd sshd console-login; do
    ln -sf ../$s.toml enabled.d/$s
done
```

- [ ] **Step 3: Full dry-run verify — all 21 services**

```bash
cd /home/ctusa/repos/zyginit
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 2
ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list | wc -l
grep -E "tier|error|warn|cycle" /tmp/zyg-dry.log
kill %1 2>/dev/null
```

Expected:
- `wc -l` output = 21
- Log shows 6 tiers (0-5), no cycle detection errors, no warnings
- No missing-dependency warnings (every `requires` resolves)

- [ ] **Step 4: Commit**

```bash
hg add services/hammerhead/{cron,inetd,sshd,console-login}.toml \
       services/hammerhead/enabled.d/{cron,inetd,sshd,console-login}
hg commit -m "services/hammerhead: add Tier 5 (cron, inetd, sshd, console-login)

All 21 zyginit services now authored. Dry-run parse confirms the full
dependency graph resolves into 6 tiers with no cycles."
```

---

## Phase 4 — Deployment tooling

### Task 15: Write install-to-be.sh

**Files:**
- Create: `scripts/install-to-be.sh`

- [ ] **Step 1: Write the installer script**

Write to `scripts/install-to-be.sh`:

```bash
#!/bin/sh
# install-to-be.sh — Install zyginit binaries + config tree into a
# mounted Boot Environment (or any target root).
#
# Usage: install-to-be.sh <target-root>
#
#   e.g., after `beadm create zyginit && beadm mount zyginit /mnt`:
#       install-to-be.sh /mnt
#
# Copies:
#   /sbin/zyginit (built binary)  → <target>/sbin/zyginit
#   /sbin/zygctl  (built binary)  → <target>/sbin/zygctl
#   services/hammerhead/*.toml    → <target>/etc/zyginit/*.toml
#   services/hammerhead/<svc>/    → <target>/etc/zyginit/<svc>/
#   services/hammerhead/enabled.d → <target>/etc/zyginit/enabled.d/
#
# Preserves existing <target>/sbin/init as <target>/sbin/init.smf and
# replaces it with a symlink to /sbin/zyginit.

set -e
set -u

if [ $# -ne 1 ]; then
    echo "usage: $0 <target-root>" >&2
    exit 1
fi

TARGET="$1"
REPO="$(cd "$(dirname "$0")/.." && pwd)"

if [ ! -d "$TARGET" ]; then
    echo "error: $TARGET does not exist or is not a directory" >&2
    exit 1
fi

if [ ! -x "$REPO/build/zyginit" ]; then
    echo "error: $REPO/build/zyginit not built yet" >&2
    exit 1
fi

if [ ! -x "$REPO/tools/zygctl/build/zygctl" ]; then
    echo "error: $REPO/tools/zygctl/build/zygctl not built yet" >&2
    exit 1
fi

echo "installing zyginit into $TARGET..."

# Binaries
install -m 0755 "$REPO/build/zyginit" "$TARGET/sbin/zyginit"
install -m 0755 "$REPO/tools/zygctl/build/zygctl" "$TARGET/sbin/zygctl"

# Config tree
mkdir -p "$TARGET/etc/zyginit"
mkdir -p "$TARGET/etc/zyginit/enabled.d"

# Copy TOMLs (flat files at top of services/hammerhead/)
for t in "$REPO"/services/hammerhead/*.toml; do
    cp "$t" "$TARGET/etc/zyginit/"
done

# Copy per-service script subdirs (root-fs/, swap/, crypto/, filesystem/,
# identity/, sysconfig/, network/) — anything that exists gets copied
for d in "$REPO"/services/hammerhead/*/; do
    [ "$(basename "$d")" = "enabled.d" ] && continue
    dname="$(basename "$d")"
    mkdir -p "$TARGET/etc/zyginit/$dname"
    cp -r "$d"* "$TARGET/etc/zyginit/$dname/" 2>/dev/null || true
    chmod -R +x "$TARGET/etc/zyginit/$dname"
done

# Symlinks in enabled.d (recreate since cp doesn't preserve across fs)
for l in "$REPO"/services/hammerhead/enabled.d/*; do
    name="$(basename "$l")"
    ln -sf "../$name.toml" "$TARGET/etc/zyginit/enabled.d/$name"
done

# Runtime dirs
mkdir -p "$TARGET/var/run/zyginit/log"

# Replace /sbin/init
if [ -e "$TARGET/sbin/init" ] && [ ! -L "$TARGET/sbin/init" ]; then
    # Preserve original (won't overwrite if already preserved)
    if [ ! -e "$TARGET/sbin/init.smf" ]; then
        mv "$TARGET/sbin/init" "$TARGET/sbin/init.smf"
        echo "  preserved original /sbin/init as /sbin/init.smf"
    fi
fi
ln -sf /sbin/zyginit "$TARGET/sbin/init"
echo "  installed /sbin/init → /sbin/zyginit"

echo "zyginit install complete in $TARGET"
echo ""
echo "verify:"
echo "  ls -la $TARGET/sbin/init"
echo "  ls $TARGET/etc/zyginit/enabled.d/"
```

- [ ] **Step 2: Make executable and add to repo**

```bash
cd /home/ctusa/repos/zyginit
chmod +x scripts/install-to-be.sh
hg add scripts/install-to-be.sh
```

- [ ] **Step 3: Commit**

```bash
hg commit -m "scripts/install-to-be.sh: deploy zyginit + hammerhead service set to a target root

Takes a target directory (typically a mounted BE) and installs:
  - /sbin/zyginit, /sbin/zygctl binaries
  - /etc/zyginit/ config tree (TOMLs + per-service script dirs + enabled.d)
  - /var/run/zyginit/log/ runtime dir
  - /sbin/init symlinked to /sbin/zyginit (old init preserved as init.smf)

Used by Phase B to populate a fresh Boot Environment."
```

---

## Phase 5 — Phase A non-PID-1 dry run on hh-prototest

### Task 16: Ship code + configs to hh-prototest

**Files:**
- N/A (operational only)

- [ ] **Step 1: Rebuild zyginit on Hammerhead**

```bash
cd /home/ctusa/repos/zyginit
tar --exclude='*/build/*' --exclude='.hg' -czf - \
    src services/hammerhead scripts reef.toml tools 2>/dev/null | \
  ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 \
    "cd /root/zyginit && tar -xzf -"

ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 \
  'cd /root/zyginit && gcc -c src/helpers.c -o build/helpers.o && reefc build -l contract --obj build/helpers.o 2>&1 | tail -3'
```

Expected: Build clean on Hammerhead.

- [ ] **Step 2: Rebuild zygctl on Hammerhead**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 \
  'cd /root/zyginit/tools/zygctl && reefc build 2>&1 | tail -3'
```

Expected: Build clean (zygctl doesn't need -l contract).

- [ ] **Step 3: Stage configs to /tmp/zyginit-dry on VM**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
set -e
rm -rf /tmp/zyginit-dry
cp -r /root/zyginit/services/hammerhead /tmp/zyginit-dry
ls /tmp/zyginit-dry/enabled.d/ | wc -l
EOF
```

Expected: 21 symlinks listed.

- [ ] **Step 4: Commit (nothing to commit; operational step)**

No commit. Continue.

---

### Task 17: Dry-run parse + dependency graph verification

**Files:**
- N/A (operational + diagnostic)

- [ ] **Step 1: Start zyginit in non-PID-1 mode on hh-prototest with the real TOML set**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
mkdir -p /tmp/zyg-run
ZYGINIT_CONFIG_DIR=/tmp/zyginit-dry \
  ZYGINIT_SOCKET=/tmp/zyg-run/zyginit.sock \
  ZYGINIT_LOG_DIR=/tmp/zyg-run/log \
  /root/zyginit/build/zyginit > /tmp/zyg-run/zyginit.out 2>&1 &
echo "started PID=$!"
sleep 3
ZYGINIT_SOCKET=/tmp/zyg-run/zyginit.sock /root/zyginit/tools/zygctl/build/zygctl list
echo "--- boot order tiers from zyginit log ---"
grep -E "tier [0-9]+" /tmp/zyg-run/zyginit.out
echo "--- errors / warnings ---"
grep -iE "error|warn|fail|cycle" /tmp/zyg-run/zyginit.out || echo "(none)"
EOF
```

Expected:
- `zygctl list` shows all 21 services
- 6 tiers (0-5) enumerated
- No errors / warnings

**STOP IF:** parse errors, cycle detection, missing dependency messages appear. Debug the TOMLs before proceeding.

- [ ] **Step 2: Start a test daemon to verify supervision on Hammerhead**

The real services would conflict with SMF's running copies. Use a scratch test service:

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
mkdir -p /tmp/zyg-test/enabled.d
cat > /tmp/zyg-test/watchdog.toml <<TOML
[service]
name = "watchdog"
type = "daemon"
[exec]
start = "/bin/sleep 300"
[restart]
on = "failure"
delay = 2
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
TOML
ln -sf ../watchdog.toml /tmp/zyg-test/enabled.d/watchdog

# Stop any previous dry-run zyginit
pkill -f '/root/zyginit/build/zyginit' 2>/dev/null
sleep 1
rm -rf /tmp/zyg-run
mkdir -p /tmp/zyg-run

ZYGINIT_CONFIG_DIR=/tmp/zyg-test \
  ZYGINIT_SOCKET=/tmp/zyg-run/zyginit.sock \
  ZYGINIT_LOG_DIR=/tmp/zyg-run/log \
  /root/zyginit/build/zyginit > /tmp/zyg-run/zyginit.out 2>&1 &
sleep 3

echo "--- initial status ---"
ZYGINIT_SOCKET=/tmp/zyg-run/zyginit.sock /root/zyginit/tools/zygctl/build/zygctl status

# Kill the sleep — should trigger restart
WPID=$(pgrep -f '/bin/sleep 300' | head -1)
echo "killing watchdog PID $WPID"
kill $WPID
sleep 3

echo "--- after kill (should show restart count 1) ---"
ZYGINIT_SOCKET=/tmp/zyg-run/zyginit.sock /root/zyginit/tools/zygctl/build/zygctl status

# Clean halt
ZYGINIT_SOCKET=/tmp/zyg-run/zyginit.sock /root/zyginit/tools/zygctl/build/zygctl halt
sleep 2

echo "--- zyginit output ---"
cat /tmp/zyg-run/zyginit.out
EOF
```

Expected:
- First status: watchdog RUNNING
- After kill: restart count = 1, watchdog back to RUNNING (or WAITING briefly)
- Halt cleanly exits zyginit, output shows "stopping all services" → "shutdown complete"
- NO uadmin call (non-PID-1 path)

- [ ] **Step 3: Commit (nothing to commit; operational)**

No commit. Document results in the team report.

---

## Phase 6 — Phase B PID-1 boot in a fresh BE

### Task 18: Create BE and install zyginit

**Files:**
- N/A (operational)

- [ ] **Step 1: Verify all pre-reqs installed on VM**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
echo "--- zyginit binary ---"
ls -la /root/zyginit/build/zyginit
echo "--- zygctl binary ---"
ls -la /root/zyginit/tools/zygctl/build/zygctl
echo "--- service set ---"
ls /root/zyginit/services/hammerhead/*.toml | wc -l
echo "--- install script ---"
ls -la /root/zyginit/scripts/install-to-be.sh
EOF
```

Expected: 21 TOMLs, both binaries, install script present.

- [ ] **Step 2: Create and mount the new BE**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
set -e
beadm create zyginit
beadm mount zyginit /mnt
beadm list
EOF
```

Expected: BE `zyginit` created, mounted at /mnt. `beadm list` shows it mounted but not active.

- [ ] **Step 3: Run the installer into /mnt**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 \
  'cd /root/zyginit && ./scripts/install-to-be.sh /mnt'
```

Expected output:
```
installing zyginit into /mnt...
  preserved original /sbin/init as /sbin/init.smf
  installed /sbin/init → /sbin/zyginit
zyginit install complete in /mnt
```

- [ ] **Step 4: Sanity-check the install**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
ls -la /mnt/sbin/init /mnt/sbin/init.smf /mnt/sbin/zyginit /mnt/sbin/zygctl
ls /mnt/etc/zyginit/enabled.d/ | wc -l
ls /mnt/etc/zyginit/*.toml | wc -l
EOF
```

Expected:
- `/mnt/sbin/init` is a symlink to `/sbin/zyginit`
- `/mnt/sbin/init.smf` is the original init binary
- 21 enabled.d symlinks
- 21 TOML files

- [ ] **Step 5: Umount and activate the BE**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
beadm umount zyginit
beadm activate zyginit
beadm list
EOF
```

Expected: `zyginit` BE shows as `R` (activated for reboot). `hammerhead` BE shows as `N` (currently running, not active for reboot).

---

### Task 19: Boot into zyginit BE and run success checklist

**Files:**
- N/A (operational + validation)

- [ ] **Step 1: Open a console session on the dev host (keep ssh open too)**

From a second terminal on the dev host:
```bash
virsh --connect=qemu:///system console hh-prototest
```

Leave this open; it's our diagnostic channel if zyginit wedges.

- [ ] **Step 2: Reboot into the zyginit BE**

From the existing ssh session:
```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 'init 6'
```

Watch the virsh console for boot output. Expected sequence (from zyginit):
- `zyginit v0.1.0 starting`
- `zyginit: running as PID 1 (init mode)`
- Tier-by-tier service startup
- `zyginit: entering event loop`

**If boot wedges:** reboot the VM, pick `hammerhead` from GRUB menu on reboot. Debug from there.

- [ ] **Step 3: SSH in and run success checklist (§9.A-D of spec)**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
echo "--- A. boot complete ---"
uptime
ps -ef | head -1
ps -ef | grep zyginit | grep -v grep

echo "--- B. service states ---"
/sbin/zygctl list
echo "---"
/sbin/zygctl status

echo "--- C. functional tests ---"
dladm show-link
ipadm show-addr
ping -c 2 192.168.122.1 || echo "ping failed"
zfs list
logger -p user.info "zyginit-first-boot marker"
tail -5 /var/adm/messages || echo "messages unavailable"

echo "--- D. zygctl end-to-end ---"
/sbin/zygctl status cron
/sbin/zygctl stop cron
/sbin/zygctl status cron
/sbin/zygctl start cron
/sbin/zygctl status cron
/sbin/zygctl log sshd | head -5
EOF
```

Check each line against the spec's §9 checklist. Record results.

- [ ] **Step 4: Document pass/fail**

Write results to `docs/superpowers/specs/2026-04-24-pid1-boot-results.md` (new file):

```markdown
# PID-1 Boot — Phase B Test Results

**Date:** <actual date>
**BE:** `zyginit`
**Host:** hh-prototest (192.168.122.197)

## Success Criteria (from spec §9)

### A. Boot completes
- [x/ ] No kernel panic
- [x/ ] zyginit event loop entered
- [x/ ] All tiers completed within 60s
- [x/ ] Console login prompt appears

### B. Service states
- (fill in)

### C. Functional tests
- (fill in)

### D. zygctl operations
- (fill in)
```

Commit this file regardless of outcome — it's the test log.

---

### Task 20: Test clean shutdown (halt, reboot, poweroff)

**Files:**
- N/A (operational)

- [ ] **Step 1: Test zygctl halt**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 '/sbin/zygctl halt'
```

Watch console. Expected:
- Reverse-tier shutdown log (`tier 5 → 4 → 3 → 2 → 1 → 0`)
- Daemons stop via contract
- Oneshot stop scripts run (swap -d, filesystem umountall, root-fs remount ro)
- `zyginit: uadmin(A_SHUTDOWN, 0, 0)` logged
- VM halts; `virsh list` shows `running` but kernel is halted (CPU stopped)

If the VM remains in `running` state but unresponsive, that's AD_HALT behavior — expected.

- [ ] **Step 2: Forced reboot from host, reboot into zyginit BE again**

```bash
virsh --connect=qemu:///system destroy hh-prototest   # force power-off since halt has no kernel running
virsh --connect=qemu:///system start hh-prototest
```

Wait for it to come up. Should boot back into zyginit BE (it's still activated).

- [ ] **Step 3: Test zygctl reboot**

```bash
ssh ... 'uname -a; /sbin/zygctl reboot'
```

Expected:
- Reverse-tier shutdown
- `uadmin(AD_BOOT)` logged
- VM reboots automatically
- Comes back up through zyginit BE (still active)

- [ ] **Step 4: Test zygctl poweroff**

```bash
ssh ... '/sbin/zygctl poweroff'
```

Expected:
- Reverse-tier shutdown
- `uadmin(AD_POWEROFF)` logged
- VM transitions to `shut off` in libvirt (`virsh list --all`)

- [ ] **Step 5: Check ZFS pool clean after each shutdown type**

```bash
virsh start hh-prototest
# wait for boot
ssh ... 'zpool status -x'
```

Expected: `all pools are healthy`. No `replay required`, no degraded devices.

- [ ] **Step 6: Append results to the test results file and commit**

Update `docs/superpowers/specs/2026-04-24-pid1-boot-results.md` with §E checklist outcomes.

```bash
hg commit docs/superpowers/specs/2026-04-24-pid1-boot-results.md \
    -m "docs: Phase B PID-1 boot test results — shutdown path"
```

---

### Task 21: Test rollback paths

**Files:**
- N/A (operational)

- [ ] **Step 1: Boot back into the original `hammerhead` BE from GRUB**

Reboot the VM, intercept the GRUB menu via `virsh console`, select `hammerhead` BE.

Expected: SMF-based boot. `svcs -a | wc -l` returns ~142 (original count). Everything working as before zyginit.

- [ ] **Step 2: Switch back to zyginit BE**

From within the `hammerhead` BE:
```bash
ssh ... 'beadm list'         # verify zyginit BE still present
ssh ... 'beadm activate zyginit && init 6'
```

Expected: reboots back into zyginit BE.

- [ ] **Step 3: Disk-level restore drill**

Shut down the VM, restore the pre-change disk, verify it boots normally, then restore the experimental disk back.

```bash
virsh --connect=qemu:///system shutdown hh-prototest   # or force destroy if needed
# wait for shut off
sudo cp /var/lib/libvirt/images/hh-prototest-backup-2026-04-24-pre-zyginit-pid1.qcow2 \
        /tmp/hh-prototest-experimental.qcow2
sudo cp /var/lib/libvirt/images/hh-prototest.qcow2 \
        /var/lib/libvirt/images/hh-prototest-experimental.qcow2.bak
sudo cp /var/lib/libvirt/images/hh-prototest-backup-2026-04-24-pre-zyginit-pid1.qcow2 \
        /var/lib/libvirt/images/hh-prototest.qcow2
virsh start hh-prototest
# wait for boot — should be the pre-zyginit state
ssh ... 'uptime; beadm list'
```

Expected: VM is in pre-zyginit state (only `hammerhead` BE present, no `zyginit` BE).

- [ ] **Step 4: Restore the experimental disk**

```bash
virsh --connect=qemu:///system shutdown hh-prototest
# wait for shut off
sudo cp /var/lib/libvirt/images/hh-prototest-experimental.qcow2.bak \
        /var/lib/libvirt/images/hh-prototest.qcow2
virsh start hh-prototest
```

Expected: back to the zyginit + hammerhead BE state.

- [ ] **Step 5: Record rollback drill results in the results file and commit**

Update `docs/superpowers/specs/2026-04-24-pid1-boot-results.md` with §F checklist outcomes. Commit.

---

## Phase 6.5 — BSD-pivot v2 service set (after iter 5 hit configd wall)

**Context:** Iter 1-5 of Task 19 (revs 24–50) exec'd the v1 21-service set on hh-prototest and surfaced that `dlmgmtd`, `ipmgmtd`, `inetd`, `rpcbind`, and `fmd` are tightly coupled to `svc.configd` via `libscf`. Setting `SMF_FMRI` in the env bypasses their refusal-to-start banner, but `scf_handle_decode_fmri()` still fails without `/etc/svc/volatile/repository_door`, leaving the daemons running but degraded — `ipmgmtd` doesn't open its door socket, `ipadm` errors with "Could not open handle to library", `lo0` is never plumbed, `rpcbind` cascades into "could not find loopback transports", and the entire network stack stays dead.

**Pivot:** Spec §1.1 (revision 2026-04-25) drops the 5 configd-coupled daemons and moves the network tier to direct `ifconfig` invocation, BSD-style — matching IRIX 6.5's `/etc/init.d/network` pattern (`/sbin/ifconfig` plumbs interfaces directly via DLPI, no `ipmgmtd` registry needed; verified on hh-prototest in spec §13). The new default service count is **15**: tier 0 root-fs (1) → tier 1 devfs/swap/crypto (3) → tier 2 filesystem/identity/sysconfig (3) → tier 3 network (1) → tier 4 syseventd/syslogd/utmpd/pfexecd (4) → tier 5 cron/sshd/console-login (3).

**This phase produces the diff** against the on-disk v1 service set: 4 `enabled.d/` symlinks dropped, `network.toml` deps loosened, `network/start.sh` rewritten as a `/etc/hostname.<if>` parser, new `network/stop.sh`, sample interface-config files, then iter 6 boot test against the slimmed set.

**Carry-forwards from iter 1-5 already on disk** (do not redo): rev 43 (root-fs ZFS dispatch), rev 44 (argv[0] supervisor fix), rev 45 (PID-1 fd setup), rev 47 (tier-ordering enforcement), rev 48 (persistent log dir), rev 49 (path corrections for `/bin/hostname`, `/sbin/swapadd`), rev 50 (cron FIFO at `/etc/cron.d/FIFO`).

### Task 22: Drop configd-coupled symlinks from `enabled.d/`

**Files:**
- Delete: `services/hammerhead/enabled.d/dlmgmtd`
- Delete: `services/hammerhead/enabled.d/ipmgmtd`
- Delete: `services/hammerhead/enabled.d/inetd`
- Delete: `services/hammerhead/enabled.d/rpcbind`

The TOML files themselves stay in `services/hammerhead/` — they're now opt-in via `zygctl enable <svc>` per spec §4.7. (`fmd` and `powerd` already have no symlink as of iter 5; nothing to do for those.)

- [ ] **Step 1: Verify pre-state**

Run: `ls services/hammerhead/enabled.d/ | wc -l`
Expected: `19`

Run: `ls services/hammerhead/enabled.d/ | sort`
Expected to include `dlmgmtd`, `ipmgmtd`, `inetd`, `rpcbind` among the 19.

- [ ] **Step 2: Remove the four symlinks**

```bash
hg remove services/hammerhead/enabled.d/dlmgmtd \
         services/hammerhead/enabled.d/ipmgmtd \
         services/hammerhead/enabled.d/inetd \
         services/hammerhead/enabled.d/rpcbind
```

- [ ] **Step 3: Verify post-state**

Run: `ls services/hammerhead/enabled.d/ | wc -l`
Expected: `15`

Run: `ls services/hammerhead/enabled.d/ | sort`
Expected (alphabetical): `console-login cron crypto devfs filesystem identity network pfexecd root-fs sshd swap sysconfig syseventd syslogd utmpd`

- [ ] **Step 4: Commit**

```bash
hg commit -m "services/hammerhead: drop configd-coupled daemons from default enabled set

Iter 1-5 found that dlmgmtd/ipmgmtd/inetd/rpcbind require svc.configd
via libscf for property reads. Without configd, scf_handle_decode_fmri()
fails and the daemons silently degrade (ipmgmtd's door never opens, ipadm
errors out, lo0 never plumbs, rpcbind cascades). Spec §1.1 v2 BSD-pivot
drops them in favor of direct ifconfig invocation. TOMLs retained for
opt-in via zygctl enable."
```

---

### Task 23: Update `network.toml` to drop dlmgmtd/ipmgmtd dependencies

**Files:**
- Modify: `services/hammerhead/network.toml`

`network.toml` currently lists `requires = ["identity", "dlmgmtd", "ipmgmtd"]`. After Task 22 those services aren't enabled, so the dependency edges would dangle. The replacement also drops the v1 comment block referring to `ipadm create-addr`.

- [ ] **Step 1: Replace the file contents**

Write `services/hammerhead/network.toml`:

```toml
# network — Bring up loopback, physical interfaces, routes (BSD-pivot v2)
#
# Direct /sbin/ifconfig invocation, no ipmgmtd or dlmgmtd. Reads
# /etc/hostname.<if> for each physical interface (line-oriented format
# documented in start.sh) and /etc/defaultrouter for static default
# routes. DHCP is handled via `ifconfig <if> dhcp start`, which forks
# /sbin/dhcpagent directly.
#
# Replaces the v1 SMF stack (network/loopback, network/physical,
# network/initial, network/netmask, network/service, network/routing-setup,
# dlmgmtd, ipmgmtd) with a single oneshot using only DLPI primitives.

[service]
name = "network"
description = "Bring up network interfaces and routes (BSD-style)"
type = "oneshot"

[exec]
start = "/etc/zyginit/network/start.sh"
stop = "/etc/zyginit/network/stop.sh"

[dependencies]
requires = ["identity"]

[restart]
on = "never"
```

- [ ] **Step 2: Verify TOML still parses (Linux dry-run)**

Run from repo root:
```bash
ZYGINIT_CONFIG_DIR=$PWD/services/hammerhead \
  ZYGINIT_SOCKET=/tmp/zyg-dry.sock \
  ./build/zyginit > /tmp/zyg-dry.log 2>&1 &
sleep 1
ZYGINIT_SOCKET=/tmp/zyg-dry.sock tools/zygctl/build/zygctl list | grep network
kill %1 2>/dev/null
```

Expected: line of form `network <tier> <state> ...` printed; no `requires unknown service` errors in `/tmp/zyg-dry.log`.

- [ ] **Step 3: Commit**

```bash
hg commit services/hammerhead/network.toml \
    -m "services/hammerhead/network: drop dlmgmtd/ipmgmtd deps; declare stop script"
```

---

### Task 24: Rewrite `network/start.sh` as `/etc/hostname.<if>` parser

**Files:**
- Modify: `services/hammerhead/network/start.sh` (full rewrite)

The v1 script used `ipadm create-if` / `ipadm create-addr` which both go through `ipmgmtd`. The v2 script invokes `/sbin/ifconfig` directly — operates at DLPI/kernel-ioctl layer, no daemon registry needed (verified in spec §13). Line forms supported (per spec §4.4):

| Line | Action |
|---|---|
| (empty/whitespace-only file) | `ifconfig <if> plumb up` |
| `# …` or blank line | skipped |
| `dhcp` | `ifconfig <if> plumb`; `ifconfig <if> dhcp start` |
| `inet <addr>/<prefix>` | `ifconfig <if> inet <addr>/<prefix> up` |
| `inet <addr> netmask <mask>` | `ifconfig <if> inet <addr> netmask <mask> up` |
| `addif <addr>/<prefix>` | additional alias address (`ifconfig <if> addif`) |
| `inet6 <addr>/<prefix>` | `ifconfig <if> inet6 <addr>/<prefix> up` |
| `up` / `down` | explicit link state |

- [ ] **Step 1: Replace the file contents**

Write `services/hammerhead/network/start.sh`:

```zsh
#!/bin/zsh
# network/start.sh — BSD-style interface/route bring-up (v2)
#
# No ipmgmtd, no dlmgmtd. Uses /sbin/ifconfig directly for plumbing,
# /sbin/dhcpagent (forked by ifconfig dhcp start) for DHCP, and
# /usr/sbin/route for static routes from /etc/defaultrouter.
#
# Walks /etc/hostname.<if> files (one per interface to configure). Each
# file is line-oriented; see the case statement below for supported syntax.

set -e
set -u

# Enable [[:space:]]## "one or more" repetition in pattern matching.
setopt EXTENDED_GLOB

IFCONFIG=/sbin/ifconfig
ROUTE=/usr/sbin/route

# --- Force-load IP STREAMS modules ---
# zyginit's tier ordering runs network/start.sh before SMF's traditional
# autoload triggers fire, so socket(PF_INET) returns EAFNOSUPPORT until
# the ip / ip6 drivers are explicitly loaded. modload is idempotent:
# returns success on already-loaded modules.
/sbin/modload /kernel/drv/ip || true
/sbin/modload /kernel/drv/ip6 || true

# --- Loopback ---
# Plumb lo0 explicitly (the kernel auto-attaches it but we want predictable
# state). `ifconfig lo0 plumb` is idempotent — repeats just succeed.
$IFCONFIG lo0 plumb || true
$IFCONFIG lo0 inet 127.0.0.1 netmask 255.0.0.0 up || true
$IFCONFIG lo0 inet6 ::1/128 up || true

# --- Physical interfaces ---
# Each /etc/hostname.<if> file marks an interface to configure (BSD style).
# The configure_interface() helper plumbs the named link and applies the
# directives in the file.
configure_interface() {
    local iface="$1"
    local cfgfile="/etc/hostname.${iface}"

    if [[ ! -e "$cfgfile" ]]; then
        # No config — leave the interface untouched. (Operator can plumb
        # by-hand or add a config file and re-run; this matches OpenBSD.)
        return 0
    fi

    # Plumb first; downstream lines may add addresses to it.
    $IFCONFIG "$iface" plumb || true

    # If the config file has no non-comment content, just bring it up.
    if ! grep -Eq '^[[:space:]]*[^#[:space:]]' "$cfgfile" 2>/dev/null; then
        $IFCONFIG "$iface" up
        return 0
    fi

    # Walk the file line by line.
    while IFS= read -r line || [[ -n "$line" ]]; do
        # Strip comments, then leading and trailing whitespace.
        # `[[:space:]]##` is zsh extended glob for "one or more whitespace
        # chars" — covers tab-indented lines and multi-space margins that
        # an editor might insert. EXTENDED_GLOB is enabled at script top;
        # the `\#` escape is needed because EXTENDED_GLOB makes bare `#` a
        # pattern operator.
        line="${line%%\#*}"
        line="${line##[[:space:]]##}"
        line="${line%%[[:space:]]##}"
        [[ -z "$line" ]] && continue

        # Split on first whitespace into verb + rest.
        local verb="${line%%[[:space:]]*}"
        local rest=""
        if [[ "$line" == *[[:space:]]* ]]; then
            rest="${line#*[[:space:]]}"
        fi

        # ${=rest} forces word-splitting on $rest — necessary because
        # ifconfig args like `192.168.122.50/24 up` need to arrive as
        # separate arguments, not one quoted string.
        case "$verb" in
            dhcp)
                $IFCONFIG "$iface" dhcp start
                ;;
            inet)
                # `inet <addr>/<prefix>` or `inet <addr> netmask <mask>`
                $IFCONFIG "$iface" inet ${=rest} up
                ;;
            addif)
                $IFCONFIG "$iface" addif ${=rest} up
                ;;
            inet6)
                $IFCONFIG "$iface" inet6 ${=rest} up
                ;;
            up|down)
                $IFCONFIG "$iface" "$verb"
                ;;
            *)
                print -u2 "network: ignoring unknown directive in $cfgfile: $line"
                ;;
        esac
    done < "$cfgfile"
}

# Iterate /etc/hostname.<if> files — the BSD-style marker for "configure
# this interface." Iter 7 boot showed /dev/net/ enumeration silently
# skipped vioif0 on Hammerhead (the directory either doesn't exist or
# doesn't contain DLPI link nodes for virtio NICs); switching to
# hostname.* lookup is both more portable and closer to OpenBSD's
# original semantics.
for cfg in /etc/hostname.*(N); do
    fname="${cfg:t}"
    # ${cfg:t} returns "hostname.vioif0"; strip the prefix.
    iface="${fname#hostname.}"
    # Defensively skip loopback (we already handled lo0 above).
    [[ "$iface" == "lo0" ]] && continue
    configure_interface "$iface"
done

# --- Static default routes ---
# /etc/defaultrouter is one IPv4 address per line. DHCP-acquired routes
# don't need this file; only set one when the operator opts in.
if [[ -r /etc/defaultrouter ]]; then
    while IFS= read -r line || [[ -n "$line" ]]; do
        # Same trim treatment as the hostname.<if> parser. `\#` is
        # escaped because EXTENDED_GLOB is set script-wide.
        line="${line%%\#*}"
        line="${line##[[:space:]]##}"
        line="${line%%[[:space:]]##}"
        [[ -z "$line" ]] && continue
        $ROUTE -n add default "$line" || true
    done < /etc/defaultrouter
fi

exit 0
```

- [ ] **Step 2: Make sure it's still executable**

```bash
chmod +x services/hammerhead/network/start.sh
ls -l services/hammerhead/network/start.sh
```

Expected: mode `-rwxr-xr-x` (or compatible).

- [ ] **Step 3: Sanity-check syntax with `zsh -n`**

```bash
zsh -n services/hammerhead/network/start.sh
```

Expected: no output (clean parse). If your dev host doesn't have `zsh`, skip — the test VM will be the source of truth.

- [ ] **Step 4: Commit**

```bash
hg commit services/hammerhead/network/start.sh \
    -m "services/hammerhead/network/start.sh: BSD-style /etc/hostname.<if> parser

Drops ipadm/dladm in favor of direct /sbin/ifconfig invocation. Walks
/dev/net/ for physical links, reads /etc/hostname.<if> per interface
(line-oriented: dhcp / inet ADDR/PREFIX / addif / inet6 / up / down),
and /etc/defaultrouter for static defaults. Verified in spec §13 that
ifconfig plumb/inet/dhcp all work without ipmgmtd."
```

---

### Task 25: Author `network/stop.sh`

**Files:**
- Create: `services/hammerhead/network/stop.sh`

Symmetric reversal: kill any `dhcpagent` we forked, then unplumb non-loopback interfaces. Called during shutdown via the `[exec] stop = ...` line added in Task 23.

- [ ] **Step 1: Write the file**

Write `services/hammerhead/network/stop.sh`:

```zsh
#!/bin/zsh
# network/stop.sh — reverse of start.sh (BSD-pivot v2)
#
# Releases DHCP leases and unplumbs all configured non-loopback interfaces.
# Best-effort: each command may fail (e.g., interface already torn down,
# dhcpagent already gone) and we continue regardless. ZFS sync happens
# in main.reef after this runs.

set -u
# NOTE: no `set -e` — every step is best-effort during shutdown.

IFCONFIG=/sbin/ifconfig

# Release any DHCP leases first so the server marks them free.
if [[ -d /dev/net ]]; then
    for dev in /dev/net/*(N); do
        link="${dev:t}"
        [[ "$link" == "lo0" ]] && continue
        # `dhcp release` is idempotent — silently no-op if no lease.
        $IFCONFIG "$link" dhcp release 2>/dev/null || true
    done
fi

# Kill the dhcpagent itself. It manages all leases globally, so one kill
# tears down every interface's state at once.
pkill -TERM -x dhcpagent 2>/dev/null || true

# Unplumb. inet6 first (Hammerhead requires this ordering for clean teardown).
if [[ -d /dev/net ]]; then
    for dev in /dev/net/*(N); do
        link="${dev:t}"
        [[ "$link" == "lo0" ]] && continue
        $IFCONFIG "$link" inet6 unplumb 2>/dev/null || true
        $IFCONFIG "$link" unplumb 2>/dev/null || true
    done
fi

exit 0
```

- [ ] **Step 2: Make executable**

```bash
chmod +x services/hammerhead/network/stop.sh
```

- [ ] **Step 3: Syntax check**

```bash
zsh -n services/hammerhead/network/stop.sh
```

Expected: no output.

- [ ] **Step 4: Commit**

```bash
hg add services/hammerhead/network/stop.sh
hg commit -m "services/hammerhead/network: add stop.sh — release DHCP, unplumb interfaces"
```

---

### Task 26: Author sample `/etc/hostname.<if>` and `/etc/defaultrouter` files

**Files:**
- Create: `services/hammerhead/examples/etc-hostname.vioif0-static`
- Create: `services/hammerhead/examples/etc-hostname.vioif0-dhcp`
- Create: `services/hammerhead/examples/etc-defaultrouter`

These are reference templates the operator copies onto the test VM during iter 6 install. They don't ship via `install-to-be.sh` (that would clobber operator network config); they're versioned artifacts the operator manually drops into `/etc/` on the target BE.

The hh-prototest VM lives on `192.168.122.0/24` behind libvirt's dnsmasq. `192.168.122.50` is inside libvirt's default dnsmasq pool (`192.168.122.2`–`192.168.122.254`); before iter 6 boot, the operator must reserve `.50` via `<host>` in `virsh net-edit default` or shrink the pool's `<range>` to exclude it. The static sample's header comment documents both options.

- [ ] **Step 1: Create the examples directory**

```bash
mkdir -p services/hammerhead/examples
```

- [ ] **Step 2: Write the static-IP sample**

Write `services/hammerhead/examples/etc-hostname.vioif0-static`:

```
# /etc/hostname.vioif0 — static IPv4 for hh-prototest libvirt VM.
# Copy to /etc/hostname.vioif0 on the target BE.
#
# 192.168.122.50 is INSIDE libvirt's default dnsmasq pool
# (192.168.122.2-192.168.122.254). Before iter 6 boot, prevent
# collision via one of:
#   1. Add a host reservation to the libvirt network XML:
#      virsh net-edit default
#      <dhcp> ... <host mac='52:54:00:...' name='hh-prototest' ip='192.168.122.50'/>
#   2. Shrink the dnsmasq range, e.g.
#      <range start='192.168.122.100' end='192.168.122.254'/>
#   3. Use an address outside the libvirt subnet entirely (requires extra routing).
#
# See spec §4.4 for the full /etc/hostname.<if> grammar.
inet 192.168.122.50/24
```

- [ ] **Step 3: Write the DHCP sample**

Write `services/hammerhead/examples/etc-hostname.vioif0-dhcp`:

```
# /etc/hostname.vioif0 — DHCP-acquired address for hh-prototest
# Copy to /etc/hostname.vioif0 on the target BE.
#
# Caveat (spec §4.4): /sbin/dhcpagent links libipadm.so.1 which talks to
# ipmgmtd's door. Without ipmgmtd running, lease application may degrade.
# Use the static sample first to derisk; switch to this once iter 6
# proves DHCP-without-ipmgmtd works.
dhcp
```

- [ ] **Step 4: Write the defaultrouter sample**

Write `services/hammerhead/examples/etc-defaultrouter`:

```
# /etc/defaultrouter — one IPv4 default-gateway address per line.
# Copy to /etc/defaultrouter on the target BE *only* when using the
# static-IP sample; DHCP supplies the route automatically.
#
# 192.168.122.1 is libvirt's default-network virtual router.
192.168.122.1
```

- [ ] **Step 5: Commit**

```bash
hg add services/hammerhead/examples/
hg commit -m "services/hammerhead/examples: sample /etc/hostname.<if> and /etc/defaultrouter

For hh-prototest libvirt VM (192.168.122.0/24). Static-IP sample uses
.50 (inside default dnsmasq pool — header comment documents the
required reservation/range-shrink); DHCP sample documents the libipadm
caveat from spec §4.4. Operator copies these onto the target BE during
iter 6."
```

---

### Task 27: Iter 6 boot test — static-IP first, DHCP follow-up

**Files:**
- N/A (operational)
- Modify: `docs/superpowers/specs/2026-04-24-pid1-boot-results.md` (the test log file from Task 19, if it exists; create otherwise)

This task replaces the boot procedure originally captured in Task 19 + Task 20, now operating on the v2 service set. The recovery ladder from spec §8.4 still applies; the disk-level backup (`hh-prototest-backup-2026-04-24-pre-zyginit-pid1.qcow2`) and the ~75s recovery cycle are proven.

- [ ] **Step 1: Restore from disk backup if the BE from iter 5 is dirty**

If the on-disk hh-prototest qcow2 is in a known-bad state from iter 5:

```bash
virsh --connect=qemu:///system destroy hh-prototest 2>/dev/null || true
sudo cp /var/lib/libvirt/images/hh-prototest-backup-2026-04-24-pre-zyginit-pid1.qcow2 \
        /var/lib/libvirt/images/hh-prototest.qcow2
virsh --connect=qemu:///system start hh-prototest
```

Wait for SSH to be reachable: `until ssh -o ConnectTimeout=2 -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 'true' 2>/dev/null; do sleep 5; done`.

If iter 5's disk is salvageable (boots into `hammerhead` BE cleanly), skip this step.

- [ ] **Step 2: Build fresh binaries on the dev host and ship them**

```bash
cd /home/ctusa/repos/zyginit
# Hammerhead build target — see CLAUDE.md / MEMORY.md
reefc build -l contract
(cd tools/zygctl && clang -c src/symlink_wrapper.c -o build/symlink_wrapper.o && reefc build --obj build/symlink_wrapper.o)
rsync -a build/zyginit tools/zygctl/build/zygctl services/ scripts/ \
    -e 'ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519' \
    root@192.168.122.197:/root/zyginit/
```

Expected: 15 TOMLs in `/root/zyginit/services/hammerhead/`, 15 enabled.d entries, both binaries fresh.

- [ ] **Step 3: Create fresh BE and install**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
set -e
# If a stale 'zyginit' BE exists from iter 5, destroy it first.
beadm list | awk '$1 == "zyginit" {print "exists"}' | grep -q exists && {
    beadm activate hammerhead
    beadm destroy -F zyginit
}
beadm create zyginit
beadm mount zyginit /mnt
cd /root/zyginit && ./scripts/install-to-be.sh /mnt
ls /mnt/etc/zyginit/enabled.d/ | wc -l
EOF
```

Expected last line: `15`.

- [ ] **Step 4: Drop the static-IP `/etc/hostname.vioif0` and `/etc/defaultrouter` onto the new BE**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
cp /root/zyginit/services/hammerhead/examples/etc-hostname.vioif0-static /mnt/etc/hostname.vioif0
cp /root/zyginit/services/hammerhead/examples/etc-defaultrouter /mnt/etc/defaultrouter
cat /mnt/etc/hostname.vioif0 /mnt/etc/defaultrouter
EOF
```

Expected: contents of both sample files printed.

- [ ] **Step 5: Activate and reboot**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 <<'EOF'
beadm umount zyginit
beadm activate zyginit
beadm list
EOF
```

In a separate terminal, attach the console (so we can watch the boot):
```bash
virsh --connect=qemu:///system console hh-prototest
```

Then reboot:
```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.197 'init 6'
```

- [ ] **Step 6: Verify SSH comes up at 192.168.122.50**

The static IP is `.50`, NOT `.197`. After reboot:

```bash
until ssh -o ConnectTimeout=2 -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.50 'true' 2>/dev/null; do
    sleep 3
done
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.50 \
    'ifconfig -a; route -n get default; /sbin/zygctl list'
```

Expected:
- `vioif0: ... inet 192.168.122.50 netmask ffffff00 ...`
- `route ... default 192.168.122.1` reachable
- `zygctl list` shows 15 services, all `RUNNING` (daemons) or `STOPPED exit=0` (oneshots), none in `MAINTENANCE`.

- [ ] **Step 7: Run §9.A-D success checklist**

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.50 <<'EOF'
echo "--- A. boot ---"
uptime
ps -p 1 -o pid,comm
ps -ef | grep -E 'zyginit|sshd|cron' | grep -v grep

echo "--- B. service states ---"
/sbin/zygctl list

echo "--- C. functional ---"
ifconfig vioif0
ping -c 2 192.168.122.1 || echo "ping failed"
zfs list | head -5
logger -p user.info "iter6-static marker"
tail -5 /var/adm/messages 2>/dev/null || echo "messages unavailable"

echo "--- D. zygctl e2e ---"
/sbin/zygctl status cron
/sbin/zygctl stop cron
/sbin/zygctl status cron
/sbin/zygctl start cron
/sbin/zygctl status cron
EOF
```

Record pass/fail for each line in `docs/superpowers/specs/2026-04-24-pid1-boot-results.md`. Spec §9 is the source of truth for what counts as pass.

- [ ] **Step 8: Iter 6 DHCP variant (only if Step 7 fully passes)**

Switch the interface to DHCP and reboot:

```bash
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.122.50 <<'EOF'
cp /root/zyginit/services/hammerhead/examples/etc-hostname.vioif0-dhcp /etc/hostname.vioif0
rm -f /etc/defaultrouter
init 6
EOF
```

After reboot, the VM may come up at `192.168.122.197` (its previous DHCP lease), at any other address libvirt's dnsmasq hands out, or — if DHCP-via-dhcpagent degrades without ipmgmtd — without an address at all.

Probe behavior:
```bash
# Watch the libvirt DHCP leases:
virsh --connect=qemu:///system net-dhcp-leases default
# And the console for ifconfig errors during boot.
```

Three outcomes:
1. **DHCP works** → record pass in spec §13 ("DHCP-via-dhcpagent without ipmgmtd: confirmed"); update `examples/etc-hostname.vioif0-dhcp` comment to remove the caveat.
2. **DHCP partially works** (lease acquired, address not applied) → log the failure mode, leave the static sample as the documented default, file follow-up to investigate `dhcpagent -a -f` adopt mode.
3. **DHCP fails entirely** → log, revert to the static sample with concrete commands:
   ```bash
   cp /root/zyginit/services/hammerhead/examples/etc-hostname.vioif0-static /etc/hostname.vioif0
   cp /root/zyginit/services/hammerhead/examples/etc-defaultrouter /etc/defaultrouter
   ```
   Then document in spec §13 / §11 that DHCP needs a separate design.

- [ ] **Step 9: Update the test results file**

Write or extend `docs/superpowers/specs/2026-04-24-pid1-boot-results.md` with iter 6 outcomes (sections A through F per spec §9). Commit:

```bash
hg add docs/superpowers/specs/2026-04-24-pid1-boot-results.md 2>/dev/null || true
hg commit docs/superpowers/specs/2026-04-24-pid1-boot-results.md \
    -m "docs: iter 6 PID-1 boot results — v2 BSD-pivot service set"
```

- [ ] **Step 10: Run shutdown + rollback drills (Task 20 + Task 21)**

Tasks 20 and 21 still apply unchanged for v2 — they exercise `zygctl halt`/`reboot`/`poweroff` and the BE rollback path. Run them now against the iter-6 BE and append results to the same `2026-04-24-pid1-boot-results.md` file.

---

## Phase 7 — Wrap-up

### Task 28: Update CLAUDE.md, README.md, ROADMAP.md with results

**Files:**
- Modify: `CLAUDE.md`
- Modify: `README.md`
- Modify: `ROADMAP.md`

- [ ] **Step 1: Mark PID-1 boot as complete in ROADMAP.md**

Open `ROADMAP.md`. Find the "PID 1 Boot Testing" section. Change the unchecked items under it to `[x]` for the ones that passed in the iter-6 boot test (per the results file from Task 27).

- [ ] **Step 2: Add a note in CLAUDE.md about the new service set**

Under the source layout section, add:

```
- `services/hammerhead/` — Production Hammerhead service set (15 services
  enabled by default after the v2 BSD-pivot; 6 more TOMLs ship for opt-in
  via `zygctl enable`. Distinct from `services/examples/` Linux scaffolding.)
- `services/hammerhead/examples/` — Reference `/etc/hostname.<if>` and
  `/etc/defaultrouter` templates for hh-prototest; operator-copied, not
  shipped via install-to-be.sh.
```

- [ ] **Step 3: Commit**

```bash
hg commit CLAUDE.md README.md ROADMAP.md \
    -m "docs: mark PID-1 boot milestone complete; document services/hammerhead/ v2"
```

---

## Self-Review

After writing the plan, cross-check each spec section against tasks:

**Spec coverage:**
- §1.1 BSD-pivot rationale → Phase 6.5 prelude documents the configd wall and §1.1's three-choice decision
- §3.1 Components → Task 15 (installer copies binaries + config tree); unchanged in v2
- §3.2 Boot sequence → Tasks 9-14 + Task 23 (TOMLs); §3.2's tier ordering is enforced by rev 47, not re-implemented in v2
- §3.3 PID-1 vs non-PID-1 → Tasks 3, 5 (detection + branching)
- §4 Service set v2 (15 services) → Tasks 22-26 produce the diff from v1's 21-service set; the 4 dropped enabled.d entries align with §4.7/§4.8
- §4.4 `/etc/hostname.<if>` line forms → Task 24's case statement implements every row of the spec table (dhcp / inet / addif / inet6 / up / down)
- §4.4 `/etc/defaultrouter` → Task 24's tail block reads it; Task 26 ships a sample
- §4.7 Optional-but-not-enabled set → Task 22 keeps the 4 TOMLs in `services/hammerhead/` while removing only the symlinks
- §5 Schema usage → Tasks 9-14 + Task 23 (TOML content)
- §6 Script conventions → Tasks 24, 25 follow the `#!/bin/zsh` + `set -e -u` template; Task 25 deliberately drops `set -e` per the convention's "best-effort during shutdown" allowance
- §7 Shutdown orchestration → Tasks 1-6 (code changes), Task 8 (integration test); v2 adds `network/stop.sh` (Task 25) which is invoked by the existing reverse-tier walk
- §8 Rollout phases → Phase 5 (dry run), Phase 6 + 6.5 (BE boot), Task 21 (rollback drill); Task 27 Step 1 reuses §8.4's disk-restore recovery
- §9 Success criteria → Tasks 19-21 + Task 27 Step 7-10 run the checklist
- §11 Deferrals → Task 7 ships `single`/`multiuser` stubs; Task 27 Step 8 outcomes feed §11's "configd-coupled daemons re-add" deferral
- §13 Open questions → Task 27 Step 8 empirically resolves the DHCP-without-ipmgmtd question

**Placeholder scan:** none — all TOMLs, scripts, and code blocks contain actual content. Task 27 Steps 7-8 do reference "record results" actions, but the recording targets are concrete (specific spec sections / file path).

**Type consistency:** shutdown-type constants (SHUT_HALT=1, SHUT_REBOOT=2, SHUT_POWEROFF=3) unchanged in v2. The `network` service name (depended on by syseventd/syslogd/utmpd/pfexecd in tier 4) is preserved across Task 23 — only the inner `requires` list of `network.toml` itself changes. `network/start.sh` and `network/stop.sh` paths match the `[exec] start/stop` lines in the revised TOML.

**Scope check:** Phase 6.5 is contained within the existing PID-1-boot milestone; the v2 pivot replaces 4 enabled-set members and one start.sh, but doesn't introduce a new subsystem. Single plan remains correct.

---

## Plan complete

Plan saved to `docs/superpowers/plans/2026-04-24-pid1-boot.md`.
