|
root / docs / superpowers / plans / 2026-04-24-pid1-boot.md
2026-04-24-pid1-boot.md markdown 3238 lines 97.9 KB

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):

/* 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:

/* 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:

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:

#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)
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
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:

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

    (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
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
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:

// 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:

    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)
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
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:

// 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):

    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:

// 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):

    // ---- 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:

import time.clock as clock

(If already imported, skip.)

  • Step 4: Rebuild and run integration tests
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
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:

import shutdown

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

// 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:

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:

    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:

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

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

    proc do_sync()

And the implementation (before end module):

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:

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:

        println("zyginit: sync()")
        shutdown.do_sync()
  • Step 5: Build and test
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
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:

// 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):

    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:

    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:

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
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
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:

    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:

    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
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
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:

#!/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:

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

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:

# 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:

#!/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
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
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
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:

# 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:

# 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:

#!/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:

# 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:

#!/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
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
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
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:

# 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:

#!/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:

#!/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:

# 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:

#!/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:

# 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:

#!/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
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
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
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:

# 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:

# 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:

# 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:

#!/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
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
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
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:

[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:

[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:

[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:

[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:

[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:

[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:

[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
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
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
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:

[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:

[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:

[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:

[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
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
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

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:

#!/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
cd /home/ctusa/repos/zyginit
chmod +x scripts/install-to-be.sh
hg add scripts/install-to-be.sh
  • Step 3: Commit
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

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
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
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

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:

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

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
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
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
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

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:

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:

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)
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):

# 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

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
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
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

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

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.

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:

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.

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
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).

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
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
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:

# 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:

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
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:

#!/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
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
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
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:

#!/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
chmod +x services/hammerhead/network/stop.sh
  • Step 3: Syntax check
zsh -n services/hammerhead/network/stop.sh

Expected: no output.

  • Step 4: Commit
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.2192.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
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
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:

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
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
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
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
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):

virsh --connect=qemu:///system console hh-prototest

Then reboot:

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:

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

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:

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:

# 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:
    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:

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
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.